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.
 
 
 
 
 
 

363 linhas
10 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. if self.meta.get("istable"):
  93. columns += [frappe._dict({"fieldname": f, "fieldtype": "Data"}) for f in
  94. frappe.db.CHILD_TABLE_COLUMNS]
  95. columns += self.columns.values()
  96. for col in columns:
  97. if len(col.fieldname) >= 64:
  98. frappe.throw(_("Fieldname is limited to 64 characters ({0})")
  99. .format(frappe.bold(col.fieldname)))
  100. if 'varchar' in frappe.db.type_map.get(col.fieldtype, ()):
  101. # validate length range
  102. new_length = cint(col.length) or cint(frappe.db.VARCHAR_LEN)
  103. if not (1 <= new_length <= 1000):
  104. frappe.throw(_("Length of {0} should be between 1 and 1000").format(col.fieldname))
  105. current_col = self.current_columns.get(col.fieldname, {})
  106. if not current_col:
  107. continue
  108. current_type = self.current_columns[col.fieldname]["type"]
  109. current_length = re.findall(r'varchar\(([\d]+)\)', current_type)
  110. if not current_length:
  111. # case when the field is no longer a varchar
  112. continue
  113. current_length = current_length[0]
  114. if cint(current_length) != cint(new_length):
  115. try:
  116. # check for truncation
  117. max_length = frappe.db.sql("""SELECT MAX(CHAR_LENGTH(`{fieldname}`)) FROM `tab{doctype}`"""
  118. .format(fieldname=col.fieldname, doctype=self.doctype))
  119. except frappe.db.InternalError as e:
  120. if frappe.db.is_missing_column(e):
  121. # Unknown column 'column_name' in 'field list'
  122. continue
  123. raise
  124. if max_length and max_length[0][0] and max_length[0][0] > new_length:
  125. if col.fieldname in self.columns:
  126. self.columns[col.fieldname].length = current_length
  127. info_message = _("Reverting length to {0} for '{1}' in '{2}'. Setting the length as {3} will cause truncation of data.") \
  128. .format(current_length, col.fieldname, self.doctype, new_length)
  129. frappe.msgprint(info_message)
  130. def is_new(self):
  131. return self.table_name not in frappe.db.get_tables()
  132. def setup_table_columns(self):
  133. # TODO: figure out a way to get key data
  134. for c in frappe.db.get_table_columns_description(self.table_name):
  135. self.current_columns[c.name.lower()] = c
  136. def alter(self):
  137. pass
  138. class DbColumn:
  139. def __init__(self, table, fieldname, fieldtype, length, default,
  140. set_index, options, unique, precision):
  141. self.table = table
  142. self.fieldname = fieldname
  143. self.fieldtype = fieldtype
  144. self.length = length
  145. self.set_index = set_index
  146. self.default = default
  147. self.options = options
  148. self.unique = unique
  149. self.precision = precision
  150. def get_definition(self, with_default=1):
  151. column_def = get_definition(self.fieldtype, precision=self.precision, length=self.length)
  152. if not column_def:
  153. return column_def
  154. if self.fieldtype in ("Check", "Int"):
  155. default_value = cint(self.default) or 0
  156. column_def += ' not null default {0}'.format(default_value)
  157. elif self.fieldtype in ("Currency", "Float", "Percent"):
  158. default_value = flt(self.default) or 0
  159. column_def += ' not null default {0}'.format(default_value)
  160. elif self.default and (self.default not in frappe.db.DEFAULT_SHORTCUTS) \
  161. and not cstr(self.default).startswith(":") and column_def not in ('text', 'longtext'):
  162. column_def += " default {}".format(frappe.db.escape(self.default))
  163. if self.unique and (column_def not in ('text', 'longtext')):
  164. column_def += ' unique'
  165. return column_def
  166. def build_for_alter_table(self, current_def):
  167. column_type = get_definition(self.fieldtype, self.precision, self.length)
  168. # no columns
  169. if not column_type:
  170. return
  171. # to add?
  172. if not current_def:
  173. self.fieldname = validate_column_name(self.fieldname)
  174. self.table.add_column.append(self)
  175. if column_type not in ('text', 'longtext'):
  176. if self.unique:
  177. self.table.add_unique.append(self)
  178. if self.set_index:
  179. self.table.add_index.append(self)
  180. return
  181. # type
  182. if (current_def['type'] != column_type):
  183. self.table.change_type.append(self)
  184. # unique
  185. if ((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')):
  186. self.table.add_unique.append(self)
  187. elif (current_def['unique'] and not self.unique) and column_type not in ('text', 'longtext'):
  188. self.table.drop_unique.append(self)
  189. # default
  190. if (self.default_changed(current_def)
  191. and (self.default not in frappe.db.DEFAULT_SHORTCUTS)
  192. and not cstr(self.default).startswith(":")
  193. and not (column_type in ['text','longtext'])):
  194. self.table.set_default.append(self)
  195. # index should be applied or dropped irrespective of type change
  196. if (current_def['index'] and not self.set_index) and column_type not in ('text', 'longtext'):
  197. self.table.drop_index.append(self)
  198. elif (not current_def['index'] and self.set_index) and not (column_type in ('text', 'longtext')):
  199. self.table.add_index.append(self)
  200. def default_changed(self, current_def):
  201. if "decimal" in current_def['type']:
  202. return self.default_changed_for_decimal(current_def)
  203. else:
  204. cur_default = current_def['default']
  205. new_default = self.default
  206. if cur_default == "NULL" or cur_default is None:
  207. cur_default = None
  208. else:
  209. # Strip quotes from default value
  210. # eg. database returns default value as "'System Manager'"
  211. cur_default = cur_default.lstrip("'").rstrip("'")
  212. fieldtype = self.fieldtype
  213. if fieldtype in ['Int', 'Check']:
  214. cur_default = cint(cur_default)
  215. new_default = cint(new_default)
  216. elif fieldtype in ['Currency', 'Float', 'Percent']:
  217. cur_default = flt(cur_default)
  218. new_default = flt(new_default)
  219. return cur_default != new_default
  220. def default_changed_for_decimal(self, current_def):
  221. try:
  222. if current_def['default'] in ("", None) and self.default in ("", None):
  223. # both none, empty
  224. return False
  225. elif current_def['default'] in ("", None):
  226. try:
  227. # check if new default value is valid
  228. float(self.default)
  229. return True
  230. except ValueError:
  231. return False
  232. elif self.default in ("", None):
  233. # new default value is empty
  234. return True
  235. else:
  236. # NOTE float() raise ValueError when "" or None is passed
  237. return float(current_def['default'])!=float(self.default)
  238. except TypeError:
  239. return True
  240. def validate_column_name(n):
  241. special_characters = re.findall(r"[\W]", n, re.UNICODE)
  242. if special_characters:
  243. special_characters = ", ".join('"{0}"'.format(c) for c in special_characters)
  244. frappe.throw(_("Fieldname {0} cannot have special characters like {1}").format(
  245. frappe.bold(cstr(n)), special_characters), frappe.db.InvalidColumnName)
  246. return n
  247. def validate_column_length(fieldname):
  248. if len(fieldname) > frappe.db.MAX_COLUMN_LENGTH:
  249. frappe.throw(_("Fieldname is limited to 64 characters ({0})").format(fieldname))
  250. def get_definition(fieldtype, precision=None, length=None):
  251. d = frappe.db.type_map.get(fieldtype)
  252. if not d:
  253. return
  254. if fieldtype == "Int" and length and length > 11:
  255. # convert int to long int if the length of the int is greater than 11
  256. d = frappe.db.type_map.get("Long Int")
  257. coltype = d[0]
  258. size = d[1] if d[1] else None
  259. if size:
  260. # This check needs to exist for backward compatibility.
  261. # Till V13, default size used for float, currency and percent are (18, 6).
  262. if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6:
  263. size = '21,9'
  264. if length:
  265. if coltype == "varchar":
  266. size = length
  267. elif coltype == "int" and length < 11:
  268. # allow setting custom length for int if length provided is less than 11
  269. # NOTE: this will only be applicable for mariadb as frappe implements int
  270. # in postgres as bigint (as seen in type_map)
  271. size = length
  272. if size is not None:
  273. coltype = "{coltype}({size})".format(coltype=coltype, size=size)
  274. return coltype
  275. def add_column(
  276. doctype,
  277. column_name,
  278. fieldtype,
  279. precision=None,
  280. length=None,
  281. default=None,
  282. not_null=False
  283. ):
  284. if column_name in frappe.db.get_table_columns(doctype):
  285. # already exists
  286. return
  287. frappe.db.commit()
  288. query = "alter table `tab%s` add column %s %s" % (
  289. doctype,
  290. column_name,
  291. get_definition(fieldtype, precision, length)
  292. )
  293. if not_null:
  294. query += " not null"
  295. if default:
  296. query += f" default '{default}'"
  297. frappe.db.sql(query)