Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

db_schema.py 13 KiB

před 13 roky
před 13 roky
před 13 roky
před 13 roky
před 13 roky
před 13 roky
před 13 roky
před 13 roky
před 13 roky
před 13 roky
před 13 roky
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. # Copyright (c) 2012 Web Notes Technologies Pvt Ltd (http://erpnext.com)
  2. #
  3. # MIT License (MIT)
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a
  6. # copy of this software and associated documentation files (the "Software"),
  7. # to deal in the Software without restriction, including without limitation
  8. # the rights to use, copy, modify, merge, publish, distribute, sublicense,
  9. # and/or sell copies of the Software, and to permit persons to whom the
  10. # Software is furnished to do so, subject to the following conditions:
  11. #
  12. # The above copyright notice and this permission notice shall be included in
  13. # all copies or substantial portions of the Software.
  14. #
  15. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  16. # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
  17. # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  18. # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
  19. # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
  20. # OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  21. #
  22. from __future__ import unicode_literals
  23. """
  24. Syncs a database table to the `DocType` (metadata)
  25. .. note:: This module is only used internally
  26. """
  27. import os
  28. import webnotes
  29. type_map = {
  30. 'currency': ('decimal', '18,6')
  31. ,'int': ('int', '11')
  32. ,'float': ('decimal', '18,6')
  33. ,'percent': ('decimal', '18,6')
  34. ,'check': ('int', '1')
  35. ,'small text': ('text', '')
  36. ,'long text': ('longtext', '')
  37. ,'code': ('text', '')
  38. ,'text editor': ('text', '')
  39. ,'date': ('date', '')
  40. ,'datetime': ('datetime', '')
  41. ,'time': ('time', '')
  42. ,'text': ('text', '')
  43. ,'data': ('varchar', '180')
  44. ,'link': ('varchar', '180')
  45. ,'password': ('varchar', '180')
  46. ,'select': ('varchar', '180')
  47. ,'read only': ('varchar', '180')
  48. }
  49. default_columns = ['name', 'creation', 'modified', 'modified_by', 'owner', 'docstatus', 'parent',\
  50. 'parentfield', 'parenttype', 'idx']
  51. default_shortcuts = ['_Login', '__user', '_Full Name', 'Today', '__today']
  52. import _mysql_exceptions
  53. # -------------------------------------------------
  54. # Class database table
  55. # -------------------------------------------------
  56. class DbTable:
  57. def __init__(self, doctype, prefix = 'tab'):
  58. self.doctype = doctype
  59. self.name = prefix + doctype
  60. self.columns = {}
  61. self.current_columns = {}
  62. # lists for change
  63. self.add_column = []
  64. self.change_type = []
  65. self.add_index = []
  66. self.drop_index = []
  67. self.set_default = []
  68. # load
  69. self.get_columns_from_docfields()
  70. def create(self):
  71. add_text = ''
  72. # columns
  73. t = self.get_column_definitions()
  74. if t: add_text += ',\n'.join(self.get_column_definitions()) + ',\n'
  75. # index
  76. t = self.get_index_definitions()
  77. if t: add_text += ',\n'.join(self.get_index_definitions()) + ',\n'
  78. # create table
  79. webnotes.conn.sql("""create table `%s` (
  80. name varchar(120) not null primary key,
  81. creation datetime,
  82. modified datetime,
  83. modified_by varchar(40),
  84. owner varchar(40),
  85. docstatus int(1) default '0',
  86. parent varchar(120),
  87. parentfield varchar(120),
  88. parenttype varchar(120),
  89. idx int(8),
  90. %sindex parent(parent))
  91. ENGINE=InnoDB
  92. CHARACTER SET=utf8""" % (self.name, add_text))
  93. def get_columns_from_docfields(self):
  94. """
  95. get columns from docfields and custom fields
  96. """
  97. fl = webnotes.conn.sql("SELECT * FROM tabDocField WHERE parent = '%s'" % self.doctype, as_dict = 1)
  98. try:
  99. custom_fl = webnotes.conn.sql("""\
  100. SELECT * FROM `tabCustom Field`
  101. WHERE dt = %s AND docstatus < 2""", self.doctype, as_dict=1)
  102. if custom_fl: fl += custom_fl
  103. except Exception, e:
  104. if e.args[0]!=1146: # ignore no custom field
  105. raise e
  106. for f in fl:
  107. self.columns[f['fieldname']] = DbColumn(self, f['fieldname'],
  108. f['fieldtype'], f.get('length'), f.get('default'),
  109. f.get('search_index'), f.get('options'))
  110. def get_columns_from_db(self):
  111. self.show_columns = webnotes.conn.sql("desc `%s`" % self.name)
  112. for c in self.show_columns:
  113. self.current_columns[c[0]] = {'name': c[0], 'type':c[1], 'index':c[3], 'default':c[4]}
  114. def get_column_definitions(self):
  115. column_list = [] + default_columns
  116. ret = []
  117. for k in self.columns.keys():
  118. if k not in column_list:
  119. d = self.columns[k].get_definition()
  120. if d:
  121. ret.append('`'+ k+ '` ' + d)
  122. column_list.append(k)
  123. return ret
  124. def get_index_definitions(self):
  125. ret = []
  126. for k in self.columns.keys():
  127. if type_map.get(self.columns[k].fieldtype) and type_map.get(self.columns[k].fieldtype.lower())[0] not in ('text', 'blob'):
  128. ret.append('index `' + k + '`(`' + k + '`)')
  129. return ret
  130. # GET foreign keys
  131. def get_foreign_keys(self):
  132. fk_list = []
  133. txt = webnotes.conn.sql("show create table `%s`" % self.name)[0][1]
  134. for line in txt.split('\n'):
  135. if line.strip().startswith('CONSTRAINT') and line.find('FOREIGN')!=-1:
  136. try:
  137. fk_list.append((line.split('`')[3], line.split('`')[1]))
  138. except IndexError, e:
  139. pass
  140. return fk_list
  141. # Drop foreign keys
  142. def drop_foreign_keys(self):
  143. if not self.drop_foreign_key:
  144. return
  145. fk_list = self.get_foreign_keys()
  146. # make dictionary of constraint names
  147. fk_dict = {}
  148. for f in fk_list:
  149. fk_dict[f[0]] = f[1]
  150. # drop
  151. for col in self.drop_foreign_key:
  152. webnotes.conn.sql("set foreign_key_checks=0")
  153. webnotes.conn.sql("alter table `%s` drop foreign key `%s`" % (self.name, fk_dict[col.fieldname]))
  154. webnotes.conn.sql("set foreign_key_checks=1")
  155. def sync(self):
  156. if not self.name in DbManager(webnotes.conn).get_tables_list(webnotes.conn.cur_db_name):
  157. self.create()
  158. else:
  159. self.alter()
  160. def alter(self):
  161. self.get_columns_from_db()
  162. for col in self.columns.values():
  163. col.check(self.current_columns.get(col.fieldname, None))
  164. for col in self.add_column:
  165. webnotes.conn.sql("alter table `%s` add column `%s` %s" % (self.name, col.fieldname, col.get_definition()))
  166. for col in self.change_type:
  167. webnotes.conn.sql("alter table `%s` change `%s` `%s` %s" % (self.name, col.fieldname, col.fieldname, col.get_definition()))
  168. for col in self.add_index:
  169. # if index key not exists
  170. if not webnotes.conn.sql("show index from `%s` where key_name = '%s'" % (self.name, col.fieldname)):
  171. webnotes.conn.sql("alter table `%s` add index `%s`(`%s`)" % (self.name, col.fieldname, col.fieldname))
  172. for col in self.drop_index:
  173. if col.fieldname != 'name': # primary key
  174. # if index key exists
  175. if webnotes.conn.sql("show index from `%s` where key_name = '%s'" % (self.name, col.fieldname)):
  176. webnotes.conn.sql("alter table `%s` drop index `%s`" % (self.name, col.fieldname))
  177. for col in self.set_default:
  178. webnotes.conn.sql("alter table `%s` alter column `%s` set default %s" % (self.name, col.fieldname, '%s'), col.default)
  179. class DbColumn:
  180. def __init__(self, table, fieldname, fieldtype, length, default, set_index, options):
  181. self.table = table
  182. self.fieldname = fieldname
  183. self.fieldtype = fieldtype
  184. self.length = length
  185. self.set_index = set_index
  186. self.default = default
  187. self.options = options
  188. def get_definition(self, with_default=1):
  189. d = type_map.get(self.fieldtype.lower())
  190. if not d:
  191. return
  192. ret = d[0]
  193. if d[1]:
  194. ret += '(' + d[1] + ')'
  195. if with_default and self.default and (self.default not in default_shortcuts) \
  196. and not self.default.startswith(":") and d[0] not in ['text', 'longblob']:
  197. ret += ' default "' + self.default.replace('"', '\"') + '"'
  198. return ret
  199. def check(self, current_def):
  200. column_def = self.get_definition(0)
  201. # no columns
  202. if not column_def:
  203. return
  204. # to add?
  205. if not current_def:
  206. self.fieldname = validate_column_name(self.fieldname)
  207. self.table.add_column.append(self)
  208. return
  209. # type
  210. if current_def['type'] != column_def:
  211. self.table.change_type.append(self)
  212. # index
  213. else:
  214. if (current_def['index'] and not self.set_index):
  215. self.table.drop_index.append(self)
  216. if (not current_def['index'] and self.set_index and not (column_def in ['text','blob'])):
  217. self.table.add_index.append(self)
  218. # default
  219. if (self.default and self.default_changed(current_def) and (self.default not in default_shortcuts) and not self.default.startswith(":") and not (column_def in ['text','blob'])):
  220. self.table.set_default.append(self)
  221. def default_changed(self, current_def):
  222. if "decimal" in current_def['type']:
  223. try:
  224. return float(current_def['default'])!=float(self.default)
  225. except TypeError, e:
  226. return True
  227. else:
  228. return current_def['default'] != self.default
  229. class DbManager:
  230. """
  231. Basically, a wrapper for oft-used mysql commands. like show tables,databases, variables etc...
  232. #TODO:
  233. 0. Simplify / create settings for the restore database source folder
  234. 0a. Merge restore database and extract_sql(from webnotes_server_tools).
  235. 1. Setter and getter for different mysql variables.
  236. 2. Setter and getter for mysql variables at global level??
  237. """
  238. def __init__(self,conn):
  239. """
  240. Pass root_conn here for access to all databases.
  241. """
  242. if conn:
  243. self.conn = conn
  244. def get_variables(self,regex):
  245. """
  246. Get variables that match the passed pattern regex
  247. """
  248. return list(self.conn.sql("SHOW VARIABLES LIKE '%s'"%regex))
  249. def get_table_schema(self,table):
  250. """
  251. Just returns the output of Desc tables.
  252. """
  253. return list(self.conn.sql("DESC `%s`"%table))
  254. def get_tables_list(self,target=None):
  255. """get list of tables"""
  256. if target:
  257. self.conn.use(target)
  258. return [t[0] for t in self.conn.sql("SHOW TABLES")]
  259. def create_user(self,user,password):
  260. #Create user if it doesn't exist.
  261. try:
  262. if password:
  263. self.conn.sql("CREATE USER '%s'@'localhost' IDENTIFIED BY '%s';" % (user[:16], password))
  264. else:
  265. self.conn.sql("CREATE USER '%s'@'localhost';"%user[:16])
  266. except Exception, e:
  267. raise e
  268. def delete_user(self,target):
  269. # delete user if exists
  270. try:
  271. self.conn.sql("DROP USER '%s'@'localhost';" % target)
  272. except Exception, e:
  273. if e.args[0]==1396:
  274. pass
  275. else:
  276. raise e
  277. def create_database(self,target):
  278. if target in self.get_database_list():
  279. self.drop_database(target)
  280. self.conn.sql("CREATE DATABASE IF NOT EXISTS `%s` ;" % target)
  281. def drop_database(self,target):
  282. try:
  283. self.conn.sql("DROP DATABASE IF EXISTS `%s`;"%target)
  284. except Exception,e:
  285. raise e
  286. def grant_all_privileges(self,target,user):
  287. try:
  288. self.conn.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'localhost';" % (target, user))
  289. except Exception,e:
  290. raise e
  291. def grant_select_privilges(self,db,table,user):
  292. try:
  293. if table:
  294. self.conn.sql("GRANT SELECT ON %s.%s to '%s'@'localhost';" % (db,table,user))
  295. else:
  296. self.conn.sql("GRANT SELECT ON %s.* to '%s'@'localhost';" % (db,user))
  297. except Exception,e:
  298. raise e
  299. def flush_privileges(self):
  300. try:
  301. self.conn.sql("FLUSH PRIVILEGES")
  302. except Exception,e:
  303. raise e
  304. def get_database_list(self):
  305. """get list of databases"""
  306. return [d[0] for d in self.conn.sql("SHOW DATABASES")]
  307. def restore_database(self,target,source,user,password):
  308. from webnotes.utils import make_esc
  309. esc = make_esc('$ ')
  310. try:
  311. ret = os.system("mysql -u %s -p%s %s < %s" % \
  312. (esc(user), esc(password), esc(target), source))
  313. except Exception,e:
  314. raise e
  315. def drop_table(self,table_name):
  316. """drop table if exists"""
  317. if not table_name in self.get_tables_list():
  318. return
  319. try:
  320. self.conn.sql("DROP TABLE IF EXISTS %s "%(table_name))
  321. except Exception,e:
  322. raise e
  323. def set_transaction_isolation_level(self,scope='SESSION',level='READ COMMITTED'):
  324. #Sets the transaction isolation level. scope = global/session
  325. try:
  326. self.conn.sql("SET %s TRANSACTION ISOLATION LEVEL %s"%(scope,level))
  327. except Exception,e:
  328. raise e
  329. def validate_column_name(n):
  330. n = n.replace(' ','_').strip().lower()
  331. import re
  332. if re.search("[\W]", n):
  333. webnotes.msgprint('err:%s is not a valid fieldname.<br>A valid name must contain letters / numbers / spaces.<br><b>Tip: </b>You can change the Label after the fieldname has been set' % n)
  334. raise Exception
  335. return n
  336. def updatedb(dt):
  337. """
  338. Syncs a `DocType` to the table
  339. * creates if required
  340. * updates columns
  341. * updates indices
  342. """
  343. res = webnotes.conn.sql("select ifnull(issingle, 0) from tabDocType where name=%s", dt)
  344. if not res:
  345. raise Exception, 'Wrong doctype "%s" in updatedb' % dt
  346. if not res[0][0]:
  347. webnotes.conn.commit()
  348. tab = DbTable(dt, 'tab')
  349. tab.sync()
  350. webnotes.conn.begin()
  351. def remove_all_foreign_keys():
  352. webnotes.conn.sql("set foreign_key_checks = 0")
  353. webnotes.conn.commit()
  354. for t in webnotes.conn.sql("select name from tabDocType where ifnull(issingle,0)=0"):
  355. dbtab = webnotes.model.db_schema.DbTable(t[0])
  356. try:
  357. fklist = dbtab.get_foreign_keys()
  358. except Exception, e:
  359. if e.args[0]==1146:
  360. fklist = []
  361. else:
  362. raise e
  363. for f in fklist:
  364. webnotes.conn.sql("alter table `tab%s` drop foreign key `%s`" % (t[0], f[1]))