Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

327 linhas
9.6 KiB

  1. import re
  2. import frappe
  3. from frappe import _
  4. from frappe.utils import cstr, cint, flt
  5. class InvalidColumnName(frappe.ValidationError): pass
  6. class DBTable:
  7. def __init__(self, doctype, meta=None):
  8. self.doctype = doctype
  9. self.table_name = 'tab{}'.format(doctype)
  10. self.meta = meta or frappe.get_meta(doctype, False)
  11. self.columns = {}
  12. self.current_columns = {}
  13. # lists for change
  14. self.add_column = []
  15. self.change_type = []
  16. self.change_name = []
  17. self.add_unique = []
  18. self.add_index = []
  19. self.drop_index = []
  20. self.set_default = []
  21. # load
  22. self.get_columns_from_docfields()
  23. def sync(self):
  24. if self.meta.get('is_virtual'):
  25. # no schema to sync for virtual doctypes
  26. return
  27. if self.is_new():
  28. self.create()
  29. else:
  30. frappe.cache().hdel('table_columns', self.table_name)
  31. self.alter()
  32. def create(self):
  33. pass
  34. def get_column_definitions(self):
  35. column_list = [] + frappe.db.DEFAULT_COLUMNS
  36. ret = []
  37. for k in list(self.columns):
  38. if k not in column_list:
  39. d = self.columns[k].get_definition()
  40. if d:
  41. ret.append('`'+ k + '` ' + d)
  42. column_list.append(k)
  43. return ret
  44. def get_index_definitions(self):
  45. ret = []
  46. for key, col in self.columns.items():
  47. if (col.set_index
  48. and not col.unique
  49. and col.fieldtype in frappe.db.type_map
  50. and frappe.db.type_map.get(col.fieldtype)[0]
  51. not in ('text', 'longtext')):
  52. ret.append('index `' + key + '`(`' + key + '`)')
  53. return ret
  54. def get_columns_from_docfields(self):
  55. """
  56. get columns from docfields and custom fields
  57. """
  58. fields = self.meta.get_fieldnames_with_value(True)
  59. # optional fields like _comments
  60. if not self.meta.get('istable'):
  61. for fieldname in frappe.db.OPTIONAL_COLUMNS:
  62. fields.append({
  63. "fieldname": fieldname,
  64. "fieldtype": "Text"
  65. })
  66. # add _seen column if track_seen
  67. if self.meta.get('track_seen'):
  68. fields.append({
  69. 'fieldname': '_seen',
  70. 'fieldtype': 'Text'
  71. })
  72. for field in fields:
  73. self.columns[field.get('fieldname')] = DbColumn(
  74. self,
  75. field.get('fieldname'),
  76. field.get('fieldtype'),
  77. field.get('length'),
  78. field.get('default'),
  79. field.get('search_index'),
  80. field.get('options'),
  81. field.get('unique'),
  82. field.get('precision')
  83. )
  84. def validate(self):
  85. """Check if change in varchar length isn't truncating the columns"""
  86. if self.is_new():
  87. return
  88. self.setup_table_columns()
  89. columns = [frappe._dict({"fieldname": f, "fieldtype": "Data"}) for f in
  90. frappe.db.STANDARD_VARCHAR_COLUMNS]
  91. columns += self.columns.values()
  92. for col in columns:
  93. if len(col.fieldname) >= 64:
  94. frappe.throw(_("Fieldname is limited to 64 characters ({0})")
  95. .format(frappe.bold(col.fieldname)))
  96. if 'varchar' in frappe.db.type_map.get(col.fieldtype, ()):
  97. # validate length range
  98. new_length = cint(col.length) or cint(frappe.db.VARCHAR_LEN)
  99. if not (1 <= new_length <= 1000):
  100. frappe.throw(_("Length of {0} should be between 1 and 1000").format(col.fieldname))
  101. current_col = self.current_columns.get(col.fieldname, {})
  102. if not current_col:
  103. continue
  104. current_type = self.current_columns[col.fieldname]["type"]
  105. current_length = re.findall(r'varchar\(([\d]+)\)', current_type)
  106. if not current_length:
  107. # case when the field is no longer a varchar
  108. continue
  109. current_length = current_length[0]
  110. if cint(current_length) != cint(new_length):
  111. try:
  112. # check for truncation
  113. max_length = frappe.db.sql("""SELECT MAX(CHAR_LENGTH(`{fieldname}`)) FROM `tab{doctype}`"""
  114. .format(fieldname=col.fieldname, doctype=self.doctype))
  115. except frappe.db.InternalError as e:
  116. if frappe.db.is_missing_column(e):
  117. # Unknown column 'column_name' in 'field list'
  118. continue
  119. raise
  120. if max_length and max_length[0][0] and max_length[0][0] > new_length:
  121. if col.fieldname in self.columns:
  122. self.columns[col.fieldname].length = current_length
  123. info_message = _("Reverting length to {0} for '{1}' in '{2}'. Setting the length as {3} will cause truncation of data.") \
  124. .format(current_length, col.fieldname, self.doctype, new_length)
  125. frappe.msgprint(info_message)
  126. def is_new(self):
  127. return self.table_name not in frappe.db.get_tables()
  128. def setup_table_columns(self):
  129. # TODO: figure out a way to get key data
  130. for c in frappe.db.get_table_columns_description(self.table_name):
  131. self.current_columns[c.name.lower()] = c
  132. def alter(self):
  133. pass
  134. class DbColumn:
  135. def __init__(self, table, fieldname, fieldtype, length, default,
  136. set_index, options, unique, precision):
  137. self.table = table
  138. self.fieldname = fieldname
  139. self.fieldtype = fieldtype
  140. self.length = length
  141. self.set_index = set_index
  142. self.default = default
  143. self.options = options
  144. self.unique = unique
  145. self.precision = precision
  146. def get_definition(self, with_default=1):
  147. column_def = get_definition(self.fieldtype, precision=self.precision, length=self.length)
  148. if not column_def:
  149. return column_def
  150. if self.fieldtype in ("Check", "Int"):
  151. default_value = cint(self.default) or 0
  152. column_def += ' not null default {0}'.format(default_value)
  153. elif self.fieldtype in ("Currency", "Float", "Percent"):
  154. default_value = flt(self.default) or 0
  155. column_def += ' not null default {0}'.format(default_value)
  156. elif self.default and (self.default not in frappe.db.DEFAULT_SHORTCUTS) \
  157. and not cstr(self.default).startswith(":") and column_def not in ('text', 'longtext'):
  158. column_def += " default {}".format(frappe.db.escape(self.default))
  159. if self.unique and (column_def not in ('text', 'longtext')):
  160. column_def += ' unique'
  161. return column_def
  162. def build_for_alter_table(self, current_def):
  163. column_type = get_definition(self.fieldtype, self.precision, self.length)
  164. # no columns
  165. if not column_type:
  166. return
  167. # to add?
  168. if not current_def:
  169. self.fieldname = validate_column_name(self.fieldname)
  170. self.table.add_column.append(self)
  171. return
  172. # type
  173. if (current_def['type'] != column_type):
  174. self.table.change_type.append(self)
  175. # unique
  176. if((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')):
  177. self.table.add_unique.append(self)
  178. # default
  179. if (self.default_changed(current_def)
  180. and (self.default not in frappe.db.DEFAULT_SHORTCUTS)
  181. and not cstr(self.default).startswith(":")
  182. and not (column_type in ['text','longtext'])):
  183. self.table.set_default.append(self)
  184. # index should be applied or dropped irrespective of type change
  185. if ((current_def['index'] and not self.set_index and not self.unique)
  186. or (current_def['unique'] and not self.unique)):
  187. # to drop unique you have to drop index
  188. self.table.drop_index.append(self)
  189. elif (not current_def['index'] and self.set_index) and not (column_type in ('text', 'longtext')):
  190. self.table.add_index.append(self)
  191. def default_changed(self, current_def):
  192. if "decimal" in current_def['type']:
  193. return self.default_changed_for_decimal(current_def)
  194. else:
  195. cur_default = current_def['default']
  196. new_default = self.default
  197. if cur_default == "NULL" or cur_default is None:
  198. cur_default = None
  199. else:
  200. # Strip quotes from default value
  201. # eg. database returns default value as "'System Manager'"
  202. cur_default = cur_default.lstrip("'").rstrip("'")
  203. fieldtype = self.fieldtype
  204. if fieldtype in ['Int', 'Check']:
  205. cur_default = cint(cur_default)
  206. new_default = cint(new_default)
  207. elif fieldtype in ['Currency', 'Float', 'Percent']:
  208. cur_default = flt(cur_default)
  209. new_default = flt(new_default)
  210. return cur_default != new_default
  211. def default_changed_for_decimal(self, current_def):
  212. try:
  213. if current_def['default'] in ("", None) and self.default in ("", None):
  214. # both none, empty
  215. return False
  216. elif current_def['default'] in ("", None):
  217. try:
  218. # check if new default value is valid
  219. float(self.default)
  220. return True
  221. except ValueError:
  222. return False
  223. elif self.default in ("", None):
  224. # new default value is empty
  225. return True
  226. else:
  227. # NOTE float() raise ValueError when "" or None is passed
  228. return float(current_def['default'])!=float(self.default)
  229. except TypeError:
  230. return True
  231. def validate_column_name(n):
  232. special_characters = re.findall(r"[\W]", n, re.UNICODE)
  233. if special_characters:
  234. special_characters = ", ".join('"{0}"'.format(c) for c in special_characters)
  235. frappe.throw(_("Fieldname {0} cannot have special characters like {1}").format(
  236. frappe.bold(cstr(n)), special_characters), frappe.db.InvalidColumnName)
  237. return n
  238. def validate_column_length(fieldname):
  239. if len(fieldname) > frappe.db.MAX_COLUMN_LENGTH:
  240. frappe.throw(_("Fieldname is limited to 64 characters ({0})").format(fieldname))
  241. def get_definition(fieldtype, precision=None, length=None):
  242. d = frappe.db.type_map.get(fieldtype)
  243. # convert int to long int if the length of the int is greater than 11
  244. if fieldtype == "Int" and length and length > 11:
  245. d = frappe.db.type_map.get("Long Int")
  246. if not d: return
  247. coltype = d[0]
  248. size = d[1] if d[1] else None
  249. if size:
  250. # This check needs to exist for backward compatibility.
  251. # Till V13, default size used for float, currency and percent are (18, 6).
  252. if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6:
  253. size = '21,9'
  254. if coltype == "varchar" and length:
  255. size = length
  256. if size is not None:
  257. coltype = "{coltype}({size})".format(coltype=coltype, size=size)
  258. return coltype
  259. def add_column(doctype, column_name, fieldtype, precision=None):
  260. if column_name in frappe.db.get_table_columns(doctype):
  261. # already exists
  262. return
  263. frappe.db.commit()
  264. frappe.db.sql("alter table `tab%s` add column %s %s" % (doctype,
  265. column_name, get_definition(fieldtype, precision)))