No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 
 
 

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