Show More
sqlite.py
230 lines
| 8.4 KiB
| text/x-python
|
PythonLexer
r1 | """ | |||
`SQLite`_ database specific implementations of changeset classes. | ||||
.. _`SQLite`: http://www.sqlite.org/ | ||||
""" | ||||
r4343 | try: # Python 3 | |||
from collections.abc import MutableMapping as DictMixin | ||||
except ImportError: # Python 2 | ||||
from UserDict import DictMixin | ||||
r1 | from copy import copy | |||
import re | ||||
from sqlalchemy.databases import sqlite as sa_base | ||||
r4343 | from sqlalchemy.schema import ForeignKeyConstraint | |||
r1 | from sqlalchemy.schema import UniqueConstraint | |||
from rhodecode.lib.dbmigrate.migrate import exceptions | ||||
from rhodecode.lib.dbmigrate.migrate.changeset import ansisql | ||||
r4342 | import sqlite3 | |||
r1 | ||||
SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler | ||||
class SQLiteCommon(object): | ||||
def _not_supported(self, op): | ||||
raise exceptions.NotSupportedError("SQLite does not support " | ||||
"%s; see http://www.sqlite.org/lang_altertable.html" % op) | ||||
class SQLiteHelper(SQLiteCommon): | ||||
r5165 | def _filter_columns(self, cols, table): | |||
"""Splits the string of columns and returns those only in the table. | ||||
:param cols: comma-delimited string of table columns | ||||
:param table: the table to check | ||||
:return: list of columns in the table | ||||
""" | ||||
columns = [] | ||||
for c in cols.split(","): | ||||
if c in table.columns: | ||||
# There was a bug in reflection of SQLite columns with | ||||
# reserved identifiers as names (SQLite can return them | ||||
# wrapped with double quotes), so strip double quotes. | ||||
columns.extend(c.strip(' "')) | ||||
return columns | ||||
def _get_constraints(self, table): | ||||
"""Retrieve information about existing constraints of the table | ||||
r1 | ||||
This feature is needed for recreate_table() to work properly. | ||||
""" | ||||
data = table.metadata.bind.execute( | ||||
"""SELECT sql | ||||
FROM sqlite_master | ||||
WHERE | ||||
type='table' AND | ||||
name=:table_name""", | ||||
table_name=table.name | ||||
).fetchone()[0] | ||||
UNIQUE_PATTERN = "CONSTRAINT (\w+) UNIQUE \(([^\)]+)\)" | ||||
constraints = [] | ||||
for name, cols in re.findall(UNIQUE_PATTERN, data): | ||||
# Filter out any columns that were dropped from the table. | ||||
r5165 | columns = self._filter_columns(cols, table) | |||
r1 | if columns: | |||
constraints.extend(UniqueConstraint(*columns, name=name)) | ||||
r5165 | ||||
FKEY_PATTERN = "CONSTRAINT (\w+) FOREIGN KEY \(([^\)]+)\)" | ||||
for name, cols in re.findall(FKEY_PATTERN, data): | ||||
# Filter out any columns that were dropped from the table. | ||||
columns = self._filter_columns(cols, table) | ||||
if columns: | ||||
constraints.extend(ForeignKeyConstraint(*columns, name=name)) | ||||
r1 | return constraints | |||
def recreate_table(self, table, column=None, delta=None, | ||||
r5165 | omit_constraints=None): | |||
r1 | table_name = self.preparer.format_table(table) | |||
# we remove all indexes so as not to have | ||||
# problems during copy and re-create | ||||
for index in table.indexes: | ||||
index.drop() | ||||
r5165 | # reflect existing constraints | |||
for constraint in self._get_constraints(table): | ||||
table.append_constraint(constraint) | ||||
# omit given constraints when creating a new table if required | ||||
r1 | table.constraints = set([ | |||
cons for cons in table.constraints | ||||
r5165 | if omit_constraints is None or cons.name not in omit_constraints | |||
r1 | ]) | |||
r5165 | ||||
# Use "PRAGMA legacy_alter_table = ON" with sqlite >= 3.26 when | ||||
# using "ALTER TABLE RENAME TO migration_tmp" to maintain legacy | ||||
# behavior. See: https://www.sqlite.org/src/info/ae9638e9c0ad0c36 | ||||
if self.connection.engine.dialect.server_version_info >= (3, 26): | ||||
r4342 | self.append('PRAGMA legacy_alter_table = ON') | |||
self.execute() | ||||
r1 | self.append('ALTER TABLE %s RENAME TO migration_tmp' % table_name) | |||
self.execute() | ||||
r5165 | if self.connection.engine.dialect.server_version_info >= (3, 26): | |||
r4342 | self.append('PRAGMA legacy_alter_table = OFF') | |||
self.execute() | ||||
r5165 | ||||
r1 | insertion_string = self._modify_table(table, column, delta) | |||
table.create(bind=self.connection) | ||||
self.append(insertion_string % {'table_name': table_name}) | ||||
self.execute() | ||||
self.append('DROP TABLE migration_tmp') | ||||
self.execute() | ||||
def visit_column(self, delta): | ||||
if isinstance(delta, DictMixin): | ||||
column = delta.result_column | ||||
table = self._to_table(delta.table) | ||||
else: | ||||
column = delta | ||||
table = self._to_table(column.table) | ||||
self.recreate_table(table,column,delta) | ||||
class SQLiteColumnGenerator(SQLiteSchemaGenerator, | ||||
ansisql.ANSIColumnGenerator, | ||||
# at the end so we get the normal | ||||
# visit_column by default | ||||
SQLiteHelper, | ||||
SQLiteCommon | ||||
): | ||||
"""SQLite ColumnGenerator""" | ||||
def _modify_table(self, table, column, delta): | ||||
columns = ' ,'.join(map( | ||||
self.preparer.format_column, | ||||
[c for c in table.columns if c.name!=column.name])) | ||||
return ('INSERT INTO %%(table_name)s (%(cols)s) ' | ||||
'SELECT %(cols)s from migration_tmp')%{'cols':columns} | ||||
def visit_column(self,column): | ||||
if column.foreign_keys: | ||||
SQLiteHelper.visit_column(self,column) | ||||
else: | ||||
super(SQLiteColumnGenerator,self).visit_column(column) | ||||
class SQLiteColumnDropper(SQLiteHelper, ansisql.ANSIColumnDropper): | ||||
"""SQLite ColumnDropper""" | ||||
def _modify_table(self, table, column, delta): | ||||
columns = ' ,'.join(map(self.preparer.format_column, table.columns)) | ||||
return 'INSERT INTO %(table_name)s SELECT ' + columns + \ | ||||
' from migration_tmp' | ||||
def visit_column(self,column): | ||||
# For SQLite, we *have* to remove the column here so the table | ||||
# is re-created properly. | ||||
column.remove_from_table(column.table,unset_table=False) | ||||
super(SQLiteColumnDropper,self).visit_column(column) | ||||
class SQLiteSchemaChanger(SQLiteHelper, ansisql.ANSISchemaChanger): | ||||
"""SQLite SchemaChanger""" | ||||
def _modify_table(self, table, column, delta): | ||||
return 'INSERT INTO %(table_name)s SELECT * from migration_tmp' | ||||
def visit_index(self, index): | ||||
"""Does not support ALTER INDEX""" | ||||
self._not_supported('ALTER INDEX') | ||||
class SQLiteConstraintGenerator(ansisql.ANSIConstraintGenerator, SQLiteHelper, SQLiteCommon): | ||||
def visit_migrate_primary_key_constraint(self, constraint): | ||||
tmpl = "CREATE UNIQUE INDEX %s ON %s ( %s )" | ||||
cols = ', '.join(map(self.preparer.format_column, constraint.columns)) | ||||
tname = self.preparer.format_table(constraint.table) | ||||
name = self.get_constraint_name(constraint) | ||||
msg = tmpl % (name, tname, cols) | ||||
self.append(msg) | ||||
self.execute() | ||||
def _modify_table(self, table, column, delta): | ||||
return 'INSERT INTO %(table_name)s SELECT * from migration_tmp' | ||||
def visit_migrate_foreign_key_constraint(self, *p, **k): | ||||
self.recreate_table(p[0].table) | ||||
def visit_migrate_unique_constraint(self, *p, **k): | ||||
self.recreate_table(p[0].table) | ||||
class SQLiteConstraintDropper(ansisql.ANSIColumnDropper, | ||||
SQLiteHelper, | ||||
ansisql.ANSIConstraintCommon): | ||||
def _modify_table(self, table, column, delta): | ||||
return 'INSERT INTO %(table_name)s SELECT * from migration_tmp' | ||||
def visit_migrate_primary_key_constraint(self, constraint): | ||||
tmpl = "DROP INDEX %s " | ||||
name = self.get_constraint_name(constraint) | ||||
msg = tmpl % (name) | ||||
self.append(msg) | ||||
self.execute() | ||||
def visit_migrate_foreign_key_constraint(self, *p, **k): | ||||
r5165 | #self._not_supported('ALTER TABLE DROP CONSTRAINT') | |||
self.recreate_table(p[0].table, omit_constraints=[p[0].name]) | ||||
r1 | ||||
def visit_migrate_check_constraint(self, *p, **k): | ||||
self._not_supported('ALTER TABLE DROP CONSTRAINT') | ||||
def visit_migrate_unique_constraint(self, *p, **k): | ||||
r5165 | self.recreate_table(p[0].table, omit_constraints=[p[0].name]) | |||
r1 | ||||
# TODO: technically primary key is a NOT NULL + UNIQUE constraint, should add NOT NULL to index | ||||
class SQLiteDialect(ansisql.ANSIDialect): | ||||
columngenerator = SQLiteColumnGenerator | ||||
columndropper = SQLiteColumnDropper | ||||
schemachanger = SQLiteSchemaChanger | ||||
constraintgenerator = SQLiteConstraintGenerator | ||||
constraintdropper = SQLiteConstraintDropper | ||||