No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

migrationhandler.py 28KB

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