diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-11-14 17:54:47 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-11-14 17:54:47 -0500 |
| commit | f252af2b21c5bafeaa30aabcf65dfed9b5c01093 (patch) | |
| tree | dd5bf4f56ac68d78edfcb37a9c0c3c380c8ef6a8 /lib | |
| parent | 9d7158a2c3869ad7a1ab07d3a41e831f6806a68c (diff) | |
| parent | 06bf218ed37ca780bc4de2ceb47769c84de70ba1 (diff) | |
| download | sqlalchemy-f252af2b21c5bafeaa30aabcf65dfed9b5c01093.tar.gz | |
merge tip
Diffstat (limited to 'lib')
49 files changed, 1444 insertions, 608 deletions
diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py index cb4e8e10b..5eea53ac6 100644 --- a/lib/sqlalchemy/__init__.py +++ b/lib/sqlalchemy/__init__.py @@ -43,6 +43,7 @@ from sqlalchemy.sql import ( subquery, text, tuple_, + type_coerce, union, union_all, update, @@ -114,6 +115,6 @@ from sqlalchemy.engine import create_engine, engine_from_config __all__ = sorted(name for name, obj in locals().items() if not (name.startswith('_') or inspect.ismodule(obj))) -__version__ = '0.6.5' +__version__ = '0.6.6' del inspect, sys diff --git a/lib/sqlalchemy/cextension/resultproxy.c b/lib/sqlalchemy/cextension/resultproxy.c index 7404b9ed2..73e127345 100644 --- a/lib/sqlalchemy/cextension/resultproxy.c +++ b/lib/sqlalchemy/cextension/resultproxy.c @@ -327,6 +327,12 @@ BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key) } static PyObject * +BaseRowProxy_getitem(PyObject *self, Py_ssize_t i) +{ + return BaseRowProxy_subscript((BaseRowProxy*)self, PyInt_FromSsize_t(i)); +} + +static PyObject * BaseRowProxy_getattro(BaseRowProxy *self, PyObject *name) { PyObject *tmp; @@ -506,7 +512,7 @@ static PySequenceMethods BaseRowProxy_as_sequence = { (lenfunc)BaseRowProxy_length, /* sq_length */ 0, /* sq_concat */ 0, /* sq_repeat */ - 0, /* sq_item */ + (ssizeargfunc)BaseRowProxy_getitem, /* sq_item */ 0, /* sq_slice */ 0, /* sq_ass_item */ 0, /* sq_ass_slice */ diff --git a/lib/sqlalchemy/dialects/informix/base.py b/lib/sqlalchemy/dialects/informix/base.py index 242b8a328..9aa23173b 100644 --- a/lib/sqlalchemy/dialects/informix/base.py +++ b/lib/sqlalchemy/dialects/informix/base.py @@ -7,7 +7,7 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php """Support for the Informix database. -This dialect is *not* tested on SQLAlchemy 0.6. +This dialect is mostly functional as of SQLAlchemy 0.6.5. """ @@ -16,7 +16,7 @@ This dialect is *not* tested on SQLAlchemy 0.6. import datetime from sqlalchemy import sql, schema, exc, pool, util -from sqlalchemy.sql import compiler +from sqlalchemy.sql import compiler, text from sqlalchemy.engine import default, reflection from sqlalchemy import types as sqltypes @@ -47,9 +47,9 @@ class InfoTime(sqltypes.Time): return value return process - colspecs = { sqltypes.DateTime : InfoDateTime, + sqltypes.TIMESTAMP: InfoDateTime, sqltypes.Time: InfoTime, } @@ -85,6 +85,9 @@ class InfoTypeCompiler(compiler.GenericTypeCompiler): def visit_TIME(self, type_): return "DATETIME HOUR TO SECOND" + def visit_TIMESTAMP(self, type_): + return "DATETIME YEAR TO SECOND" + def visit_large_binary(self, type_): return "BYTE" @@ -92,17 +95,16 @@ class InfoTypeCompiler(compiler.GenericTypeCompiler): return "SMALLINT" class InfoSQLCompiler(compiler.SQLCompiler): - def default_from(self): return " from systables where tabname = 'systables' " def get_select_precolumns(self, select): - s = select._distinct and "DISTINCT " or "" - # only has limit + s = "" + if select._offset: + s += "SKIP %s " % select._offset if select._limit: - s += " FIRST %s " % select._limit - else: - s += "" + s += "FIRST %s " % select._limit + s += select._distinct and "DISTINCT " or "" return s def visit_select(self, select, asfrom=False, parens=True, **kw): @@ -114,8 +116,6 @@ class InfoSQLCompiler(compiler.SQLCompiler): return text def limit_clause(self, select): - if select._offset is not None and select._offset > 0: - raise NotImplementedError("Informix does not support OFFSET") return "" def visit_function(self, func, **kw): @@ -128,14 +128,32 @@ class InfoSQLCompiler(compiler.SQLCompiler): else: return compiler.SQLCompiler.visit_function(self, func, **kw) + def visit_mod(self, binary, **kw): + return "MOD(%s, %s)" % (self.process(binary.left), self.process(binary.right)) + class InfoDDLCompiler(compiler.DDLCompiler): - def get_column_specification(self, column, first_pk=False): + + def visit_add_constraint(self, create): + preparer = self.preparer + return "ALTER TABLE %s ADD CONSTRAINT %s" % ( + self.preparer.format_table(create.element.table), + self.process(create.element) + ) + + def get_column_specification(self, column, **kw): colspec = self.preparer.format_column(column) - if column.primary_key and \ - len(column.foreign_keys)==0 and \ - column.autoincrement and \ - isinstance(column.type, sqltypes.Integer) and first_pk: + first = None + if column.primary_key and column.autoincrement: + try: + first = [c for c in column.table.primary_key.columns + if (c.autoincrement and + isinstance(c.type, sqltypes.Integer) and + not c.foreign_keys)].pop(0) + except IndexError: + pass + + if column is first: colspec += " SERIAL" else: colspec += " " + self.dialect.type_compiler.process(column.type) @@ -148,18 +166,53 @@ class InfoDDLCompiler(compiler.DDLCompiler): return colspec + def get_column_default_string(self, column): + if (isinstance(column.server_default, schema.DefaultClause) and + isinstance(column.server_default.arg, basestring)): + if isinstance(column.type, (sqltypes.Integer, sqltypes.Numeric)): + return self.sql_compiler.process(text(column.server_default.arg)) + + return super(InfoDDLCompiler, self).get_column_default_string(column) + + ### Informix wants the constraint name at the end, hence this ist c&p from sql/compiler.py + def visit_primary_key_constraint(self, constraint): + if len(constraint) == 0: + return '' + text = "PRIMARY KEY " + text += "(%s)" % ', '.join(self.preparer.quote(c.name, c.quote) + for c in constraint) + text += self.define_constraint_deferrability(constraint) + + if constraint.name is not None: + text += " CONSTRAINT %s" % self.preparer.format_constraint(constraint) + return text + + def visit_foreign_key_constraint(self, constraint): + preparer = self.dialect.identifier_preparer + remote_table = list(constraint._elements.values())[0].column.table + text = "FOREIGN KEY (%s) REFERENCES %s (%s)" % ( + ', '.join(preparer.quote(f.parent.name, f.parent.quote) + for f in constraint._elements.values()), + preparer.format_table(remote_table), + ', '.join(preparer.quote(f.column.name, f.column.quote) + for f in constraint._elements.values()) + ) + text += self.define_constraint_cascades(constraint) + text += self.define_constraint_deferrability(constraint) + + if constraint.name is not None: + text += " CONSTRAINT %s " % \ + preparer.format_constraint(constraint) + return text + + def visit_unique_constraint(self, constraint): + text = "UNIQUE (%s)" % (', '.join(self.preparer.quote(c.name, c.quote) for c in constraint)) + text += self.define_constraint_deferrability(constraint) + + if constraint.name is not None: + text += "CONSTRAINT %s " % self.preparer.format_constraint(constraint) + return text -class InfoIdentifierPreparer(compiler.IdentifierPreparer): - def __init__(self, dialect): - super(InfoIdentifierPreparer, self).\ - __init__(dialect, initial_quote="'") - - def format_constraint(self, constraint): - # informix doesnt support names for constraints - return '' - - def _requires_quotes(self, value): - return False class InformixDialect(default.DefaultDialect): name = 'informix' @@ -169,9 +222,13 @@ class InformixDialect(default.DefaultDialect): type_compiler = InfoTypeCompiler statement_compiler = InfoSQLCompiler ddl_compiler = InfoDDLCompiler - preparer = InfoIdentifierPreparer colspecs = colspecs ischema_names = ischema_names + default_paramstyle = 'qmark' + + def __init__(self, has_transactions=True, *args, **kwargs): + self.has_transactions = has_transactions + default.DefaultDialect.__init__(self, *args, **kwargs) def initialize(self, connection): super(InformixDialect, self).initialize(connection) @@ -182,43 +239,78 @@ class InformixDialect(default.DefaultDialect): else: self.max_identifier_length = 128 - def do_begin(self, connect): - cu = connect.cursor() + def do_begin(self, connection): + cu = connection.cursor() cu.execute('SET LOCK MODE TO WAIT') - #cu.execute('SET ISOLATION TO REPEATABLE READ') + if self.has_transactions: + cu.execute('SET ISOLATION TO REPEATABLE READ') + + def do_commit(self, connection): + if self.has_transactions: + connection.commit() + + def do_rollback(self, connection): + if self.has_transactions: + connection.rollback() + + def _get_table_names(self, connection, schema, type, **kw): + schema = schema or self.default_schema_name + s = "select tabname, owner from systables where owner=? and tabtype=?" + return [row[0] for row in connection.execute(s, schema, type)] @reflection.cache def get_table_names(self, connection, schema=None, **kw): - s = "select tabname from systables" + return self._get_table_names(connection, schema, 'T', **kw) + + @reflection.cache + def get_view_names(self, connection, schema=None, **kw): + return self._get_table_names(connection, schema, 'V', **kw) + + @reflection.cache + def get_schema_names(self, connection, **kw): + s = "select owner from systables" return [row[0] for row in connection.execute(s)] def has_table(self, connection, table_name, schema=None): + schema = schema or self.default_schema_name cursor = connection.execute( - """select tabname from systables where tabname=?""", - table_name.lower()) + """select tabname from systables where tabname=? and owner=?""", + table_name, schema) return cursor.first() is not None @reflection.cache def get_columns(self, connection, table_name, schema=None, **kw): + schema = schema or self.default_schema_name c = connection.execute( """select colname, coltype, collength, t3.default, t1.colno from syscolumns as t1 , systables as t2 , OUTER sysdefaults as t3 - where t1.tabid = t2.tabid and t2.tabname=? + where t1.tabid = t2.tabid and t2.tabname=? and t2.owner=? and t3.tabid = t2.tabid and t3.colno = t1.colno - order by t1.colno""", table.name.lower()) + order by t1.colno""", table_name, schema) + + primary_cols = self.get_primary_keys(connection, table_name, schema, **kw) + columns = [] + rows = c.fetchall() for name, colattr, collength, default, colno in rows: name = name.lower() - if include_columns and name not in include_columns: - continue + + autoincrement = False + primary_key = False + + if name in primary_cols: + primary_key = True # in 7.31, coltype = 0x000 # ^^-- column type # ^-- 1 not null, 0 null - nullable, coltype = divmod(colattr, 256) + not_nullable, coltype = divmod(colattr, 256) if coltype not in (0, 13) and default: default = default.split()[-1] + if coltype == 6: # Serial, mark as autoincrement + autoincrement = True + if coltype == 0 or coltype == 13: # char, varchar coltype = ischema_names[coltype](collength) if default: @@ -236,32 +328,34 @@ class InformixDialect(default.DefaultDialect): (coltype, name)) coltype = sqltypes.NULLTYPE - # TODO: nullability ?? - nullable = True - - column_info = dict(name=name, type=coltype, nullable=nullable, - default=default) + column_info = dict(name=name, type=coltype, nullable=not not_nullable, + default=default, autoincrement=autoincrement, + primary_key=primary_key) columns.append(column_info) return columns @reflection.cache def get_foreign_keys(self, connection, table_name, schema=None, **kw): - # FK + schema_sel = schema or self.default_schema_name c = connection.execute( - """select t1.constrname as cons_name , t1.constrtype as cons_type , - t4.colname as local_column , t7.tabname as remote_table , - t6.colname as remote_column + """select t1.constrname as cons_name, + t4.colname as local_column, t7.tabname as remote_table, + t6.colname as remote_column, t7.owner as remote_owner from sysconstraints as t1 , systables as t2 , sysindexes as t3 , syscolumns as t4 , sysreferences as t5 , syscolumns as t6 , systables as t7 , sysconstraints as t8 , sysindexes as t9 - where t1.tabid = t2.tabid and t2.tabname=? and t1.constrtype = 'R' + where t1.tabid = t2.tabid and t2.tabname=? and t2.owner=? and t1.constrtype = 'R' and t3.tabid = t2.tabid and t3.idxname = t1.idxname - and t4.tabid = t2.tabid and t4.colno = t3.part1 + and t4.tabid = t2.tabid and t4.colno in (t3.part1, t3.part2, t3.part3, + t3.part4, t3.part5, t3.part6, t3.part7, t3.part8, t3.part9, t3.part10, + t3.part11, t3.part11, t3.part12, t3.part13, t3.part4, t3.part15, t3.part16) and t5.constrid = t1.constrid and t8.constrid = t5.primary - and t6.tabid = t5.ptabid and t6.colno = t9.part1 and t9.idxname = + and t6.tabid = t5.ptabid and t6.colno in (t9.part1, t9.part2, t9.part3, + t9.part4, t9.part5, t9.part6, t9.part7, t9.part8, t9.part9, t9.part10, + t9.part11, t9.part11, t9.part12, t9.part13, t9.part4, t9.part15, t9.part16) and t9.idxname = t8.idxname - and t7.tabid = t5.ptabid""", table.name.lower()) + and t7.tabid = t5.ptabid""", table_name, schema_sel) def fkey_rec(): @@ -275,8 +369,9 @@ class InformixDialect(default.DefaultDialect): fkeys = util.defaultdict(fkey_rec) - for cons_name, cons_type, local_column, \ - remote_table, remote_column in rows: + rows = c.fetchall() + for cons_name, local_column, \ + remote_table, remote_column, remote_owner in rows: rec = fkeys[cons_name] rec['name'] = cons_name @@ -285,25 +380,91 @@ class InformixDialect(default.DefaultDialect): if not rec['referred_table']: rec['referred_table'] = remote_table + if schema is not None: + rec['referred_schema'] = remote_owner - local_cols.append(local_column) - remote_cols.append(remote_column) + if local_column not in local_cols: + local_cols.append(local_column) + if remote_column not in remote_cols: + remote_cols.append(remote_column) return fkeys.values() @reflection.cache def get_primary_keys(self, connection, table_name, schema=None, **kw): + schema = schema or self.default_schema_name + + # Select the column positions from sysindexes for sysconstraints + data = connection.execute( + """select t2.* + from systables as t1, sysindexes as t2, sysconstraints as t3 + where t1.tabid=t2.tabid and t1.tabname=? and t1.owner=? + and t2.idxname=t3.idxname and t3.constrtype='P'""", + table_name, schema + ).fetchall() + + colpositions = set() + + for row in data: + colpos = set([getattr(row, 'part%d' % x) for x in range(1,16)]) + colpositions |= colpos + + if not len(colpositions): + return [] + + # Select the column names using the columnpositions + # TODO: Maybe cache a bit of those col infos (eg select all colnames for one table) + place_holder = ','.join('?'*len(colpositions)) c = connection.execute( - """select t4.colname as local_column - from sysconstraints as t1 , systables as t2 , - sysindexes as t3 , syscolumns as t4 - where t1.tabid = t2.tabid and t2.tabname=? and t1.constrtype = 'P' - and t3.tabid = t2.tabid and t3.idxname = t1.idxname - and t4.tabid = t2.tabid and t4.colno = t3.part1""", - table.name.lower()) - return [r[0] for r in c.fetchall()] + """select t1.colname + from syscolumns as t1, systables as t2 + where t2.tabname=? and t1.tabid = t2.tabid and + t1.colno in (%s)""" % place_holder, + table_name, *colpositions + ).fetchall() + + return reduce(lambda x,y: list(x)+list(y), c, []) @reflection.cache def get_indexes(self, connection, table_name, schema, **kw): - # TODO - return [] + # TODO: schema... + c = connection.execute( + """select t1.* + from sysindexes as t1 , systables as t2 + where t1.tabid = t2.tabid and t2.tabname=?""", + table_name) + + indexes = [] + for row in c.fetchall(): + colnames = [getattr(row, 'part%d' % x) for x in range(1,16)] + colnames = [x for x in colnames if x] + place_holder = ','.join('?'*len(colnames)) + c = connection.execute( + """select t1.colname + from syscolumns as t1, systables as t2 + where t2.tabname=? and t1.tabid = t2.tabid and + t1.colno in (%s)""" % place_holder, + table_name, *colnames + ).fetchall() + c = reduce(lambda x,y: list(x)+list(y), c, []) + indexes.append({ + 'name': row.idxname, + 'unique': row.idxtype.lower() == 'u', + 'column_names': c + }) + return indexes + + @reflection.cache + def get_view_definition(self, connection, view_name, schema=None, **kw): + schema = schema or self.default_schema_name + c = connection.execute( + """select t1.viewtext + from sysviews as t1 , systables as t2 + where t1.tabid=t2.tabid and t2.tabname=? + and t2.owner=? order by seqno""", + view_name, schema).fetchall() + + return ''.join([row[0] for row in c]) + + def _get_default_schema_name(self, connection): + return connection.execute('select CURRENT_ROLE from systables').scalar() diff --git a/lib/sqlalchemy/dialects/informix/informixdb.py b/lib/sqlalchemy/dialects/informix/informixdb.py index 8edcc953b..f11c57bb6 100644 --- a/lib/sqlalchemy/dialects/informix/informixdb.py +++ b/lib/sqlalchemy/dialects/informix/informixdb.py @@ -1,16 +1,38 @@ +""" +Support for the informixdb DBAPI. + +informixdb is available at: + + http://informixdb.sourceforge.net/ + +Connecting +^^^^^^^^^^ + +Sample informix connection:: + + engine = create_engine('informix+informixdb://user:password@host/dbname') + +""" + +import re + from sqlalchemy.dialects.informix.base import InformixDialect from sqlalchemy.engine import default +VERSION_RE = re.compile(r'(\d+)\.(\d+)(.+\d+)') + class InformixExecutionContext_informixdb(default.DefaultExecutionContext): def post_exec(self): if self.isinsert: - self._lastrowid = [self.cursor.sqlerrd[1]] + self._lastrowid = self.cursor.sqlerrd[1] + + def get_lastrowid(self): + return self._lastrowid class InformixDialect_informixdb(InformixDialect): driver = 'informixdb' - default_paramstyle = 'qmark' - execution_context_cls = InformixExecutionContext_informixdb + execution_ctx_cls = InformixExecutionContext_informixdb @classmethod def dbapi(cls): @@ -31,13 +53,8 @@ class InformixDialect_informixdb(InformixDialect): def _get_server_version_info(self, connection): # http://informixdb.sourceforge.net/manual.html#inspecting-version-numbers - version = [] - for n in connection.connection.dbms_version.split('.'): - try: - version.append(int(n)) - except ValueError: - version.append(n) - return tuple(version) + v = VERSION_RE.split(connection.connection.dbms_version) + return (int(v[1]), int(v[2]), v[3]) def is_disconnect(self, e): if isinstance(e, self.dbapi.OperationalError): diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 95a5bf4c4..5c3b72647 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -114,6 +114,8 @@ Known Issues ------------ * No support for more than one ``IDENTITY`` column per table +* reflection of indexes does not work with versions older than + SQL Server 2005 """ import datetime, decimal, inspect, operator, sys, re @@ -755,20 +757,20 @@ class MSSQLCompiler(compiler.SQLCompiler): return None def visit_table(self, table, mssql_aliased=False, **kwargs): - if mssql_aliased: + if mssql_aliased is table: return super(MSSQLCompiler, self).visit_table(table, **kwargs) # alias schema-qualified tables alias = self._schema_aliased_table(table) if alias is not None: - return self.process(alias, mssql_aliased=True, **kwargs) + return self.process(alias, mssql_aliased=table, **kwargs) else: return super(MSSQLCompiler, self).visit_table(table, **kwargs) def visit_alias(self, alias, **kwargs): # translate for schema-qualified table aliases self.tablealiases[alias.original] = alias - kwargs['mssql_aliased'] = True + kwargs['mssql_aliased'] = alias.original return super(MSSQLCompiler, self).visit_alias(alias, **kwargs) def visit_extract(self, extract, **kw): @@ -1124,26 +1126,55 @@ class MSDialect(default.DefaultDialect): view_names = [r[0] for r in connection.execute(s)] return view_names - # The cursor reports it is closed after executing the sp. @reflection.cache def get_indexes(self, connection, tablename, schema=None, **kw): + # using system catalogs, don't support index reflection + # below MS 2005 + if self.server_version_info < MS_2005_VERSION: + return [] + current_schema = schema or self.default_schema_name - col_finder = re.compile("(\w+)") full_tname = "%s.%s" % (current_schema, tablename) - indexes = [] - s = sql.text("exec sp_helpindex '%s'" % full_tname) - rp = connection.execute(s) - if rp.closed: - # did not work for this setup. - return [] + + rp = connection.execute( + sql.text("select ind.index_id, ind.is_unique, ind.name " + "from sys.indexes as ind join sys.tables as tab on " + "ind.object_id=tab.object_id " + "join sys.schemas as sch on sch.schema_id=tab.schema_id " + "where tab.name = :tabname " + "and sch.name=:schname " + "and ind.is_primary_key=0", + bindparams=[ + sql.bindparam('tabname', tablename, sqltypes.Unicode), + sql.bindparam('schname', current_schema, sqltypes.Unicode) + ] + ) + ) + indexes = {} for row in rp: - if 'primary key' not in row['index_description']: - indexes.append({ - 'name' : row['index_name'], - 'column_names' : col_finder.findall(row['index_keys']), - 'unique': 'unique' in row['index_description'] - }) - return indexes + indexes[row['index_id']] = { + 'name':row['name'], + 'unique':row['is_unique'] == 1, + 'column_names':[] + } + rp = connection.execute( + sql.text("select ind_col.index_id, col.name from sys.columns as col " + "join sys.index_columns as ind_col on " + "ind_col.column_id=col.column_id " + "join sys.tables as tab on tab.object_id=col.object_id " + "join sys.schemas as sch on sch.schema_id=tab.schema_id " + "where tab.name=:tabname " + "and sch.name=:schname", + bindparams=[ + sql.bindparam('tabname', tablename, sqltypes.Unicode), + sql.bindparam('schname', current_schema, sqltypes.Unicode) + ]), + ) + for row in rp: + if row['index_id'] in indexes: + indexes[row['index_id']]['column_names'].append(row['name']) + + return indexes.values() @reflection.cache def get_view_definition(self, connection, viewname, schema=None, **kw): @@ -1207,13 +1238,13 @@ class MSDialect(default.DefaultDialect): "Did not recognize type '%s' of column '%s'" % (type, name)) coltype = sqltypes.NULLTYPE + else: + if issubclass(coltype, sqltypes.Numeric) and \ + coltype is not MSReal: + kwargs['scale'] = numericscale + kwargs['precision'] = numericprec - if issubclass(coltype, sqltypes.Numeric) and \ - coltype is not MSReal: - kwargs['scale'] = numericscale - kwargs['precision'] = numericprec - - coltype = coltype(**kwargs) + coltype = coltype(**kwargs) cdict = { 'name' : name, 'type' : coltype, diff --git a/lib/sqlalchemy/dialects/mssql/information_schema.py b/lib/sqlalchemy/dialects/mssql/information_schema.py index cd1606dbf..4dd6436cd 100644 --- a/lib/sqlalchemy/dialects/mssql/information_schema.py +++ b/lib/sqlalchemy/dialects/mssql/information_schema.py @@ -1,3 +1,5 @@ +# TODO: should be using the sys. catalog with SQL Server, not information schema + from sqlalchemy import Table, MetaData, Column, ForeignKey from sqlalchemy.types import String, Unicode, Integer, TypeDecorator diff --git a/lib/sqlalchemy/dialects/mssql/pymssql.py b/lib/sqlalchemy/dialects/mssql/pymssql.py index b6728c6b0..c5f471942 100644 --- a/lib/sqlalchemy/dialects/mssql/pymssql.py +++ b/lib/sqlalchemy/dialects/mssql/pymssql.py @@ -85,7 +85,9 @@ class MSDialect_pymssql(MSDialect): def create_connect_args(self, url): opts = url.translate_connect_args(username='user') opts.update(url.query) - opts.pop('port', None) + port = opts.pop('port', None) + if port and 'host' in opts: + opts['host'] = "%s:%s" % (opts['host'], port) return [[], opts] def is_disconnect(self, e): @@ -99,4 +101,4 @@ class MSDialect_pymssql(MSDialect): else: return False -dialect = MSDialect_pymssql
\ No newline at end of file +dialect = MSDialect_pymssql diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index a2d3748f3..660d201d1 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -2371,8 +2371,8 @@ class MySQLTableDefinitionParser(object): r'(?: +COLLATE +(?P<collate>[\w_]+))?' r'(?: +(?P<notnull>NOT NULL))?' r'(?: +DEFAULT +(?P<default>' - r'(?:NULL|\x27(?:\x27\x27|[^\x27])*\x27|\w+)' - r'(?:ON UPDATE \w+)?' + r'(?:NULL|\x27(?:\x27\x27|[^\x27])*\x27|\w+' + r'(?: +ON UPDATE \w+)?)' r'))?' r'(?: +(?P<autoincr>AUTO_INCREMENT))?' r'(?: +COMMENT +(P<comment>(?:\x27\x27|[^\x27])+))?' diff --git a/lib/sqlalchemy/dialects/mysql/zxjdbc.py b/lib/sqlalchemy/dialects/mysql/zxjdbc.py index 06d3e6616..0c0c39b67 100644 --- a/lib/sqlalchemy/dialects/mysql/zxjdbc.py +++ b/lib/sqlalchemy/dialects/mysql/zxjdbc.py @@ -92,7 +92,7 @@ class MySQLDialect_zxjdbc(ZxJDBCConnector, MySQLDialect): def _extract_error_code(self, exception): # e.g.: DBAPIError: (Error) Table 'test.u2' doesn't exist # [SQLCode: 1146], [SQLState: 42S02] 'DESCRIBE `u2`' () - m = re.compile(r"\[SQLCode\: (\d+)\]").search(str(exception.orig.args)) + m = re.compile(r"\[SQLCode\: (\d+)\]").search(str(exception.args)) c = m.group(1) if c: return int(c) diff --git a/lib/sqlalchemy/dialects/oracle/base.py b/lib/sqlalchemy/dialects/oracle/base.py index 0aa348953..256972696 100644 --- a/lib/sqlalchemy/dialects/oracle/base.py +++ b/lib/sqlalchemy/dialects/oracle/base.py @@ -640,9 +640,11 @@ class OracleDialect(default.DefaultDialect): def initialize(self, connection): super(OracleDialect, self).initialize(connection) - self.implicit_returning = self.server_version_info > (10, ) and \ - self.__dict__.get('implicit_returning', True) - + self.implicit_returning = self.__dict__.get( + 'implicit_returning', + self.server_version_info > (10, ) + ) + if self._is_oracle_8: self.colspecs = self.colspecs.copy() self.colspecs.pop(sqltypes.Interval) diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 89769b8c0..0d103cb0d 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -171,7 +171,7 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): """ __visit_name__ = 'ARRAY' - def __init__(self, item_type, mutable=True): + def __init__(self, item_type, mutable=True, as_tuple=False): """Construct an ARRAY. E.g.:: @@ -186,9 +186,14 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): ``ARRAY(ARRAY(Integer))`` or such. The type mapping figures out on the fly - :param mutable: Defaults to True: specify whether lists passed to this + :param mutable=True: Specify whether lists passed to this class should be considered mutable. If so, generic copy operations (typically used by the ORM) will shallow-copy values. + + :param as_tuple=False: Specify whether return results should be converted + to tuples from lists. DBAPIs such as psycopg2 return lists by default. + When tuples are returned, the results are hashable. This flag can only + be set to ``True`` when ``mutable`` is set to ``False``. (new in 0.6.5) """ if isinstance(item_type, ARRAY): @@ -198,7 +203,12 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): item_type = item_type() self.item_type = item_type self.mutable = mutable - + if mutable and as_tuple: + raise exc.ArgumentError( + "mutable must be set to False if as_tuple is True." + ) + self.as_tuple = as_tuple + def copy_value(self, value): if value is None: return None @@ -224,7 +234,8 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): def adapt(self, impltype): return impltype( self.item_type, - mutable=self.mutable + mutable=self.mutable, + as_tuple=self.as_tuple ) def bind_processor(self, dialect): @@ -252,19 +263,28 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): if item_proc: def convert_item(item): if isinstance(item, list): - return [convert_item(child) for child in item] + r = [convert_item(child) for child in item] + if self.as_tuple: + r = tuple(r) + return r else: return item_proc(item) else: def convert_item(item): if isinstance(item, list): - return [convert_item(child) for child in item] + r = [convert_item(child) for child in item] + if self.as_tuple: + r = tuple(r) + return r else: return item def process(value): if value is None: return value - return [convert_item(item) for item in value] + r = [convert_item(item) for item in value] + if self.as_tuple: + r = tuple(r) + return r return process PGArray = ARRAY @@ -1033,28 +1053,32 @@ class PGDialect(default.DefaultDialect): else: args = () - if attype in self.ischema_names: - coltype = self.ischema_names[attype] - elif attype in enums: - enum = enums[attype] - coltype = ENUM - if "." in attype: - kwargs['schema'], kwargs['name'] = attype.split('.') - else: - kwargs['name'] = attype - args = tuple(enum['labels']) - elif attype in domains: - domain = domains[attype] - if domain['attype'] in self.ischema_names: + while True: + if attype in self.ischema_names: + coltype = self.ischema_names[attype] + break + elif attype in enums: + enum = enums[attype] + coltype = ENUM + if "." in attype: + kwargs['schema'], kwargs['name'] = attype.split('.') + else: + kwargs['name'] = attype + args = tuple(enum['labels']) + break + elif attype in domains: + domain = domains[attype] + attype = domain['attype'] # A table can't override whether the domain is nullable. nullable = domain['nullable'] if domain['default'] and not default: # It can, however, override the default # value, but can't set it to null. default = domain['default'] - coltype = self.ischema_names[domain['attype']] - else: - coltype = None + continue + else: + coltype = None + break if coltype: coltype = coltype(*args, **kwargs) diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index b84b18e68..994904b6a 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -270,7 +270,21 @@ class SQLiteDDLCompiler(compiler.DDLCompiler): return super(SQLiteDDLCompiler, self).\ visit_primary_key_constraint(constraint) - + + def visit_foreign_key_constraint(self, constraint): + + local_table = constraint._elements.values()[0].parent.table + remote_table = list(constraint._elements.values())[0].column.table + + if local_table.schema != remote_table.schema: + return None + else: + 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.""" + + return preparer.format_table(table, use_schema=False) def visit_create_index(self, create): index = create.element diff --git a/lib/sqlalchemy/dialects/sqlite/pysqlite.py b/lib/sqlalchemy/dialects/sqlite/pysqlite.py index 575cb37f2..b2295f49b 100644 --- a/lib/sqlalchemy/dialects/sqlite/pysqlite.py +++ b/lib/sqlalchemy/dialects/sqlite/pysqlite.py @@ -68,12 +68,13 @@ pysqlite's driver does not. Additionally, SQLAlchemy does not at this time automatically render the "cast" syntax required for the freestanding functions "current_timestamp" and "current_date" to return datetime/date types natively. Unfortunately, pysqlite -does not provide the standard DBAPI types in `cursor.description`, +does not provide the standard DBAPI types in ``cursor.description``, leaving SQLAlchemy with no way to detect these types on the fly without expensive per-row type checks. -Usage of PARSE_DECLTYPES can be forced if one configures -"native_datetime=True" on create_engine():: +Keeping in mind that pysqlite's parsing option is not recommended, +nor should be necessary, for use with SQLAlchemy, usage of PARSE_DECLTYPES +can be forced if one configures "native_datetime=True" on create_engine():: engine = create_engine('sqlite://', connect_args={'detect_types': sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES}, diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index 43d3dd038..36b86cabf 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -278,14 +278,15 @@ def _coerce_config(configuration, prefix): for key in configuration if key.startswith(prefix)) for option, type_ in ( - ('convert_unicode', bool), + ('convert_unicode', util.bool_or_str('force')), ('pool_timeout', int), - ('echo', bool), - ('echo_pool', bool), + ('echo', util.bool_or_str('debug')), + ('echo_pool', util.bool_or_str('debug')), ('pool_recycle', int), ('pool_size', int), ('max_overflow', int), ('pool_threadlocal', bool), + ('use_native_unicode', bool), ): util.coerce_kw_type(options, option, type_) return options diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 79cadaea9..aa24a6529 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1069,7 +1069,7 @@ class Connection(Connectable): def _begin_impl(self): if self._echo: - self.engine.logger.info("BEGIN") + self.engine.logger.info("BEGIN (implicit)") try: self.engine.dialect.do_begin(self.connection) except Exception, e: @@ -2090,6 +2090,14 @@ class RowProxy(BaseRowProxy): def itervalues(self): return iter(self) +try: + # Register RowProxy with Sequence, + # so sequence protocol is implemented + from collections import Sequence + Sequence.register(RowProxy) +except ImportError: + pass + class ResultMetaData(object): """Handle cursor.description, applying additional info from an execution @@ -2249,7 +2257,7 @@ class ResultProxy(object): self.context = context self.dialect = context.dialect self.closed = False - self.cursor = context.cursor + self.cursor = self._saved_cursor = context.cursor self.connection = context.root_connection self._echo = self.connection._echo and \ context.engine._should_log_debug() @@ -2304,12 +2312,12 @@ class ResultProxy(object): regardless of database backend. """ - return self.cursor.lastrowid + return self._saved_cursor.lastrowid def _cursor_description(self): """May be overridden by subclasses.""" - return self.cursor.description + return self._saved_cursor.description def _autoclose(self): """called by the Connection to autoclose cursors that have no pending diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 390094c7d..13755d49a 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -565,7 +565,6 @@ class DefaultExecutionContext(base.ExecutionContext): in all cases. """ - return self.cursor.lastrowid def handle_dbapi_exception(self, e): diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index 003969f56..42ba226ee 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -26,7 +26,11 @@ class ArgumentError(SQLAlchemyError): class CircularDependencyError(SQLAlchemyError): """Raised by topological sorts when a circular dependency is detected""" - + def __init__(self, message, cycles, edges): + message += ": cycles: %r all edges: %r" % (cycles, edges) + SQLAlchemyError.__init__(self, message) + self.cycles = cycles + self.edges = edges class CompileError(SQLAlchemyError): """Raised when an error occurs during SQL compilation""" diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index 10b8fc2d7..3ae81a977 100755 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -358,10 +358,10 @@ and simply pass it to declarative classes:: Base.metadata.reflect(some_engine) class User(Base): - __table__ = metadata['user'] + __table__ = metadata.tables['user'] class Address(Base): - __table__ = metadata['address'] + __table__ = metadata.tables['address'] Some configuration schemes may find it more appropriate to use ``__table__``, such as those which already take advantage of the data-driven nature of @@ -589,13 +589,14 @@ keys, as a :class:`ForeignKey` itself contains references to columns which can't be properly recreated at this level. For columns that have foreign keys, as well as for the variety of mapper-level constructs that require destination-explicit context, the -:func:`~sqlalchemy.util.classproperty` decorator is provided so that +:func:`~.declared_attr` decorator (renamed from ``sqlalchemy.util.classproperty`` in 0.6.5) +is provided so that patterns common to many classes can be defined as callables:: - from sqlalchemy.util import classproperty + from sqlalchemy.ext.declarative import declared_attr class ReferenceAddressMixin(object): - @classproperty + @declared_attr def address_id(cls): return Column(Integer, ForeignKey('address.id')) @@ -608,14 +609,14 @@ point at which the ``User`` class is constructed, and the declarative extension can use the resulting :class:`Column` object as returned by the method without the need to copy it. -Columns generated by :func:`~sqlalchemy.util.classproperty` can also be +Columns generated by :func:`~.declared_attr` can also be referenced by ``__mapper_args__`` to a limited degree, currently by ``polymorphic_on`` and ``version_id_col``, by specifying the classdecorator itself into the dictionary - the declarative extension will resolve them at class construction time:: class MyMixin: - @classproperty + @declared_attr def type_(cls): return Column(String(50)) @@ -625,26 +626,23 @@ will resolve them at class construction time:: __tablename__='test' id = Column(Integer, primary_key=True) -.. note:: The usage of :func:`~sqlalchemy.util.classproperty` with mixin - columns is a new feature as of SQLAlchemy 0.6.2. - Mixing in Relationships ~~~~~~~~~~~~~~~~~~~~~~~ Relationships created by :func:`~sqlalchemy.orm.relationship` are provided with declarative mixin classes exclusively using the -:func:`~sqlalchemy.util.classproperty` approach, eliminating any ambiguity +:func:`.declared_attr` approach, eliminating any ambiguity which could arise when copying a relationship and its possibly column-bound contents. Below is an example which combines a foreign key column and a relationship so that two classes ``Foo`` and ``Bar`` can both be configured to reference a common target class via many-to-one:: class RefTargetMixin(object): - @classproperty + @declared_attr def target_id(cls): return Column('target_id', ForeignKey('target.id')) - @classproperty + @declared_attr def target(cls): return relationship("Target") @@ -667,20 +665,16 @@ To reference the mixin class in these expressions, use the given ``cls`` to get it's name:: class RefTargetMixin(object): - @classproperty + @declared_attr def target_id(cls): return Column('target_id', ForeignKey('target.id')) - @classproperty + @declared_attr def target(cls): return relationship("Target", primaryjoin="Target.id==%s.target_id" % cls.__name__ ) -.. note:: The usage of :func:`~sqlalchemy.util.classproperty` with mixin - relationships is a new feature as of SQLAlchemy 0.6.2. - - Mixing in deferred(), column_property(), etc. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -688,21 +682,18 @@ Like :func:`~sqlalchemy.orm.relationship`, all :class:`~sqlalchemy.orm.interfaces.MapperProperty` subclasses such as :func:`~sqlalchemy.orm.deferred`, :func:`~sqlalchemy.orm.column_property`, etc. ultimately involve references to columns, and therefore, when -used with declarative mixins, have the :func:`~sqlalchemy.util.classproperty` +used with declarative mixins, have the :func:`.declared_attr` requirement so that no reliance on copying is needed:: class SomethingMixin(object): - @classproperty + @declared_attr def dprop(cls): return deferred(Column(Integer)) class Something(Base, SomethingMixin): __tablename__ = "something" -.. note:: The usage of :func:`~sqlalchemy.util.classproperty` with mixin - mapper properties is a new feature as of SQLAlchemy 0.6.2. - Controlling table inheritance with mixins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -721,10 +712,10 @@ where you wanted to use that mixin in a single table inheritance hierarchy, you can explicitly specify ``__tablename__`` as ``None`` to indicate that the class should not have a table mapped:: - from sqlalchemy.util import classproperty + from sqlalchemy.ext.declarative import declared_attr class Tablename: - @classproperty + @declared_attr def __tablename__(cls): return cls.__name__.lower() @@ -748,11 +739,11 @@ has a mapped table. As an example, here's a mixin that will only allow single table inheritance:: - from sqlalchemy.util import classproperty + from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import has_inherited_table class Tablename: - @classproperty + @declared_attr def __tablename__(cls): if has_inherited_table(cls): return None @@ -772,11 +763,11 @@ table inheritance, you would need a slightly different mixin and use it on any joined table child classes in addition to their parent classes:: - from sqlalchemy.util import classproperty + from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import has_inherited_table class Tablename: - @classproperty + @declared_attr def __tablename__(cls): if (has_inherited_table(cls) and Tablename not in cls.__bases__): @@ -806,11 +797,11 @@ In the case of ``__table_args__`` or ``__mapper_args__`` specified with declarative mixins, you may want to combine some parameters from several mixins with those you wish to define on the class iteself. The -:func:`~sqlalchemy.util.classproperty` decorator can be used +:func:`.declared_attr` decorator can be used here to create user-defined collation routines that pull from multiple collections:: - from sqlalchemy.util import classproperty + from sqlalchemy.ext.declarative import declared_attr class MySQLSettings: __table_args__ = {'mysql_engine':'InnoDB'} @@ -821,7 +812,7 @@ from multiple collections:: class MyModel(Base,MySQLSettings,MyOtherMixin): __tablename__='my_model' - @classproperty + @declared_attr def __table_args__(self): args = dict() args.update(MySQLSettings.__table_args__) @@ -830,6 +821,81 @@ from multiple collections:: id = Column(Integer, primary_key=True) +Defining Indexes in Mixins +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you need to define a multi-column index that applies to all tables +that make use of a particular mixin, you will need to do this in a +metaclass as shown in the following example:: + + from sqlalchemy.ext.declarative import DeclarativeMeta + + class MyMixinMeta(DeclarativeMeta): + + def __init__(cls,*args,**kw): + if getattr(cls,'_decl_class_registry',None) is None: + return + super(MyMeta,cls).__init__(*args,**kw) + # Index creation done here + Index('test',cls.a,cls.b) + + class MyMixin(object): + __metaclass__=MyMixinMeta + a = Column(Integer) + b = Column(Integer) + + class MyModel(Base,MyMixin): + __tablename__ = 'atable' + c = Column(Integer,primary_key=True) + +Using multiple Mixins that require Metaclasses +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you end up in a situation where you need to use multiple mixins and +more than one of them uses a metaclass to, for example, create a +multi-column index, then you will need to create a metaclass that +correctly combines the actions of the other metaclasses. For example:: + + class MyMeta1(DeclarativeMeta): + + def __init__(cls,*args,**kw): + if getattr(cls,'_decl_class_registry',None) is None: + return + super(MyMeta1,cls).__init__(*args,**kw) + Index('ab',cls.a,cls.b) + + class MyMixin1(object): + __metaclass__=MyMeta1 + a = Column(Integer) + b = Column(Integer) + + class MyMeta2(DeclarativeMeta): + + def __init__(cls,*args,**kw): + if getattr(cls,'_decl_class_registry',None) is None: + return + super(MyMeta2,cls).__init__(*args,**kw) + Index('cd',cls.c,cls.d) + + class MyMixin2(object): + __metaclass__=MyMeta2 + c = Column(Integer) + d = Column(Integer) + + class CombinedMeta(MyMeta1,MyMeta2): + # This is needed to successfully combine + # two mixins which both have metaclasses + pass + + class MyModel(Base,MyMixin1,MyMixin2): + __tablename__ = 'awooooga' + __metaclass__ = CombinedMeta + z = Column(Integer,primary_key=True) + +For this reason, if a mixin requires a custom metaclass, this should +be mentioned in any documentation of that mixin to avoid confusion +later down the line. + Class Constructor ================= @@ -865,7 +931,7 @@ from sqlalchemy.orm.interfaces import MapperProperty from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty from sqlalchemy.orm.util import _is_mapped_class from sqlalchemy import util, exceptions -from sqlalchemy.sql import util as sql_util +from sqlalchemy.sql import util as sql_util, expression __all__ = 'declarative_base', 'synonym_for', \ @@ -907,56 +973,70 @@ def _as_declarative(cls, classname, dict_): tablename = None parent_columns = () + declarative_props = (declared_attr, util.classproperty) + for base in cls.__mro__: - if _is_mapped_class(base): + class_mapped = _is_mapped_class(base) + if class_mapped: parent_columns = base.__table__.c.keys() - else: - for name,obj in vars(base).items(): - if name == '__mapper_args__': - if not mapper_args: - mapper_args = cls.__mapper_args__ - elif name == '__tablename__': - if not tablename: - tablename = cls.__tablename__ - elif name == '__table_args__': - if not table_args: - table_args = cls.__table_args__ - if base is not cls: - inherited_table_args = True - elif base is not cls: - # we're a mixin. - - if isinstance(obj, Column): - if obj.foreign_keys: - raise exceptions.InvalidRequestError( - "Columns with foreign keys to other columns " - "must be declared as @classproperty callables " - "on declarative mixin classes. ") - if name not in dict_ and not ( - '__table__' in dict_ and - name in dict_['__table__'].c - ): - potential_columns[name] = \ - column_copies[obj] = \ - obj.copy() - column_copies[obj]._creation_order = \ - obj._creation_order - elif isinstance(obj, MapperProperty): + + for name,obj in vars(base).items(): + if name == '__mapper_args__': + if not mapper_args and ( + not class_mapped or + isinstance(obj, declarative_props) + ): + mapper_args = cls.__mapper_args__ + elif name == '__tablename__': + if not tablename and ( + not class_mapped or + isinstance(obj, declarative_props) + ): + tablename = cls.__tablename__ + elif name == '__table_args__': + if not table_args and ( + not class_mapped or + isinstance(obj, declarative_props) + ): + table_args = cls.__table_args__ + if base is not cls: + inherited_table_args = True + elif class_mapped: + continue + elif base is not cls: + # we're a mixin. + + if isinstance(obj, Column): + if obj.foreign_keys: raise exceptions.InvalidRequestError( - "Mapper properties (i.e. deferred," - "column_property(), relationship(), etc.) must " - "be declared as @classproperty callables " - "on declarative mixin classes.") - elif isinstance(obj, util.classproperty): - dict_[name] = ret = \ - column_copies[obj] = getattr(cls, name) - if isinstance(ret, (Column, MapperProperty)) and \ - ret.doc is None: - ret.doc = obj.__doc__ + "Columns with foreign keys to other columns " + "must be declared as @classproperty callables " + "on declarative mixin classes. ") + if name not in dict_ and not ( + '__table__' in dict_ and + (obj.name or name) in dict_['__table__'].c + ) and name not in potential_columns: + potential_columns[name] = \ + column_copies[obj] = \ + obj.copy() + column_copies[obj]._creation_order = \ + obj._creation_order + elif isinstance(obj, MapperProperty): + raise exceptions.InvalidRequestError( + "Mapper properties (i.e. deferred," + "column_property(), relationship(), etc.) must " + "be declared as @classproperty callables " + "on declarative mixin classes.") + elif isinstance(obj, declarative_props): + dict_[name] = ret = \ + column_copies[obj] = getattr(cls, name) + if isinstance(ret, (Column, MapperProperty)) and \ + ret.doc is None: + ret.doc = obj.__doc__ # apply inherited columns as we should for k, v in potential_columns.items(): - if tablename or k not in parent_columns: + if tablename or (v.name or k) not in parent_columns: dict_[k] = v if inherited_table_args and not tablename: @@ -967,12 +1047,19 @@ def _as_declarative(cls, classname, dict_): for k, v in mapper_args.iteritems(): mapper_args[k] = column_copies.get(v,v) + + if classname in cls._decl_class_registry: + util.warn("The classname %r is already in the registry of this" + " declarative base, mapped to %r" % ( + classname, + cls._decl_class_registry[classname] + )) cls._decl_class_registry[classname] = cls our_stuff = util.OrderedDict() for k in dict_: value = dict_[k] - if isinstance(value, util.classproperty): + if isinstance(value, declarative_props): value = getattr(cls, k) if (isinstance(value, tuple) and len(value) == 1 and @@ -1083,7 +1170,7 @@ def _as_declarative(cls, classname, dict_): "Can't place __table_args__ on an inherited class " "with no table." ) - + # add any columns declared here to the inherited table. for c in cols: if c.primary_key: @@ -1112,7 +1199,25 @@ def _as_declarative(cls, classname, dict_): set([c.key for c in inherited_table.c if c not in inherited_mapper._columntoproperty]) exclude_properties.difference_update([c.key for c in cols]) - + + # look through columns in the current mapper that + # are keyed to a propname different than the colname + # (if names were the same, we'd have popped it out above, + # in which case the mapper makes this combination). + # See if the superclass has a similar column property. + # If so, join them together. + for k, col in our_stuff.items(): + if not isinstance(col, expression.ColumnElement): + continue + if k in inherited_mapper._props: + p = inherited_mapper._props[k] + if isinstance(p, ColumnProperty): + # note here we place the superclass column + # first. this corresponds to the + # append() in mapper._configure_property(). + # change this ordering when we do [ticket:1892] + our_stuff[k] = p.columns + [col] + cls.__mapper__ = mapper_cls(cls, table, properties=our_stuff, @@ -1193,7 +1298,7 @@ def _deferred_relationship(cls, prop): return x except NameError, n: raise exceptions.InvalidRequestError( - "When compiling mapper %s, expression %r failed to " + "When initializing mapper %s, expression %r failed to " "locate a name (%r). If this is a class name, consider " "adding this relationship() to the %r class after " "both dependent classes have been defined." % @@ -1262,6 +1367,63 @@ def comparable_using(comparator_factory): return comparable_property(comparator_factory, fn) return decorate +class declared_attr(property): + """Mark a class-level method as representing the definition of + a mapped property or special declarative member name. + + .. note:: @declared_attr is available as + ``sqlalchemy.util.classproperty`` for SQLAlchemy versions + 0.6.2, 0.6.3, 0.6.4. + + @declared_attr turns the attribute into a scalar-like + property that can be invoked from the uninstantiated class. + Declarative treats attributes specifically marked with + @declared_attr as returning a construct that is specific + to mapping or declarative table configuration. The name + of the attribute is that of what the non-dynamic version + of the attribute would be. + + @declared_attr is more often than not applicable to mixins, + to define relationships that are to be applied to different + implementors of the class:: + + class ProvidesUser(object): + "A mixin that adds a 'user' relationship to classes." + + @declared_attr + def user(self): + return relationship("User") + + It also can be applied to mapped classes, such as to provide + a "polymorphic" scheme for inheritance:: + + class Employee(Base): + id = Column(Integer, primary_key=True) + type = Column(String(50), nullable=False) + + @declared_attr + def __tablename__(cls): + return cls.__name__.lower() + + @declared_attr + def __mapper_args__(cls): + if cls.__name__ == 'Employee': + return { + "polymorphic_on":cls.type, + "polymorphic_identity":"Employee" + } + else: + return {"polymorphic_identity":cls.__name__} + + """ + + def __init__(self, fget, *arg, **kw): + super(declared_attr, self).__init__(fget, *arg, **kw) + self.__doc__ = fget.__doc__ + + def __get__(desc, self, cls): + return desc.fget(cls) + def _declarative_constructor(self, **kwargs): """A simple constructor that allows initialization from kwargs. diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 39c68f0aa..18031e15f 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -84,6 +84,7 @@ __all__ = ( 'eagerload', 'eagerload_all', 'extension', + 'immediateload', 'join', 'joinedload', 'joinedload_all', @@ -255,7 +256,19 @@ def relationship(argument, secondary=None, **kwargs): * ``all`` - shorthand for "save-update,merge, refresh-expire, expunge, delete" - + + :param cascade_backrefs=True: + a boolean value indicating if the ``save-update`` cascade should + operate along a backref event. When set to ``False`` on a + one-to-many relationship that has a many-to-one backref, assigning + a persistent object to the many-to-one attribute on a transient object + will not add the transient to the session. Similarly, when + set to ``False`` on a many-to-one relationship that has a one-to-many + backref, appending a persistent object to the one-to-many collection + on a transient object will not add the transient to the session. + + ``cascade_backrefs`` is new in 0.6.5. + :param collection_class: a class or callable that returns a new list-holding object. will be used in place of a plain list for storing elements. @@ -323,7 +336,12 @@ def relationship(argument, secondary=None, **kwargs): ``select``. Values include: * ``select`` - items should be loaded lazily when the property is first - accessed, using a separate SELECT statement. + accessed, using a separate SELECT statement, or identity map + fetch for simple many-to-one references. + + * ``immediate`` - items should be loaded as the parents are loaded, + using a separate SELECT statement, or identity map fetch for + simple many-to-one references. (new as of 0.6.5) * ``joined`` - items should be loaded "eagerly" in the same query as that of the parent, using a JOIN or LEFT OUTER JOIN. Whether @@ -945,11 +963,24 @@ def compile_mappers(): m.compile() def clear_mappers(): - """Remove all mappers that have been created thus far. - - The mapped classes will return to their initial "unmapped" state and can - be re-mapped with new mappers. - + """Remove all mappers from all classes. + + This function removes all instrumentation from classes and disposes + of their associated mappers. Once called, the classes are unmapped + and can be later re-mapped with new mappers. + + :func:`.clear_mappers` is *not* for normal use, as there is literally no + valid usage for it outside of very specific testing scenarios. Normally, + mappers are permanent structural components of user-defined classes, and + are never discarded independently of their class. If a mapped class itself + is garbage collected, its mapper is automatically disposed of as well. As + such, :func:`.clear_mappers` is only for usage in test suites that re-use + the same classes with different mappings, which is itself an extremely rare + use case - the only such use case is in fact SQLAlchemy's own test suite, + and possibly the test suites of other ORM extension libraries which + intend to test various combinations of mapper construction upon a fixed + set of classes. + """ mapperlib._COMPILE_MUTEX.acquire() try: @@ -1110,7 +1141,7 @@ def subqueryload_all(*keys): query.options(subqueryload_all(User.orders, Order.items, Item.keywords)) - See also: :func:`joinedload_all`, :func:`lazyload` + See also: :func:`joinedload_all`, :func:`lazyload`, :func:`immediateload` """ return strategies.EagerLazyOption(keys, lazy="subquery", chained=True) @@ -1122,7 +1153,7 @@ def lazyload(*keys): Used with :meth:`~sqlalchemy.orm.query.Query.options`. - See also: :func:`eagerload`, :func:`subqueryload` + See also: :func:`eagerload`, :func:`subqueryload`, :func:`immediateload` """ return strategies.EagerLazyOption(keys, lazy=True) @@ -1133,11 +1164,24 @@ def noload(*keys): Used with :meth:`~sqlalchemy.orm.query.Query.options`. - See also: :func:`lazyload`, :func:`eagerload`, :func:`subqueryload` + See also: :func:`lazyload`, :func:`eagerload`, :func:`subqueryload`, :func:`immediateload` """ return strategies.EagerLazyOption(keys, lazy=None) +def immediateload(*keys): + """Return a ``MapperOption`` that will convert the property of the given + name into an immediate load. + + Used with :meth:`~sqlalchemy.orm.query.Query.options`. + + See also: :func:`lazyload`, :func:`eagerload`, :func:`subqueryload` + + New as of verison 0.6.5. + + """ + return strategies.EagerLazyOption(keys, lazy='immediate') + def contains_alias(alias): """Return a ``MapperOption`` that will indicate to the query that the main table has been aliased. diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index d71db0faa..9c20b7eaf 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -23,10 +23,7 @@ from sqlalchemy import util from sqlalchemy.orm import interfaces, collections, exc import sqlalchemy.exceptions as sa_exc -# lazy imports -_entity_info = None -identity_equal = None -state = None +mapperutil = util.importlater("sqlalchemy.orm", "util") PASSIVE_NO_RESULT = util.symbol('PASSIVE_NO_RESULT') ATTR_WAS_SET = util.symbol('ATTR_WAS_SET') @@ -385,7 +382,7 @@ class AttributeImpl(object): # Return a new, empty value return self.initialize(state, dict_) - + def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF): self.set(state, dict_, value, initiator, passive=passive) @@ -557,7 +554,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): compare_function=compare_function, **kwargs) if compare_function is None: - self.is_equal = identity_equal + self.is_equal = mapperutil.identity_equal def delete(self, state, dict_): old = self.get(state, dict_) diff --git a/lib/sqlalchemy/orm/dependency.py b/lib/sqlalchemy/orm/dependency.py index 662cfc67b..4458a8547 100644 --- a/lib/sqlalchemy/orm/dependency.py +++ b/lib/sqlalchemy/orm/dependency.py @@ -806,8 +806,10 @@ class DetectKeySwitch(DependencyProcessor): if not issubclass(state.class_, self.parent.class_): continue dict_ = state.dict - related = state.get_impl(self.key).get(state, dict_, passive=self.passive_updates) - if related is not attributes.PASSIVE_NO_RESULT and related is not None: + related = state.get_impl(self.key).get(state, dict_, + passive=self.passive_updates) + if related is not attributes.PASSIVE_NO_RESULT and \ + related is not None: related_state = attributes.instance_state(dict_[self.key]) if related_state in switchers: uowcommit.register_object(state, diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index cc8ff2abc..d74baf99a 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -36,7 +36,7 @@ class DynaLoader(strategies.AbstractRelationshipLoader): ) def create_row_processor(self, selectcontext, path, mapper, row, adapter): - return (None, None) + return None, None, None log.class_logger(DynaLoader) diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 7ad85b5e6..f6c7dd03d 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -76,7 +76,7 @@ class MapperExtension(object): mapper activity will not be performed. """ - + def instrument_class(self, mapper, class_): """Receive a class when the mapper is first constructed, and has applied instrumentation to the mapped class. @@ -418,6 +418,13 @@ class MapperProperty(object): attribute access, loading behavior, and dependency calculations. """ + cascade = () + """The set of 'cascade' attribute names. + + This collection is checked before the 'cascade_iterator' method is called. + + """ + def setup(self, context, entity, path, adapter, **kwargs): """Called by Query for the purposes of constructing a SQL statement. @@ -429,38 +436,8 @@ class MapperProperty(object): pass def create_row_processor(self, selectcontext, path, mapper, row, adapter): - """Return a 2-tuple consiting of two row processing functions and - an instance post-processing function. - - Input arguments are the query.SelectionContext and the *first* - applicable row of a result set obtained within - query.Query.instances(), called only the first time a particular - mapper's populate_instance() method is invoked for the overall result. - - The settings contained within the SelectionContext as well as the - columns present in the row (which will be the same columns present in - all rows) are used to determine the presence and behavior of the - returned callables. The callables will then be used to process all - rows and instances. - - Callables are of the following form:: - - def new_execute(state, dict_, row, isnew): - # process incoming instance state and given row. - # the instance is - # "new" and was just created upon receipt of this row. - "isnew" indicates if the instance was newly created as a - result of reading this row - - def existing_execute(state, dict_, row): - # process incoming instance state and given row. the - # instance is - # "existing" and was created based on a previous row. - - return (new_execute, existing_execute) - - Either of the three tuples can be ``None`` in which case no function - is called. + """Return a 3-tuple consisting of three row processing functions. + """ raise NotImplementedError() @@ -469,6 +446,11 @@ class MapperProperty(object): halt_on=None): """Iterate through instances related to the given instance for a particular 'cascade', starting with this MapperProperty. + + Return an iterator3-tuples (instance, mapper, state). + + Note that the 'cascade' collection on this MapperProperty is + checked first for the given type before cascade_iterator is called. See PropertyLoader for the related instance implementation. """ diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index b75583c6e..ffe78e2f3 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -30,6 +30,9 @@ from sqlalchemy.orm.util import ( ExtensionCarrier, _INSTRUMENTOR, _class_to_mapper, _state_mapper, class_mapper, instance_str, state_str, ) +import sys +sessionlib = util.importlater("sqlalchemy.orm", "session") +properties = util.importlater("sqlalchemy.orm", "properties") __all__ = ( 'Mapper', @@ -56,13 +59,6 @@ NO_ATTRIBUTE = util.symbol('NO_ATTRIBUTE') # lock used to synchronize the "mapper compile" step _COMPILE_MUTEX = util.threading.RLock() -# initialize these lazily -ColumnProperty = None -RelationshipProperty = None -ConcreteInheritedProperty = None -_expire_state = None -_state_session = None - class Mapper(object): """Define the correlation of class attributes to database table columns. @@ -193,7 +189,7 @@ class Mapper(object): else: self.polymorphic_map = _polymorphic_map - if include_properties: + if include_properties is not None: self.include_properties = util.to_set(include_properties) else: self.include_properties = None @@ -595,7 +591,7 @@ class Mapper(object): self._configure_property( col.key, - ColumnProperty(col, _instrument=instrument), + properties.ColumnProperty(col, _instrument=instrument), init=False, setparent=True) def _adapt_inherited_property(self, key, prop, init): @@ -604,7 +600,7 @@ class Mapper(object): elif key not in self._props: self._configure_property( key, - ConcreteInheritedProperty(), + properties.ConcreteInheritedProperty(), init=init, setparent=True) def _configure_property(self, key, prop, init=True, setparent=True): @@ -612,7 +608,7 @@ class Mapper(object): if not isinstance(prop, MapperProperty): # we were passed a Column or a list of Columns; - # generate a ColumnProperty + # generate a properties.ColumnProperty columns = util.to_list(prop) column = columns[0] if not expression.is_column(column): @@ -622,12 +618,12 @@ class Mapper(object): prop = self._props.get(key, None) - if isinstance(prop, ColumnProperty): + if isinstance(prop, properties.ColumnProperty): # TODO: the "property already exists" case is still not # well defined here. assuming single-column, etc. if prop.parent is not self: - # existing ColumnProperty from an inheriting mapper. + # existing properties.ColumnProperty from an inheriting mapper. # make a copy and append our column to it prop = prop.copy() else: @@ -638,11 +634,13 @@ class Mapper(object): "or more attributes for these same-named columns " "explicitly." % (prop.columns[-1], column, key)) - + + # this hypothetically changes to + # prop.columns.insert(0, column) when we do [ticket:1892] prop.columns.append(column) - self._log("appending to existing ColumnProperty %s" % (key)) + self._log("appending to existing properties.ColumnProperty %s" % (key)) - elif prop is None or isinstance(prop, ConcreteInheritedProperty): + elif prop is None or isinstance(prop, properties.ConcreteInheritedProperty): mapped_column = [] for c in columns: mc = self.mapped_table.corresponding_column(c) @@ -663,7 +661,7 @@ class Mapper(object): "force this column to be mapped as a read-only " "attribute." % (key, self, c)) mapped_column.append(mc) - prop = ColumnProperty(*mapped_column) + prop = properties.ColumnProperty(*mapped_column) else: raise sa_exc.ArgumentError( "WARNING: when configuring property '%s' on %s, " @@ -677,7 +675,7 @@ class Mapper(object): "columns get mapped." % (key, self, column.key, prop)) - if isinstance(prop, ColumnProperty): + if isinstance(prop, properties.ColumnProperty): col = self.mapped_table.corresponding_column(prop.columns[0]) # if the column is not present in the mapped table, @@ -716,7 +714,7 @@ class Mapper(object): col not in self._cols_by_table[col.table]: self._cols_by_table[col.table].add(col) - # if this ColumnProperty represents the "polymorphic + # if this properties.ColumnProperty represents the "polymorphic # discriminator" column, mark it. We'll need this when rendering # columns in SELECT statements. if not hasattr(prop, '_is_polymorphic_discriminator'): @@ -787,12 +785,13 @@ class Mapper(object): # the order of mapper compilation for mapper in list(_mapper_registry): if getattr(mapper, '_compile_failed', False): - raise sa_exc.InvalidRequestError( - "One or more mappers failed to compile. " - "Exception was probably " - "suppressed within a hasattr() call. " - "Message was: %s" % - mapper._compile_failed) + e = sa_exc.InvalidRequestError( + "One or more mappers failed to initialize - " + "can't proceed with initialization of other " + "mappers. Original exception was: %s" + % mapper._compile_failed) + e._compile_failed = mapper._compile_failed + raise e if not mapper.compiled: mapper._post_configure_properties() @@ -801,9 +800,9 @@ class Mapper(object): finally: _already_compiling = False except: - import sys exc = sys.exc_info()[1] - self._compile_failed = exc + if not hasattr(exc, '_compile_failed'): + self._compile_failed = exc raise finally: self._expire_memoizations() @@ -1388,25 +1387,30 @@ class Mapper(object): """ visited_instances = util.IdentitySet() - visitables = [(self._props.itervalues(), 'property', state)] + prp, mpp = object(), object() + + visitables = [(deque(self._props.values()), prp, state)] while visitables: iterator, item_type, parent_state = visitables[-1] - try: - if item_type == 'property': - prop = iterator.next() - visitables.append( - (prop.cascade_iterator(type_, parent_state, - visited_instances, halt_on), 'mapper', None) - ) - elif item_type == 'mapper': - instance, instance_mapper, corresponding_state = \ - iterator.next() - yield (instance, instance_mapper) - visitables.append((instance_mapper._props.itervalues(), - 'property', corresponding_state)) - except StopIteration: + if not iterator: visitables.pop() + continue + + if item_type is prp: + prop = iterator.popleft() + if type_ not in prop.cascade: + continue + queue = deque(prop.cascade_iterator(type_, parent_state, + visited_instances, halt_on)) + if queue: + visitables.append((queue,mpp, None)) + elif item_type is mpp: + instance, instance_mapper, corresponding_state = \ + iterator.popleft() + yield (instance, instance_mapper) + visitables.append((deque(instance_mapper._props.values()), + prp, corresponding_state)) @_memoized_compiled_property def _compiled_cache(self): @@ -1877,7 +1881,7 @@ class Mapper(object): ) if readonly: - _expire_state(state, state.dict, readonly) + sessionlib._expire_state(state, state.dict, readonly) # if eager_defaults option is enabled, # refresh whatever has been expired. @@ -1919,7 +1923,7 @@ class Mapper(object): self._set_state_attr_by_column(state, dict_, c, params[c.key]) if postfetch_cols: - _expire_state(state, state.dict, + sessionlib._expire_state(state, state.dict, [self._columntoproperty[c].key for c in postfetch_cols] ) @@ -2115,10 +2119,11 @@ class Mapper(object): state.load_path = load_path if not new_populators: - new_populators[:], existing_populators[:] = \ - self._populators(context, path, row, - adapter) - + self._populators(context, path, row, adapter, + new_populators, + existing_populators + ) + if isnew: populators = new_populators else: @@ -2289,20 +2294,24 @@ class Mapper(object): return instance return _instance - def _populators(self, context, path, row, adapter): + def _populators(self, context, path, row, adapter, + new_populators, existing_populators): """Produce a collection of attribute level row processor callables.""" - new_populators, existing_populators = [], [] + delayed_populators = [] for prop in self._props.itervalues(): - newpop, existingpop = prop.create_row_processor( + newpop, existingpop, delayedpop = prop.create_row_processor( context, path, self, row, adapter) if newpop: new_populators.append((prop.key, newpop)) if existingpop: existing_populators.append((prop.key, existingpop)) - return new_populators, existing_populators - + if delayedpop: + delayed_populators.append((prop.key, delayedpop)) + if delayed_populators: + new_populators.extend(delayed_populators) + def _configure_subclass_mapper(self, context, path, adapter): """Produce a mapper level row processor callable factory for mappers inheriting this one.""" @@ -2362,6 +2371,11 @@ def validates(*names): can then raise validation exceptions to halt the process from continuing, or can modify or replace the value before proceeding. The function should otherwise return the given value. + + Note that a validator for a collection **cannot** issue a load of that + collection within the validation routine - this usage raises + an assertion to avoid recursion overflows. This is a reentrant + condition which is not supported. """ def wrap(fn): @@ -2407,7 +2421,7 @@ def _load_scalar_attributes(state, attribute_names): """initiate a column-based attribute refresh operation.""" mapper = _state_mapper(state) - session = _state_session(state) + session = sessionlib._state_session(state) if not session: raise orm_exc.DetachedInstanceError( "Instance %s is not bound to a Session; " diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 075be1251..062eef04d 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -311,7 +311,7 @@ class DescriptorProperty(MapperProperty): pass def create_row_processor(self, selectcontext, path, mapper, row, adapter): - return (None, None) + return None, None, None def merge(self, session, source_state, source_dict, dest_state, dest_dict, load, _recursive): @@ -447,8 +447,10 @@ class RelationshipProperty(StrategizedProperty): comparator_factory=None, single_parent=False, innerjoin=False, doc=None, + cascade_backrefs=True, load_on_pending=False, - strategy_class=None, _local_remote_pairs=None, query_class=None): + strategy_class=None, _local_remote_pairs=None, + query_class=None): self.uselist = uselist self.argument = argument @@ -463,6 +465,7 @@ class RelationshipProperty(StrategizedProperty): self._user_defined_foreign_keys = foreign_keys self.collection_class = collection_class self.passive_deletes = passive_deletes + self.cascade_backrefs = cascade_backrefs self.passive_updates = passive_updates self.remote_side = remote_side self.enable_typechecks = enable_typechecks @@ -868,7 +871,8 @@ class RelationshipProperty(StrategizedProperty): # cascade using the mapper local to this # object, so that its individual properties are located instance_mapper = instance_state.manager.mapper - yield (c, instance_mapper, instance_state) + yield c, instance_mapper, instance_state + def _add_reverse_property(self, key): other = self.mapper.get_property(key, _compile_mappers=False) @@ -1482,6 +1486,3 @@ class RelationshipProperty(StrategizedProperty): PropertyLoader = RelationProperty = RelationshipProperty log.class_logger(RelationshipProperty) -mapper.ColumnProperty = ColumnProperty -mapper.RelationshipProperty = RelationshipProperty -mapper.ConcreteInheritedProperty = ConcreteInheritedProperty diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 7f9cf1e2e..ef9a509a4 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -32,7 +32,7 @@ from sqlalchemy.orm import ( from sqlalchemy.orm.util import ( AliasedClass, ORMAdapter, _entity_descriptor, _entity_info, _is_aliased_class, _is_mapped_class, _orm_columns, _orm_selectable, - join as orm_join,with_parent + join as orm_join,with_parent, _attr_as_key ) @@ -90,6 +90,7 @@ class Query(object): _only_load_props = None _refresh_state = None _from_obj = () + _select_from_entity = None _filter_aliases = None _from_obj_alias = None _joinpath = _joinpoint = util.frozendict() @@ -266,7 +267,8 @@ class Query(object): return self._entities[0] def _mapper_zero(self): - return self._entity_zero().entity_zero + return self._select_from_entity or \ + self._entity_zero().entity_zero def _extension_zero(self): ent = self._entity_zero() @@ -283,8 +285,9 @@ class Query(object): def _joinpoint_zero(self): return self._joinpoint.get( - '_joinpoint_entity', - self._entity_zero().entity_zero) + '_joinpoint_entity', + self._mapper_zero() + ) def _mapper_zero_or_none(self): if not getattr(self._entities[0], 'primary_entity', False): @@ -422,8 +425,8 @@ class Query(object): return stmt._annotate({'_halt_adapt': True}) def subquery(self): - """return the full SELECT statement represented by this Query, - embedded within an Alias. + """return the full SELECT statement represented by this :class:`.Query`, + embedded within an :class:`.Alias`. Eager JOIN generation within the query is disabled. @@ -433,7 +436,33 @@ class Query(object): """ return self.enable_eagerloads(False).statement.alias() + + def label(self, name): + """Return the full SELECT statement represented by this :class:`.Query`, converted + to a scalar subquery with a label of the given name. + + Analagous to :meth:`sqlalchemy.sql._SelectBaseMixin.label`. + + New in 0.6.5. + + """ + + return self.enable_eagerloads(False).statement.label(name) + + + def as_scalar(self): + """Return the full SELECT statement represented by this :class:`.Query`, converted + to a scalar subquery. + + Analagous to :meth:`sqlalchemy.sql._SelectBaseMixin.as_scalar`. + New in 0.6.5. + + """ + + return self.enable_eagerloads(False).statement.as_scalar() + + def __clause_element__(self): return self.enable_eagerloads(False).with_labels().statement @@ -495,7 +524,12 @@ class Query(object): @property def whereclause(self): - """The WHERE criterion for this Query.""" + """A readonly attribute which returns the current WHERE criterion for this Query. + + This returned value is a SQL expression construct, or ``None`` if no + criterion has been established. + + """ return self._criterion @_generative() @@ -750,7 +784,36 @@ class Query(object): # end Py2K except StopIteration: return None + + @_generative() + def with_entities(self, *entities): + """Return a new :class:`.Query` replacing the SELECT list with the given + entities. + + e.g.:: + + # Users, filtered on some arbitrary criterion + # and then ordered by related email address + q = session.query(User).\\ + join(User.address).\\ + filter(User.name.like('%ed%')).\\ + order_by(Address.email) + # given *only* User.id==5, Address.email, and 'q', what + # would the *next* User in the result be ? + subq = q.with_entities(Address.email).\\ + order_by(None).\\ + filter(User.id==5).\\ + subquery() + q = q.join((subq, subq.c.email < Address.email)).\\ + limit(1) + + New in 0.6.5. + + """ + self._set_entities(entities) + + @_generative() def add_columns(self, *column): """Add one or more column expressions to the list @@ -807,7 +870,7 @@ class Query(object): opt.process_query(self) @_generative() - def with_hint(self, selectable, text, dialect_name=None): + def with_hint(self, selectable, text, dialect_name='*'): """Add an indexing hint for the given entity or selectable to this :class:`Query`. @@ -1168,7 +1231,7 @@ class Query(object): arg1, arg2 = arg1 else: arg2 = None - + # determine onclause/right_entity. there # is a little bit of legacy behavior still at work here # which means they might be in either order. may possibly @@ -1249,7 +1312,7 @@ class Query(object): (left, right)) left_mapper, left_selectable, left_is_aliased = _entity_info(left) - right_mapper, right_selectable, is_aliased_class = _entity_info(right) + right_mapper, right_selectable, right_is_aliased = _entity_info(right) if right_mapper and prop and \ not right_mapper.common_parent(prop.mapper): @@ -1278,7 +1341,7 @@ class Query(object): need_adapter = True aliased_entity = right_mapper and \ - not is_aliased_class and \ + not right_is_aliased and \ ( right_mapper.with_polymorphic or isinstance( @@ -1341,8 +1404,16 @@ class Query(object): ) ) - join_to_left = not is_aliased_class and not left_is_aliased - + # this is an overly broad assumption here, but there's a + # very wide variety of situations where we rely upon orm.join's + # adaption to glue clauses together, with joined-table inheritance's + # wide array of variables taking up most of the space. + # Setting the flag here is still a guess, so it is a bug + # that we don't have definitive criterion to determine when + # adaption should be enabled (or perhaps that we're even doing the + # whole thing the way we are here). + join_to_left = not right_is_aliased and not left_is_aliased + if self._from_obj and left_selectable is not None: replace_clause_index, clause = sql_util.find_join_source( self._from_obj, @@ -1350,10 +1421,16 @@ class Query(object): if clause is not None: # the entire query's FROM clause is an alias of itself (i.e. # from_self(), similar). if the left clause is that one, - # ensure it aliases to the left side. + # ensure it adapts to the left side. if self._from_obj_alias and clause is self._from_obj[0]: join_to_left = True - + + # An exception case where adaption to the left edge is not + # desirable. See above note on join_to_left. + if join_to_left and isinstance(clause, expression.Join) and \ + sql_util.clause_is_present(left_selectable, clause): + join_to_left = False + clause = orm_join(clause, right, onclause, isouter=outerjoin, @@ -1402,20 +1479,23 @@ class Query(object): @_generative(_no_clauseelement_condition) def select_from(self, *from_obj): - """Set the `from_obj` parameter of the query and return the newly - resulting ``Query``. This replaces the table which this Query selects - from with the given table. + """Set the FROM clause of this :class:`.Query` explicitly. - ``select_from()`` also accepts class arguments. Though usually not - necessary, can ensure that the full selectable of the given mapper is - applied, e.g. for joined-table mappers. - - """ + Sending a mapped class or entity here effectively replaces the + "left edge" of any calls to :meth:`.Query.join`, when no + joinpoint is otherwise established - usually, the default "join + point" is the leftmost entity in the :class:`.Query` object's + list of entities to be selected. + Mapped entities or plain :class:`.Table` or other selectables + can be sent here which will form the default FROM clause. + + """ obj = [] for fo in from_obj: if _is_mapped_class(fo): mapper, selectable, is_aliased_class = _entity_info(fo) + self._select_from_entity = fo obj.append(selectable) elif not isinstance(fo, expression.FromClause): raise sa_exc.ArgumentError( @@ -1424,7 +1504,7 @@ class Query(object): obj.append(fo) self._set_select_from(*obj) - + def __getitem__(self, item): if isinstance(item, slice): start, stop, step = util.decode_slice(item) @@ -1695,10 +1775,8 @@ class Query(object): query_entity.row_processor(self, context, custom_rows) for query_entity in self._entities ]) - - if not single_entity: - labels = [l for l in labels if l] - + + while True: context.progress = {} context.partials = {} @@ -2007,8 +2085,7 @@ class Query(object): Also, the ``before_delete()`` and ``after_delete()`` :class:`~sqlalchemy.orm.interfaces.MapperExtension` methods are not called from this method. For a delete hook here, use the - ``after_bulk_delete()`` - :class:`~sqlalchemy.orm.interfaces.MapperExtension` method. + :meth:`.SessionExtension.after_bulk_delete()` event hook. """ #TODO: lots of duplication and ifs - probably needs to be @@ -2133,8 +2210,7 @@ class Query(object): Also, the ``before_update()`` and ``after_update()`` :class:`~sqlalchemy.orm.interfaces.MapperExtension` methods are not called from this method. For an update hook here, use the - ``after_bulk_update()`` - :class:`~sqlalchemy.orm.interfaces.SessionExtension` method. + :meth:`.SessionExtension.after_bulk_update()` event hook. """ @@ -2180,7 +2256,7 @@ class Query(object): value_evaluators = {} for key,value in values.iteritems(): - key = expression._column_as_key(key) + key = _attr_as_key(key) value_evaluators[key] = evaluator_compiler.process( expression._literal_as_binds(value)) except evaluator.UnevaluatableError: @@ -2235,7 +2311,7 @@ class Query(object): if identity_key in session.identity_map: session.expire( session.identity_map[identity_key], - [expression._column_as_key(k) for k in values] + [_attr_as_key(k) for k in values] ) for ext in session.extensions: diff --git a/lib/sqlalchemy/orm/scoping.py b/lib/sqlalchemy/orm/scoping.py index 63b5ab8b2..94b81b1e8 100644 --- a/lib/sqlalchemy/orm/scoping.py +++ b/lib/sqlalchemy/orm/scoping.py @@ -6,7 +6,8 @@ import sqlalchemy.exceptions as sa_exc from sqlalchemy.util import ScopedRegistry, ThreadLocalRegistry, \ - to_list, get_cls_kwargs, deprecated + to_list, get_cls_kwargs, deprecated,\ + warn from sqlalchemy.orm import ( EXT_CONTINUE, MapperExtension, class_mapper, object_session ) @@ -45,7 +46,8 @@ class ScopedSession(object): scope = kwargs.pop('scope', False) if scope is not None: if self.registry.has(): - raise sa_exc.InvalidRequestError("Scoped session is already present; no new arguments may be specified.") + raise sa_exc.InvalidRequestError("Scoped session is already present; " + "no new arguments may be specified.") else: sess = self.session_factory(**kwargs) self.registry.set(sess) @@ -85,6 +87,11 @@ class ScopedSession(object): def configure(self, **kwargs): """reconfigure the sessionmaker used by this ScopedSession.""" + + if self.registry.has(): + warn('At least one scoped session is already present. ' + ' configure() can not affect sessions that have ' + 'already been created.') self.session_factory.configure(**kwargs) diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 54b41fcc6..b9b935c88 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -22,6 +22,7 @@ from sqlalchemy.orm.util import ( from sqlalchemy.orm.mapper import Mapper, _none_set from sqlalchemy.orm.unitofwork import UOWTransaction from sqlalchemy.orm import identity +import sys __all__ = ['Session', 'SessionTransaction', 'SessionExtension'] @@ -105,13 +106,13 @@ def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False, The full resolution is described in the ``get_bind()`` method of ``Session``. Usage looks like:: - sess = Session(binds={ + Session = sessionmaker(binds={ SomeMappedClass: create_engine('postgresql://engine1'), somemapper: create_engine('postgresql://engine2'), some_table: create_engine('postgresql://engine3'), }) - Also see the ``bind_mapper()`` and ``bind_table()`` methods. + Also see the :meth:`.Session.bind_mapper` and :meth:`.Session.bind_table` methods. :param \class_: Specify an alternate class other than ``sqlalchemy.orm.session.Session`` which should be used by the returned @@ -142,8 +143,9 @@ def sessionmaker(bind=None, class_=None, autoflush=True, autocommit=False, as returned by the ``query()`` method. Defaults to :class:`~sqlalchemy.orm.query.Query`. - :param twophase: When ``True``, all transactions will be started using - :mod:`~sqlalchemy.engine_TwoPhaseTransaction`. During a ``commit()``, + :param twophase: When ``True``, all transactions will be started as + a "two phase" transaction, i.e. using the "two phase" semantics + of the database in use along with an XID. During a ``commit()``, after ``flush()`` has been issued for all attached databases, the ``prepare()`` method on each database's ``TwoPhaseTransaction`` will be called. This allows each database to roll back the entire @@ -206,7 +208,9 @@ class SessionTransaction(object): single: thread safety; SessionTransaction """ - + + _rollback_exception = None + def __init__(self, session, parent=None, nested=False): self.session = session self._connections = {} @@ -229,9 +233,21 @@ class SessionTransaction(object): def _assert_is_active(self): self._assert_is_open() if not self._active: - raise sa_exc.InvalidRequestError( - "The transaction is inactive due to a rollback in a " - "subtransaction. Issue rollback() to cancel the transaction.") + if self._rollback_exception: + raise sa_exc.InvalidRequestError( + "This Session's transaction has been rolled back " + "due to a previous exception during flush." + " To begin a new transaction with this Session, " + "first issue Session.rollback()." + " Original exception was: %s" + % self._rollback_exception + ) + else: + raise sa_exc.InvalidRequestError( + "This Session's transaction has been rolled back " + "by a nested rollback() call. To begin a new " + "transaction, issue Session.rollback() first." + ) def _assert_is_open(self, error_msg="The transaction is closed"): if self.session is None: @@ -288,14 +304,16 @@ class SessionTransaction(object): assert not self.session._deleted for s in self.session.identity_map.all_states(): - _expire_state(s, s.dict, None, instance_dict=self.session.identity_map) + _expire_state(s, s.dict, None, + instance_dict=self.session.identity_map) def _remove_snapshot(self): assert self._is_transaction_boundary if not self.nested and self.session.expire_on_commit: for s in self.session.identity_map.all_states(): - _expire_state(s, s.dict, None, instance_dict=self.session.identity_map) + _expire_state(s, s.dict, None, + instance_dict=self.session.identity_map) def _connection_for_bind(self, bind): self._assert_is_active() @@ -379,7 +397,7 @@ class SessionTransaction(object): self.close() return self._parent - def rollback(self): + def rollback(self, _capture_exception=False): self._assert_is_open() stx = self.session.transaction @@ -397,6 +415,8 @@ class SessionTransaction(object): transaction._deactivate() self.close() + if self._parent and _capture_exception: + self._parent._rollback_exception = sys.exc_info()[1] return self._parent def _rollback_impl(self): @@ -415,7 +435,8 @@ class SessionTransaction(object): def close(self): self.session.transaction = self._parent if self._parent is None: - for connection, transaction, autoclose in set(self._connections.values()): + for connection, transaction, autoclose in \ + set(self._connections.values()): if autoclose: connection.close() else: @@ -511,20 +532,13 @@ class Session(object): transaction or nested transaction, an error is raised, unless ``subtransactions=True`` or ``nested=True`` is specified. - The ``subtransactions=True`` flag indicates that this ``begin()`` can - create a subtransaction if a transaction is already in progress. A - subtransaction is a non-transactional, delimiting construct that - allows matching begin()/commit() pairs to be nested together, with - only the outermost begin/commit pair actually affecting transactional - state. When a rollback is issued, the subtransaction will directly - roll back the innermost real transaction, however each subtransaction - still must be explicitly rolled back to maintain proper stacking of - subtransactions. - - If no transaction is in progress, then a real transaction is begun. - + The ``subtransactions=True`` flag indicates that this :meth:`~.Session.begin` + can create a subtransaction if a transaction is already in progress. + For documentation on subtransactions, please see :ref:`session_subtransactions`. + The ``nested`` flag begins a SAVEPOINT transaction and is equivalent - to calling ``begin_nested()``. + to calling :meth:`~.Session.begin_nested`. For documentation on SAVEPOINT + transactions, please see :ref:`session_begin_nested`. """ if self.transaction is not None: @@ -546,10 +560,8 @@ class Session(object): The target database(s) must support SQL SAVEPOINTs or a SQLAlchemy-supported vendor implementation of the idea. - The nested transaction is a real transation, unlike a "subtransaction" - which corresponds to multiple ``begin()`` calls. The next - ``rollback()`` or ``commit()`` call will operate upon this nested - transaction. + For documentation on SAVEPOINT + transactions, please see :ref:`session_begin_nested`. """ return self.begin(nested=True) @@ -572,9 +584,16 @@ class Session(object): def commit(self): """Flush pending changes and commit the current transaction. - + If no transaction is in progress, this method raises an InvalidRequestError. + + By default, the :class:`.Session` also expires all database + loaded state on all ORM-managed attributes after transaction commit. + This so that subsequent operations load the most recent + data from the database. This behavior can be disabled using + the ``expire_on_commit=False`` option to :func:`.sessionmaker` or + the :class:`.Session` constructor. If a subtransaction is in effect (which occurs when begin() is called multiple times), the subtransaction will be closed, and the next call @@ -1133,6 +1152,8 @@ class Session(object): This operation cascades to associated instances if the association is mapped with ``cascade="merge"``. + See :ref:`unitofwork_merging` for a detailed discussion of merging. + """ if 'dont_load' in kw: load = not kw['dont_load'] @@ -1451,7 +1472,7 @@ class Session(object): ext.after_flush(self, flush_context) transaction.commit() except: - transaction.rollback() + transaction.rollback(_capture_exception=True) raise flush_context.finalize_flush_changes() @@ -1467,22 +1488,42 @@ class Session(object): ext.after_flush_postexec(self, flush_context) def is_modified(self, instance, include_collections=True, passive=False): - """Return True if instance has modified attributes. + """Return ``True`` if instance has modified attributes. This method retrieves a history instance for each instrumented attribute on the instance and performs a comparison of the current - value to its previously committed value. Note that instances present - in the 'dirty' collection may result in a value of ``False`` when - tested with this method. + value to its previously committed value. - `include_collections` indicates if multivalued collections should be + ``include_collections`` indicates if multivalued collections should be included in the operation. Setting this to False is a way to detect only local-column based properties (i.e. scalar columns or many-to-one foreign keys) that would result in an UPDATE for this instance upon flush. - The `passive` flag indicates if unloaded attributes and collections + The ``passive`` flag indicates if unloaded attributes and collections should not be loaded in the course of performing this test. + + A few caveats to this method apply: + + * Instances present in the 'dirty' collection may result in a value + of ``False`` when tested with this method. This because while + the object may have received attribute set events, there may be + no net changes on its state. + * Scalar attributes may not have recorded the "previously" set + value when a new value was applied, if the attribute was not loaded, + or was expired, at the time the new value was received - in these + cases, the attribute is assumed to have a change, even if there is + ultimately no net change against its database value. SQLAlchemy in + most cases does not need the "old" value when a set event occurs, so + it skips the expense of a SQL call if the old value isn't present, + based on the assumption that an UPDATE of the scalar value is + usually needed, and in those few cases where it isn't, is less + expensive on average than issuing a defensive SELECT. + + The "old" value is fetched unconditionally only if the attribute + container has the "active_history" flag set to ``True``. This flag + is set typically for primary key attributes and scalar references + that are not a simple many-to-one. """ try: @@ -1654,8 +1695,3 @@ def _state_session(state): pass return None -# Lazy initialization to avoid circular imports -unitofwork._state_session = _state_session -from sqlalchemy.orm import mapper -mapper._expire_state = _expire_state -mapper._state_session = _state_session diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index e6502df8c..ad1d4a8f0 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -338,8 +338,14 @@ class InstanceState(object): previous = attr.copy(previous) self.committed_state[attr.key] = previous - - if not self.modified: + + + # the "or not self.modified" is defensive at + # this point. The assertion below is expected + # to be True: + # assert self._strong_obj is None or self.modified + + if self._strong_obj is None or not self.modified: instance_dict = self._instance_dict() if instance_dict: instance_dict._modified.add(self) diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 4c848996f..4e2021674 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -88,7 +88,7 @@ class UninstrumentedColumnLoader(LoaderStrategy): column_collection.append(c) def create_row_processor(self, selectcontext, path, mapper, row, adapter): - return None, None + return None, None, None class ColumnLoader(LoaderStrategy): """Strategize the loading of a plain column-based MapperProperty.""" @@ -127,11 +127,11 @@ class ColumnLoader(LoaderStrategy): if col is not None and col in row: def new_execute(state, dict_, row): dict_[key] = row[col] - return new_execute, None + return new_execute, None, None else: def new_execute(state, dict_, row): state.expire_attribute_pre_commit(dict_, key) - return new_execute, None + return new_execute, None, None log.class_logger(ColumnLoader) @@ -184,7 +184,7 @@ class CompositeColumnLoader(ColumnLoader): def new_execute(state, dict_, row): dict_[key] = composite_class(*[row[c] for c in columns]) - return new_execute, None + return new_execute, None, None log.class_logger(CompositeColumnLoader) @@ -211,7 +211,7 @@ class DeferredColumnLoader(LoaderStrategy): # fire off on next access. state.reset(dict_, key) - return new_execute, None + return new_execute, None, None def init(self): if hasattr(self.parent_property, 'composite_class'): @@ -348,7 +348,7 @@ class NoLoader(AbstractRelationshipLoader): def create_row_processor(self, selectcontext, path, mapper, row, adapter): def new_execute(state, dict_, row): state.initialize(self.key) - return new_execute, None + return new_execute, None, None log.class_logger(NoLoader) @@ -509,7 +509,7 @@ class LazyLoader(AbstractRelationshipLoader): # any existing state. state.reset(dict_, key) - return new_execute, None + return new_execute, None, None @classmethod def _create_lazy_clause(cls, prop, reverse_direction=False): @@ -683,6 +683,23 @@ class LoadLazyAttribute(object): else: return None +class ImmediateLoader(AbstractRelationshipLoader): + def init_class_attribute(self, mapper): + self.parent_property.\ + _get_strategy(LazyLoader).\ + init_class_attribute(mapper) + + def setup_query(self, context, entity, + path, adapter, column_collection=None, + parentmapper=None, **kwargs): + pass + + def create_row_processor(self, context, path, mapper, row, adapter): + def execute(state, dict_, row): + state.get_impl(self.key).get(state, dict_) + + return None, None, execute + class SubqueryLoader(AbstractRelationshipLoader): def init(self): super(SubqueryLoader, self).init() @@ -723,14 +740,16 @@ class SubqueryLoader(AbstractRelationshipLoader): ("orig_query", SubqueryLoader), context.query) + subq_mapper = mapperutil._class_to_mapper(subq_path[0]) + # determine attributes of the leftmost mapper - if self.parent.isa(subq_path[0]) and self.key==subq_path[1]: + if self.parent.isa(subq_mapper) and self.key==subq_path[1]: leftmost_mapper, leftmost_prop = \ self.parent, self.parent_property else: leftmost_mapper, leftmost_prop = \ - subq_path[0], \ - subq_path[0].get_property(subq_path[1]) + subq_mapper, \ + subq_mapper.get_property(subq_path[1]) leftmost_cols, remote_cols = self._local_remote_columns(leftmost_prop) leftmost_attr = [ @@ -859,7 +878,7 @@ class SubqueryLoader(AbstractRelationshipLoader): path = interfaces._reduce_path(path) if ('subquery', path) not in context.attributes: - return None, None + return None, None, None local_cols, remote_cols = self._local_remote_columns(self.parent_property) @@ -903,7 +922,7 @@ class SubqueryLoader(AbstractRelationshipLoader): state.get_impl(self.key).\ set_committed_value(state, dict_, scalar) - return execute, None + return execute, None, None log.class_logger(SubqueryLoader) @@ -921,6 +940,7 @@ class EagerLoader(AbstractRelationshipLoader): def setup_query(self, context, entity, path, adapter, \ column_collection=None, parentmapper=None, + allow_innerjoin=True, **kwargs): """Add a left outer join to the statement thats being constructed.""" @@ -971,10 +991,18 @@ class EagerLoader(AbstractRelationshipLoader): if self.parent_property.direction != interfaces.MANYTOONE: context.multi_row_eager_loaders = True + innerjoin = allow_innerjoin and context.attributes.get( + ("eager_join_type", path), + self.parent_property.innerjoin) + if not innerjoin: + # if this is an outer join, all eager joins from + # here must also be outer joins + allow_innerjoin = False + context.create_eager_joins.append( (self._create_eager_join, context, entity, path, adapter, - parentmapper, clauses) + parentmapper, clauses, innerjoin) ) add_to_collection = context.secondary_columns @@ -989,10 +1017,12 @@ class EagerLoader(AbstractRelationshipLoader): path + (self.mapper,), clauses, parentmapper=self.mapper, - column_collection=add_to_collection) + column_collection=add_to_collection, + allow_innerjoin=allow_innerjoin) def _create_eager_join(self, context, entity, - path, adapter, parentmapper, clauses): + path, adapter, parentmapper, + clauses, innerjoin): if parentmapper is None: localparent = entity.mapper @@ -1047,10 +1077,6 @@ class EagerLoader(AbstractRelationshipLoader): else: onclause = self.parent_property - innerjoin = context.attributes.get( - ("eager_join_type", path), - self.parent_property.innerjoin) - context.eager_joins[entity_key] = eagerjoin = \ mapperutil.join( towrap, @@ -1156,7 +1182,7 @@ class EagerLoader(AbstractRelationshipLoader): "Multiple rows returned with " "uselist=False for eagerly-loaded attribute '%s' " % self) - return new_execute, existing_execute + return new_execute, existing_execute, None else: def new_execute(state, dict_, row): collection = attributes.init_state_collection( @@ -1181,7 +1207,7 @@ class EagerLoader(AbstractRelationshipLoader): 'append_without_event') context.attributes[(state, key)] = result_list _instance(row, result_list) - return new_execute, existing_execute + return new_execute, existing_execute, None else: return self.parent_property.\ _get_strategy(LazyLoader).\ @@ -1221,6 +1247,8 @@ def factory(identifier): return LazyLoader elif identifier == 'subquery': return SubqueryLoader + elif identifier == 'immediate': + return ImmediateLoader else: return LazyLoader diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py index 830ac3c0c..673591e8e 100644 --- a/lib/sqlalchemy/orm/unitofwork.py +++ b/lib/sqlalchemy/orm/unitofwork.py @@ -16,9 +16,7 @@ from sqlalchemy import util, topological from sqlalchemy.orm import attributes, interfaces from sqlalchemy.orm import util as mapperutil from sqlalchemy.orm.util import _state_mapper - -# Load lazily -_state_session = None +session = util.importlater("sqlalchemy.orm", "session") class UOWEventHandler(interfaces.AttributeExtension): """An event handler added to all relationship attributes which handles @@ -33,15 +31,18 @@ class UOWEventHandler(interfaces.AttributeExtension): def append(self, state, item, initiator): # process "save_update" cascade rules for when # an instance is appended to the list of another instance - sess = _state_session(state) + + sess = session._state_session(state) if sess: prop = _state_mapper(state).get_property(self.key) - if prop.cascade.save_update and item not in sess: + if prop.cascade.save_update and \ + (prop.cascade_backrefs or self.key == initiator.key) and \ + item not in sess: sess.add(item) return item def remove(self, state, item, initiator): - sess = _state_session(state) + sess = session._state_session(state) if sess: prop = _state_mapper(state).get_property(self.key) # expunge pending orphans @@ -55,11 +56,13 @@ class UOWEventHandler(interfaces.AttributeExtension): # is attached to another instance if oldvalue is newvalue: return newvalue - sess = _state_session(state) + + sess = session._state_session(state) if sess: prop = _state_mapper(state).get_property(self.key) if newvalue is not None and \ prop.cascade.save_update and \ + (prop.cascade_backrefs or self.key == initiator.key) and \ newvalue not in sess: sess.add(newvalue) if prop.cascade.delete_orphan and \ diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 297146943..9506bdce1 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -13,7 +13,7 @@ from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE,\ AttributeExtension from sqlalchemy.orm import attributes, exc -mapperlib = None +mapperlib = util.importlater("sqlalchemy.orm", "mapperlib") all_cascades = frozenset(("delete", "delete-orphan", "all", "merge", "expunge", "save-update", "refresh-expire", @@ -530,10 +530,6 @@ def _entity_info(entity, compile=True): if isinstance(entity, AliasedClass): return entity._AliasedClass__mapper, entity._AliasedClass__alias, True - global mapperlib - if mapperlib is None: - from sqlalchemy.orm import mapperlib - if isinstance(entity, mapperlib.Mapper): mapper = entity @@ -580,6 +576,12 @@ def _orm_selectable(entity): mapper, selectable, is_aliased_class = _entity_info(entity) return selectable +def _attr_as_key(attr): + if hasattr(attr, 'key'): + return attr.key + else: + return expression._column_as_key(attr) + def _is_aliased_class(entity): return isinstance(entity, AliasedClass) @@ -622,24 +624,28 @@ def class_mapper(class_, compile=True): def _class_to_mapper(class_or_mapper, compile=True): if _is_aliased_class(class_or_mapper): return class_or_mapper._AliasedClass__mapper + elif isinstance(class_or_mapper, type): - return class_mapper(class_or_mapper, compile=compile) - elif hasattr(class_or_mapper, 'compile'): - if compile: - return class_or_mapper.compile() - else: - return class_or_mapper + try: + class_manager = attributes.manager_of_class(class_or_mapper) + mapper = class_manager.mapper + except exc.NO_STATE: + raise exc.UnmappedClassError(class_or_mapper) + elif isinstance(class_or_mapper, mapperlib.Mapper): + mapper = class_or_mapper else: raise exc.UnmappedClassError(class_or_mapper) + + if compile: + return mapper.compile() + else: + return mapper def has_identity(object): state = attributes.instance_state(object) return state.has_identity def _is_mapped_class(cls): - global mapperlib - if mapperlib is None: - from sqlalchemy.orm import mapperlib if isinstance(cls, (AliasedClass, mapperlib.Mapper)): return True if isinstance(cls, expression.ClauseElement): @@ -682,8 +688,3 @@ def identity_equal(a, b): return False return state_a.key == state_b.key - -# TODO: Avoid circular import. -attributes.identity_equal = identity_equal -attributes._is_aliased_class = _is_aliased_class -attributes._entity_info = _entity_info diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index 98472f9f1..a332cec36 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -32,7 +32,9 @@ import re, inspect from sqlalchemy import exc, util, dialects from sqlalchemy.sql import expression, visitors -URL = None +sqlutil = util.importlater("sqlalchemy.sql", "util") +url = util.importlater("sqlalchemy.engine", "url") + __all__ = ['SchemaItem', 'Table', 'Column', 'ForeignKey', 'Sequence', 'Index', 'ForeignKeyConstraint', 'PrimaryKeyConstraint', 'CheckConstraint', @@ -467,18 +469,34 @@ class Table(SchemaItem, expression.TableClause): """ - try: - if schema is RETAIN_SCHEMA: - schema = self.schema - key = _get_table_key(self.name, schema) + if schema is RETAIN_SCHEMA: + schema = self.schema + key = _get_table_key(self.name, schema) + if key in metadata.tables: + util.warn("Table '%s' already exists within the given " + "MetaData - not copying." % self.description) return metadata.tables[key] - except KeyError: - args = [] - for c in self.columns: - args.append(c.copy(schema=schema)) - for c in self.constraints: - args.append(c.copy(schema=schema)) - return Table(self.name, metadata, schema=schema, *args) + + args = [] + for c in self.columns: + args.append(c.copy(schema=schema)) + for c in self.constraints: + args.append(c.copy(schema=schema)) + table = Table( + self.name, metadata, schema=schema, + *args, **self.kwargs + ) + for index in self.indexes: + # skip indexes that would be generated + # by the 'index' flag on Column + if len(index.columns) == 1 and \ + list(index.columns)[0].index: + continue + Index(index.name, + unique=index.unique, + *[table.c[col] for col in index.columns.keys()], + **index.kwargs) + return table class Column(SchemaItem, expression.ColumnClause): """Represents a column in a database table.""" @@ -890,6 +908,7 @@ class Column(SchemaItem, expression.ColumnClause): server_default=self.server_default, onupdate=self.onupdate, server_onupdate=self.server_onupdate, + info=self.info, *args ) if hasattr(self, '_table_events'): @@ -906,6 +925,10 @@ class Column(SchemaItem, expression.ColumnClause): """ fk = [ForeignKey(f.column) for f in self.foreign_keys] + if name is None and self.name is None: + raise exc.InvalidRequestError("Cannot initialize a sub-selectable" + " with this Column object until it's 'name' has " + "been assigned.") c = self._constructor( name or self.name, self.type, @@ -1959,11 +1982,7 @@ class MetaData(SchemaItem): def _bind_to(self, bind): """Bind this MetaData to an Engine, Connection, string or URL.""" - global URL - if URL is None: - from sqlalchemy.engine.url import URL - - if isinstance(bind, (basestring, URL)): + if isinstance(bind, (basestring, url.URL)): from sqlalchemy import create_engine self._bind = create_engine(bind) else: @@ -1987,10 +2006,9 @@ class MetaData(SchemaItem): """Returns a list of ``Table`` objects sorted in order of dependency. """ - from sqlalchemy.sql.util import sort_tables - return sort_tables(self.tables.itervalues()) + return sqlutil.sort_tables(self.tables.itervalues()) - def reflect(self, bind=None, schema=None, only=None): + def reflect(self, bind=None, schema=None, views=False, only=None): """Load all available table definitions from the database. Automatically creates ``Table`` entries in this ``MetaData`` for any @@ -2006,7 +2024,10 @@ class MetaData(SchemaItem): :param schema: Optional, query and reflect tables from an alterate schema. - + + :param views: + If True, also reflect views. + :param only: Optional. Load only a sub-set of available named tables. May be specified as a sequence of names or a callable. @@ -2035,6 +2056,11 @@ class MetaData(SchemaItem): available = util.OrderedSet(bind.engine.table_names(schema, connection=conn)) + if views: + available.update( + bind.dialect.get_view_names(conn or bind, schema) + ) + current = set(self.tables.iterkeys()) if only is None: @@ -2177,11 +2203,7 @@ class ThreadLocalMetaData(MetaData): def _bind_to(self, bind): """Bind to a Connectable in the caller's thread.""" - global URL - if URL is None: - from sqlalchemy.engine.url import URL - - if isinstance(bind, (basestring, URL)): + if isinstance(bind, (basestring, url.URL)): try: self.context._engine = self.__engines[bind] except KeyError: diff --git a/lib/sqlalchemy/sql/__init__.py b/lib/sqlalchemy/sql/__init__.py index aa18eac17..2bb5f6ab4 100644 --- a/lib/sqlalchemy/sql/__init__.py +++ b/lib/sqlalchemy/sql/__init__.py @@ -47,6 +47,7 @@ from sqlalchemy.sql.expression import ( table, text, tuple_, + type_coerce, union, union_all, update, diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index fcff5e355..4b41c6ed3 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -153,6 +153,10 @@ class _CompileLabel(visitors.Visitable): def __init__(self, col, name): self.element = col self.name = name + + @property + def type(self): + return self.element.type @property def quote(self): @@ -317,7 +321,7 @@ class SQLCompiler(engine.Compiled): if result_map is not None: result_map[labelname.lower()] = \ - (label.name, (label, label.element, labelname), label.element.type) + (label.name, (label, label.element, labelname), label.type) return self.process(label.element, within_columns_clause=True, @@ -329,9 +333,13 @@ class SQLCompiler(engine.Compiled): return self.process(label.element, within_columns_clause=False, **kw) - + def visit_column(self, column, result_map=None, **kwargs): name = column.name + if name is None: + raise exc.CompileError("Cannot compile Column object until " + "it's 'name' is assigned.") + if not column.is_literal and isinstance(name, sql._generated_label): name = self._truncated_identifier("colident", name) @@ -1298,7 +1306,7 @@ class DDLCompiler(engine.Compiled): text += "FOREIGN KEY(%s) REFERENCES %s (%s)" % ( ', '.join(preparer.quote(f.parent.name, f.parent.quote) for f in constraint._elements.values()), - preparer.format_table(remote_table), + self.define_constraint_remote_table(constraint, remote_table, preparer), ', '.join(preparer.quote(f.column.name, f.column.quote) for f in constraint._elements.values()) ) @@ -1306,6 +1314,11 @@ class DDLCompiler(engine.Compiled): text += self.define_constraint_deferrability(constraint) return text + def define_constraint_remote_table(self, constraint, table, preparer): + """Format the remote table clause of a CREATE CONSTRAINT clause.""" + + return preparer.format_table(table) + def visit_unique_constraint(self, constraint): text = "" if constraint.name is not None: diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 1b1cfee8a..c3dc339a5 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -29,13 +29,15 @@ to stay the same in future releases. import itertools, re from operator import attrgetter -from sqlalchemy import util, exc #, types as sqltypes +from sqlalchemy import util, exc from sqlalchemy.sql import operators from sqlalchemy.sql.visitors import Visitable, cloned_traverse import operator -functions, sql_util, sqltypes = None, None, None -DefaultDialect = None +functions = util.importlater("sqlalchemy.sql", "functions") +sqlutil = util.importlater("sqlalchemy.sql", "util") +sqltypes = util.importlater("sqlalchemy", "types") +default = util.importlater("sqlalchemy.engine", "default") __all__ = [ 'Alias', 'ClauseElement', 'ColumnCollection', 'ColumnElement', @@ -45,8 +47,8 @@ __all__ = [ 'except_', 'except_all', 'exists', 'extract', 'func', 'modifier', 'collate', 'insert', 'intersect', 'intersect_all', 'join', 'label', 'literal', 'literal_column', 'not_', 'null', 'or_', 'outparam', - 'outerjoin', 'select', 'subquery', 'table', 'text', 'tuple_', 'union', - 'union_all', 'update', ] + 'outerjoin', 'select', 'subquery', 'table', 'text', 'tuple_', 'type_coerce', + 'union', 'union_all', 'update', ] PARSE_AUTOCOMMIT = util._symbol('PARSE_AUTOCOMMIT') @@ -666,6 +668,54 @@ def tuple_(*expr): """ return _Tuple(*expr) + +def type_coerce(expr, type_): + """Coerce the given expression into the given type, on the Python side only. + + :func:`.type_coerce` is roughly similar to :func:.`cast`, except no + "CAST" expression is rendered - the given type is only applied towards + expression typing and against received result values. + + e.g.:: + + from sqlalchemy.types import TypeDecorator + import uuid + + class AsGuid(TypeDecorator): + impl = String + + def process_bind_param(self, value, dialect): + if value is not None: + return str(value) + else: + return None + + def process_result_value(self, value, dialect): + if value is not None: + return uuid.UUID(value) + else: + return None + + conn.execute( + select([type_coerce(mytable.c.ident, AsGuid)]).\\ + where( + type_coerce(mytable.c.ident, AsGuid) == + uuid.uuid3(uuid.NAMESPACE_URL, 'bar') + ) + ) + + """ + if hasattr(expr, '__clause_expr__'): + return type_coerce(expr.__clause_expr__()) + + elif not isinstance(expr, Visitable): + if expr is None: + return null() + else: + return literal(expr, type_=type_) + else: + return _Label(None, expr, type_=type_) + def label(name, obj): """Return a :class:`_Label` object for the @@ -909,9 +959,6 @@ class _FunctionGenerator(object): o = self.opts.copy() o.update(kwargs) if len(self.__names) == 1: - global functions - if functions is None: - from sqlalchemy.sql import functions func = getattr(functions, self.__names[-1].lower(), None) if func is not None and \ isinstance(func, type) and \ @@ -1157,10 +1204,7 @@ class ClauseElement(Visitable): dictionary. """ - global sql_util - if sql_util is None: - from sqlalchemy.sql import util as sql_util - return sql_util.Annotated(self, values) + return sqlutil.Annotated(self, values) def _deannotate(self): """return a copy of this ClauseElement with an empty annotations @@ -1341,10 +1385,7 @@ class ClauseElement(Visitable): dialect = self.bind.dialect bind = self.bind else: - global DefaultDialect - if DefaultDialect is None: - from sqlalchemy.engine.default import DefaultDialect - dialect = DefaultDialect() + dialect = default.DefaultDialect() compiler = self._compiler(dialect, bind=bind, **kw) compiler.compile() return compiler @@ -1852,17 +1893,19 @@ class ColumnElement(ClauseElement, _CompareMixin): descending selectable. """ - - if name: - co = ColumnClause(name, selectable, type_=getattr(self, - 'type', None)) + if name is None: + name = self.anon_label + # TODO: may want to change this to anon_label, + # or some value that is more useful than the + # compiled form of the expression + key = str(self) else: - name = str(self) - co = ColumnClause(self.anon_label, selectable, - type_=getattr(self, 'type', None)) - + key = name + + co = ColumnClause(name, selectable, type_=getattr(self, + 'type', None)) co.proxies = [self] - selectable.columns[name] = co + selectable.columns[key] = co return co def compare(self, other, use_proxies=False, equivalents=None, **kw): @@ -2104,10 +2147,7 @@ class FromClause(Selectable): """ - global sql_util - if sql_util is None: - from sqlalchemy.sql import util as sql_util - return sql_util.ClauseAdapter(alias).traverse(self) + return sqlutil.ClauseAdapter(alias).traverse(self) def correspond_on_equivalents(self, column, equivalents): """Return corresponding_column for the given column, or if None @@ -2199,7 +2239,7 @@ class FromClause(Selectable): def _reset_exported(self): """delete memoized collections when a FromClause is cloned.""" - for attr in '_columns', '_primary_key_foreign_keys', \ + for attr in '_columns', '_primary_key', '_foreign_keys', \ 'locate_all_froms': self.__dict__.pop(attr, None) @@ -3048,10 +3088,7 @@ class Join(FromClause): columns = [c for c in self.left.columns] + \ [c for c in self.right.columns] - global sql_util - if not sql_util: - from sqlalchemy.sql import util as sql_util - self._primary_key.extend(sql_util.reduce_columns( + self._primary_key.extend(sqlutil.reduce_columns( (c for c in columns if c.primary_key), self.onclause)) self._columns.update((col._label, col) for col in columns) self._foreign_keys.update(itertools.chain( @@ -3068,14 +3105,11 @@ class Join(FromClause): return self.left, self.right, self.onclause def _match_primaries(self, left, right): - global sql_util - if not sql_util: - from sqlalchemy.sql import util as sql_util if isinstance(left, Join): left_right = left.right else: left_right = None - return sql_util.join_condition(left, right, a_subset=left_right) + return sqlutil.join_condition(left, right, a_subset=left_right) def select(self, whereclause=None, fold_equivalents=False, **kwargs): """Create a :class:`Select` from this :class:`Join`. @@ -3095,11 +3129,8 @@ class Join(FromClause): underlying :func:`select()` function. """ - global sql_util - if not sql_util: - from sqlalchemy.sql import util as sql_util if fold_equivalents: - collist = sql_util.folded_equivalents(self) + collist = sqlutil.folded_equivalents(self) else: collist = [self.left, self.right] @@ -3683,8 +3714,7 @@ class _ScalarSelect(_Grouping): def __init__(self, element): self.element = element - cols = list(element.c) - self.type = cols[0].type + self.type = element._scalar_type() @property def columns(self): @@ -3735,7 +3765,10 @@ class CompoundSelect(_SelectBaseMixin, FromClause): self.selects.append(s.self_group(self)) _SelectBaseMixin.__init__(self, **kwargs) - + + def _scalar_type(self): + return self.selects[0]._scalar_type() + def self_group(self, against=None): return _FromGrouping(self) @@ -3908,6 +3941,11 @@ class Select(_SelectBaseMixin, FromClause): return froms + def _scalar_type(self): + elem = self._raw_columns[0] + cols = list(elem._select_iterable) + return cols[0].type + @property def froms(self): """Return the displayed list of FromClause elements.""" @@ -3915,16 +3953,21 @@ class Select(_SelectBaseMixin, FromClause): return self._get_display_froms() @_generative - def with_hint(self, selectable, text, dialect_name=None): + def with_hint(self, selectable, text, dialect_name='*'): """Add an indexing hint for the given selectable to this :class:`Select`. - The text of the hint is written specific to a specific backend, and - typically uses Python string substitution syntax to render the name - of the table or alias, such as for Oracle:: + The text of the hint is rendered in the appropriate + location for the database backend in use, relative + to the given :class:`.Table` or :class:`.Alias` passed as the + *selectable* argument. The dialect implementation + typically uses Python string substitution syntax + with the token ``%(name)s`` to render the name of + the table or alias. E.g. when using Oracle, the + following:: - select([mytable]).with_hint(mytable, "+ index(%(name)s - ix_mytable)") + select([mytable]).\\ + with_hint(mytable, "+ index(%(name)s ix_mytable)") Would render SQL as:: @@ -3934,13 +3977,11 @@ class Select(_SelectBaseMixin, FromClause): hint to a particular backend. Such as, to add hints for both Oracle and Sybase simultaneously:: - select([mytable]).\ - with_hint(mytable, "+ index(%(name)s ix_mytable)", 'oracle').\ + select([mytable]).\\ + with_hint(mytable, "+ index(%(name)s ix_mytable)", 'oracle').\\ with_hint(mytable, "WITH INDEX ix_mytable", 'sybase') """ - if not dialect_name: - dialect_name = '*' self._hints = self._hints.union({(selectable, dialect_name):text}) @property diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index bd4f70247..638549e12 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -92,6 +92,25 @@ def find_columns(clause): visitors.traverse(clause, {}, {'column':cols.add}) return cols +def clause_is_present(clause, search): + """Given a target clause and a second to search within, return True + if the target is plainly present in the search without any + subqueries or aliases involved. + + Basically descends through Joins. + + """ + + stack = [search] + while stack: + elem = stack.pop() + if clause is elem: + return True + elif isinstance(elem, expression.Join): + stack.extend((elem.left, elem.right)) + return False + + def bind_values(clause): """Return an ordered list of "bound" values in the given clause. diff --git a/lib/sqlalchemy/test/__init__.py b/lib/sqlalchemy/test/__init__.py index d69cedefd..7356945d2 100644 --- a/lib/sqlalchemy/test/__init__.py +++ b/lib/sqlalchemy/test/__init__.py @@ -6,7 +6,8 @@ by noseplugin.NoseSQLAlchemy. """ -from sqlalchemy.test import testing, engines, requires, profiling, pickleable, config +from sqlalchemy_nose import config +from sqlalchemy.test import testing, engines, requires, profiling, pickleable from sqlalchemy.test.schema import Column, Table from sqlalchemy.test.testing import \ AssertsCompiledSQL, \ diff --git a/lib/sqlalchemy/test/engines.py b/lib/sqlalchemy/test/engines.py index 9e77f38d7..870f984ec 100644 --- a/lib/sqlalchemy/test/engines.py +++ b/lib/sqlalchemy/test/engines.py @@ -1,6 +1,6 @@ import sys, types, weakref from collections import deque -import config +from sqlalchemy_nose import config from sqlalchemy.util import function_named, callable import re import warnings diff --git a/lib/sqlalchemy/test/profiling.py b/lib/sqlalchemy/test/profiling.py index c5256affa..835253a3a 100644 --- a/lib/sqlalchemy/test/profiling.py +++ b/lib/sqlalchemy/test/profiling.py @@ -6,7 +6,7 @@ in a more fine-grained way than nose's profiling plugin. """ import os, sys -from sqlalchemy.test import config +from sqlalchemy_nose import config from sqlalchemy.test.util import function_named, gc_collect from nose import SkipTest diff --git a/lib/sqlalchemy/test/requires.py b/lib/sqlalchemy/test/requires.py index 501f0e24d..d29b7abc2 100644 --- a/lib/sqlalchemy/test/requires.py +++ b/lib/sqlalchemy/test/requires.py @@ -52,6 +52,7 @@ def boolean_col_expressions(fn): no_support('mssql', 'not supported by database'), no_support('sybase', 'not supported by database'), no_support('maxdb', 'FIXME: verify not supported by database'), + no_support('informix', 'not supported by database'), ) def identity(fn): @@ -120,6 +121,7 @@ def savepoints(fn): no_support('sqlite', 'not supported by database'), no_support('sybase', 'FIXME: guessing, needs confirmation'), exclude('mysql', '<', (5, 0, 3), 'not supported by database'), + exclude('informix', '<', (11, 55, 'xC3'), 'not supported by database'), ) def denormalized_names(fn): @@ -148,6 +150,7 @@ def sequences(fn): no_support('mysql', 'no SEQUENCE support'), no_support('sqlite', 'no SEQUENCE support'), no_support('sybase', 'no SEQUENCE support'), + no_support('informix', 'no SEQUENCE support'), ) def update_nowait(fn): @@ -176,6 +179,7 @@ def intersect(fn): fails_on('firebird', 'no support for INTERSECT'), fails_on('mysql', 'no support for INTERSECT'), fails_on('sybase', 'no support for INTERSECT'), + fails_on('informix', 'no support for INTERSECT'), ) def except_(fn): @@ -185,6 +189,7 @@ def except_(fn): fails_on('firebird', 'no support for EXCEPT'), fails_on('mysql', 'no support for EXCEPT'), fails_on('sybase', 'no support for EXCEPT'), + fails_on('informix', 'no support for EXCEPT'), ) def offset(fn): @@ -247,6 +252,18 @@ def sane_rowcount(fn): skip_if(lambda: not testing.db.dialect.supports_sane_rowcount) ) +def cextensions(fn): + return _chain_decorators_on( + fn, + skip_if(lambda: not _has_cextensions(), "C extensions not installed") + ) + +def dbapi_lastrowid(fn): + return _chain_decorators_on( + fn, + fails_on_everything_except('mysql+mysqldb', 'mysql+oursql', 'sqlite+pysqlite') + ) + def sane_multi_rowcount(fn): return _chain_decorators_on( fn, @@ -268,7 +285,23 @@ def python2(fn): "Python version 2.xx is required." ) ) + +def python26(fn): + return _chain_decorators_on( + fn, + skip_if( + lambda: sys.version_info < (2, 6), + "Python version 2.6 or greater is required" + ) + ) +def _has_cextensions(): + try: + from sqlalchemy import cresultproxy, cprocessors + return True + except ImportError: + return False + def _has_sqlite(): from sqlalchemy import create_engine try: diff --git a/lib/sqlalchemy/test/testing.py b/lib/sqlalchemy/test/testing.py index 41ba3038f..12cbe5e02 100644 --- a/lib/sqlalchemy/test/testing.py +++ b/lib/sqlalchemy/test/testing.py @@ -8,7 +8,8 @@ import types import warnings from cStringIO import StringIO -from sqlalchemy.test import config, assertsql, util as testutil +from sqlalchemy_nose import config +from sqlalchemy.test import assertsql, util as testutil from sqlalchemy.util import function_named, py3k from engines import drop_all_tables @@ -207,9 +208,9 @@ def _block_unconditionally(db, reason): return function_named(maybe, fn_name) return decorate -def only_on(db, reason): +def only_on(dbs, reason): carp = _should_carp_about_exclusion(reason) - spec = db_spec(db) + spec = db_spec(*util.to_list(dbs)) def decorate(fn): fn_name = fn.__name__ def maybe(*args, **kw): diff --git a/lib/sqlalchemy/test/util.py b/lib/sqlalchemy/test/util.py index ff2c3d7b7..f2b6b49ea 100644 --- a/lib/sqlalchemy/test/util.py +++ b/lib/sqlalchemy/test/util.py @@ -22,8 +22,6 @@ else: def lazy_gc(): pass - - def picklers(): picklers = set() # Py2K diff --git a/lib/sqlalchemy/topological.py b/lib/sqlalchemy/topological.py index 6c3e90d98..0f4f32461 100644 --- a/lib/sqlalchemy/topological.py +++ b/lib/sqlalchemy/topological.py @@ -9,6 +9,7 @@ from sqlalchemy.exc import CircularDependencyError from sqlalchemy import util + __all__ = ['sort', 'sort_as_subsets', 'find_cycles'] def sort_as_subsets(tuples, allitems): @@ -27,8 +28,10 @@ def sort_as_subsets(tuples, allitems): if not output: raise CircularDependencyError( - "Circular dependency detected: cycles: %r all edges: %s" % - (find_cycles(tuples, allitems), _dump_edges(edges, True))) + "Circular dependency detected", + find_cycles(tuples, allitems), + _gen_edges(edges) + ) todo.difference_update(output) yield output @@ -72,14 +75,9 @@ def find_cycles(tuples, allitems): node = stack.pop() return output -def _dump_edges(edges, reverse): - l = [] - for left in edges: - for right in edges[left]: - if reverse: - l.append((right, left)) - else: - l.append((left, right)) - return repr(l) - - +def _gen_edges(edges): + return set([ + (right, left) + for left in edges + for right in edges[left] + ]) diff --git a/lib/sqlalchemy/types.py b/lib/sqlalchemy/types.py index af7ef22e6..9f322d1eb 100644 --- a/lib/sqlalchemy/types.py +++ b/lib/sqlalchemy/types.py @@ -34,8 +34,8 @@ from sqlalchemy.sql.visitors import Visitable from sqlalchemy import util from sqlalchemy import processors import collections +default = util.importlater("sqlalchemy.engine", "default") -DefaultDialect = None NoneType = type(None) if util.jython: import array @@ -132,18 +132,25 @@ class AbstractType(Visitable): # ClauseElement.compile()....this is a mistake. if not dialect: - global DefaultDialect - if DefaultDialect is None: - from sqlalchemy.engine.default import DefaultDialect - dialect = DefaultDialect() + dialect = self._default_dialect return dialect.type_compiler.process(self) - + + @property + def _default_dialect(self): + if self.__class__.__module__.startswith("sqlalchemy.dialects"): + tokens = self.__class__.__module__.split(".")[0:3] + mod = ".".join(tokens) + return getattr(__import__(mod).dialects, tokens[-1]).dialect() + else: + return default.DefaultDialect() + def __str__(self): # Py3K #return unicode(self.compile()) # Py2K - return unicode(self.compile()).encode('ascii', 'backslashreplace') + return unicode(self.compile()).\ + encode('ascii', 'backslashreplace') # end Py2K def __init__(self, *args, **kwargs): @@ -346,21 +353,19 @@ class TypeDecorator(AbstractType): "require a class-level variable " "'impl' which refers to the class of " "type being decorated") - self.impl = self.__class__.impl(*args, **kwargs) + self.impl = to_instance(self.__class__.impl, *args, **kwargs) def adapt(self, cls): return cls() def dialect_impl(self, dialect): key = (dialect.__class__, dialect.server_version_info) + try: return self._impl_dict[key] except KeyError: pass - # adapt the TypeDecorator first, in - # the case that the dialect maps the TD - # to one of its native types (i.e. PGInterval) adapted = dialect.type_descriptor(self) if adapted is not self: self._impl_dict[key] = adapted @@ -369,7 +374,7 @@ class TypeDecorator(AbstractType): # otherwise adapt the impl type, link # to a copy of this TypeDecorator and return # that. - typedesc = self.load_dialect_impl(dialect) + typedesc = self.load_dialect_impl(dialect).dialect_impl(dialect) tt = self.copy() if not isinstance(tt, self.__class__): raise AssertionError('Type object %s does not properly ' @@ -381,27 +386,33 @@ class TypeDecorator(AbstractType): return tt @util.memoized_property + def _impl_dict(self): + return {} + + @util.memoized_property def _type_affinity(self): return self.impl._type_affinity def type_engine(self, dialect): - impl = self.dialect_impl(dialect) - if not isinstance(impl, TypeDecorator): - return impl + """Return a TypeEngine instance for this TypeDecorator. + + """ + adapted = dialect.type_descriptor(self) + if adapted is not self: + return adapted + elif isinstance(self.impl, TypeDecorator): + return self.impl.type_engine(dialect) else: - return impl.impl + return self.load_dialect_impl(dialect) def load_dialect_impl(self, dialect): - """Loads the dialect-specific implementation of this type. + """User hook which can be overridden to provide a different 'impl' + type per-dialect. - by default calls dialect.type_descriptor(self.impl), but - can be overridden to provide different behavior. + by default returns self.impl. """ - if isinstance(self.impl, TypeDecorator): - return self.impl.dialect_impl(dialect) - else: - return dialect.type_descriptor(self.impl) + return self.impl def __getattr__(self, key): """Proxy all other undefined accessors to the underlying @@ -503,9 +514,11 @@ class TypeDecorator(AbstractType): return self.impl.is_mutable() def _adapt_expression(self, op, othertype): - return self.impl._adapt_expression(op, othertype) - - + op, typ =self.impl._adapt_expression(op, othertype) + if typ is self.impl: + return op, self + else: + return op, typ class MutableType(object): """A mixin that marks a :class:`TypeEngine` as representing @@ -593,12 +606,12 @@ class MutableType(object): """Compare *x* == *y*.""" return x == y -def to_instance(typeobj): +def to_instance(typeobj, *arg, **kw): if typeobj is None: return NULLTYPE if util.callable(typeobj): - return typeobj() + return typeobj(*arg, **kw) else: return typeobj @@ -639,7 +652,7 @@ class NullType(TypeEngine): __visit_name__ = 'null' def _adapt_expression(self, op, othertype): - if othertype is NULLTYPE or not operators.is_commutative(op): + if isinstance(othertype, NullType) or not operators.is_commutative(op): return op, self else: return othertype._adapt_expression(op, self) diff --git a/lib/sqlalchemy/util.py b/lib/sqlalchemy/util.py index 10931be5e..8665cd0d4 100644 --- a/lib/sqlalchemy/util.py +++ b/lib/sqlalchemy/util.py @@ -584,6 +584,18 @@ def asbool(obj): raise ValueError("String is not true/false: %r" % obj) return bool(obj) +def bool_or_str(*text): + """Return a callable that will evaulate a string as + boolean, or one of a set of "alternate" string values. + + """ + def bool_or_value(obj): + if obj in text: + return obj + else: + return asbool(obj) + return bool_or_value + def coerce_kw_type(kw, key, type_, flexi_bool=True): """If 'key' is present in dict 'kw', coerce its value to type 'type\_' if necessary. If 'flexi_bool' is True, the string '0' is considered false @@ -745,7 +757,7 @@ class NamedTuple(tuple): return t def keys(self): - return self._labels + return [l for l in self._labels if l is not None] class OrderedProperties(object): @@ -1546,7 +1558,51 @@ class group_expirable_memoized_property(object): self.attributes.append(fn.__name__) return memoized_property(fn) - +class importlater(object): + """Deferred import object. + + e.g.:: + + somesubmod = importlater("mypackage.somemodule", "somesubmod") + + is equivalent to:: + + from mypackage.somemodule import somesubmod + + except evaluted upon attribute access to "somesubmod". + + """ + def __init__(self, path, addtl=None): + self._il_path = path + self._il_addtl = addtl + + @memoized_property + def _il_module(self): + m = __import__(self._il_path) + for token in self._il_path.split(".")[1:]: + m = getattr(m, token) + if self._il_addtl: + try: + return getattr(m, self._il_addtl) + except AttributeError: + raise AttributeError( + "Module %s has no attribute '%s'" % + (self._il_path, self._il_addtl) + ) + else: + return m + + def __getattr__(self, key): + try: + attr = getattr(self._il_module, key) + except AttributeError: + raise AttributeError( + "Module %s has no attribute '%s'" % + (self._il_path, key) + ) + self.__dict__[key] = attr + return attr + class WeakIdentityMapping(weakref.WeakKeyDictionary): """A WeakKeyDictionary with an object identity index. @@ -1801,8 +1857,12 @@ class classproperty(property): """A decorator that behaves like @property except that operates on classes rather than instances. - This is helpful when you need to compute __table_args__ and/or - __mapper_args__ when using declarative.""" + The decorator is currently special when using the declarative + module, but note that the + :class:`~.sqlalchemy.ext.declarative.declared_attr` + decorator should be used for this purpose with declarative. + + """ def __init__(self, fget, *arg, **kw): super(classproperty, self).__init__(fget, *arg, **kw) diff --git a/lib/sqlalchemy_nose/__init__.py b/lib/sqlalchemy_nose/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/lib/sqlalchemy_nose/__init__.py diff --git a/lib/sqlalchemy/test/config.py b/lib/sqlalchemy_nose/config.py index 7d528a04b..7d528a04b 100644 --- a/lib/sqlalchemy/test/config.py +++ b/lib/sqlalchemy_nose/config.py diff --git a/lib/sqlalchemy/test/noseplugin.py b/lib/sqlalchemy_nose/noseplugin.py index 6a3106e69..8732142f7 100644 --- a/lib/sqlalchemy/test/noseplugin.py +++ b/lib/sqlalchemy_nose/noseplugin.py @@ -10,9 +10,9 @@ import StringIO import nose.case from nose.plugins import Plugin -from sqlalchemy import util, log as sqla_log -from sqlalchemy.test import testing, config, requires -from sqlalchemy.test.config import ( +from sqlalchemy_nose import config + +from sqlalchemy_nose.config import ( _create_testing_engine, _engine_pool, _engine_strategy, _engine_uri, _list_dbs, _log, _prep_testing_database, _require, _reverse_topological, _server_side_cursors, _set_table_options, base_config, db, db_label, db_url, file_config, post_configure) @@ -78,6 +78,10 @@ class NoseSQLAlchemy(Plugin): self.options = options def begin(self): + global testing, requires, util + from sqlalchemy.test import testing, requires + from sqlalchemy import util + testing.db = db testing.requires = requires |
