|
@@ -4,8 +4,11 @@ import re
|
4
|
4
|
import logging as logger
|
5
|
5
|
|
6
|
6
|
import sqlalchemy as sqla
|
|
7
|
+from sqlalchemy.ext.compiler import compiles
|
7
|
8
|
from django.conf import settings
|
8
|
9
|
|
|
10
|
+from Database.sqlalter import *
|
|
11
|
+
|
9
|
12
|
#Logger config
|
10
|
13
|
logger.getLogger().setLevel('DEBUG')
|
11
|
14
|
#To be able to use dango confs
|
|
@@ -38,26 +41,32 @@ class SqlWrapper(object):
|
38
|
41
|
##Wrapper instance list
|
39
|
42
|
wrapinstance = dict()
|
40
|
43
|
|
41
|
|
- def __init__(self, name="default", alchemy_logs=None, read_db = "default", write_db = "default"):
|
|
44
|
+ def __init__(self, name=None, alchemy_logs=None, read_db = "default", write_db = "default"):
|
42
|
45
|
""" Instanciate a new SqlWrapper
|
43
|
46
|
@param name str: The wrapper name
|
44
|
47
|
@param alchemy_logs bool: If true activate sqlalchemy logger
|
45
|
48
|
@param read_db str: The name of the db conf
|
46
|
49
|
@param write_db str: The name of the db conf
|
|
50
|
+
|
|
51
|
+ @todo Better use of name (should use self.cfg['wrapper'][name] to get engines configs
|
|
52
|
+ @todo Is it a really good idea to store instance in class scope ? Maybe not !!
|
47
|
53
|
"""
|
48
|
54
|
|
49
|
55
|
self.sqlalogging = False if alchemy_logs == None else bool(alchemy_logs)
|
50
|
56
|
|
51
|
|
- self.name = name
|
|
57
|
+ if name == None:
|
|
58
|
+ self.name = read_db+'+'+write_db
|
|
59
|
+ else:
|
|
60
|
+ self.name = name
|
52
|
61
|
|
53
|
62
|
self.r_dbconf = read_db
|
54
|
63
|
self.w_dbconf = write_db
|
55
|
64
|
|
56
|
65
|
self.checkConf() #raise if errors in configuration
|
57
|
66
|
|
58
|
|
- if name in self.__class__.wrapinstance:
|
59
|
|
- logger.warning("A SqlWrapper with the name "+name+" allready exist. Replacing the old one by the new one")
|
60
|
|
- SqlWrapper.wrapinstance[name] = self
|
|
67
|
+ if self.name in self.__class__.wrapinstance:
|
|
68
|
+ logger.warning("A SqlWrapper with the name "+self.name+" allready exist. Replacing the old one by the new one")
|
|
69
|
+ SqlWrapper.wrapinstance[self.name] = self
|
61
|
70
|
|
62
|
71
|
#Engine and wrapper initialisation
|
63
|
72
|
self.r_engine = self._getEngine(True, self.sqlalogging)
|
|
@@ -66,8 +75,10 @@ class SqlWrapper(object):
|
66
|
75
|
self.w_conn = None
|
67
|
76
|
|
68
|
77
|
|
69
|
|
- self.meta = None #TODO : use it to load all db schema in 1 request and don't load it each table instanciation
|
|
78
|
+ self.metadata = None #TODO : use it to load all db schema in 1 request and don't load it each table instanciation
|
70
|
79
|
self.meta_crea = None
|
|
80
|
+
|
|
81
|
+ logger.debug("New wrapper instance : <"+self.name+" read:"+str(self.r_engine)+" write:"+str(self.w_engine))
|
71
|
82
|
pass
|
72
|
83
|
|
73
|
84
|
@property
|
|
@@ -75,6 +86,19 @@ class SqlWrapper(object):
|
75
|
86
|
@property
|
76
|
87
|
def engines_cfg(self): return self.__class__.ENGINES;
|
77
|
88
|
|
|
89
|
+ @property
|
|
90
|
+ def meta(self):
|
|
91
|
+ if self.metadata == None:
|
|
92
|
+ self.renewMetaData()
|
|
93
|
+ return self.metadata
|
|
94
|
+
|
|
95
|
+ def renewMetaData(self):
|
|
96
|
+ """ (Re)load the database schema """
|
|
97
|
+ if self.metadata == None:
|
|
98
|
+ self.metadata = sqla.MetaData(bind=self.r_engine, reflect=True)
|
|
99
|
+ else:
|
|
100
|
+ self.metadata = sqla.MetaData(bind=self.r_engine, reflect=True)
|
|
101
|
+
|
78
|
102
|
@property
|
79
|
103
|
def rconn(self):
|
80
|
104
|
""" Return the read connection
|
|
@@ -88,7 +112,6 @@ class SqlWrapper(object):
|
88
|
112
|
"""
|
89
|
113
|
return self.getConnection(False)
|
90
|
114
|
|
91
|
|
-
|
92
|
115
|
def getConnection(self, read):
|
93
|
116
|
""" Return an opened connection
|
94
|
117
|
@param read bool: If true return the reading connection
|
|
@@ -129,11 +152,17 @@ class SqlWrapper(object):
|
129
|
152
|
@return None
|
130
|
153
|
"""
|
131
|
154
|
if read or read == None:
|
132
|
|
- self.r_conn.close()
|
|
155
|
+ if self.r_conn == None:
|
|
156
|
+ logger.info('Unable to close read connection : connection not opened')
|
|
157
|
+ else:
|
|
158
|
+ self.r_conn.close()
|
133
|
159
|
self.r_conn = None
|
134
|
160
|
|
135
|
161
|
if not read or read == None:
|
136
|
|
- self.w_conn.close()
|
|
162
|
+ if self.r_conn == None:
|
|
163
|
+ logger.info('Unable to close write connection : connection not opened')
|
|
164
|
+ else:
|
|
165
|
+ self.w_conn.close()
|
137
|
166
|
self.w_conn = None
|
138
|
167
|
|
139
|
168
|
def reconnect(self, read = None):
|
|
@@ -156,7 +185,8 @@ class SqlWrapper(object):
|
156
|
185
|
"""
|
157
|
186
|
if not isinstance(tname, str):
|
158
|
187
|
return TypeError('Excepting a <class str> but got a '+str(type(tname)))
|
159
|
|
- return sqla.Table(tname, sqla.MetaData(), autoload_with=self.r_engine, autoload=True)
|
|
188
|
+ #return sqla.Table(tname, self.meta, autoload_with=self.r_engine, autoload=True)
|
|
189
|
+ return sqla.Table(tname, self.meta)
|
160
|
190
|
|
161
|
191
|
def _getEngine(self, read=True, sqlalogging = None):
|
162
|
192
|
""" Return a sqlalchemy engine
|
|
@@ -194,7 +224,11 @@ class SqlWrapper(object):
|
194
|
224
|
conn_str += '%s@%s/%s'%(user,host,cfg['NAME'])
|
195
|
225
|
|
196
|
226
|
|
197
|
|
- return sqla.create_engine(conn_str, encoding=edata['encoding'], echo=self.sqlalogging)
|
|
227
|
+ ret = sqla.create_engine(conn_str, encoding=edata['encoding'], echo=self.sqlalogging)
|
|
228
|
+
|
|
229
|
+ logger.debug("Getting engine :"+str(ret))
|
|
230
|
+
|
|
231
|
+ return ret
|
198
|
232
|
|
199
|
233
|
@classmethod
|
200
|
234
|
def getWrapper(c, name):
|
|
@@ -264,11 +298,14 @@ class SqlWrapper(object):
|
264
|
298
|
"""
|
265
|
299
|
self.meta_crea = sqla.MetaData()
|
266
|
300
|
|
|
301
|
+ logger.info("Running function createAllFromConf")
|
267
|
302
|
for i,table in enumerate(schema):
|
268
|
303
|
self.createTable(**table)
|
269
|
304
|
|
270
|
|
- self.meta_crea.create_all(self.w_engine)
|
|
305
|
+ self.meta_crea.create_all(bind = self.w_engine)
|
|
306
|
+ logger.info("All tables created")
|
271
|
307
|
self.meta_crea = None
|
|
308
|
+ self.renewMetaData()
|
272
|
309
|
pass
|
273
|
310
|
|
274
|
311
|
def createTable(self, name, columns, **kw):
|
|
@@ -295,6 +332,7 @@ class SqlWrapper(object):
|
295
|
332
|
if crea_now:
|
296
|
333
|
self.meta_crea.create_all(self.w_engine)
|
297
|
334
|
|
|
335
|
+ #logger.debug("Table '"+name+"' created")
|
298
|
336
|
pass
|
299
|
337
|
|
300
|
338
|
def createColumn(self, **kwargs):
|
|
@@ -306,12 +344,13 @@ class SqlWrapper(object):
|
306
|
344
|
- extra : a dict like { "primarykey":True, "nullable":False, "default":"test"...}
|
307
|
345
|
@param **kwargs
|
308
|
346
|
"""
|
309
|
|
- if not 'name' in kwargs or not 'type' in kwargs:
|
|
347
|
+ if not 'name' in kwargs or ('type' not in kwargs and 'type_' not in kwargs):
|
310
|
348
|
pass#ERROR
|
311
|
349
|
|
312
|
350
|
#Converting parameters
|
313
|
|
- kwargs['type_'] = self._strToSqlAType(kwargs['type'])
|
314
|
|
- del kwargs['type']
|
|
351
|
+ if 'type_' not in kwargs and 'type' in kwargs:
|
|
352
|
+ kwargs['type_'] = self._strToSqlAType(kwargs['type'])
|
|
353
|
+ del kwargs['type']
|
315
|
354
|
|
316
|
355
|
if 'extra' in kwargs:
|
317
|
356
|
#put the extra keys in kwargs
|
|
@@ -332,12 +371,12 @@ class SqlWrapper(object):
|
332
|
371
|
del kwargs['primarykey']
|
333
|
372
|
|
334
|
373
|
|
335
|
|
- logger.debug('Column creation arguments : '+str(kwargs))
|
336
|
374
|
res = sqla.Column(**kwargs)
|
337
|
375
|
|
338
|
376
|
if fk != None:
|
339
|
377
|
res.append_foreign_key(fk)
|
340
|
378
|
|
|
379
|
+ #logger.debug("Column '"+kwargs['name']+"' created")
|
341
|
380
|
return res
|
342
|
381
|
|
343
|
382
|
def _strToSqlAType(self, strtype):
|
|
@@ -357,6 +396,112 @@ class SqlWrapper(object):
|
357
|
396
|
check_length = re.search(re.compile('VARCHAR\(([\d]+)\)', re.IGNORECASE), vstr)
|
358
|
397
|
column_length = int(check_length.groups()[0]) if check_length else None
|
359
|
398
|
return sqla.VARCHAR(length=column_length)
|
|
399
|
+
|
|
400
|
+ @classmethod
|
|
401
|
+ def engineFamily(c, engine):
|
|
402
|
+ """ Given an engine return the db family
|
|
403
|
+ @see SqlWrapper::ENGINES
|
|
404
|
+ @return A str or None
|
|
405
|
+ """
|
|
406
|
+ for fam in c.ENGINES:
|
|
407
|
+ if engine.driver == c.ENGINES[fam]['driver']:
|
|
408
|
+ return fam
|
|
409
|
+ return None
|
|
410
|
+
|
|
411
|
+ @property
|
|
412
|
+ def wEngineFamily(self):
|
|
413
|
+ """ Return the db family of the write engine
|
|
414
|
+ @return a string or None
|
|
415
|
+ """
|
|
416
|
+ return self.__class__.engineFamily(self.w_engine)
|
|
417
|
+ @property
|
|
418
|
+ def rEngineFamily(self):
|
|
419
|
+ """ Return the db family of the read engine
|
|
420
|
+ @return a string or None
|
|
421
|
+ """
|
|
422
|
+ return self.__class__.engineFamily(self.r_engine)
|
|
423
|
+
|
|
424
|
+ def dropColumn(self, tname, colname):
|
|
425
|
+ """ Drop a column from a table
|
|
426
|
+ @param tname str|sqlalchemy.Table: The table name or a Table object
|
|
427
|
+ @param colname str|sqlalchemy.Column: The column name or a column object
|
|
428
|
+ @return None
|
|
429
|
+ """
|
|
430
|
+ if tname not in self.meta.tables: #Useless ?
|
|
431
|
+ raise NameError("The table '"+tname+"' dont exist")
|
|
432
|
+ table = self.Table(tname)
|
|
433
|
+ col = sqla.Column(colname)
|
|
434
|
+
|
|
435
|
+ ddl = DropColumn(table, col)
|
|
436
|
+ sql = ddl.compile(dialect=self.w_engine.dialect)
|
|
437
|
+ sql = str(sql)
|
|
438
|
+ logger.debug("Executing SQL : '"+sql+"'")
|
|
439
|
+ ret = bool(self.w_engine.execute(sql))
|
|
440
|
+
|
|
441
|
+ self.renewMetaData()
|
|
442
|
+ return ret
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+ def addColumn(self, tname, colname, coltype):
|
|
446
|
+ """ Add a column to a table
|
|
447
|
+ @param tname str: The table name
|
|
448
|
+ @param colname str: The column name
|
|
449
|
+ @param coltype str: The new column type
|
|
450
|
+
|
|
451
|
+ @return True if query success False if it fails
|
|
452
|
+ """
|
|
453
|
+ newcol = self.createColumn(name=colname, type_ = coltype)
|
|
454
|
+ if tname not in self.meta.tables: #Useless ?
|
|
455
|
+ raise NameError("The table '"+tname+"' dont exist")
|
|
456
|
+ table = self.Table(tname)
|
|
457
|
+
|
|
458
|
+ ddl = AddColumn(table, newcol)
|
|
459
|
+ sql = ddl.compile(dialect=self.w_engine.dialect)
|
|
460
|
+ sql = str(sql)
|
|
461
|
+ logger.debug("Executing SQL : '"+sql+"'")
|
|
462
|
+ ret = bool(self.wconn.execute(sql))
|
|
463
|
+
|
|
464
|
+ self.renewMetaData()
|
|
465
|
+ return ret
|
|
466
|
+
|
|
467
|
+ def alterColumn(self, tname, colname, col_newtype):
|
|
468
|
+ """ Change the type of a column
|
|
469
|
+ @param tname str: The table name
|
|
470
|
+ @param colname str: The column name
|
|
471
|
+ @param col_newtype str: The column new type
|
|
472
|
+
|
|
473
|
+ @return True if query successs False if it fails
|
|
474
|
+ """
|
360
|
475
|
|
|
476
|
+ if self.wEngineFamily == 'sqlite':
|
|
477
|
+ raise NotImplementedError('AlterColumn not yet implemented for sqlite engines')
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+ col = self.createColumn(name=colname, type_=col_newtype)
|
|
481
|
+ table = self.Table(tname)
|
|
482
|
+
|
|
483
|
+ typepref = 'TYPE ' if self.wEngineFamily == 'postgresql' else ''
|
|
484
|
+
|
|
485
|
+ query = 'ALTER TABLE %s ALTER COLUMN %s %s'%(table.name, col.name, typepref+col.type)
|
361
|
486
|
|
|
487
|
+ logger.debug("Executing SQL : '"+query+"'")
|
|
488
|
+
|
|
489
|
+ ret = bool(self.wconn.execute(query))
|
|
490
|
+
|
|
491
|
+ self.renewMetaData()
|
|
492
|
+ return ret
|
|
493
|
+
|
|
494
|
+ def _debug__printSchema(self):
|
|
495
|
+ """ Debug function to print the db schema """
|
|
496
|
+ print(self.meta)
|
|
497
|
+ for tname in self.meta.tables:
|
|
498
|
+ self._debug__printTable(tname)
|
|
499
|
+
|
|
500
|
+ def _debug__printTable(self, tname):
|
|
501
|
+ t = self.meta.tables[tname]
|
|
502
|
+ tstr = 'Table : "'+tname+'" :\n'
|
|
503
|
+ for c in t.c:
|
|
504
|
+ tstr += '\t\t"'+c.name+'"('+str(c.type)+') \n'
|
|
505
|
+ print(tstr)
|
|
506
|
+
|
362
|
507
|
|