1
0
Fork 0
mirror of https://github.com/yweber/lodel2.git synced 2026-10-27 06:41:33 +01:00

Implemented alter table in sqlwrapper

Adding a column is implemented for mysql, postgresql and sqlite.
Droping and altering a column is only implemented for mysql and postgresql for the moment.

Quick&dirty tests has been done  for sqlite and mysql only.
This commit is contained in:
Yann 2015-06-04 16:07:31 +02:00
commit 6ab79eb412
2 changed files with 251 additions and 16 deletions

90
Database/sqlalter.py Normal file
View file

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
import os
import sqlalchemy as sqla
from sqlalchemy.ext.compiler import compiles
## @file sqlalter.py
# This file defines all DDL (data definition langage) for the ALTER TABLE instructions
#
# It uses the SqlAlchemy compilation and quoting methos to generate SQL
class AddColumn(sqla.schema.DDLElement):
""" Defines the ddl for adding a column to a table """
def __init__(self,table, column):
""" Instanciate the DDL
@param table sqlalchemy.Table: A sqlalchemy table object
@param column sqlalchemy.Column: A sqlalchemy column object
"""
self.col = column
self.table = table
@compiles(AddColumn, 'mysql')
@compiles(AddColumn, 'postgresql')
@compiles(AddColumn, 'sqlite')
def visit_add_column(element, ddlcompiler, **kw):
""" Compiles the AddColumn DDL for mysql, postgresql and sqlite"""
prep = ddlcompiler.sql_compiler.preparer
tname = prep.format_table(element.table)
colname = prep.format_column(element.col)
return 'ALTER TABLE %s ADD COLUMN %s %s'%(tname, colname, element.col.type)
@compiles(AddColumn)
def visit_add_column(element, ddlcompiler, **kw):
raise NotImplementedError('Add column not yet implemented for '+str(ddlcompiler.dialect.name))
class DropColumn(sqla.schema.DDLElement):
""" Defines the DDL for droping a column from a table """
def __init__(self, table, column):
""" Instanciate the DDL
@param table sqlalchemy.Table: A sqlalchemy table object
@param column sqlalchemy.Column: A sqlalchemy column object representing the column to drop
"""
self.col = column
self.table = table
@compiles(DropColumn,'mysql')
@compiles(DropColumn, 'postgresql')
def visit_drop_column(element, ddlcompiler, **kw):
""" Compiles the DropColumn DDL for mysql & postgresql """
prep = ddlcompiler.sql_compiler.preparer
tname = prep.format_table(element.table)
colname = prep.format_column(element.col)
return 'ALTER TABLE %s DROP COLUMN %s'%(tname, colname)
@compiles(DropColumn)
def visit_drop_column(element, ddlcompiler, **kw):
raise NotImplementedError('Drop column not yet implemented for '+str(ddlcompiler.dialect.name))
class AlterColumn(sqla.schema.DDLElement):
""" Defines the DDL for changing the type of a column """
def __init__(self, table, column):
""" Instanciate the DDL
@param table sqlalchemy.Table: A sqlalchemy Table object
@param column sqlalchemy.Column: A sqlalchemy Column object representing the new column
"""
self.col = column
self.table = table
@compiles(AlterColumn, 'mysql')
def visit_alter_column(element, ddlcompiler, **kw):
""" Compiles the AlterColumn DDL for mysql """
prep = ddlcompiler.sql_compiler.preparer
tname = prep.format_table(element.table)
colname = prep.format_column(element.col)
return 'ALTER TABLE %s ALTER COLUMN %s %s'%(tname, colname, element.col.type)
@compiles(AlterColumn, 'postgresql')
def visit_alter_column(element, ddlcompiler, **kw):
""" Compiles the AlterColumn DDL for postgresql """
prep = ddlcompiler.sql_compiler.preparer
tname = prep.format_table(element.table)
colname = prep.format_column(element.col)
return 'ALTER TABLE %s ALTER COLUMN %s TYPE %s'%(tname, colname, element.col.type)
@compiles(AlterColumn)
def visit_alter_column(element, ddlcompiler, **kw):
raise NotImplementedError('Alter column not yet implemented for '+str(ddlcompiler.dialect.name))

View file

