diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-02-16 16:43:42 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-02-16 16:43:42 -0500 |
| commit | 198a900bd19dd6ef21f642dccfc8f5bd3321181a (patch) | |
| tree | bc2c34c169a29a706e04f922618a1eaef536d4bb | |
| parent | 937e81be16454b16f57f09603e7215d86be0b130 (diff) | |
| parent | e47f99450378a7b1ea29a8493c0692bcf8669da3 (diff) | |
| download | sqlalchemy-198a900bd19dd6ef21f642dccfc8f5bd3321181a.tar.gz | |
Merge branch 'master' of https://github.com/eblume/sqlalchemy into t
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/sqlite/__init__.py | 16 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/sqlite/base.py | 303 | ||||
| -rw-r--r-- | test/dialect/test_sqlite.py | 68 | ||||
| -rw-r--r-- | test/engine/test_reflection.py | 10 |
5 files changed, 237 insertions, 161 deletions
diff --git a/.gitignore b/.gitignore index ba308575e..c2fa6b203 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc *.pyo +*.egg /build/ /dist/ /doc/build/output/ diff --git a/lib/sqlalchemy/dialects/sqlite/__init__.py b/lib/sqlalchemy/dialects/sqlite/__init__.py index a9b23575b..80846c9ec 100644 --- a/lib/sqlalchemy/dialects/sqlite/__init__.py +++ b/lib/sqlalchemy/dialects/sqlite/__init__.py @@ -9,13 +9,11 @@ from sqlalchemy.dialects.sqlite import base, pysqlite # default dialect base.dialect = pysqlite.dialect - -from sqlalchemy.dialects.sqlite.base import \ - BLOB, BOOLEAN, CHAR, DATE, DATETIME, DECIMAL, FLOAT, INTEGER, REAL,\ - NUMERIC, SMALLINT, TEXT, TIME, TIMESTAMP, VARCHAR, dialect - -__all__ = ( - 'BLOB', 'BOOLEAN', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'FLOAT', - 'INTEGER', 'NUMERIC', 'SMALLINT', 'TEXT', 'TIME', 'TIMESTAMP', 'VARCHAR', - 'REAL', 'dialect' +from sqlalchemy.dialects.sqlite.base import ( + BLOB, BOOLEAN, CHAR, DATE, DATETIME, DECIMAL, FLOAT, INTEGER, REAL, + NUMERIC, SMALLINT, TEXT, TIME, TIMESTAMP, VARCHAR, dialect, ) + +__all__ = ('BLOB', 'BOOLEAN', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', + 'FLOAT', 'INTEGER', 'NUMERIC', 'SMALLINT', 'TEXT', 'TIME', + 'TIMESTAMP', 'VARCHAR', 'REAL', 'dialect') diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index 4e1223eea..fb4657675 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -12,15 +12,15 @@ Date and Time Types ------------------- -SQLite does not have built-in DATE, TIME, or DATETIME types, and pysqlite -does not provide out of the box functionality for translating values between -Python `datetime` objects and a SQLite-supported format. SQLAlchemy's own +SQLite does not have built-in DATE, TIME, or DATETIME types, and pysqlite does +not provide out of the box functionality for translating values between Python +`datetime` objects and a SQLite-supported format. SQLAlchemy's own :class:`~sqlalchemy.types.DateTime` and related types provide date formatting and parsing functionality when SQlite is used. The implementation classes are :class:`~.sqlite.DATETIME`, :class:`~.sqlite.DATE` and :class:`~.sqlite.TIME`. These types represent dates and times as ISO formatted strings, which also -nicely support ordering. There's no reliance on typical "libc" internals -for these functions so historical dates are fully supported. +nicely support ordering. There's no reliance on typical "libc" internals for +these functions so historical dates are fully supported. Auto Incrementing Behavior -------------------------- @@ -30,15 +30,15 @@ Background on SQLite's autoincrement is at: http://sqlite.org/autoinc.html Two things to note: * The AUTOINCREMENT keyword is **not** required for SQLite tables to - generate primary key values automatically. AUTOINCREMENT only means that - the algorithm used to generate ROWID values should be slightly different. + generate primary key values automatically. AUTOINCREMENT only means that the + algorithm used to generate ROWID values should be slightly different. * SQLite does **not** generate primary key (i.e. ROWID) values, even for one column, if the table has a composite (i.e. multi-column) primary key. This is regardless of the AUTOINCREMENT keyword being present or not. -To specifically render the AUTOINCREMENT keyword on the primary key -column when rendering DDL, add the flag ``sqlite_autoincrement=True`` -to the Table construct:: +To specifically render the AUTOINCREMENT keyword on the primary key column when +rendering DDL, add the flag ``sqlite_autoincrement=True`` to the Table +construct:: Table('sometable', metadata, Column('id', Integer, primary_key=True), @@ -47,47 +47,46 @@ to the Table construct:: Transaction Isolation Level --------------------------- -:func:`.create_engine` accepts an ``isolation_level`` parameter which -results in the command ``PRAGMA read_uncommitted <level>`` being invoked for -every new connection. Valid values for this parameter are ``SERIALIZABLE`` -and ``READ UNCOMMITTED`` corresponding to a value of 0 and 1, respectively. -See the section :ref:`pysqlite_serializable` for an important workaround -when using serializable isolation with Pysqlite. +:func:`.create_engine` accepts an ``isolation_level`` parameter which results +in the command ``PRAGMA read_uncommitted <level>`` being invoked for every new +connection. Valid values for this parameter are ``SERIALIZABLE`` and ``READ +UNCOMMITTED`` corresponding to a value of 0 and 1, respectively. See the +section :ref:`pysqlite_serializable` for an important workaround when using +serializable isolation with Pysqlite. Database Locking Behavior / Concurrency --------------------------------------- -Note that SQLite is not designed for a high level of concurrency. The -database itself, being a file, is locked completely during write operations -and within transactions, meaning exactly one connection has exclusive access -to the database during this period - all other connections will be blocked -during this time. +Note that SQLite is not designed for a high level of concurrency. The database +itself, being a file, is locked completely during write operations and within +transactions, meaning exactly one connection has exclusive access to the +database during this period - all other connections will be blocked during this +time. The Python DBAPI specification also calls for a connection model that is always -in a transaction; there is no BEGIN method, only commit and rollback. This +in a transaction; there is no BEGIN method, only commit and rollback. This implies that a SQLite DBAPI driver would technically allow only serialized -access to a particular database file at all times. The pysqlite driver -attempts to ameliorate this by deferring the actual BEGIN statement until -the first DML (INSERT, UPDATE, or DELETE) is received within a -transaction. While this breaks serializable isolation, it at least delays -the exclusive locking inherent in SQLite's design. - -SQLAlchemy's default mode of usage with the ORM is known -as "autocommit=False", which means the moment the :class:`.Session` begins to -be used, a transaction is begun. As the :class:`.Session` is used, the -autoflush feature, also on by default, will flush out pending changes to the -database before each query. The effect of this is that a :class:`.Session` -used in its default mode will often emit DML early on, long before the -transaction is actually committed. This again will have the effect of -serializing access to the SQLite database. If highly concurrent reads are -desired against the SQLite database, it is advised that the autoflush feature -be disabled, and potentially even that autocommit be re-enabled, which has -the effect of each SQL statement and flush committing changes immediately. - -For more information on SQLite's lack of concurrency by design, please -see `Situations Where Another RDBMS May Work Better - High -Concurrency <http://www.sqlite.org/whentouse.html>`_ near the bottom of -the page. +access to a particular database file at all times. The pysqlite driver attempts +to ameliorate this by deferring the actual BEGIN statement until the first DML +(INSERT, UPDATE, or DELETE) is received within a transaction. While this breaks +serializable isolation, it at least delays the exclusive locking inherent in +SQLite's design. + +SQLAlchemy's default mode of usage with the ORM is known as "autocommit=False", +which means the moment the :class:`.Session` begins to be used, a transaction +is begun. As the :class:`.Session` is used, the autoflush feature, also on by +default, will flush out pending changes to the database before each query. The +effect of this is that a :class:`.Session` used in its default mode will often +emit DML early on, long before the transaction is actually committed. This +again will have the effect of serializing access to the SQLite database. If +highly concurrent reads are desired against the SQLite database, it is advised +that the autoflush feature be disabled, and potentially even that autocommit be +re-enabled, which has the effect of each SQL statement and flush committing +changes immediately. + +For more information on SQLite's lack of concurrency by design, please see +`Situations Where Another RDBMS May Work Better - High Concurrency +<http://www.sqlite.org/whentouse.html>`_ near the bottom of the page. .. _sqlite_foreign_keys: @@ -95,8 +94,8 @@ Foreign Key Support ------------------- SQLite supports FOREIGN KEY syntax when emitting CREATE statements for tables, -however by default these constraints have no effect on the operation -of the table. +however by default these constraints have no effect on the operation of the +table. Constraint checking on SQLite has three prerequisites: @@ -106,8 +105,8 @@ Constraint checking on SQLite has three prerequisites: * The ``PRAGMA foreign_keys = ON`` statement must be emitted on all connections before use. -SQLAlchemy allows for the ``PRAGMA`` statement to be emitted automatically -for new connections through the usage of events:: +SQLAlchemy allows for the ``PRAGMA`` statement to be emitted automatically for +new connections through the usage of events:: from sqlalchemy.engine import Engine from sqlalchemy import event @@ -120,8 +119,8 @@ for new connections through the usage of events:: .. seealso:: - `SQLite Foreign Key Support <http://www.sqlite.org/foreignkeys.html>`_ - - on the SQLite web site. + `SQLite Foreign Key Support <http://www.sqlite.org/foreignkeys.html>`_ - on + the SQLite web site. :ref:`event_toplevel` - SQLAlchemy event API. @@ -130,16 +129,15 @@ for new connections through the usage of events:: import datetime import re +from ... import processors from ... import sql, exc -from ...engine import default, reflection from ... import types as sqltypes, schema as sa_schema from ... import util +from ...engine import default, reflection from ...sql import compiler -from ... import processors -from ...types import BIGINT, BLOB, BOOLEAN, CHAR,\ - DECIMAL, FLOAT, REAL, INTEGER, NUMERIC, SMALLINT, TEXT,\ - TIMESTAMP, VARCHAR +from ...types import (BLOB, BOOLEAN, CHAR, DATE, DECIMAL, FLOAT, INTEGER, REAL, + NUMERIC, SMALLINT, TEXT, TIMESTAMP, VARCHAR) class _DateTimeMixin(object): @@ -190,14 +188,14 @@ class DATETIME(_DateTimeMixin, sqltypes.DateTime): regexp=r"(\d+)/(\d+)/(\d+) (\d+)-(\d+)-(\d+)" ) - :param storage_format: format string which will be applied to the - dict with keys year, month, day, hour, minute, second, and microsecond. + :param storage_format: format string which will be applied to the dict with + keys year, month, day, hour, minute, second, and microsecond. - :param regexp: regular expression which will be applied to - incoming result rows. If the regexp contains named groups, the - resulting match dict is applied to the Python datetime() constructor - as keyword arguments. Otherwise, if positional groups are used, the - the datetime() constructor is called with positional arguments via + :param regexp: regular expression which will be applied to incoming result + rows. If the regexp contains named groups, the resulting match dict is + applied to the Python datetime() constructor as keyword arguments. + Otherwise, if positional groups are used, the the datetime() constructor + is called with positional arguments via ``*map(int, match_obj.groups(0))``. """ @@ -219,7 +217,6 @@ class DATETIME(_DateTimeMixin, sqltypes.DateTime): "%(hour)02d:%(minute)02d:%(second)02d" ) - def bind_processor(self, dialect): datetime_datetime = datetime.datetime datetime_date = datetime.date @@ -344,15 +341,14 @@ class TIME(_DateTimeMixin, sqltypes.Time): regexp=re.compile("(\d+)-(\d+)-(\d+)-(?:-(\d+))?") ) - :param storage_format: format string which will be applied to the - dict with keys hour, minute, second, and microsecond. + :param storage_format: format string which will be applied to the dict with + keys hour, minute, second, and microsecond. - :param regexp: regular expression which will be applied to - incoming result rows. If the regexp contains named groups, the - resulting match dict is applied to the Python time() constructor - as keyword arguments. Otherwise, if positional groups are used, the - the time() constructor is called with positional arguments via - ``*map(int, match_obj.groups(0))``. + :param regexp: regular expression which will be applied to incoming result + rows. If the regexp contains named groups, the resulting match dict is + applied to the Python time() constructor as keyword arguments. Otherwise, + if positional groups are used, the the time() constructor is called with + positional arguments via ``*map(int, match_obj.groups(0))``. """ _storage_format = "%(hour)02d:%(minute)02d:%(second)02d.%(microsecond)06d" @@ -407,6 +403,7 @@ ischema_names = { 'CHAR': sqltypes.CHAR, 'DATE': sqltypes.DATE, 'DATETIME': sqltypes.DATETIME, + 'DOUBLE': sqltypes.FLOAT, 'DECIMAL': sqltypes.DECIMAL, 'FLOAT': sqltypes.FLOAT, 'INT': sqltypes.INTEGER, @@ -427,17 +424,17 @@ class SQLiteCompiler(compiler.SQLCompiler): extract_map = util.update_copy( compiler.SQLCompiler.extract_map, { - 'month': '%m', - 'day': '%d', - 'year': '%Y', - 'second': '%S', - 'hour': '%H', - 'doy': '%j', - 'minute': '%M', - 'epoch': '%s', - 'dow': '%w', - 'week': '%W' - }) + 'month': '%m', + 'day': '%d', + 'year': '%Y', + 'second': '%S', + 'hour': '%H', + 'doy': '%j', + 'minute': '%M', + 'epoch': '%s', + 'dow': '%w', + 'week': '%W', + }) def visit_now_func(self, fn, **kw): return "CURRENT_TIMESTAMP" @@ -500,11 +497,11 @@ class SQLiteDDLCompiler(compiler.DDLCompiler): colspec += " NOT NULL" if (column.primary_key and - column.table.dialect_options['sqlite']['autoincrement'] and - len(column.table.primary_key.columns) == 1 and - issubclass(column.type._type_affinity, sqltypes.Integer) and - not column.foreign_keys): - colspec += " PRIMARY KEY AUTOINCREMENT" + column.table.dialect_options['sqlite']['autoincrement'] and + len(column.table.primary_key.columns) == 1 and + issubclass(column.type._type_affinity, sqltypes.Integer) and + not column.foreign_keys): + colspec += " PRIMARY KEY AUTOINCREMENT" return colspec @@ -514,14 +511,14 @@ class SQLiteDDLCompiler(compiler.DDLCompiler): # with the column itself. if len(constraint.columns) == 1: c = list(constraint)[0] - if c.primary_key and \ - c.table.dialect_options['sqlite']['autoincrement'] and \ - issubclass(c.type._type_affinity, sqltypes.Integer) and \ - not c.foreign_keys: + if (c.primary_key and + c.table.dialect_options['sqlite']['autoincrement'] and + issubclass(c.type._type_affinity, sqltypes.Integer) and + not c.foreign_keys): return None - return super(SQLiteDDLCompiler, self).\ - visit_primary_key_constraint(constraint) + return super(SQLiteDDLCompiler, self).visit_primary_key_constraint( + constraint) def visit_foreign_key_constraint(self, constraint): @@ -531,7 +528,8 @@ class SQLiteDDLCompiler(compiler.DDLCompiler): if local_table.schema != remote_table.schema: return None else: - return super(SQLiteDDLCompiler, self).visit_foreign_key_constraint(constraint) + return super(SQLiteDDLCompiler, self).visit_foreign_key_constraint( + constraint) def define_constraint_remote_table(self, constraint, table, preparer): """Format the remote table clause of a CREATE CONSTRAINT clause.""" @@ -539,8 +537,8 @@ class SQLiteDDLCompiler(compiler.DDLCompiler): return preparer.format_table(table, use_schema=False) def visit_create_index(self, create): - return super(SQLiteDDLCompiler, self).\ - visit_create_index(create, include_table_schema=False) + return super(SQLiteDDLCompiler, self).visit_create_index( + create, include_table_schema=False) class SQLiteTypeCompiler(compiler.GenericTypeCompiler): @@ -577,10 +575,10 @@ class SQLiteIdentifierPreparer(compiler.IdentifierPreparer): name = index.name result = self.quote(name, index.quote) if (not self.omit_schema and - use_schema and - getattr(index.table, "schema", None)): - result = self.quote_schema( - index.table.schema, index.table.quote_schema) + "." + result + use_schema and + getattr(index.table, "schema", None)): + result = self.quote_schema(index.table.schema, + index.table.quote_schema) + "." + result return result @@ -590,11 +588,9 @@ class SQLiteExecutionContext(default.DefaultExecutionContext): return self.execution_options.get("sqlite_raw_colnames", False) def _translate_colname(self, colname): - # adjust for dotted column names. SQLite - # in the case of UNION may store col names as - # "tablename.colname" - # in cursor.description - if not self._preserve_raw_colnames and "." in colname: + # adjust for dotted column names. SQLite in the case of UNION may store + # col names as "tablename.colname" in cursor.description + if not self._preserve_raw_colnames and "." in colname: return colname.split(".")[1], colname else: return colname, None @@ -636,30 +632,27 @@ class SQLiteDialect(default.DefaultDialect): default.DefaultDialect.__init__(self, **kwargs) self.isolation_level = isolation_level - # this flag used by pysqlite dialect, and perhaps others in the - # future, to indicate the driver is handling date/timestamp - # conversions (and perhaps datetime/time as well on some - # hypothetical driver ?) + # this flag used by pysqlite dialect, and perhaps others in the future, + # to indicate the driver is handling date/timestamp conversions (and + # perhaps datetime/time as well on some hypothetical driver ?) self.native_datetime = native_datetime if self.dbapi is not None: - self.supports_default_values = \ - self.dbapi.sqlite_version_info >= (3, 3, 8) - self.supports_cast = \ - self.dbapi.sqlite_version_info >= (3, 2, 3) - self.supports_multivalues_insert = \ - self.dbapi.sqlite_version_info >= (3, 7, 11) - # http://www.sqlite.org/releaselog/3_7_11.html - + self.supports_default_values = ( + self.dbapi.sqlite_version_info >= (3, 3, 8)) + self.supports_cast = ( + self.dbapi.sqlite_version_info >= (3, 2, 3)) + self.supports_multivalues_insert = ( + # http://www.sqlite.org/releaselog/3_7_11.html + self.dbapi.sqlite_version_info >= (3, 7, 11)) # see http://www.sqlalchemy.org/trac/ticket/2568 # as well as http://www.sqlite.org/src/info/600482d161 - self._broken_fk_pragma_quotes = \ - self.dbapi.sqlite_version_info < (3, 6, 14) - + self._broken_fk_pragma_quotes = ( + self.dbapi.sqlite_version_info < (3, 6, 14)) _isolation_lookup = { 'READ UNCOMMITTED': 1, - 'SERIALIZABLE': 0 + 'SERIALIZABLE': 0, } def set_isolation_level(self, connection, level): @@ -743,7 +736,7 @@ class SQLiteDialect(default.DefaultDialect): while not cursor.closed and cursor.fetchone() is not None: pass - return (row is not None) + return row is not None @reflection.cache def get_view_names(self, connection, schema=None, **kw): @@ -769,7 +762,6 @@ class SQLiteDialect(default.DefaultDialect): @reflection.cache def get_view_definition(self, connection, view_name, schema=None, **kw): - quote = self.identifier_preparer.quote_identifier if schema is not None: qschema = self.identifier_preparer.quote_identifier(schema) master = '%s.sqlite_master' % qschema @@ -807,33 +799,25 @@ class SQLiteDialect(default.DefaultDialect): rows = c.fetchall() columns = [] for row in rows: - (name, type_, nullable, default, primary_key) = \ - (row[1], row[2].upper(), not row[3], - row[4], row[5]) + (name, type_, nullable, default, primary_key) = ( + row[1], row[2].upper(), not row[3], row[4], row[5]) columns.append(self._get_column_info(name, type_, nullable, - default, primary_key)) + default, primary_key)) return columns - def _get_column_info(self, name, type_, nullable, - default, primary_key): - - match = re.match(r'(\w+)(\(.*?\))?', type_) + def _get_column_info(self, name, type_, nullable, default, primary_key): + match = re.match(r'([\w ]+)(\(.*?\))?', type_) if match: coltype = match.group(1) args = match.group(2) else: - coltype = "VARCHAR" + coltype = '' args = '' - try: - coltype = self.ischema_names[coltype] - if args is not None: - args = re.findall(r'(\d+)', args) - coltype = coltype(*[int(a) for a in args]) - except KeyError: - util.warn("Did not recognize type '%s' of column '%s'" % - (coltype, name)) - coltype = sqltypes.NullType() + coltype = self._resolve_col_affinity(coltype) + if args is not None: + args = re.findall(r'(\d+)', args) + coltype = coltype(*[int(a) for a in args]) if default is not None: default = util.text_type(default) @@ -844,9 +828,38 @@ class SQLiteDialect(default.DefaultDialect): 'nullable': nullable, 'default': default, 'autoincrement': default is None, - 'primary_key': primary_key + 'primary_key': primary_key, } + def _resolve_col_affinity(self, coltype): + """Return a data type from a reflected column, using affinity tules. + + SQLite's goal for universal compatability introduces some complexity + during reflection, as a column's defined type might not actually be a + type that SQLite understands - or indeed, my not be defined *at all*. + Internally, SQLite handles this with a 'data type affinity' for each + column definition, mapping to one of 'TEXT', 'NUMERIC', 'INTEGER', + 'REAL', or 'NONE' (raw bits). The algorithm that determines this is + listed in http://www.sqlite.org/datatype3.html section 2.1. + + This method allows SQLAlchemy to support that algorithm, while still + providing access to smarter reflection utilities by regcognizing + column definitions that SQLite only supports through affinity (like + DATE and DOUBLE). + """ + if coltype in self.ischema_names: + return self.ischema_names[coltype] + if 'INT' in coltype: + return sqltypes.INTEGER + elif 'CHAR' in coltype or 'CLOB' in coltype or 'TEXT' in coltype: + return sqltypes.TEXT, + elif 'BLOB' in coltype or not coltype: + return sqltypes.NullType + elif 'REAL' in coltype or 'FLOA' in coltype or 'DOUB' in coltype: + return sqltype.REAL + else: + return sqltypes.NUMERIC + @reflection.cache def get_pk_constraint(self, connection, table_name, schema=None, **kw): cols = self.get_columns(connection, table_name, schema, **kw) @@ -878,8 +891,8 @@ class SQLiteDialect(default.DefaultDialect): return fkeys def _parse_fk(self, fks, fkeys, numerical_id, rtbl, lcol, rcol): - # sqlite won't return rcol if the table - # was created with REFERENCES <tablename>, no col + # sqlite won't return rcol if the table was created with REFERENCES + # <tablename>, no col if rcol is None: rcol = lcol @@ -894,7 +907,7 @@ class SQLiteDialect(default.DefaultDialect): 'constrained_columns': [], 'referred_schema': None, 'referred_table': rtbl, - 'referred_columns': [] + 'referred_columns': [], } fkeys.append(fk) fks[numerical_id] = fk diff --git a/test/dialect/test_sqlite.py b/test/dialect/test_sqlite.py index 1b424e6c8..319b708e7 100644 --- a/test/dialect/test_sqlite.py +++ b/test/dialect/test_sqlite.py @@ -1,10 +1,12 @@ #!coding: utf-8 """SQLite-specific tests.""" +import os +from collections import Counter +import datetime from sqlalchemy.testing import eq_, assert_raises, \ assert_raises_message -import datetime from sqlalchemy import Table, String, select, Text, CHAR, bindparam, Column,\ Unicode, Date, MetaData, UnicodeText, Time, Integer, TIMESTAMP, \ Boolean, func, NUMERIC, DateTime, extract, ForeignKey, text, Numeric,\ @@ -18,7 +20,6 @@ from sqlalchemy.engine.url import make_url from sqlalchemy.testing import fixtures, AssertsCompiledSQL, \ AssertsExecutionResults, engines from sqlalchemy import testing -import os from sqlalchemy.schema import CreateTable class TestTypes(fixtures.TestBase, AssertsExecutionResults): @@ -29,7 +30,7 @@ class TestTypes(fixtures.TestBase, AssertsExecutionResults): """Test that the boolean only treats 1 as True """ - + meta = MetaData(testing.db) t = Table('bool_table', meta, Column('id', Integer, primary_key=True), Column('boo', @@ -227,8 +228,8 @@ class TestTypes(fixtures.TestBase, AssertsExecutionResults): ) t.create() t2 = Table('t', MetaData(), autoload=True, autoload_with=testing.db) - assert isinstance(t2.c.x.type, sqltypes.NullType) - assert isinstance(t2.c.y.type, sqltypes.NullType) + assert isinstance(t2.c.x.type, sqltypes.Numeric) + assert isinstance(t2.c.y.type, sqltypes.Numeric) class DateTimeTest(fixtures.TestBase, AssertsCompiledSQL): @@ -1022,3 +1023,60 @@ class ReflectFKConstraintTest(fixtures.TestBase): set([con.name for con in c.constraints]), set([None, None]) ) + + +class ColumnTypeAffinityReflectionTest(fixtures.TestBase): + """Tests on data type affinities for SQLite during relection. + + See http://www.sqlite.org/datatype3.html - section 2. + """ + __only_on__ = 'sqlite' + + def setup(self): + testing.db.execute(""" + CREATE TABLE a ( + "id" INTEGER PRIMARY KEY, + "foo" DOUBLE, + "bar" DECIMAL(19,4), + "baz" VARCHAR(200), + "boff", + "biff" LONGTEXT + )""") + # These example names come from section 2.2 of the datatype docs, + # after pruning out types which we convert to more convenient types + self.example_typenames_integer = ["TINYINT", "MEDIUMINT", "INT2", + "UNSIGNED BIG INT", "INT8"] + self.example_typenames_text = ["CHARACTER(20)", "CLOB", + "VARYING CHARACTER(70)", "NATIVE CHARACTER(70)"] + self.example_typenames_none = [""] + self.example_typenames_real = ["DOUBLE PRECISION"] + cols = ["i%d %s" % (n, t) for n, t in enumerate( + self.example_typenames_integer)] + cols += ["t%d %s" % (n, t) for n, t in enumerate( + self.example_typenames_text)] + cols += ["o%d %s" % (n, t) for n, t in enumerate( + self.example_typenames_none)] + cols += ["n%d %s" % (n, t) for n, t in enumerate( + self.example_typenames_real)] + cols = ','.join(cols) + testing.db.execute("CREATE TABLE b (%s)" % (cols,)) + + def teardown(self): + testing.db.execute("drop table a") + testing.db.execute("drop table b") + + def test_can_reflect_with_affinity(self): + "Test that 'affinity-types' don't break reflection outright." + meta = MetaData() + a = Table('a', meta, autoload=True, autoload_with=testing.db) + eq_(len(a.columns), 6) + + def test_correct_reflection_with_affinity(self): + "Test that coltypes are detected correctly from affinity rules." + meta = MetaData() + b = Table('b', meta, autoload=True, autoload_with=testing.db) + typecounts = Counter(type(col.type) for col in b.columns) + eq_(typecounts[sqltypes.INTEGER], len(self.example_typenames_integer)) + eq_(typecounts[sqltypes.TEXT], len(self.example_typenames_text)) + eq_(typecounts[sqltypes.NullType], len(self.example_typenames_none)) + eq_(typecounts[sqltypes.REAL], len(self.example_typenames_real)) diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py index 77e754475..8a1d7b70e 100644 --- a/test/engine/test_reflection.py +++ b/test/engine/test_reflection.py @@ -4,8 +4,8 @@ import unicodedata import sqlalchemy as sa from sqlalchemy import schema, events, event, inspect from sqlalchemy import MetaData, Integer, String -from sqlalchemy.testing import ComparesTables, \ - engines, AssertsCompiledSQL, fixtures +from sqlalchemy.testing import (ComparesTables, engines, AssertsCompiledSQL, + fixtures, skip) from sqlalchemy.testing.schema import Table, Column from sqlalchemy.testing import eq_, assert_raises, assert_raises_message from sqlalchemy import testing @@ -317,8 +317,14 @@ class ReflectionTest(fixtures.TestBase, ComparesTables): t2a = Table('test2', m2, autoload=True) assert t2a._autoincrement_column is t2a.c.id2 + @skip('sqlite') @testing.provide_metadata def test_unknown_types(self): + """Test the handling of unknown types for the given dialect. + + sqlite is skipped because it has special rules for unknown types using + 'affinity types' - this feature is tested in that dialect's test spec. + """ meta = self.metadata t = Table("test", meta, Column('foo', sa.DateTime)) |
