説明なし
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

migrationhandler.py 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. # -*- coding: utf-8 -*-
  2. import copy
  3. import pymysql
  4. from Lodel.settings import Settings
  5. import EditorialModel
  6. import EditorialModel.classtypes
  7. import EditorialModel.fieldtypes
  8. import EditorialModel.fieldtypes.generic
  9. from EditorialModel.fieldtypes.generic import MultiValueFieldType
  10. from DataSource.MySQL import fieldtypes as fieldtypes_utils
  11. from DataSource.MySQL import utils
  12. import DataSource.dummy
  13. # The global MH algorithm is as follow :
  14. # A create_table(table_name, pk_name, pk_opt) method that create a table
  15. # with one pk field
  16. # An add_column(table_name, field_name, field_opt) method that add a column to a table
  17. #
  18. # The create_default_table method will call both methods to create the object and relation tables
  19. #
  20. # Supported operations :
  21. # - EmClass creation
  22. # - EmClass deletion
  23. # - EmField creation
  24. # - EmField deletion
  25. # - rel2type attribute creation
  26. # - rel2type attribute deletion
  27. #
  28. # Unsupported operations :
  29. # - EmClass rename
  30. # - EmField rename
  31. # - rel2type field rename
  32. # - rel2type attribute rename
  33. # - EmFieldType changes
  34. #
  35. # @todo Unified datasources and migration handlers via utils functions
  36. ## @brief Modify a MySQL database given editorial model changes
  37. class MigrationHandler(DataSource.dummy.migrationhandler.MigrationHandler):
  38. ## @brief Construct a MysqlMigrationHandler
  39. # @param module : sql module
  40. # @param conn_args dict : A dict containing connection options
  41. # @param db_engine str : Name of a MySQL db engine (default is InnoDB, don't change it ! ;) )
  42. # @param **kwargs : arguement given to the module.connect() method
  43. def __init__(self, module=pymysql, conn_args=None, db_engine='InnoDB'):
  44. # Database connection
  45. self._dbmodule = module
  46. if conn_args is None:
  47. conn_args = copy.copy(Settings.datasource_options['default'])
  48. self._dbmodule = conn_args['module']
  49. del conn_args['module']
  50. self.db_conn = self._dbmodule.connect(**conn_args)
  51. # Fetch options
  52. mh_settings = Settings.migrationhandler_options
  53. defaults = {
  54. 'debug': False,
  55. 'dryrun': False,
  56. 'foreign_keys': True,
  57. 'drop_if_exists': False
  58. }
  59. for opt_name in defaults:
  60. if opt_name in mh_settings:
  61. opt_val = mh_settings[opt_name]
  62. else:
  63. opt_val = defaults[opt_name]
  64. setattr(self, opt_name, opt_val)
  65. self.db_engine = db_engine
  66. #Create default tables
  67. self._create_default_tables(self.drop_if_exists)
  68. ## @brief Modify the db given an EM change
  69. #
  70. # @note Here we don't care about the relation parameter of _add_column() method because the
  71. # only case in wich we want to add a field that is linked with the relation table is for rel2type
  72. # attr creation. The relation parameter is set to True in the add_relationnal_field() method
  73. # @param em model : The EditorialModel.model object to provide the global context
  74. # @param uid int : The uid of the change EmComponent
  75. # @param initial_state dict | None : dict with field name as key and field value as value. Representing the original state. None mean creation of a new component.
  76. # @param new_state dict | None : dict with field name as key and field value as value. Representing the new state. None mean component deletion
  77. # @param engine str : Mysql db engine, should be "InnoDB"
  78. # @throw EditorialModel.exceptions.MigrationHandlerChangeError if the change was refused
  79. def register_change(self, em, uid, initial_state, new_state, engine=None):
  80. if engine is None:
  81. engine = self.db_engine
  82. if isinstance(em.component(uid), EditorialModel.classes.EmClass):
  83. if initial_state is None:
  84. #EmClass creation
  85. self.create_emclass_table(em, uid, engine)
  86. elif new_state is None:
  87. #EmClass deletion
  88. self.delete_emclass_table(em, uid)
  89. elif isinstance(em.component(uid), EditorialModel.fields.EmField):
  90. emfield = em.component(uid)
  91. if emfield.rel_field_id is None:
  92. #non relationnal field
  93. if initial_state is None:
  94. #non relationnal EmField creation
  95. if emfield.name not in EditorialModel.classtypes.common_fields.keys():
  96. self.add_col_from_emfield(em, uid)
  97. elif new_state is None:
  98. #non relationnal EmField deletion
  99. if emfield.name not in EditorialModel.classtypes.common_fields.keys():
  100. self.delete_col_from_emfield(em, uid)
  101. else:
  102. #relationnal field
  103. if initial_state is None:
  104. #Rel2type attr creation
  105. self.add_relationnal_field(em, uid)
  106. elif new_state is None:
  107. #Rel2type attr deletion
  108. self.del_relationnal_field(em, uid)
  109. ## @brief dumdumdummy
  110. # @note implemented to avoid the log message of EditorialModel.migrationhandler.dummy.DummyMigrationHandler
  111. def register_model_state(self, em, state_hash):
  112. pass
  113. ## @brief Add a relationnal field
  114. # Add a rel2type attribute
  115. # @note this function handles the table creation
  116. # @param edmod Model : EditorialModel.model.Model instance
  117. # @param rfuid int : Relationnal field uid
  118. def add_relationnal_field(self, edmod, rfuid):
  119. emfield = edmod.component(rfuid)
  120. if not isinstance(emfield, EditorialModel.fields.EmField):
  121. raise ValueError("The given uid is not an EmField uid")
  122. r2tf = edmod.component(emfield.rel_field_id)
  123. tname = self._r2t2table_name(edmod, r2tf)
  124. pkname, pkftype = self._relation_pk
  125. #If not exists create a relational table
  126. self._create_table(
  127. tname,
  128. pkname,
  129. pkftype,
  130. self.db_engine,
  131. if_exists='nothing',
  132. noauto_inc = True,
  133. )
  134. #Add a foreign key if wanted
  135. if self.foreign_keys:
  136. self._add_fk(tname, utils.common_tables['relation'], pkname, pkname)
  137. #Add the column
  138. self._add_column(tname, emfield.name, emfield.fieldtype_instance(), relation=True)
  139. #Update table triggers
  140. self._generate_triggers(tname, self._r2type2cols(edmod, r2tf))
  141. ## @brief Delete a rel2type attribute
  142. #
  143. # Delete a rel2type attribute
  144. # @note this method handles the table deletion
  145. # @param edmod Model : EditorialModel.model.Model instance
  146. # @param rfuid int : Relationnal field uid
  147. def del_relationnal_field(self, edmod, rfuid):
  148. emfield = edmod.component(rfuid)
  149. if not isinstance(emfield, EditorialModel.fields.EmField):
  150. raise ValueError("The given uid is not an EmField uid")
  151. r2tf = edmod.component(emfield.rel_field_id)
  152. tname = self._r2t2table_name(edmod, r2tf)
  153. if len(self._r2type2cols(edmod, r2tf)) == 1:
  154. #The table can be deleted (no more attribute for this rel2type)
  155. self._query("""DROP TABLE {table_name}""".format(table_name=tname))
  156. else:
  157. self._del_column(tname, emfield.name)
  158. #Update table triggers
  159. self._generate_triggers(tname, self._r2type2cols(edmod, r2tf))
  160. ## @brief Given an EmField uid add a column to the corresponding table
  161. # @param edmod Model : A Model instance
  162. # @param uid int : An EmField uid
  163. def add_col_from_emfield(self, edmod, uid):
  164. emfield = edmod.component(uid)
  165. if not isinstance(emfield, EditorialModel.fields.EmField):
  166. raise ValueError("The given uid is not an EmField uid")
  167. emclass = emfield.em_class
  168. tname = utils.object_table_name(emclass.name)
  169. self._add_column(tname, emfield.name, emfield.fieldtype_instance())
  170. # Refresh the table triggers
  171. cols_l = self._class2cols(emclass)
  172. self._generate_triggers(tname, cols_l)
  173. ## @brief Given a class uid create the coressponding table
  174. # @param edmod Model : A Model instance
  175. # @param uid int : An EmField uid
  176. # @param engine str : Db engine (should be "InnoDB")
  177. def create_emclass_table(self, edmod, uid, engine):
  178. emclass = edmod.component(uid)
  179. if not isinstance(emclass, EditorialModel.classes.EmClass):
  180. raise ValueError("The given uid is not an EmClass uid")
  181. pkname, pktype = self._object_pk
  182. table_name = utils.object_table_name(emclass.name)
  183. self._create_table(
  184. table_name,
  185. pkname,
  186. pktype,
  187. engine=engine,
  188. noauto_inc = True
  189. )
  190. if self.foreign_keys:
  191. self._add_fk(table_name, utils.common_tables['object'], pkname, pkname)
  192. ## @brief Given an EmClass uid delete the corresponding table
  193. # @param edmod Model : A Model instance
  194. # @param uid int : An EmField uid
  195. def delete_emclass_table(self, edmod, uid):
  196. emclass = edmod.component(uid)
  197. if not isinstance(emclass, EditorialModel.classes.EmClass):
  198. raise ValueError("The give uid is not an EmClass uid")
  199. tname = utils.object_table_name(emclass.name)
  200. # Delete the table triggers to prevent errors
  201. self._generate_triggers(tname, dict())
  202. tname = utils.escape_idname(tname)
  203. self._query("""DROP TABLE {table_name};""".format(table_name=tname))
  204. ## @brief Given an EmField delete the corresponding column
  205. # @param edmod Model : an @ref EditorialModel.model.Model instance
  206. # @param uid int : an EmField uid
  207. def delete_col_from_emfield(self, edmod, uid):
  208. emfield = edmod.component(uid)
  209. if not isinstance(emfield, EditorialModel.fields.EmField):
  210. raise ValueError("The given uid is not an EmField uid")
  211. if isinstance(emfield.fieldtype_instance(), MultiValueFieldType):
  212. return self._del_column_multivalue(emfield)
  213. emclass = emfield.em_class
  214. tname = utils.object_table_name(emclass.name)
  215. # Delete the table triggers to prevent errors
  216. self._generate_triggers(tname, dict())
  217. self._del_column(tname, emfield.name)
  218. # Refresh the table triggers
  219. cols_l = self._class2cols(emclass)
  220. self._generate_triggers(tname, cols_l)
  221. ## @brief Delete a column from a table
  222. # @param tname str : The table name
  223. # @param fname str : The column name
  224. def _del_column(self, tname, fname):
  225. tname = utils.escape_idname(tname)
  226. fname = utils.escape_idname(fname)
  227. self._query("""ALTER TABLE {table_name} DROP COLUMN {col_name};""".format(table_name=tname, col_name=fname))
  228. ## @brief Construct a table name given a rela2type EmField instance
  229. # @param edmod Model : A Model instance
  230. # @param emfield EmField : An EmField instance
  231. # @return a table name
  232. def _r2t2table_name(self, edmod, emfield):
  233. emclass = emfield.em_class
  234. emtype = edmod.component(emfield.rel_to_type_id)
  235. return utils.r2t_table_name(emclass.name, emtype.name)
  236. ## @brief Generate a columns_fieldtype dict given a rel2type EmField
  237. # @param edmod Model : an @ref EditorialModel.model.Model instance
  238. # @param emfield EmField : and @ref EditorialModel.fields.EmField instance
  239. def _r2type2cols(self, edmod, emfield):
  240. return {f.name: f.fieldtype_instance() for f in edmod.components('EmField') if f.rel_field_id == emfield.uid}
  241. ## @brief Generate a columns_fieldtype dict given an EmClass
  242. # @param emclass EmClass : An EmClass instance
  243. # @return A dict with column name as key and EmFieldType instance as value
  244. def _class2cols(self, emclass):
  245. if not isinstance(emclass, EditorialModel.classes.EmClass):
  246. raise ValueError("The given uid is not an EmClass uid")
  247. return {f.name: f.fieldtype_instance() for f in emclass.fields() if f.name not in EditorialModel.classtypes.common_fields.keys()}
  248. ## @brief Create object and relations tables
  249. # @param drop_if_exist bool : If true drop tables if exists
  250. def _create_default_tables(self, drop_if_exist=False):
  251. if_exists = 'drop' if drop_if_exist else 'nothing'
  252. #Object table
  253. tname = utils.common_tables['object']
  254. pk_name, pk_ftype = self._object_pk
  255. self._create_table(tname, pk_name, pk_ftype, engine=self.db_engine, if_exists=if_exists)
  256. #Adding columns
  257. cols = {fname: self._common_field_to_ftype(fname) for fname in EditorialModel.classtypes.common_fields}
  258. for fname, ftype in cols.items():
  259. if fname != pk_name:
  260. self._add_column(tname, fname, ftype, relation=False)
  261. #Creating triggers
  262. self._generate_triggers(tname, cols)
  263. object_tname = tname
  264. #Relation table
  265. tname = utils.common_tables['relation']
  266. pk_name, pk_ftype = self._relation_pk
  267. self._create_table(tname, pk_name, pk_ftype, engine=self.db_engine, if_exists=if_exists)
  268. #Adding columns
  269. for fname, ftype in self._relation_cols.items():
  270. self._add_column(tname, fname, ftype, relation=True)
  271. #Creating triggers
  272. self._generate_triggers(tname, self._relation_cols)
  273. # Creating foreign keys between relation and object table
  274. sup_cname, sub_cname = self.get_sup_and_sub_cols()
  275. ## @brief Returns the fieldname for superior and subordinate in relation table
  276. # @return a tuple (superior_name, subordinate_name)
  277. @classmethod
  278. def get_sup_and_sub_cols(cls):
  279. sup = None
  280. sub = None
  281. for fname, finfo in EditorialModel.classtypes.relations_common_fields.items():
  282. if finfo['fieldtype'] == 'leo':
  283. if finfo['superior']:
  284. sup = fname
  285. else:
  286. sub = fname
  287. return utils.column_name(sup), utils.column_name(sub)
  288. ## @brief Create a table with primary key
  289. # @param table_name str : table name
  290. # @param pk_name str | tuple : pk column name (give tuple for multi pk)
  291. # @param pk_ftype fieldtype | tuple : pk fieldtype (give a tuple for multi pk)
  292. # @param engine str : The engine to use with this table
  293. # @param charset str : The charset of this table
  294. # @param if_exists str : takes values in ['nothing', 'drop']
  295. # @param noauto_inc bool : if True forbids autoincrement on PK
  296. def _create_table(self, table_name, pk_name, pk_ftype, engine, charset='utf8', if_exists='nothing', noauto_inc = False):
  297. #Escaped table name
  298. etname = utils.escape_idname(table_name)
  299. if not isinstance(pk_name, tuple):
  300. pk_name = tuple([pk_name])
  301. pk_ftype = tuple([pk_ftype])
  302. if len(pk_name) != len(pk_ftype):
  303. raise ValueError("You have to give as many pk_name as pk_ftype")
  304. pk_instr_cols = ''
  305. pk_format = "{pk_name} {pk_type} {pk_specs},\n"
  306. for i in range(len(pk_name)):
  307. instr_type, pk_type, pk_specs = fieldtypes_utils.fieldtype_db_init(pk_ftype[i], noauto_inc)
  308. if instr_type != 'column':
  309. raise ValueError("Migration handler doesn't support MultiValueFieldType as primary keys")
  310. pk_instr_cols += pk_format.format(
  311. pk_name = utils.escape_idname(pk_name[i]),
  312. pk_type = pk_type,
  313. pk_specs = pk_specs
  314. )
  315. pk_instr_cols += "PRIMARY KEY("+(','.join([utils.escape_idname(pkn) for pkn in pk_name]))+')'
  316. if if_exists == 'drop':
  317. self._query("""DROP TABLE IF EXISTS {table_name};""".format(table_name=etname))
  318. qres = """CREATE TABLE IF NOT EXISTS {table_name} (
  319. {pk_cols}
  320. ) ENGINE={engine} DEFAULT CHARSET={charset};""".format(
  321. table_name = table_name,
  322. pk_cols = pk_instr_cols,
  323. engine = engine,
  324. charset = charset
  325. )
  326. self._query(qres)
  327. ## @brief Add a column to a table
  328. # @param table_name str : The table name
  329. # @param col_name str : The columns name
  330. # @param col_fieldtype EmFieldype the fieldtype
  331. # @param relation bool | None : a flag to indicate if we add a column in a table linked with an bject or with a relation (used only when the column is MultiValueFieldType )
  332. # @param drop_if_exists bool : if True delete the column before re-adding it
  333. # @return True if the column was added else return False
  334. def _add_column(self, table_name, col_name, col_fieldtype, drop_if_exists=False, relation=False):
  335. instr, col_type, col_specs = fieldtypes_utils.fieldtype_db_init(col_fieldtype)
  336. if instr == 'table':
  337. # multivalue field. We are not going to add a column in this table
  338. # but in corresponding multivalue table
  339. self._add_column_multivalue(
  340. ref_table_name = table_name,
  341. key_infos = col_type,
  342. column_infos = (col_name, col_specs),
  343. relation = relation
  344. )
  345. return True
  346. col_name = utils.column_name(col_name)
  347. add_col = """ALTER TABLE {table_name}
  348. ADD COLUMN {col_name} {col_type} {col_specs};"""
  349. etname = utils.escape_idname(table_name)
  350. ecname = utils.escape_idname(col_name)
  351. if instr is None:
  352. return True
  353. if instr != "column":
  354. raise RuntimeError("Bad implementation")
  355. add_col = add_col.format(
  356. table_name=etname,
  357. col_name=ecname,
  358. col_type=col_type,
  359. col_specs=col_specs,
  360. )
  361. try:
  362. self._query(add_col)
  363. except self._dbmodule.err.InternalError:
  364. if drop_if_exists:
  365. self._del_column(table_name, col_name)
  366. self._add_column(table_name, col_name, col_fieldtype, drop_if_exists)
  367. else:
  368. #LOG
  369. print("Aborded, column `%s` exists" % col_name)
  370. return False
  371. if isinstance(col_fieldtype, EditorialModel.fieldtypes.generic.ReferenceFieldType):
  372. # We have to create a FK !
  373. if col_fieldtype.reference == 'object':
  374. dst_table_name = utils.common_tables['object']
  375. dst_col_name, _ = self._object_pk
  376. elif col_fieldtypes.reference == 'relation':
  377. dst_table_name = utils.common_tables['relation']
  378. dst_col_name, _ = self._relation_pk
  379. fk_name = 'fk_%s-%s_%s-%s' % (
  380. table_name,
  381. col_name,
  382. dst_table_name,
  383. dst_col_name,
  384. )
  385. self._add_fk(
  386. src_table_name = table_name,
  387. dst_table_name = dst_table_name,
  388. src_col_name = col_name,
  389. dst_col_name = dst_col_name,
  390. fk_name = fk_name
  391. )
  392. return True
  393. ## @brief Add a column to a multivalue table
  394. #
  395. # Add a column (and create a table if not existing) for storing multivalue
  396. # datas. (typically i18n)
  397. # @param ref_table_name str : Referenced table name
  398. # @param key_infos tuple : tuple(key_name, key_fieldtype)
  399. # @param column_infos tuple : tuple(col_name, col_fieldtype)
  400. # @param relation bool : pass True if concern a LeRelation or False of concern a LeObject
  401. def _add_column_multivalue(self, ref_table_name, key_infos, column_infos, relation):
  402. key_name, key_ftype = key_infos
  403. col_name, col_ftype = column_infos
  404. table_name = utils.multivalue_table_name(ref_table_name, key_name)
  405. if relation:
  406. pk_infos = self._relation_pk
  407. else:
  408. pk_infos = self._object_pk
  409. # table creation
  410. self._create_table(
  411. table_name = table_name,
  412. pk_name = (key_name, pk_infos[0]),
  413. pk_ftype = (key_ftype, pk_infos[1]),
  414. engine = self.db_engine,
  415. if_exists = 'nothing',
  416. noauto_inc = True
  417. )
  418. # with FK
  419. self._add_fk(table_name, ref_table_name, pk_infos[0], pk_infos[0])
  420. # adding the column
  421. self._add_column(table_name, col_name, col_ftype)
  422. ## @brief Delete a multivalue column
  423. # @param emfield EmField : EmField instance
  424. # @note untested
  425. def _del_column_multivalue(self, emfield):
  426. ftype = emfield.fieldtype_instance()
  427. if not isinstance(ftype, MultiValueFieldType):
  428. raise ValueError("Except an emfield with multivalue fieldtype")
  429. tname = utils.object_table_name(emfield.em_class.name)
  430. tname = utils.multivalue_table_name(tname, ftype.keyname)
  431. self._del_column(tname, emfield.name)
  432. if len([ f for f in emfield.em_class.fields() if isinstance(f.fieldtype_instance(), MultiValueFieldType)]) == 0:
  433. try:
  434. self._query("DROP TABLE %s;" % utils.escape_idname(tname))
  435. except self._dbmodule.err.InternalError as expt:
  436. print(expt)
  437. ## @brief Add a foreign key
  438. # @param src_table_name str : The name of the table where we will add the FK
  439. # @param dst_table_name str : The name of the table the FK will point on
  440. # @param src_col_name str : The name of the concerned column in the src_table
  441. # @param dst_col_name str : The name of the concerned column in the dst_table
  442. # @param fk_name str|None : The foreign key name, if None the name will be generated (not a good idea)
  443. def _add_fk(self, src_table_name, dst_table_name, src_col_name, dst_col_name, fk_name=None):
  444. stname = utils.escape_idname(src_table_name)
  445. dtname = utils.escape_idname(dst_table_name)
  446. scname = utils.escape_idname(src_col_name)
  447. dcname = utils.escape_idname(dst_col_name)
  448. if fk_name is None:
  449. fk_name = utils.get_fk_name(src_table_name, dst_table_name)
  450. self._del_fk(src_table_name, dst_table_name, fk_name)
  451. self._query("""ALTER TABLE {src_table}
  452. ADD CONSTRAINT {fk_name}
  453. FOREIGN KEY ({src_col}) references {dst_table}({dst_col})
  454. ON DELETE CASCADE
  455. ON UPDATE CASCADE;""".format(
  456. fk_name=utils.escape_idname(fk_name),
  457. src_table=stname,
  458. src_col=scname,
  459. dst_table=dtname,
  460. dst_col=dcname
  461. ))
  462. ## @brief Given a source and a destination table, delete the corresponding FK
  463. # @param src_table_name str : The name of the table where the FK is
  464. # @param dst_table_name str : The name of the table the FK point on
  465. # @param fk_name str|None : the foreign key name, if None try to guess it
  466. # @warning fails silently
  467. def _del_fk(self, src_table_name, dst_table_name, fk_name=None):
  468. if fk_name is None:
  469. fk_name = utils.get_fk_name(src_table_name, dst_table_name)
  470. fk_name = utils.escape_idname(fk_name)
  471. try:
  472. self._query("""ALTER TABLE {src_table}
  473. DROP FOREIGN KEY {fk_name}""".format(
  474. src_table=utils.escape_idname(src_table_name),
  475. fk_name=fk_name
  476. ))
  477. except self._dbmodule.err.InternalError:
  478. # If the FK don't exists we do not care
  479. pass
  480. ## @brief Generate triggers given a table_name and its columns fieldtypes
  481. # @param table_name str : Table name
  482. # @param cols_ftype dict : with col name as key and column fieldtype as value
  483. def _generate_triggers(self, table_name, cols_ftype):
  484. colval_l_upd = dict() # param for update trigger
  485. colval_l_ins = dict() # param for insert trigger
  486. for cname, cftype in cols_ftype.items():
  487. if isinstance(cftype, EditorialModel.fieldtypes.datetime.EmFieldType):
  488. if cftype.now_on_update:
  489. colval_l_upd[cname] = 'NOW()'
  490. if cftype.now_on_create:
  491. colval_l_ins[cname] = 'NOW()'
  492. self._table_trigger(table_name, 'UPDATE', colval_l_upd)
  493. self._table_trigger(table_name, 'INSERT', colval_l_ins)
  494. ## @brief Create trigger for a table
  495. #
  496. # Primarly designed to create trigger for DATETIME types
  497. # The method generates triggers of the form
  498. # <pre>
  499. # CREATE TRIGGER BEFORE &lt;moment&gt; ON &lt;table_name&gt;
  500. # FOR EACH ROW SET lt;for colname, colval in &lt;cols_val&gt;
  501. # NEW.lt;colname> = lt;colval&gt;,
  502. # lt;endfor&gt;;
  503. # </pre>
  504. # @param table_name str : The table name
  505. # @param moment str : can be 'update' or 'insert'
  506. # @param cols_val dict : Dict with column name as key and column value as value
  507. def _table_trigger(self, table_name, moment, cols_val):
  508. trigger_name = utils.escape_idname("%s_%s_trig" % (table_name, moment))
  509. #Try to delete the trigger
  510. drop_trig = """DROP TRIGGER IF EXISTS {trigger_name};""".format(trigger_name=trigger_name)
  511. self._query(drop_trig)
  512. col_val_l = ', '.join(["NEW.%s = %s" % (utils.escape_idname(utils.column_name(cname)), cval)for cname, cval in cols_val.items()])
  513. #Create a trigger if needed
  514. if len(col_val_l) > 0:
  515. trig_q = """CREATE TRIGGER {trigger_name} BEFORE {moment} ON {table_name}
  516. FOR EACH ROW SET {col_val_list};""".format(
  517. trigger_name=trigger_name,
  518. table_name=utils.escape_idname(table_name),
  519. moment=moment, col_val_list=col_val_l
  520. )
  521. self._query(trig_q)
  522. ## @brief Delete all table created by the MH
  523. # @param model Model : the Editorial model
  524. def __purge_db(self, model):
  525. for uid in [
  526. field
  527. for field in model.components('EmField')
  528. if isinstance(field.fieldtype_instance(), MultiValueFieldType)
  529. ]:
  530. self._del_column_multivalue(field)
  531. for uid in [c.uid for c in model.components('EmClass')]:
  532. try:
  533. self.delete_emclass_table(model, uid)
  534. except self._dbmodule.err.InternalError as expt:
  535. print(expt)
  536. for tname in [utils.r2t_table_name(f.em_class.name, model.component(f.rel_to_type_id).name) for f in model.components('EmField') if f.fieldtype == 'rel2type']:
  537. try:
  538. self._query("DROP TABLE %s;" % tname)
  539. except self._dbmodule.err.InternalError as expt:
  540. print(expt)
  541. for tname in [utils.common_tables['relation'], utils.common_tables['relation']]:
  542. try:
  543. self._query("DROP TABLE %s;" % tname)
  544. except self._dbmodule.err.InternalError as expt:
  545. print(expt)
  546. ## @brief Return primary key name & fieldtype for relation or object
  547. @classmethod
  548. def extract_pk(cls, common_fields):
  549. for fname, finfo in common_fields.items():
  550. if finfo['fieldtype'] == 'pk':
  551. fto = EditorialModel.fieldtypes.generic.GenericFieldType.from_name('pk')
  552. finfo_cp = copy.copy(finfo)
  553. del(finfo_cp['fieldtype'])
  554. return (utils.column_name(fname), fto(**finfo_cp))
  555. raise RuntimeError("No primary key found in common fields : %s" % common_fields)
  556. ## @brief Exec a query
  557. # @param query str : SQL query
  558. def _query(self, query):
  559. if self.debug:
  560. print(query + "\n")
  561. if not self.dryrun:
  562. with self.db_conn.cursor() as cur:
  563. cur.execute(query)
  564. self.db_conn.commit() # autocommit
  565. ## @brief Given a common field name return an EmFieldType instance
  566. # @param cls
  567. # @param cname str : Common field name
  568. # @return An EmFieldType instance
  569. @classmethod
  570. def _common_field_to_ftype(cls, cname):
  571. fta = copy.copy(EditorialModel.classtypes.common_fields[cname])
  572. fto = EditorialModel.fieldtypes.generic.GenericFieldType.from_name(fta['fieldtype'])
  573. del fta['fieldtype']
  574. return fto(**fta)
  575. ## @brief Returns a tuple (pkname, pk_ftype)
  576. @property
  577. def _object_pk(self):
  578. return self.extract_pk(EditorialModel.classtypes.common_fields)
  579. ## @brief Returns a tuple (rel_pkname, rel_ftype)
  580. @property
  581. def _relation_pk(self):
  582. return self.extract_pk(EditorialModel.classtypes.relations_common_fields)
  583. ## @brief Returns a dict { colname:fieldtype } of relation table columns
  584. @property
  585. def _relation_cols(self):
  586. res = dict()
  587. for fieldname, fieldinfo in EditorialModel.classtypes.relations_common_fields.items():
  588. finfo = copy.copy(fieldinfo)
  589. fieldtype_name = finfo['fieldtype']
  590. del(finfo['fieldtype'])
  591. res[fieldname] = EditorialModel.fieldtypes.generic.GenericFieldType.from_name(fieldtype_name)(**finfo)
  592. return res