Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 
 

493 строки
14 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. from __future__ import unicode_literals
  4. """
  5. Syncs a database table to the `DocType` (metadata)
  6. .. note:: This module is only used internally
  7. """
  8. import re
  9. import os
  10. import frappe
  11. from frappe import _
  12. from frappe.utils import cstr, cint
  13. class InvalidColumnName(frappe.ValidationError): pass
  14. type_map = {
  15. 'Currency': ('decimal', '18,6')
  16. ,'Int': ('int', '11')
  17. ,'Float': ('decimal', '18,6')
  18. ,'Percent': ('decimal', '18,6')
  19. ,'Check': ('int', '1')
  20. ,'Small Text': ('text', '')
  21. ,'Long Text': ('longtext', '')
  22. ,'Code': ('longtext', '')
  23. ,'Text Editor': ('longtext', '')
  24. ,'Date': ('date', '')
  25. ,'Datetime': ('datetime', '6')
  26. ,'Time': ('time', '6')
  27. ,'Text': ('text', '')
  28. ,'Data': ('varchar', '255')
  29. ,'Link': ('varchar', '255')
  30. ,'Dynamic Link':('varchar', '255')
  31. ,'Password': ('varchar', '255')
  32. ,'Select': ('varchar', '255')
  33. ,'Read Only': ('varchar', '255')
  34. ,'Attach': ('varchar', '255')
  35. ,'Attach Image':('varchar', '255')
  36. }
  37. default_columns = ['name', 'creation', 'modified', 'modified_by', 'owner',
  38. 'docstatus', 'parent', 'parentfield', 'parenttype', 'idx']
  39. default_shortcuts = ['_Login', '__user', '_Full Name', 'Today', '__today', "now", "Now"]
  40. def updatedb(dt):
  41. """
  42. Syncs a `DocType` to the table
  43. * creates if required
  44. * updates columns
  45. * updates indices
  46. """
  47. res = frappe.db.sql("select ifnull(issingle, 0) from tabDocType where name=%s", (dt,))
  48. if not res:
  49. raise Exception, 'Wrong doctype "%s" in updatedb' % dt
  50. if not res[0][0]:
  51. frappe.db.commit()
  52. tab = DbTable(dt, 'tab')
  53. tab.sync()
  54. frappe.db.begin()
  55. class DbTable:
  56. def __init__(self, doctype, prefix = 'tab'):
  57. self.doctype = doctype
  58. self.name = prefix + doctype
  59. self.columns = {}
  60. self.current_columns = {}
  61. # lists for change
  62. self.add_column = []
  63. self.change_type = []
  64. self.add_index = []
  65. self.drop_index = []
  66. self.set_default = []
  67. # load
  68. self.get_columns_from_docfields()
  69. def sync(self):
  70. if not self.name in DbManager(frappe.db).get_tables_list(frappe.db.cur_db_name):
  71. self.create()
  72. else:
  73. self.alter()
  74. def create(self):
  75. add_text = ''
  76. # columns
  77. column_defs = self.get_column_definitions()
  78. if column_defs: add_text += ',\n'.join(column_defs) + ',\n'
  79. # index
  80. index_defs = self.get_index_definitions()
  81. if index_defs: add_text += ',\n'.join(index_defs) + ',\n'
  82. # create table
  83. frappe.db.sql("""create table `%s` (
  84. name varchar(255) not null primary key,
  85. creation datetime(6),
  86. modified datetime(6),
  87. modified_by varchar(255),
  88. owner varchar(255),
  89. docstatus int(1) default '0',
  90. parent varchar(255),
  91. parentfield varchar(255),
  92. parenttype varchar(255),
  93. idx int(8),
  94. %sindex parent(parent))
  95. ENGINE=InnoDB
  96. ROW_FORMAT=COMPRESSED
  97. CHARACTER SET=utf8mb4
  98. COLLATE=utf8mb4_unicode_ci""" % (self.name, add_text))
  99. def get_column_definitions(self):
  100. column_list = [] + default_columns
  101. ret = []
  102. for k in self.columns.keys():
  103. if k not in column_list:
  104. d = self.columns[k].get_definition()
  105. if d:
  106. ret.append('`'+ k+ '` ' + d)
  107. column_list.append(k)
  108. return ret
  109. def get_index_definitions(self):
  110. ret = []
  111. for key, col in self.columns.items():
  112. if col.set_index and col.fieldtype in type_map and \
  113. type_map.get(col.fieldtype)[0] not in ('text', 'longtext'):
  114. ret.append('index `' + key + '`(`' + key + '`)')
  115. return ret
  116. def get_columns_from_docfields(self):
  117. """
  118. get columns from docfields and custom fields
  119. """
  120. fl = frappe.db.sql("SELECT * FROM tabDocField WHERE parent = %s", self.doctype, as_dict = 1)
  121. precisions = {}
  122. uniques = {}
  123. if not frappe.flags.in_install:
  124. custom_fl = frappe.db.sql("""\
  125. SELECT * FROM `tabCustom Field`
  126. WHERE dt = %s AND docstatus < 2""", (self.doctype,), as_dict=1)
  127. if custom_fl: fl += custom_fl
  128. # get precision from property setters
  129. for ps in frappe.get_all("Property Setter", fields=["field_name", "value"],
  130. filters={"doc_type": self.doctype, "doctype_or_field": "DocField", "property": "precision"}):
  131. precisions[ps.field_name] = ps.value
  132. # apply unique from property setters
  133. for ps in frappe.get_all("Property Setter", fields=["field_name", "value"],
  134. filters={"doc_type": self.doctype, "doctype_or_field": "DocField", "property": "unique"}):
  135. uniques[ps.field_name] = cint(ps.value)
  136. for f in fl:
  137. self.columns[f['fieldname']] = DbColumn(self, f['fieldname'],
  138. f['fieldtype'], f.get('length'), f.get('default'), f.get('search_index'),
  139. f.get('options'), uniques.get(f["fieldname"], f.get('unique')), precisions.get(f['fieldname']) or f.get('precision'))
  140. def get_columns_from_db(self):
  141. self.show_columns = frappe.db.sql("desc `%s`" % self.name)
  142. for c in self.show_columns:
  143. self.current_columns[c[0]] = {'name': c[0],
  144. 'type':c[1], 'index':c[3]=="MUL", 'default':c[4], "unique":c[3]=="UNI"}
  145. # GET foreign keys
  146. def get_foreign_keys(self):
  147. fk_list = []
  148. txt = frappe.db.sql("show create table `%s`" % self.name)[0][1]
  149. for line in txt.split('\n'):
  150. if line.strip().startswith('CONSTRAINT') and line.find('FOREIGN')!=-1:
  151. try:
  152. fk_list.append((line.split('`')[3], line.split('`')[1]))
  153. except IndexError:
  154. pass
  155. return fk_list
  156. # Drop foreign keys
  157. def drop_foreign_keys(self):
  158. if not self.drop_foreign_key:
  159. return
  160. fk_list = self.get_foreign_keys()
  161. # make dictionary of constraint names
  162. fk_dict = {}
  163. for f in fk_list:
  164. fk_dict[f[0]] = f[1]
  165. # drop
  166. for col in self.drop_foreign_key:
  167. frappe.db.sql("set foreign_key_checks=0")
  168. frappe.db.sql("alter table `%s` drop foreign key `%s`" % (self.name, fk_dict[col.fieldname]))
  169. frappe.db.sql("set foreign_key_checks=1")
  170. def alter(self):
  171. self.get_columns_from_db()
  172. for col in self.columns.values():
  173. col.build_for_alter_table(self.current_columns.get(col.fieldname, None))
  174. query = []
  175. for col in self.add_column:
  176. query.append("add column `{}` {}".format(col.fieldname, col.get_definition()))
  177. for col in self.change_type:
  178. query.append("change `{}` `{}` {}".format(col.fieldname, col.fieldname, col.get_definition()))
  179. for col in self.add_index:
  180. # if index key not exists
  181. if not frappe.db.sql("show index from `%s` where key_name = %s" %
  182. (self.name, '%s'), col.fieldname):
  183. query.append("add index `{}`(`{}`)".format(col.fieldname, col.fieldname))
  184. for col in self.drop_index:
  185. if col.fieldname != 'name': # primary key
  186. # if index key exists
  187. if frappe.db.sql("""show index from `{0}`
  188. where key_name=%s
  189. and Non_unique=%s""".format(self.name), (col.fieldname, 1 if col.unique else 0)):
  190. query.append("drop index `{}`".format(col.fieldname))
  191. for col in self.set_default:
  192. if col.fieldname=="name":
  193. continue
  194. if col.fieldtype=="Check":
  195. col_default = cint(col.default)
  196. elif not col.default:
  197. col_default = "null"
  198. else:
  199. col_default = '"{}"'.format(col.default.replace('"', '\\"'))
  200. query.append('alter column `{}` set default {}'.format(col.fieldname, col_default))
  201. if query:
  202. try:
  203. frappe.db.sql("alter table `{}` {}".format(self.name, ", ".join(query)))
  204. except Exception, e:
  205. # sanitize
  206. if e.args[0]==1060:
  207. frappe.throw(str(e))
  208. elif e.args[0]==1062:
  209. fieldname = str(e).split("'")[-2]
  210. frappe.throw(_("{0} field cannot be set as unique, as there are non-unique existing values".format(fieldname)))
  211. else:
  212. raise e
  213. class DbColumn:
  214. def __init__(self, table, fieldname, fieldtype, length, default,
  215. set_index, options, unique, precision):
  216. self.table = table
  217. self.fieldname = fieldname
  218. self.fieldtype = fieldtype
  219. self.length = length
  220. self.set_index = set_index
  221. self.default = default
  222. self.options = options
  223. self.unique = unique
  224. self.precision = precision
  225. def get_definition(self, with_default=1):
  226. column_def = get_definition(self.fieldtype, self.precision)
  227. if not column_def:
  228. return column_def
  229. if self.fieldtype=="Check":
  230. default_value = cint(self.default) or 0
  231. column_def += ' not null default {0}'.format(default_value)
  232. elif self.default and (self.default not in default_shortcuts) \
  233. and not self.default.startswith(":") and column_def not in ('text', 'longtext'):
  234. column_def += ' default "' + self.default.replace('"', '\"') + '"'
  235. if self.unique and (column_def not in ('text', 'longtext')):
  236. column_def += ' unique'
  237. return column_def
  238. def build_for_alter_table(self, current_def):
  239. column_def = get_definition(self.fieldtype)
  240. # no columns
  241. if not column_def:
  242. return
  243. # to add?
  244. if not current_def:
  245. self.fieldname = validate_column_name(self.fieldname)
  246. self.table.add_column.append(self)
  247. return
  248. # type
  249. if (current_def['type'] != column_def) or \
  250. ((self.unique and not current_def['unique']) and column_def not in ('text', 'longtext')):
  251. self.table.change_type.append(self)
  252. else:
  253. # default
  254. if (self.default_changed(current_def) \
  255. and (self.default not in default_shortcuts) \
  256. and not cstr(self.default).startswith(":") \
  257. and not (column_def in ['text','longtext'])):
  258. self.table.set_default.append(self)
  259. # index should be applied or dropped irrespective of type change
  260. if ( (current_def['index'] and not self.set_index and not self.unique)
  261. or (current_def['unique'] and not self.unique) ):
  262. # to drop unique you have to drop index
  263. self.table.drop_index.append(self)
  264. elif (not current_def['index'] and self.set_index) and not (column_def in ('text', 'longtext')):
  265. self.table.add_index.append(self)
  266. def default_changed(self, current_def):
  267. if "decimal" in current_def['type']:
  268. return self.default_changed_for_decimal(current_def)
  269. else:
  270. return current_def['default'] != self.default
  271. def default_changed_for_decimal(self, current_def):
  272. try:
  273. if current_def['default'] in ("", None) and self.default in ("", None):
  274. # both none, empty
  275. return False
  276. elif current_def['default'] in ("", None):
  277. try:
  278. # check if new default value is valid
  279. float(self.default)
  280. return True
  281. except ValueError:
  282. return False
  283. elif self.default in ("", None):
  284. # new default value is empty
  285. return True
  286. else:
  287. # NOTE float() raise ValueError when "" or None is passed
  288. return float(current_def['default'])!=float(self.default)
  289. except TypeError:
  290. return True
  291. class DbManager:
  292. """
  293. Basically, a wrapper for oft-used mysql commands. like show tables,databases, variables etc...
  294. #TODO:
  295. 0. Simplify / create settings for the restore database source folder
  296. 0a. Merge restore database and extract_sql(from frappe_server_tools).
  297. 1. Setter and getter for different mysql variables.
  298. 2. Setter and getter for mysql variables at global level??
  299. """
  300. def __init__(self,db):
  301. """
  302. Pass root_conn here for access to all databases.
  303. """
  304. if db:
  305. self.db = db
  306. def get_variables(self,regex):
  307. """
  308. Get variables that match the passed pattern regex
  309. """
  310. return list(self.db.sql("SHOW VARIABLES LIKE '%s'"%regex))
  311. def get_table_schema(self,table):
  312. """
  313. Just returns the output of Desc tables.
  314. """
  315. return list(self.db.sql("DESC `%s`"%table))
  316. def get_tables_list(self,target=None):
  317. """get list of tables"""
  318. if target:
  319. self.db.use(target)
  320. return [t[0] for t in self.db.sql("SHOW TABLES")]
  321. def create_user(self, user, password, host):
  322. #Create user if it doesn't exist.
  323. try:
  324. if password:
  325. self.db.sql("CREATE USER '%s'@'%s' IDENTIFIED BY '%s';" % (user[:16], host, password))
  326. else:
  327. self.db.sql("CREATE USER '%s'@'%s';" % (user[:16], host))
  328. except Exception:
  329. raise
  330. def delete_user(self, target, host):
  331. # delete user if exists
  332. try:
  333. self.db.sql("DROP USER '%s'@'%s';" % (target, host))
  334. except Exception, e:
  335. if e.args[0]==1396:
  336. pass
  337. else:
  338. raise
  339. def create_database(self,target):
  340. if target in self.get_database_list():
  341. self.drop_database(target)
  342. self.db.sql("CREATE DATABASE IF NOT EXISTS `%s` ;" % target)
  343. def drop_database(self,target):
  344. self.db.sql("DROP DATABASE IF EXISTS `%s`;"%target)
  345. def grant_all_privileges(self, target, user, host):
  346. self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
  347. def grant_select_privilges(self, db, table, user, host):
  348. if table:
  349. self.db.sql("GRANT SELECT ON %s.%s to '%s'@'%s';" % (db, table, user, host))
  350. else:
  351. self.db.sql("GRANT SELECT ON %s.* to '%s'@'%s';" % (db, user, host))
  352. def flush_privileges(self):
  353. self.db.sql("FLUSH PRIVILEGES")
  354. def get_database_list(self):
  355. """get list of databases"""
  356. return [d[0] for d in self.db.sql("SHOW DATABASES")]
  357. def restore_database(self,target,source,user,password):
  358. from frappe.utils import make_esc
  359. esc = make_esc('$ ')
  360. os.system("mysql -u %s -p%s -h%s %s < %s" % \
  361. (esc(user), esc(password), esc(frappe.db.host), esc(target), source))
  362. def drop_table(self,table_name):
  363. """drop table if exists"""
  364. if not table_name in self.get_tables_list():
  365. return
  366. self.db.sql("DROP TABLE IF EXISTS %s "%(table_name))
  367. def validate_column_name(n):
  368. n = n.replace(' ','_').strip().lower()
  369. special_characters = re.findall("[\W]", n, re.UNICODE)
  370. if special_characters:
  371. special_characters = ", ".join('"{0}"'.format(c) for c in special_characters)
  372. frappe.throw(_("Fieldname {0} cannot have special characters like {1}").format(cstr(n), special_characters), InvalidColumnName)
  373. return n
  374. def remove_all_foreign_keys():
  375. frappe.db.sql("set foreign_key_checks = 0")
  376. frappe.db.commit()
  377. for t in frappe.db.sql("select name from tabDocType where ifnull(issingle,0)=0"):
  378. dbtab = DbTable(t[0])
  379. try:
  380. fklist = dbtab.get_foreign_keys()
  381. except Exception, e:
  382. if e.args[0]==1146:
  383. fklist = []
  384. else:
  385. raise
  386. for f in fklist:
  387. frappe.db.sql("alter table `tab%s` drop foreign key `%s`" % (t[0], f[1]))
  388. def get_definition(fieldtype, precision=None):
  389. d = type_map.get(fieldtype)
  390. if not d:
  391. return
  392. ret = d[0]
  393. if d[1]:
  394. length = d[1]
  395. if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6:
  396. length = '18,9'
  397. ret += '(' + length + ')'
  398. return ret
  399. def add_column(doctype, column_name, fieldtype, precision=None):
  400. frappe.db.commit()
  401. frappe.db.sql("alter table `tab%s` add column %s %s" % (doctype,
  402. column_name, get_definition(fieldtype, precision)))