|
@@ -1,156 +1,547 @@
|
1
|
1
|
# -*- coding: utf-8 -*-
|
2
|
2
|
|
3
|
|
-## @package EditorialModel.migrationhandler.sql
|
4
|
|
-# @brief A dummy migration handler
|
5
|
|
-#
|
6
|
|
-# According to it every modifications are possible
|
7
|
|
-#
|
|
3
|
+import copy
|
|
4
|
+import pymysql
|
8
|
5
|
|
9
|
6
|
import EditorialModel
|
|
7
|
+from DataSource.MySQL.common_utils import MySQL
|
10
|
8
|
from DataSource.dummy.migrationhandler import DummyMigrationHandler
|
11
|
|
-from EditorialModel.fieldtypes.generic import GenericFieldType
|
12
|
|
-from EditorialModel.model import Model
|
13
|
|
-from mosql.db import Database
|
14
|
|
-from Lodel.utils.mosql import create, alter_add
|
|
9
|
+
|
|
10
|
+# The global MH algorithm is as follow :
|
|
11
|
+# A create_table(table_name, pk_name, pk_opt) method that create a table
|
|
12
|
+# with one pk field
|
|
13
|
+# An add_column(table_name, field_name, field_opt) method that add a column to a table
|
|
14
|
+#
|
|
15
|
+# The create_default_table method will call both methods to create the object and relation tables
|
|
16
|
+#
|
|
17
|
+# Supported operations :
|
|
18
|
+# - EmClass creation
|
|
19
|
+# - EmClass deletion (untested)
|
|
20
|
+# - EmField creation
|
|
21
|
+# - EmField deletion (untested)
|
|
22
|
+# - rel2type attribute creation
|
|
23
|
+# - rel2type attribute deletion (unstested)
|
|
24
|
+#
|
|
25
|
+# Unsupported operations :
|
|
26
|
+# - EmClass rename
|
|
27
|
+# - EmField rename
|
|
28
|
+# - rel2type field rename
|
|
29
|
+# - rel2type attribute rename
|
|
30
|
+# - EmFieldType changes
|
|
31
|
+#
|
|
32
|
+# @todo Unified datasources and migration handlers via utils functions
|
15
|
33
|
|
16
|
34
|
|
17
|
|
-## Manage Model changes
|
18
|
|
-class SQLMigrationHandler(DummyMigrationHandler):
|
|
35
|
+## @brief Modify a MySQL database given editorial model changes
|
|
36
|
+class MysqlMigrationHandler(DummyMigrationHandler):
|
19
|
37
|
|
20
|
|
- fieldtype_to_sql = {
|
21
|
|
- 'char': "CHAR(255)",
|
22
|
|
- 'integer': 'INT'
|
23
|
|
- }
|
|
38
|
+ ## @brief Construct a MysqlMigrationHandler
|
|
39
|
+ # @param host str : The db host
|
|
40
|
+ # @param user str : The db user
|
|
41
|
+ # @param password str : The db password
|
|
42
|
+ # @param db str : The db name
|
|
43
|
+ def __init__(self, host, user, passwd, db, module=pymysql, db_engine='InnoDB', foreign_keys=True, debug=False, dryrun=False, drop_if_exists=False):
|
|
44
|
+ self.datasource = MySQL
|
|
45
|
+ self._dbmodule = module
|
|
46
|
+ #Connect to MySQL
|
|
47
|
+ self.db = self._dbmodule.connect(host=host, user=user, passwd=passwd, db=db)
|
|
48
|
+ self.debug = debug
|
|
49
|
+ self.dryrun = dryrun
|
|
50
|
+ self.db_engine = db_engine
|
|
51
|
+ self.foreign_keys = foreign_keys if db_engine == 'InnoDB' else False
|
|
52
|
+ self.drop_if_exists = drop_if_exists
|
|
53
|
+ #Create default tables
|
|
54
|
+ self._create_default_tables(self.drop_if_exists)
|
24
|
55
|
|
25
|
|
- def __init__(self, module=None, *conn_args, **conn_kargs):
|
26
|
|
- super(SQLMigrationHandler, self).__init__(False)
|
|
56
|
+ ## @brief Delete all table created by the MH
|
|
57
|
+ # @param model Model : the Editorial model
|
|
58
|
+ def __purge_db(self, model):
|
|
59
|
+ for uid in [c.uid for c in model.components('EmClass')]:
|
|
60
|
+ try:
|
|
61
|
+ self.delete_emclass_table(model, uid)
|
|
62
|
+ except self._dbmodule.err.InternalError as e:
|
|
63
|
+ print(e)
|
27
|
64
|
|
28
|
|
- self.db = Database(module, *conn_args, **conn_kargs)
|
29
|
|
- self._pk_column = (EditorialModel.classtypes.pk_name(), 'INTEGER PRIMARY KEY AUTOINCREMENT')
|
30
|
|
- self._main_table_name = 'object'
|
31
|
|
- self._relation_table_name = 'relation'
|
|
65
|
+ for tname in [MySQL.get_r2t2table_name(f.em_class.name, model.component(f.rel_to_type_id).name) for f in model.components('EmField') if f.fieldtype == 'rel2type']:
|
|
66
|
+ try:
|
|
67
|
+ self._query("DROP TABLE %s;" % tname)
|
|
68
|
+ except self._dbmodule.err.InternalError as e:
|
|
69
|
+ print(e)
|
32
|
70
|
|
33
|
|
- self._install_tables()
|
|
71
|
+ for tname in [MySQL.relations_table_name, MySQL.objects_table_name]:
|
|
72
|
+ try:
|
|
73
|
+ self._query("DROP TABLE %s;" % tname)
|
|
74
|
+ except self._dbmodule.err.InternalError as e:
|
|
75
|
+ print(e)
|
34
|
76
|
|
35
|
|
- ## @brief Record a change in the EditorialModel and indicate wether or not it is possible to make it
|
36
|
|
- # @note The states ( initial_state and new_state ) contains only fields that changes
|
37
|
|
- # @param model model : The EditorialModel.model object to provide the global context
|
|
77
|
+ ## @brief Modify the db given an EM change
|
|
78
|
+ # @param em model : The EditorialModel.model object to provide the global context
|
38
|
79
|
# @param uid int : The uid of the change EmComponent
|
39
|
80
|
# @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.
|
40
|
81
|
# @param new_state dict | None : dict with field name as key and field value as value. Representing the new state. None mean component deletion
|
41
|
82
|
# @throw EditorialModel.exceptions.MigrationHandlerChangeError if the change was refused
|
42
|
|
- def register_change(self, model, uid, initial_state, new_state):
|
43
|
|
- # find type of component change
|
44
|
|
- if initial_state is None:
|
45
|
|
- state_change = 'new'
|
46
|
|
- elif new_state is None:
|
47
|
|
- state_change = 'del'
|
|
83
|
+ def register_change(self, em, uid, initial_state, new_state, engine=None):
|
|
84
|
+ if engine is None:
|
|
85
|
+ engine = self.db_engine
|
|
86
|
+ if isinstance(em.component(uid), EditorialModel.classes.EmClass):
|
|
87
|
+ if initial_state is None:
|
|
88
|
+ #EmClass creation
|
|
89
|
+ self.create_emclass_table(em, uid, engine)
|
|
90
|
+ elif new_state is None:
|
|
91
|
+ #EmClass deletion
|
|
92
|
+ self.delete_emclass_table(em, uid)
|
|
93
|
+ elif isinstance(em.component(uid), EditorialModel.fields.EmField):
|
|
94
|
+ emfield = em.component(uid)
|
|
95
|
+ if emfield.rel_field_id is None:
|
|
96
|
+ #non relationnal field
|
|
97
|
+ if initial_state is None:
|
|
98
|
+ #non relationnal EmField creation
|
|
99
|
+ if emfield.name not in EditorialModel.classtypes.common_fields.keys():
|
|
100
|
+ self.add_col_from_emfield(em, uid)
|
|
101
|
+ elif new_state is None:
|
|
102
|
+ #non relationnal EmField deletion
|
|
103
|
+ if emfield.name not in EditorialModel.classtypes.common_fields.keys():
|
|
104
|
+ self.del_col_from_emfield(em, uid)
|
|
105
|
+ else:
|
|
106
|
+ #relationnal field
|
|
107
|
+ if initial_state is None:
|
|
108
|
+ #Rel2type attr creation
|
|
109
|
+ self.add_relationnal_field(em, uid)
|
|
110
|
+ elif new_state is None:
|
|
111
|
+ #Rel2type attr deletion
|
|
112
|
+ self.del_relationnal_field(em, uid)
|
|
113
|
+
|
|
114
|
+ ## @brief dumdumdummy
|
|
115
|
+ # @note implemented to avoid the log message of EditorialModel.migrationhandler.dummy.DummyMigrationHandler
|
|
116
|
+ def register_model_state(self, em, state_hash):
|
|
117
|
+ pass
|
|
118
|
+
|
|
119
|
+ ## @brief Exec a query
|
|
120
|
+ # @param query str : SQL query
|
|
121
|
+ def _query(self, query):
|
|
122
|
+ if self.debug:
|
|
123
|
+ print(query + "\n")
|
|
124
|
+ if not self.dryrun:
|
|
125
|
+ with self.db.cursor() as cur:
|
|
126
|
+ cur.execute(query)
|
|
127
|
+ self.db.commit() # autocommit
|
|
128
|
+
|
|
129
|
+ ## @brief Add a relationnal field
|
|
130
|
+ # Add a rel2type attribute
|
|
131
|
+ # @note this function handles the table creation
|
|
132
|
+ # @param em Model : EditorialModel.model.Model instance
|
|
133
|
+ # @param rfuid int : Relationnal field uid
|
|
134
|
+ def add_relationnal_field(self, em, rfuid):
|
|
135
|
+ emfield = em.component(rfuid)
|
|
136
|
+ if not isinstance(emfield, EditorialModel.fields.EmField):
|
|
137
|
+ raise ValueError("The given uid is not an EmField uid")
|
|
138
|
+
|
|
139
|
+ r2tf = em.component(emfield.rel_field_id)
|
|
140
|
+ tname = self._r2t2table_name(em, r2tf)
|
|
141
|
+ pkname, pkftype = self._relation_pk
|
|
142
|
+
|
|
143
|
+ #If not exists create a relational table
|
|
144
|
+ self._create_table(tname, pkname, pkftype, self.db_engine, if_exists='nothing')
|
|
145
|
+ #Add a foreign key if wanted
|
|
146
|
+ if self.foreign_keys:
|
|
147
|
+ self._add_fk(tname, self.datasource.relations_table_name, pkname, pkname)
|
|
148
|
+ #Add the column
|
|
149
|
+ self._add_column(tname, emfield.name, emfield.fieldtype_instance())
|
|
150
|
+ #Update table triggers
|
|
151
|
+ self._generate_triggers(tname, self._r2type2cols(em, r2tf))
|
|
152
|
+
|
|
153
|
+ ## @brief Delete a rel2type attribute
|
|
154
|
+ #
|
|
155
|
+ # Delete a rel2type attribute
|
|
156
|
+ # @note this method handles the table deletion
|
|
157
|
+ # @param em Model : EditorialModel.model.Model instance
|
|
158
|
+ # @param rfuid int : Relationnal field uid
|
|
159
|
+ def del_relationnal_field(self, em, rfuid):
|
|
160
|
+ emfield = em.component(rfuid)
|
|
161
|
+ if not isinstance(emfield, EditorialModel.fields.EmField):
|
|
162
|
+ raise ValueError("The given uid is not an EmField uid")
|
|
163
|
+
|
|
164
|
+ r2tf = em.component(emfield.rel_field_id)
|
|
165
|
+ tname = self._r2t2table_name(em, r2tf)
|
|
166
|
+
|
|
167
|
+ if len(self._r2type2cols(em, r2tf)) == 1:
|
|
168
|
+ #The table can be deleted (no more attribute for this rel2type)
|
|
169
|
+ self._query("""DROP TABLE {table_name}""".format(table_name=tname))
|
48
|
170
|
else:
|
49
|
|
- state_change = 'upgrade'
|
50
|
|
-
|
51
|
|
- # call method to handle the database change
|
52
|
|
- component_name = Model.name_from_emclass(type(model.component(uid)))
|
53
|
|
- handler_func = component_name.lower() + '_' + state_change
|
54
|
|
- if hasattr(self, handler_func):
|
55
|
|
- getattr(self, handler_func)(model, uid, initial_state, new_state)
|
56
|
|
-
|
57
|
|
- # New Class, a table must be created
|
58
|
|
- def emclass_new(self, model, uid, initial_state, new_state):
|
59
|
|
- class_table_name = self._class_table_name(new_state['name'])
|
60
|
|
- self._query_bd(
|
61
|
|
- create(table=class_table_name, column=[self._pk_column])
|
62
|
|
- )
|
|
171
|
+ self._del_column(tname, emfield.name)
|
|
172
|
+ #Update table triggers
|
|
173
|
+ self._generate_triggers(tname, self._r2type2cols(em, r2tf))
|
63
|
174
|
|
64
|
|
- # New Field, must create a column in Class table or in Class_Type relational attribute table
|
65
|
|
- # @todo common fields creation does not allow to add new common fields. It should
|
66
|
|
- def emfield_new(self, model, uid, initial_state, new_state):
|
67
|
|
-
|
68
|
|
- # field is of type rel2type, create the relational class_type table and return
|
69
|
|
- if new_state['fieldtype'] == 'rel2type':
|
70
|
|
- # find relational_type name, and class name of the field
|
71
|
|
- class_name = self._class_table_name_from_field(model, new_state)
|
72
|
|
- type_name = model.component(new_state['rel_to_type_id']).name
|
73
|
|
- table_name = self._relational_table_name(class_name, type_name)
|
74
|
|
- self._query_bd(
|
75
|
|
- create(table=table_name, column=[self._pk_column]),
|
76
|
|
- )
|
77
|
|
- return
|
78
|
|
-
|
79
|
|
- # Column creation
|
80
|
|
- #
|
81
|
|
- # field is internal, create a column in the objects table
|
82
|
|
- if new_state['internal']:
|
83
|
|
- if new_state['fieldtype'] == 'pk': # this column has already beeen created by self._install_tables()
|
84
|
|
- return
|
85
|
|
- if new_state['name'] in EditorialModel.classtypes.common_fields: # this column has already beeen created by self._install_tables()
|
86
|
|
- return
|
87
|
|
-
|
88
|
|
- # field is relational (rel_field_id), create a column in the class_type table
|
89
|
|
- elif new_state['rel_field_id']:
|
90
|
|
- class_name = self._class_table_name_from_field(model, new_state)
|
91
|
|
- rel_type_id = model.component(new_state['rel_field_id']).rel_to_type_id
|
92
|
|
- type_name = model.component(rel_type_id).name
|
93
|
|
- table_name = self._relational_table_name(class_name, type_name)
|
94
|
|
-
|
95
|
|
- # else create a column in the class table
|
|
175
|
+ ## @brief Given an EmField uid add a column to the corresponding table
|
|
176
|
+ # @param em Model : A Model instance
|
|
177
|
+ # @param uid int : An EmField uid
|
|
178
|
+ def add_col_from_emfield(self, em, uid):
|
|
179
|
+ emfield = em.component(uid)
|
|
180
|
+ if not isinstance(emfield, EditorialModel.fields.EmField):
|
|
181
|
+ raise ValueError("The given uid is not an EmField uid")
|
|
182
|
+
|
|
183
|
+ emclass = emfield.em_class
|
|
184
|
+ tname = self._emclass2table_name(emclass)
|
|
185
|
+ self._add_column(tname, emfield.name, emfield.fieldtype_instance())
|
|
186
|
+ # Refresh the table triggers
|
|
187
|
+ cols_l = self._class2cols(emclass)
|
|
188
|
+ self._generate_triggers(tname, cols_l)
|
|
189
|
+
|
|
190
|
+ ## @brief Given a class uid create the coressponding table
|
|
191
|
+ # @param em Model : A Model instance
|
|
192
|
+ # @param uid int : An EmField uid
|
|
193
|
+ def create_emclass_table(self, em, uid, engine):
|
|
194
|
+ emclass = em.component(uid)
|
|
195
|
+ if not isinstance(emclass, EditorialModel.classes.EmClass):
|
|
196
|
+ raise ValueError("The given uid is not an EmClass uid")
|
|
197
|
+ pkname, pktype = self._common_field_pk
|
|
198
|
+ table_name = self._emclass2table_name(emclass)
|
|
199
|
+ self._create_table(table_name, pkname, pktype, engine=engine)
|
|
200
|
+
|
|
201
|
+ if self.foreign_keys:
|
|
202
|
+ self._add_fk(table_name, self.datasource.objects_table_name, pkname, pkname)
|
|
203
|
+
|
|
204
|
+ ## @brief Given an EmClass uid delete the corresponding table
|
|
205
|
+ # @param em Model : A Model instance
|
|
206
|
+ # @param uid int : An EmField uid
|
|
207
|
+ def delete_emclass_table(self, em, uid):
|
|
208
|
+ emclass = em.component(uid)
|
|
209
|
+ if not isinstance(emclass, EditorialModel.classes.EmClass):
|
|
210
|
+ raise ValueError("The give uid is not an EmClass uid")
|
|
211
|
+ tname = self._emclass2table_name(emclass)
|
|
212
|
+ # Delete the table triggers to prevent errors
|
|
213
|
+ self._generate_triggers(tname, dict())
|
|
214
|
+
|
|
215
|
+ tname = self.datasource.escape_idname(tname)
|
|
216
|
+
|
|
217
|
+ self._query("""DROP TABLE {table_name};""".format(table_name=tname))
|
|
218
|
+
|
|
219
|
+ ## @brief Given an EmField delete the corresponding column
|
|
220
|
+ # @param em Model : an @ref EditorialModel.model.Model instance
|
|
221
|
+ # @param uid int : an EmField uid
|
|
222
|
+ def delete_col_from_emfield(self, em, uid):
|
|
223
|
+ emfield = em.component(uid)
|
|
224
|
+ if not isinstance(emfield, EditorialModel.fields.EmField):
|
|
225
|
+ raise ValueError("The given uid is not an EmField uid")
|
|
226
|
+
|
|
227
|
+ emclass = emfield.em_class
|
|
228
|
+ tname = self._emclass2table_name(emclass)
|
|
229
|
+ # Delete the table triggers to prevent errors
|
|
230
|
+ self._generate_triggers(tname, dict())
|
|
231
|
+
|
|
232
|
+ self._del_column(tname, emfield.name)
|
|
233
|
+ # Refresh the table triggers
|
|
234
|
+ cols_ls = self._class2cols(emclass)
|
|
235
|
+ self._generate_triggers(tname, cols_l)
|
|
236
|
+
|
|
237
|
+ ## @brief Delete a column from a table
|
|
238
|
+ # @param tname str : The table name
|
|
239
|
+ # @param fname str : The column name
|
|
240
|
+ def _del_column(self, tname, fname):
|
|
241
|
+ tname = self.datasource.escape_idname(tname)
|
|
242
|
+ fname = self.datasource.escape_idname(fname)
|
|
243
|
+
|
|
244
|
+ self._query("""ALTER TABLE {table_name} DROP COLUMN {col_name};""".format(table_name=tname, col_name=fname))
|
|
245
|
+
|
|
246
|
+ ## @brief Construct a table name given an EmClass instance
|
|
247
|
+ # @param emclass EmClass : An EmClass instance
|
|
248
|
+ # @return a table name
|
|
249
|
+ def _emclass2table_name(self, emclass):
|
|
250
|
+ return self.datasource.get_table_name_from_class(emclass.name)
|
|
251
|
+ #return "class_%s"%emclass.name
|
|
252
|
+
|
|
253
|
+ ## @brief Construct a table name given a rela2type EmField instance
|
|
254
|
+ # @param em Model : A Model instance
|
|
255
|
+ # @param emfield EmField : An EmField instance
|
|
256
|
+ # @return a table name
|
|
257
|
+ def _r2t2table_name(self, em, emfield):
|
|
258
|
+ emclass = emfield.em_class
|
|
259
|
+ emtype = em.component(emfield.rel_to_type_id)
|
|
260
|
+ return self.datasource.get_r2t2table_name(emclass.name, emtype.name)
|
|
261
|
+ #return "%s_%s_%s"%(emclass.name, emtype.name, emfield.name)
|
|
262
|
+
|
|
263
|
+ ## @brief Generate a columns_fieldtype dict given a rel2type EmField
|
|
264
|
+ # @param em Model : an @ref EditorialModel.model.Model instance
|
|
265
|
+ # @param emfield EmField : and @ref EditorialModel.fields.EmField instance
|
|
266
|
+ def _r2type2cols(self, em, emfield):
|
|
267
|
+ return {f.name: f.fieldtype_instance() for f in em.components('EmField') if f.rel_field_id == emfield.uid}
|
|
268
|
+
|
|
269
|
+ ## @brief Generate a columns_fieldtype dict given an EmClass
|
|
270
|
+ # @param emclass EmClass : An EmClass instance
|
|
271
|
+ # @return A dict with column name as key and EmFieldType instance as value
|
|
272
|
+ def _class2cols(self, emclass):
|
|
273
|
+ if not isinstance(emclass, EditorialModel.classes.EmClass):
|
|
274
|
+ raise ValueError("The given uid is not an EmClass uid")
|
|
275
|
+ return {f.name: f.fieldtype_instance() for f in emclass.fields() if f.name not in EditorialModel.classtypes.common_fields.keys()}
|
|
276
|
+
|
|
277
|
+ ## @brief Create object and relations tables
|
|
278
|
+ # @param drop_if_exist bool : If true drop tables if exists
|
|
279
|
+ def _create_default_tables(self, drop_if_exist=False):
|
|
280
|
+ if_exists = 'drop' if drop_if_exist else 'nothing'
|
|
281
|
+ #Object tablea
|
|
282
|
+ tname = self.datasource.objects_table_name
|
|
283
|
+ pk_name, pk_ftype = self._common_field_pk
|
|
284
|
+ self._create_table(tname, pk_name, pk_ftype, engine=self.db_engine, if_exists=if_exists)
|
|
285
|
+ #Adding columns
|
|
286
|
+ cols = {fname: self._common_field_to_ftype(fname) for fname in EditorialModel.classtypes.common_fields}
|
|
287
|
+ for fname, ftype in cols.items():
|
|
288
|
+ if fname != pk_name:
|
|
289
|
+ self._add_column(tname, fname, ftype)
|
|
290
|
+ #Creating triggers
|
|
291
|
+ self._generate_triggers(tname, cols)
|
|
292
|
+
|
|
293
|
+ #Relation table
|
|
294
|
+ tname = self.datasource.relations_table_name
|
|
295
|
+ pk_name, pk_ftype = self._relation_pk
|
|
296
|
+ self._create_table(tname, pk_name, pk_ftype, engine=self.db_engine, if_exists=if_exists)
|
|
297
|
+ #Adding columns
|
|
298
|
+ for fname, ftype in self._relation_cols.items():
|
|
299
|
+ self._add_column(tname, fname, ftype)
|
|
300
|
+ #Creating triggers
|
|
301
|
+ self._generate_triggers(tname, self._relation_cols)
|
|
302
|
+
|
|
303
|
+ ## @return true if the name changes
|
|
304
|
+ def _name_change(self, initial_state, new_state):
|
|
305
|
+ return 'name' in initial_state and initial_state['name'] != new_state['name']
|
|
306
|
+
|
|
307
|
+ ## @brief Create a table with primary key
|
|
308
|
+ # @param table_name str : table name
|
|
309
|
+ # @param pk_name str : pk column name
|
|
310
|
+ # @param pk_specs str : see @ref _field_to_sql()
|
|
311
|
+ # @param engine str : The engine to use with this table
|
|
312
|
+ # @param charset str : The charset of this table
|
|
313
|
+ # @param if_exist str : takes values in ['nothing', 'drop']
|
|
314
|
+ def _create_table(self, table_name, pk_name, pk_ftype, engine, charset='utf8', if_exists='nothing'):
|
|
315
|
+ #Escaped table name
|
|
316
|
+ etname = self.datasource.escape_idname(table_name)
|
|
317
|
+ pk_type = self._field_to_type(pk_ftype)
|
|
318
|
+ pk_specs = self._field_to_specs(pk_ftype)
|
|
319
|
+
|
|
320
|
+ if if_exists == 'drop':
|
|
321
|
+ self._query("""DROP TABLE IF EXISTS {table_name};""".format(table_name=etname))
|
|
322
|
+ qres = """
|
|
323
|
+CREATE TABLE {table_name} (
|
|
324
|
+{pk_name} {pk_type} {pk_specs},
|
|
325
|
+PRIMARY KEY({pk_name})
|
|
326
|
+) ENGINE={engine} DEFAULT CHARSET={charset};"""
|
|
327
|
+ elif if_exists == 'nothing':
|
|
328
|
+ qres = """CREATE TABLE IF NOT EXISTS {table_name} (
|
|
329
|
+{pk_name} {pk_type} {pk_specs},
|
|
330
|
+PRIMARY KEY({pk_name})
|
|
331
|
+) ENGINE={engine} DEFAULT CHARSET={charset};"""
|
96
|
332
|
else:
|
97
|
|
- table_name = self._class_table_name_from_field(model, new_state)
|
|
333
|
+ raise ValueError("Unexpected value for argument if_exists '%s'." % if_exists)
|
98
|
334
|
|
99
|
|
- field_definition = self._fieldtype_definition(new_state['fieldtype'], new_state)
|
100
|
|
- self._query_bd(
|
101
|
|
- alter_add(table=table_name, column=[(new_state['name'],field_definition)])
|
102
|
|
- )
|
|
335
|
+ self._query(qres.format(
|
|
336
|
+ table_name=self.datasource.escape_idname(table_name),
|
|
337
|
+ pk_name=self.datasource.escape_idname(pk_name),
|
|
338
|
+ pk_type=pk_type,
|
|
339
|
+ pk_specs=pk_specs,
|
|
340
|
+ engine=engine,
|
|
341
|
+ charset=charset
|
|
342
|
+ ))
|
103
|
343
|
|
104
|
|
- ## convert fieldtype name to SQL definition
|
105
|
|
- def _fieldtype_definition(self, fieldtype, options):
|
106
|
|
- basic_type = GenericFieldType.from_name(fieldtype).ftype
|
107
|
|
- if basic_type == 'int':
|
108
|
|
- return 'INT'
|
109
|
|
- elif basic_type == 'char':
|
110
|
|
- max_length = options['max_length'] if 'max_length' in options else 255
|
111
|
|
- return 'CHAR(%s)' % max_length
|
112
|
|
- elif basic_type == 'text':
|
113
|
|
- return 'TEXT'
|
114
|
|
- elif basic_type == 'bool':
|
115
|
|
- return 'BOOLEAN'
|
116
|
|
- elif basic_type == 'datetime':
|
117
|
|
- definition = 'DATETIME'
|
118
|
|
- if 'now_on_create' in options and options['now_on_create']:
|
119
|
|
- definition += ' DEFAULT CURRENT_TIMESTAMP'
|
120
|
|
- #if 'now_on_update' in options and options['now_on_update']:
|
121
|
|
- #definition += ' ON UPDATE CURRENT_TIMESTAMP'
|
122
|
|
- return definition
|
123
|
|
-
|
124
|
|
- raise EditorialModel.exceptions.MigrationHandlerChangeError("Basic type '%s' of fieldtype '%s' is not compatible with SQL migration Handler" % basic_type, fieldtype)
|
125
|
|
-
|
126
|
|
- ## Test if internal tables must be created, create it if it must
|
127
|
|
- def _install_tables(self):
|
128
|
|
- # create common fields definition
|
129
|
|
- common_fields = [self._pk_column]
|
130
|
|
- for name, options in EditorialModel.classtypes.common_fields.items():
|
131
|
|
- if options['fieldtype'] != 'pk':
|
132
|
|
- common_fields.append((name, self._fieldtype_definition(options['fieldtype'], options)))
|
133
|
|
-
|
134
|
|
- # create common tables
|
135
|
|
- self._query_bd(
|
136
|
|
- create(table=self._main_table_name, column=common_fields),
|
137
|
|
- create(table=self._relation_table_name, column=[('relation_id','INTEGER PRIMARY KEY AUTOINCREMENT'), ('superior_id','INT'), ('subdordinate_id','INT'), ('nature','CHAR(255)'), ('depth','INT'), ('rank','INT')])
|
|
344
|
+ ## @brief Add a column to a table
|
|
345
|
+ # @param table_name str : The table name
|
|
346
|
+ # @param col_name str : The columns name
|
|
347
|
+ # @param col_fieldtype EmFieldype the fieldtype
|
|
348
|
+ def _add_column(self, table_name, col_name, col_fieldtype, drop_if_exists=False):
|
|
349
|
+ add_col = """ALTER TABLE {table_name}
|
|
350
|
+ADD COLUMN {col_name} {col_type} {col_specs};"""
|
|
351
|
+
|
|
352
|
+ etname = self.datasource.escape_idname(table_name)
|
|
353
|
+ ecname = self.datasource.escape_idname(col_name)
|
|
354
|
+
|
|
355
|
+ add_col = add_col.format(
|
|
356
|
+ table_name=etname,
|
|
357
|
+ col_name=ecname,
|
|
358
|
+ col_type=self._field_to_type(col_fieldtype),
|
|
359
|
+ col_specs=self._field_to_specs(col_fieldtype),
|
138
|
360
|
)
|
|
361
|
+ try:
|
|
362
|
+ self._query(add_col)
|
|
363
|
+ except self._dbmodule.err.InternalError as e:
|
|
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)
|
139
|
370
|
|
140
|
|
- def _query_bd(self, *queries):
|
141
|
|
- with self.db as cur:
|
142
|
|
- for query in queries:
|
143
|
|
- print(query)
|
144
|
|
- cur.execute(query)
|
|
371
|
+ ## @brief Add a foreign key
|
|
372
|
+ # @param src_table_name str : The name of the table where we will add the FK
|
|
373
|
+ # @param dst_table_name str : The name of the table the FK will point on
|
|
374
|
+ # @param src_col_name str : The name of the concerned column in the src_table
|
|
375
|
+ # @param dst_col_name str : The name of the concerned column in the dst_table
|
|
376
|
+ def _add_fk(self, src_table_name, dst_table_name, src_col_name, dst_col_name):
|
|
377
|
+ stname = self.datasource.escape_idname(src_table_name)
|
|
378
|
+ dtname = self.datasource.escape_idname(dst_table_name)
|
|
379
|
+ scname = self.datasource.escape_idname(src_col_name)
|
|
380
|
+ dcname = self.datasource.escape_idname(dst_col_name)
|
|
381
|
+
|
|
382
|
+ fk_name = self.datasource.get_fk_name(src_table_name, dst_table_name)
|
|
383
|
+
|
|
384
|
+ self._del_fk(src_table_name, dst_table_name)
|
|
385
|
+
|
|
386
|
+ self._query("""ALTER TABLE {src_table}
|
|
387
|
+ADD CONSTRAINT {fk_name}
|
|
388
|
+FOREIGN KEY ({src_col}) references {dst_table}({dst_col});""".format(
|
|
389
|
+ fk_name=self.datasource.escape_idname(fk_name),
|
|
390
|
+ src_table=stname,
|
|
391
|
+ src_col=scname,
|
|
392
|
+ dst_table=dtname,
|
|
393
|
+ dst_col=dcname
|
|
394
|
+ ))
|
|
395
|
+
|
|
396
|
+ ## @brief Given a source and a destination table, delete the corresponding FK
|
|
397
|
+ # @param src_table_name str : The name of the table where the FK is
|
|
398
|
+ # @param dst_table_name str : The name of the table the FK point on
|
|
399
|
+ # @warning fails silently
|
|
400
|
+ def _del_fk(self, src_table_name, dst_table_name):
|
|
401
|
+ try:
|
|
402
|
+ self._query("""ALTER TABLE {src_table}
|
|
403
|
+DROP FOREIGN KEY {fk_name}""".format(
|
|
404
|
+ src_table=self.datasource.escape_idname(src_table_name),
|
|
405
|
+ fk_name=self.datasource.escape_idname(self.datasource.get_fk_name(src_table_name, dst_table_name))
|
|
406
|
+ ))
|
|
407
|
+ except self._dbmodule.err.InternalError:
|
|
408
|
+ # If the FK don't exists we do not care
|
|
409
|
+ pass
|
|
410
|
+
|
|
411
|
+ ## @brief Generate triggers given a table_name and its columns fieldtypes
|
|
412
|
+ # @param table_name str : Table name
|
|
413
|
+ # @param cols_ftype dict : with col name as key and column fieldtype as value
|
|
414
|
+ def _generate_triggers(self, table_name, cols_ftype):
|
|
415
|
+ colval_l_upd = dict() # param for update trigger
|
|
416
|
+ colval_l_ins = dict() # param for insert trigger
|
|
417
|
+
|
|
418
|
+ for cname, cftype in cols_ftype.items():
|
|
419
|
+ if cftype.ftype == 'datetime':
|
|
420
|
+ if cftype.now_on_update:
|
|
421
|
+ colval_l_upd[cname] = 'NOW()'
|
|
422
|
+ if cftype.now_on_create:
|
|
423
|
+ colval_l_ins[cname] = 'NOW()'
|
|
424
|
+
|
|
425
|
+ self._table_trigger(table_name, 'UPDATE', colval_l_upd)
|
|
426
|
+ self._table_trigger(table_name, 'INSERT', colval_l_ins)
|
|
427
|
+
|
|
428
|
+ ## @brief Create trigger for a table
|
|
429
|
+ #
|
|
430
|
+ # Primarly designed to create trigger for DATETIME types
|
|
431
|
+ # The method generates triggers of the form
|
|
432
|
+ #
|
|
433
|
+ # CREATE TRIGGER BEFORE <moment> ON <table_name>
|
|
434
|
+ # FOR EACH ROW SET <for colname, colval in cols_val>
|
|
435
|
+ # NEW.<colname> = <colval>,
|
|
436
|
+ # <endfor>;
|
|
437
|
+ # @param table_name str : The table name
|
|
438
|
+ # @param moment str : can be 'update' or 'insert'
|
|
439
|
+ # @param cols_val dict : Dict with column name as key and column value as value
|
|
440
|
+ def _table_trigger(self, table_name, moment, cols_val):
|
|
441
|
+ trigger_name = self.datasource.escape_idname("%s_%s_trig" % (table_name, moment))
|
|
442
|
+ #Try to delete the trigger
|
|
443
|
+ drop_trig = """DROP TRIGGER IF EXISTS {trigger_name};""".format(trigger_name=trigger_name)
|
|
444
|
+ self._query(drop_trig)
|
|
445
|
+
|
|
446
|
+ col_val_l = ', '.join(["NEW.%s = %s" % (self.datasource.escape_idname(cname), cval)for cname, cval in cols_val.items()])
|
|
447
|
+ #Create a trigger if needed
|
|
448
|
+ if len(col_val_l) > 0:
|
|
449
|
+ trig_q = """CREATE TRIGGER {trigger_name} BEFORE {moment} ON {table_name}
|
|
450
|
+FOR EACH ROW SET {col_val_list};""".format(
|
|
451
|
+ trigger_name=trigger_name,
|
|
452
|
+ table_name=self.datasource.escape_idname(table_name),
|
|
453
|
+ moment=moment, col_val_list=col_val_l
|
|
454
|
+ )
|
|
455
|
+ self._query(trig_q)
|
|
456
|
+
|
|
457
|
+ ## @brief Identifier escaping
|
|
458
|
+ # @param idname str : An SQL identifier
|
|
459
|
+ #def _idname_escape(self, idname):
|
|
460
|
+ # if '`' in idname:
|
|
461
|
+ # raise ValueError("Invalid name : '%s'"%idname)
|
|
462
|
+ # return '`%s`'%idname
|
|
463
|
+
|
|
464
|
+ ## @brief Returns column specs from fieldtype
|
|
465
|
+ # @param emfieldtype EmFieldType : An EmFieldType insance
|
|
466
|
+ # @todo escape default value
|
|
467
|
+ def _field_to_specs(self, emfieldtype):
|
|
468
|
+ colspec = ''
|
|
469
|
+ if not emfieldtype.nullable:
|
|
470
|
+ colspec = 'NOT NULL'
|
|
471
|
+ if hasattr(emfieldtype, 'default'):
|
|
472
|
+ colspec += ' DEFAULT '
|
|
473
|
+ if emfieldtype.default is None:
|
|
474
|
+ colspec += 'NULL '
|
|
475
|
+ else:
|
|
476
|
+ colspec += emfieldtype.default # ESCAPE VALUE HERE !!!!
|
|
477
|
+
|
|
478
|
+ if emfieldtype.name == 'pk':
|
|
479
|
+ colspec += ' AUTO_INCREMENT'
|
|
480
|
+
|
|
481
|
+ return colspec
|
|
482
|
+
|
|
483
|
+ ## @brief Given a fieldtype return a MySQL type specifier
|
|
484
|
+ # @param emfieldtype EmFieldType : A fieldtype
|
|
485
|
+ # @return the corresponding MySQL type
|
|
486
|
+ def _field_to_type(self, emfieldtype):
|
|
487
|
+ ftype = emfieldtype.ftype
|
|
488
|
+
|
|
489
|
+ if ftype == 'char' or ftype == 'str':
|
|
490
|
+ res = "VARCHAR(%d)" % emfieldtype.max_length
|
|
491
|
+ elif ftype == 'text':
|
|
492
|
+ res = "TEXT"
|
|
493
|
+ elif ftype == 'datetime':
|
|
494
|
+ res = "DATETIME"
|
|
495
|
+ # client side workaround for only one column with CURRENT_TIMESTAMP : giving NULL to timestamp that don't allows NULL
|
|
496
|
+ # cf. https://dev.mysql.com/doc/refman/5.0/en/timestamp-initialization.html#idm139961275230400
|
|
497
|
+ # The solution for the migration handler is to create triggers :
|
|
498
|
+ # CREATE TRIGGER trigger_name BEFORE INSERT ON `my_super_table`
|
|
499
|
+ # FOR EACH ROW SET NEW.my_date_column = NOW();
|
|
500
|
+ # and
|
|
501
|
+ # CREATE TRIGGER trigger_name BEFORE UPDATE ON
|
|
502
|
+
|
|
503
|
+ elif ftype == 'bool':
|
|
504
|
+ res = "BOOL"
|
|
505
|
+ elif ftype == 'int':
|
|
506
|
+ res = "INT"
|
|
507
|
+ elif ftype == 'rel2type':
|
|
508
|
+ res = "INT"
|
|
509
|
+ else:
|
|
510
|
+ raise ValueError("Unsuported fieldtype ftype : %s" % ftype)
|
|
511
|
+
|
|
512
|
+ return res
|
|
513
|
+
|
|
514
|
+ ## @brief Returns a tuple (pkname, pk_ftype)
|
|
515
|
+ @property
|
|
516
|
+ def _common_field_pk(self):
|
|
517
|
+ for fname, fta in EditorialModel.classtypes.common_fields.items():
|
|
518
|
+ if fta['fieldtype'] == 'pk':
|
|
519
|
+ return (fname, self._common_field_to_ftype(fname))
|
|
520
|
+ return (None, None)
|
145
|
521
|
|
146
|
|
- def _class_table_name(self, class_name):
|
147
|
|
- return 'class_' + class_name
|
|
522
|
+ ## @brief Returns a tuple (rel_pkname, rel_ftype)
|
|
523
|
+ # @todo do it
|
|
524
|
+ @property
|
|
525
|
+ def _relation_pk(self):
|
|
526
|
+ return (MySQL.relations_pkname, EditorialModel.fieldtypes.pk.EmFieldType())
|
148
|
527
|
|
149
|
|
- def _relational_table_name(self, class_name, type_name):
|
150
|
|
- return 'r2t_' + class_name + '_' + type_name
|
|
528
|
+ ## @brief Returns a dict { colname:fieldtype } of relation table columns
|
|
529
|
+ @property
|
|
530
|
+ def _relation_cols(self):
|
|
531
|
+ from_name = EditorialModel.fieldtypes.generic.GenericFieldType.from_name
|
|
532
|
+ return {
|
|
533
|
+ 'id_sup': from_name('integer')(),
|
|
534
|
+ 'id_sub': from_name('integer')(),
|
|
535
|
+ 'rank': from_name('integer')(nullable=True),
|
|
536
|
+ 'depth': from_name('integer')(nullable=True),
|
|
537
|
+ 'nature': from_name('char')(max_lenght=10, nullable=True),
|
|
538
|
+ }
|
151
|
539
|
|
152
|
|
- def _class_table_name_from_field(self, model, field):
|
153
|
|
- class_id = field['class_id']
|
154
|
|
- class_name = model.component(class_id).name
|
155
|
|
- class_table_name = self._class_table_name(class_name)
|
156
|
|
- return class_table_name
|
|
540
|
+ ## @brief Given a common field name return an EmFieldType instance
|
|
541
|
+ # @param cname str : Common field name
|
|
542
|
+ # @return An EmFieldType instance
|
|
543
|
+ def _common_field_to_ftype(self, cname):
|
|
544
|
+ fta = copy.copy(EditorialModel.classtypes.common_fields[cname])
|
|
545
|
+ fto = EditorialModel.fieldtypes.generic.GenericFieldType.from_name(fta['fieldtype'])
|
|
546
|
+ del fta['fieldtype']
|
|
547
|
+ return fto(**fta)
|