@ -4,8 +4,11 @@ import re
import logging as logger
import sqlalchemy as sqla
from sqlalchemy.ext.compiler import compiles
from django.conf import settings
from Database.sqlalter import *
#Logger config
logger.getLogger().setLevel('DEBUG')
#To be able to use dango confs
@ -38,26 +41,32 @@ class SqlWrapper(object):
##Wrapper instance list
wrapinstance = dict()
def __init__(self, name="default", alchemy_logs=None, read_db = "default", write_db = "default"):
def __init__(self, name=None, alchemy_logs=None, read_db = "default", write_db = "default"):
""" Instanciate a new SqlWrapper
@param name str: The wrapper name
@param alchemy_logs bool: If true activate sqlalchemy logger
@param read_db str: The name of the db conf
@param write_db str: The name of the db conf
@todo Better use of name (should use self.cfg['wrapper'][name] to get engines configs
@todo Is it a really good idea to store instance in class scope ? Maybe not !!
"""
self.sqlalogging = False if alchemy_logs == None else bool(alchemy_logs)
self.name = name
if name == None:
self.name = read_db+'+'+write_db
else:
self.name = name
self.r_dbconf = read_db
self.w_dbconf = write_db
self.checkConf() #raise if errors in configuration
if name in self.__class__.wrapinstance:
logger.warning("A SqlWrapper with the name "+name+" allready exist. Replacing the old one by the new one")
SqlWrapper.wrapinstance[name] = self
if self.name in self.__class__.wrapinstance:
logger.warning("A SqlWrapper with the name "+self.name+" allready exist. Replacing the old one by the new one")
SqlWrapper.wrapinstance[self.name] = self
#Engine and wrapper initialisation
self.r_engine = self._getEngine(True, self.sqlalogging)
@ -66,8 +75,10 @@ class SqlWrapper(object):
self.w_conn = None
self.meta = None #TODO : use it to load all db schema in 1 request and don't load it each table instanciation
self.metadata = None #TODO : use it to load all db schema in 1 request and don't load it each table instanciation
self.meta_crea = None
logger.debug("New wrapper instance : <"+self.name+" read:"+str(self.r_engine)+" write:"+str(self.w_engine))
pass
@property
@ -75,6 +86,19 @@ class SqlWrapper(object):
@property
def engines_cfg(self): return self.__class__.ENGINES;
@property
def meta(self):
if self.metadata == None:
self.renewMetaData()
return self.metadata
def renewMetaData(self):
""" (Re)load the database schema """
if self.metadata == None:
self.metadata = sqla.MetaData(bind=self.r_engine, reflect=True)
else:
self.metadata = sqla.MetaData(bind=self.r_engine, reflect=True)
@property
def rconn(self):
""" Return the read connection
@ -88,7 +112,6 @@ class SqlWrapper(object):
"""
return self.getConnection(False)
def getConnection(self, read):
""" Return an opened connection
@param read bool: If true return the reading connection
@ -129,11 +152,17 @@ class SqlWrapper(object):
@return None
"""
if read or read == None:
self.r_conn.close()
if self.r_conn == None:
logger.info('Unable to close read connection : connection not opened')
else:
self.r_conn.close()
self.r_conn = None
if not read or read == None:
self.w_conn.close()
if self.r_conn == None:
logger.info('Unable to close write connection : connection not opened')
else:
self.w_conn.close()
self.w_conn = None
def reconnect(self, read = None):
@ -156,7 +185,8 @@ class SqlWrapper(object):
"""
if not isinstance(tname, str):
return TypeError('Excepting a <class str> but got a '+str(type(tname)))
return sqla.Table(tname, sqla.MetaData(), autoload_with=self.r_engine, autoload=True)
#return sqla.Table(tname, self.meta, autoload_with=self.r_engine, autoload=True)
return sqla.Table(tname, self.meta)
def _getEngine(self, read=True, sqlalogging = None):
""" Return a sqlalchemy engine
@ -194,7 +224,11 @@ class SqlWrapper(object):
conn_str += '%s@%s/%s'%(user,host,cfg['NAME'])
return sqla.create_engine(conn_str, encoding=edata['encoding'], echo=self.sqlalogging)
ret = sqla.create_engine(conn_str, encoding=edata['encoding'], echo=self.sqlalogging)
logger.debug("Getting engine :"+str(ret))
return ret
@classmethod
def getWrapper(c, name):
@ -264,11 +298,14 @@ class SqlWrapper(object):
"""
self.meta_crea = sqla.MetaData()
logger.info("Running function createAllFromConf")
for i,table in enumerate(schema):
self.createTable(**table)
self.meta_crea.create_all(self.w_engine)
self.meta_crea.create_all(bind = self.w_engine)
logger.info("All tables created")
self.meta_crea = None
self.renewMetaData()
pass
def createTable(self, name, columns, **kw):
@ -295,6 +332,7 @@ class SqlWrapper(object):
if crea_now:
self.meta_crea.create_all(self.w_engine)
#logger.debug("Table '"+name+"' created")
pass
def createColumn(self, **kwargs):
@ -306,12 +344,13 @@ class SqlWrapper(object):
- extra : a dict like { "primarykey":True, "nullable":False, "default":"test"...}
@param **kwargs
"""
if not 'name' in kwargs or not 'type' in kwargs:
if not 'name' in kwargs or ('type' not in kwargs and 'type_' not in kwargs):
pass#ERROR
#Converting parameters
kwargs['type_'] = self._strToSqlAType(kwargs['type'])
del kwargs['type']
if 'type_' not in kwargs and 'type' in kwargs:
kwargs['type_'] = self._strToSqlAType(kwargs['type'])
del kwargs['type']
if 'extra' in kwargs:
#put the extra keys in kwargs
@ -332,12 +371,12 @@ class SqlWrapper(object):
del kwargs['primarykey']
logger.debug('Column creation arguments : '+str(kwargs))
res = sqla.Column(**kwargs)
if fk != None:
res.append_foreign_key(fk)
#logger.debug("Column '"+kwargs['name']+"' created")
return res
def _strToSqlAType(self, strtype):
@ -357,6 +396,112 @@ class SqlWrapper(object):
check_length = re.search(re.compile('VARCHAR\(([\d]+)\)', re.IGNORECASE), vstr)
column_length = int(check_length.groups()[0]) if check_length else None
return sqla.VARCHAR(length=column_length)
@classmethod
def engineFamily(c, engine):
""" Given an engine return the db family
@see SqlWrapper::ENGINES
@return A str or None
"""
for fam in c.ENGINES:
if engine.driver == c.ENGINES[fam]['driver']:
return fam
return None
@property
def wEngineFamily(self):
""" Return the db family of the write engine
@return a string or None
"""
return self.__class__.engineFamily(self.w_engine)
@property
def rEngineFamily(self):
""" Return the db family of the read engine
@return a string or None
"""
return self.__class__.engineFamily(self.r_engine)
def dropColumn(self, tname, colname):
""" Drop a column from a table
@param tname str|sqlalchemy.Table: The table name or a Table object
@param colname str|sqlalchemy.Column: The column name or a column object
@return None
"""
if tname not in self.meta.tables: #Useless ?
raise NameError("The table '"+tname+"' dont exist")
table = self.Table(tname)
col = sqla.Column(colname)
ddl = DropColumn(table, col)
sql = ddl.compile(dialect=self.w_engine.dialect)
sql = str(sql)
logger.debug("Executing SQL : '"+sql+"'")
ret = bool(self.w_engine.execute(sql))
self.renewMetaData()
return ret
def addColumn(self, tname, colname, coltype):
""" Add a column to a table
@param tname str: The table name
@param colname str: The column name
@param coltype str: The new column type
@return True if query success False if it fails
"""
newcol = self.createColumn(name=colname, type_ = coltype)
if tname not in self.meta.tables: #Useless ?
raise NameError("The table '"+tname+"' dont exist")
table = self.Table(tname)
ddl = AddColumn(table, newcol)
sql = ddl.compile(dialect=self.w_engine.dialect)
sql = str(sql)
logger.debug("Executing SQL : '"+sql+"'")
ret = bool(self.wconn.execute(sql))
self.renewMetaData()
return ret
def alterColumn(self, tname, colname, col_newtype):
""" Change the type of a column
@param tname str: The table name
@param colname str: The column name
@param col_newtype str: The column new type
@return True if query successs False if it fails
"""
if self.wEngineFamily == 'sqlite':
raise NotImplementedError('AlterColumn not yet implemented for sqlite engines')
col = self.createColumn(name=colname, type_=col_newtype)
table = self.Table(tname)
typepref = 'TYPE ' if self.wEngineFamily == 'postgresql' else ''
query = 'ALTER TABLE %s ALTER COLUMN %s %s'%(table.name, col.name, typepref+col.type)
logger.debug("Executing SQL : '"+query+"'")
ret = bool(self.wconn.execute(query))
self.renewMetaData()
return ret
def _debug__printSchema(self):
""" Debug function to print the db schema """
print(self.meta)
for tname in self.meta.tables:
self._debug__printTable(tname)
def _debug__printTable(self, tname):
t = self.meta.tables[tname]
tstr = 'Table : "'+tname+'" :\n'
for c in t.c:
tstr += '\t\t"'+c.name+'"('+str(c.type)+') \n'
print(tstr)