summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2010-11-14 17:54:47 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2010-11-14 17:54:47 -0500
commitf252af2b21c5bafeaa30aabcf65dfed9b5c01093 (patch)
treedd5bf4f56ac68d78edfcb37a9c0c3c380c8ef6a8 /lib
parent9d7158a2c3869ad7a1ab07d3a41e831f6806a68c (diff)
parent06bf218ed37ca780bc4de2ceb47769c84de70ba1 (diff)
downloadsqlalchemy-f252af2b21c5bafeaa30aabcf65dfed9b5c01093.tar.gz
merge tip
Diffstat (limited to 'lib')
-rw-r--r--lib/sqlalchemy/__init__.py3
-rw-r--r--lib/sqlalchemy/cextension/resultproxy.c8
-rw-r--r--lib/sqlalchemy/dialects/informix/base.py293
-rw-r--r--lib/sqlalchemy/dialects/informix/informixdb.py37
-rw-r--r--lib/sqlalchemy/dialects/mssql/base.py79
-rw-r--r--lib/sqlalchemy/dialects/mssql/information_schema.py2
-rw-r--r--lib/sqlalchemy/dialects/mssql/pymssql.py6
-rw-r--r--lib/sqlalchemy/dialects/mysql/base.py4
-rw-r--r--lib/sqlalchemy/dialects/mysql/zxjdbc.py2
-rw-r--r--lib/sqlalchemy/dialects/oracle/base.py8
-rw-r--r--lib/sqlalchemy/dialects/postgresql/base.py70
-rw-r--r--lib/sqlalchemy/dialects/sqlite/base.py16
-rw-r--r--lib/sqlalchemy/dialects/sqlite/pysqlite.py7
-rw-r--r--lib/sqlalchemy/engine/__init__.py7
-rw-r--r--lib/sqlalchemy/engine/base.py16
-rw-r--r--lib/sqlalchemy/engine/default.py1
-rw-r--r--lib/sqlalchemy/exc.py6
-rwxr-xr-xlib/sqlalchemy/ext/declarative.py326
-rw-r--r--lib/sqlalchemy/orm/__init__.py64
-rw-r--r--lib/sqlalchemy/orm/attributes.py9
-rw-r--r--lib/sqlalchemy/orm/dependency.py6
-rw-r--r--lib/sqlalchemy/orm/dynamic.py2
-rw-r--r--lib/sqlalchemy/orm/interfaces.py48
-rw-r--r--lib/sqlalchemy/orm/mapper.py122
-rw-r--r--lib/sqlalchemy/orm/properties.py13
-rw-r--r--lib/sqlalchemy/orm/query.py144
-rw-r--r--lib/sqlalchemy/orm/scoping.py11
-rw-r--r--lib/sqlalchemy/orm/session.py120
-rw-r--r--lib/sqlalchemy/orm/state.py10
-rw-r--r--lib/sqlalchemy/orm/strategies.py70
-rw-r--r--lib/sqlalchemy/orm/unitofwork.py17
-rw-r--r--lib/sqlalchemy/orm/util.py39
-rw-r--r--lib/sqlalchemy/schema.py74
-rw-r--r--lib/sqlalchemy/sql/__init__.py1
-rw-r--r--lib/sqlalchemy/sql/compiler.py19
-rw-r--r--lib/sqlalchemy/sql/expression.py151
-rw-r--r--lib/sqlalchemy/sql/util.py19
-rw-r--r--lib/sqlalchemy/test/__init__.py3
-rw-r--r--lib/sqlalchemy/test/engines.py2
-rw-r--r--lib/sqlalchemy/test/profiling.py2
-rw-r--r--lib/sqlalchemy/test/requires.py33
-rw-r--r--lib/sqlalchemy/test/testing.py7
-rw-r--r--lib/sqlalchemy/test/util.py2
-rw-r--r--lib/sqlalchemy/topological.py24
-rw-r--r--lib/sqlalchemy/types.py71
-rw-r--r--lib/sqlalchemy/util.py68
-rw-r--r--lib/sqlalchemy_nose/__init__.py0
-rw-r--r--lib/sqlalchemy_nose/config.py (renamed from lib/sqlalchemy/test/config.py)0
-rw-r--r--lib/sqlalchemy_nose/noseplugin.py (renamed from lib/sqlalchemy/test/noseplugin.py)10
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