diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/sqlalchemy/dialects/mssql/base.py | 144 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/mysql/base.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/postgresql/pg8000.py | 17 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/sqlite/base.py | 6 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/__init__.py | 1 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/base.py | 83 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/interfaces.py | 137 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/result.py | 16 | ||||
| -rw-r--r-- | lib/sqlalchemy/events.py | 108 | ||||
| -rw-r--r-- | lib/sqlalchemy/ext/declarative/base.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/dynamic.py | 5 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/persistence.py | 17 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/query.py | 7 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/session.py | 5 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 20 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/functions.py | 9 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/schema.py | 20 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/type_api.py | 9 |
19 files changed, 528 insertions, 82 deletions
diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 59cbb80bb..4c0f6ae33 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -12,18 +12,69 @@ Auto Increment Behavior ----------------------- -``IDENTITY`` columns are supported by using SQLAlchemy -``schema.Sequence()`` objects. In other words:: +SQL Server provides so-called "auto incrementing" behavior using the ``IDENTITY`` +construct, which can be placed on an integer primary key. SQLAlchemy +considers ``IDENTITY`` within its default "autoincrement" behavior, +described at :paramref:`.Column.autoincrement`; this means +that by default, the first integer primary key column in a :class:`.Table` +will be considered to be the identity column and will generate DDL as such:: + + from sqlalchemy import Table, MetaData, Column, Integer + + m = MetaData() + t = Table('t', m, + Column('id', Integer, primary_key=True), + Column('x', Integer)) + m.create_all(engine) + +The above example will generate DDL as: + +.. sourcecode:: sql + + CREATE TABLE t ( + id INTEGER NOT NULL IDENTITY(1,1), + x INTEGER NULL, + PRIMARY KEY (id) + ) + +For the case where this default generation of ``IDENTITY`` is not desired, +specify ``autoincrement=False`` on all integer primary key columns:: + + m = MetaData() + t = Table('t', m, + Column('id', Integer, primary_key=True, autoincrement=False), + Column('x', Integer)) + m.create_all(engine) + +.. note:: + + An INSERT statement which refers to an explicit value for such + a column is prohibited by SQL Server, however SQLAlchemy will detect this + and modify the ``IDENTITY_INSERT`` flag accordingly at statement execution + time. As this is not a high performing process, care should be taken to set + the ``autoincrement`` flag appropriately for columns that will not actually + require IDENTITY behavior. + +Controlling "Start" and "Increment" +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Specific control over the parameters of the ``IDENTITY`` value is supported +using the :class:`.schema.Sequence` object. While this object normally represents +an explicit "sequence" for supporting backends, on SQL Server it is re-purposed +to specify behavior regarding the identity column, including support +of the "start" and "increment" values:: from sqlalchemy import Table, Integer, Sequence, Column Table('test', metadata, Column('id', Integer, - Sequence('blah',100,10), primary_key=True), + Sequence('blah', start=100, increment=10), primary_key=True), Column('name', String(20)) ).create(some_engine) -would yield:: +would yield: + +.. sourcecode:: sql CREATE TABLE test ( id INTEGER NOT NULL IDENTITY(100,10) PRIMARY KEY, @@ -33,14 +84,85 @@ would yield:: Note that the ``start`` and ``increment`` values for sequences are optional and will default to 1,1. -Implicit ``autoincrement`` behavior works the same in MSSQL as it -does in other dialects and results in an ``IDENTITY`` column. +INSERT behavior +^^^^^^^^^^^^^^^^ + +Handling of the ``IDENTITY`` column at INSERT time involves two key techniques. +The most common is being able to fetch the "last inserted value" for a given +``IDENTITY`` column, a process which SQLAlchemy performs implicitly in many +cases, most importantly within the ORM. + +The process for fetching this value has several variants: + +* In the vast majority of cases, RETURNING is used in conjunction with INSERT + statements on SQL Server in order to get newly generated primary key values: + + .. sourcecode:: sql + + INSERT INTO t (x) OUTPUT inserted.id VALUES (?) + +* When RETURNING is not available or has been disabled via + ``implicit_returning=False``, either the ``scope_identity()`` function or + the ``@@identity`` variable is used; behavior varies by backend: + + * when using PyODBC, the phrase ``; select scope_identity()`` will be appended + to the end of the INSERT statement; a second result set will be fetched + in order to receive the value. Given a table as:: + + t = Table('t', m, Column('id', Integer, primary_key=True), + Column('x', Integer), + implicit_returning=False) + + an INSERT will look like: + + .. sourcecode:: sql + + INSERT INTO t (x) VALUES (?); select scope_identity() + + * Other dialects such as pymssql will call upon + ``SELECT scope_identity() AS lastrowid`` subsequent to an INSERT statement. + If the flag ``use_scope_identity=False`` is passed to :func:`.create_engine`, + the statement ``SELECT @@identity AS lastrowid`` is used instead. + +A table that contains an ``IDENTITY`` column will prohibit an INSERT statement +that refers to the identity column explicitly. The SQLAlchemy dialect will +detect when an INSERT construct, created using a core :func:`.insert` +construct (not a plain string SQL), refers to the identity column, and +in this case will emit ``SET IDENTITY_INSERT ON`` prior to the insert statement +proceeding, and ``SET IDENTITY_INSERT OFF`` subsequent to the execution. +Given this example:: + + m = MetaData() + t = Table('t', m, Column('id', Integer, primary_key=True), + Column('x', Integer)) + m.create_all(engine) + + engine.execute(t.insert(), {'id': 1, 'x':1}, {'id':2, 'x':2}) + +The above column will be created with IDENTITY, however the INSERT statement +we emit is specifying explicit values. In the echo output we can see +how SQLAlchemy handles this: + +.. sourcecode:: sql + + CREATE TABLE t ( + id INTEGER NOT NULL IDENTITY(1,1), + x INTEGER NULL, + PRIMARY KEY (id) + ) + + COMMIT + SET IDENTITY_INSERT t ON + INSERT INTO t (id, x) VALUES (?, ?) + ((1, 1), (2, 2)) + SET IDENTITY_INSERT t OFF + COMMIT + + -* Support for ``SET IDENTITY_INSERT ON`` mode (automagic on / off for - ``INSERT`` s) +This +is an auxilliary use case suitable for testing and bulk insert scenarios. -* Support for auto-fetching of ``@@IDENTITY/@@SCOPE_IDENTITY()`` on - ``INSERT`` Collation Support ----------------- @@ -1232,6 +1354,8 @@ class MSDialect(default.DefaultDialect): if self.server_version_info >= MS_2005_VERSION and \ 'implicit_returning' not in self.__dict__: self.implicit_returning = True + if self.server_version_info >= MS_2008_VERSION: + self.supports_multivalues_insert = True def _get_default_schema_name(self, connection): query = sql.text(""" diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 6d8b6a07c..77fb34fd8 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -2225,7 +2225,7 @@ class MySQLDialect(default.DefaultDialect): return [row['data'][0:row['gtrid_length']] for row in resultset] def is_disconnect(self, e, connection, cursor): - if isinstance(e, self.dbapi.OperationalError): + if isinstance(e, (self.dbapi.OperationalError, self.dbapi.ProgrammingError)): return self._extract_error_code(e) in \ (2006, 2013, 2014, 2045, 2055) elif isinstance(e, self.dbapi.InterfaceError): diff --git a/lib/sqlalchemy/dialects/postgresql/pg8000.py b/lib/sqlalchemy/dialects/postgresql/pg8000.py index 8ed7c9423..dc5ed6e73 100644 --- a/lib/sqlalchemy/dialects/postgresql/pg8000.py +++ b/lib/sqlalchemy/dialects/postgresql/pg8000.py @@ -16,14 +16,15 @@ postgresql+pg8000://user:password@host:port/dbname[?key=value&key=value...] Unicode ------- -When communicating with the server, pg8000 uses the character set that the -server asks it to use (the client encoding). By default the client encoding is -the database's character set (chosen when the database is created), but the -client encoding can be changed in a number of ways (eg. setting CLIENT_ENCODING -in postgresql.conf). - -Set the "encoding" parameter on create_engine(), to the same as the client -encoding, usually "utf-8". +When communicating with the server, pg8000 **always uses the server-side +character set**. SQLAlchemy has no ability to modify what character set +pg8000 chooses to use, and additionally SQLAlchemy does no unicode conversion +of any kind with the pg8000 backend. The origin of the client encoding setting +is ultimately the CLIENT_ENCODING setting in postgresql.conf. + +It is not necessary, though is also harmless, to pass the "encoding" parameter +to :func:`.create_engine` when using pg8000. + .. _pg8000_isolation_level: diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index de8b01e6f..9474d6ffc 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -22,8 +22,10 @@ These types represent dates and times as ISO formatted strings, which also nicely support ordering. There's no reliance on typical "libc" internals for these functions so historical dates are fully supported. -Auto Incrementing Behavior --------------------------- +.. _sqlite_autoincrement: + +SQLite Auto Incrementing Behavior +---------------------------------- Background on SQLite's autoincrement is at: http://sqlite.org/autoinc.html diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index fcb38b09c..9c6460858 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -54,6 +54,7 @@ from .interfaces import ( Connectable, Dialect, ExecutionContext, + ExceptionContext, # backwards compat Compiled, diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 249c494fe..6da41927f 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -12,8 +12,8 @@ from __future__ import with_statement import sys from .. import exc, util, log, interfaces -from ..sql import expression, util as sql_util, schema, ddl -from .interfaces import Connectable, Compiled +from ..sql import util as sql_util +from .interfaces import Connectable, ExceptionContext from .util import _distill_params import contextlib @@ -1070,10 +1070,16 @@ class Connection(Connectable): exc_info = sys.exc_info() + if context and context.exception is None: + context.exception = e + if not self._is_disconnect: - self._is_disconnect = isinstance(e, self.dialect.dbapi.Error) and \ + self._is_disconnect = \ + isinstance(e, self.dialect.dbapi.Error) and \ not self.closed and \ self.dialect.is_disconnect(e, self.__connection, cursor) + if context: + context.is_disconnect = self._is_disconnect if self._reentrant_error: util.raise_from_cause( @@ -1090,14 +1096,51 @@ class Connection(Connectable): should_wrap = isinstance(e, self.dialect.dbapi.Error) or \ (statement is not None and context is None) - if should_wrap and context: - if self._has_events or self.engine._has_events: + if should_wrap: + sqlalchemy_exception = exc.DBAPIError.instance( + statement, + parameters, + e, + self.dialect.dbapi.Error, + connection_invalidated=self._is_disconnect) + else: + sqlalchemy_exception = None + + newraise = None + + if self._has_events or self.engine._has_events: + # legacy dbapi_error event + if should_wrap and context: self.dispatch.dbapi_error(self, cursor, statement, parameters, context, e) + + # new handle_error event + ctx = ExceptionContextImpl( + e, sqlalchemy_exception, self, cursor, statement, + parameters, context, self._is_disconnect) + + for fn in self.dispatch.handle_error: + try: + # handler returns an exception; + # call next handler in a chain + per_fn = fn(ctx) + if per_fn is not None: + ctx.chained_exception = newraise = per_fn + except Exception as _raised: + # handler raises an exception - stop processing + newraise = _raised + break + + if sqlalchemy_exception and \ + self._is_disconnect != ctx.is_disconnect: + sqlalchemy_exception.connection_invalidated = \ + self._is_disconnect = ctx.is_disconnect + + if should_wrap and context: context.handle_dbapi_exception(e) if not self._is_disconnect: @@ -1105,18 +1148,15 @@ class Connection(Connectable): self._safe_close_cursor(cursor) self._autorollback() - if should_wrap: + if newraise: + util.raise_from_cause(newraise, exc_info) + elif should_wrap: util.raise_from_cause( - exc.DBAPIError.instance( - statement, - parameters, - e, - self.dialect.dbapi.Error, - connection_invalidated=self._is_disconnect), + sqlalchemy_exception, exc_info ) - - util.reraise(*exc_info) + else: + util.reraise(*exc_info) finally: del self._reentrant_error @@ -1202,6 +1242,21 @@ class Connection(Connectable): **kwargs).traverse_single(element) +class ExceptionContextImpl(ExceptionContext): + """Implement the :class:`.ExceptionContext` interface.""" + + def __init__(self, exception, sqlalchemy_exception, + connection, cursor, statement, parameters, + context, is_disconnect): + self.connection = connection + self.sqlalchemy_exception = sqlalchemy_exception + self.original_exception = exception + self.execution_context = context + self.statement = statement + self.parameters = parameters + self.is_disconnect = is_disconnect + + class Transaction(object): """Represent a database transaction in progress. diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index 22801c284..1807e4283 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -707,6 +707,41 @@ class ExecutionContext(object): and updates. """ + exception = None + """A DBAPI-level exception that was caught when this ExecutionContext + attempted to execute a statement. + + This attribute is meaningful only within the + :meth:`.ConnectionEvents.dbapi_error` event. + + .. versionadded:: 0.9.7 + + .. seealso:: + + :attr:`.ExecutionContext.is_disconnect` + + :meth:`.ConnectionEvents.dbapi_error` + + """ + + is_disconnect = None + """Boolean flag set to True or False when a DBAPI-level exception + is caught when this ExecutionContext attempted to execute a statement. + + This attribute is meaningful only within the + :meth:`.ConnectionEvents.dbapi_error` event. + + .. versionadded:: 0.9.7 + + .. seealso:: + + :attr:`.ExecutionContext.exception` + + :meth:`.ConnectionEvents.dbapi_error` + + """ + + def create_cursor(self): """Return a new cursor generated from this ExecutionContext's connection. @@ -847,3 +882,105 @@ class Connectable(object): def _execute_clauseelement(self, elem, multiparams=None, params=None): raise NotImplementedError() + +class ExceptionContext(object): + """Encapsulate information about an error condition in progress. + + This object exists solely to be passed to the + :meth:`.ConnectionEvents.handle_error` event, supporting an interface that + can be extended without backwards-incompatibility. + + .. versionadded:: 0.9.7 + + """ + + connection = None + """The :class:`.Connection` in use during the exception. + + This member is always present. + + """ + + cursor = None + """The DBAPI cursor object. + + May be None. + + """ + + statement = None + """String SQL statement that was emitted directly to the DBAPI. + + May be None. + + """ + + parameters = None + """Parameter collection that was emitted directly to the DBAPI. + + May be None. + + """ + + original_exception = None + """The exception object which was caught. + + This member is always present. + + """ + + sqlalchemy_exception = None + """The :class:`sqlalchemy.exc.StatementError` which wraps the original, + and will be raised if exception handling is not circumvented by the event. + + May be None, as not all exception types are wrapped by SQLAlchemy. + For DBAPI-level exceptions that subclass the dbapi's Error class, this + field will always be present. + + """ + + chained_exception = None + """The exception that was returned by the previous handler in the + exception chain, if any. + + If present, this exception will be the one ultimately raised by + SQLAlchemy unless a subsequent handler replaces it. + + May be None. + + """ + + execution_context = None + """The :class:`.ExecutionContext` corresponding to the execution + operation in progress. + + This is present for statement execution operations, but not for + operations such as transaction begin/end. It also is not present when + the exception was raised before the :class:`.ExecutionContext` + could be constructed. + + Note that the :attr:`.ExceptionContext.statement` and + :attr:`.ExceptionContext.parameters` members may represent a + different value than that of the :class:`.ExecutionContext`, + potentially in the case where a + :meth:`.ConnectionEvents.before_cursor_execute` event or similar + modified the statement/parameters to be sent. + + May be None. + + """ + + is_disconnect = None + """Represent whether the exception as occurred represents a "disconnect" + condition. + + This flag will always be True or False within the scope of the + :meth:`.ConnectionEvents.handle_error` handler. + + SQLAlchemy will defer to this flag in order to determine whether or not + the connection should be invalidated subsequently. That is, by + assigning to this flag, a "disconnect" event which then results in + a connection and pool invalidation can be invoked or prevented by + changing this flag. + + """ diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 6c98dae18..0838d810d 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -269,9 +269,6 @@ class ResultMetaData(object): # high precedence keymap. keymap.update(primary_keymap) - if parent._echo: - context.engine.logger.debug( - "Col %r", tuple(x[0] for x in metadata)) @util.pending_deprecation("0.8", "sqlite dialect uses " "_translate_colname() now") @@ -406,7 +403,18 @@ class ResultProxy(object): def _init_metadata(self): metadata = self._cursor_description() if metadata is not None: - self._metadata = ResultMetaData(self, metadata) + if self.context.compiled and \ + 'compiled_cache' in self.context.execution_options: + if self.context.compiled._cached_metadata: + self._metadata = self.context.compiled._cached_metadata + else: + self._metadata = self.context.compiled._cached_metadata = \ + ResultMetaData(self, metadata) + else: + self._metadata = ResultMetaData(self, metadata) + if self._echo: + self.context.engine.logger.debug( + "Col %r", tuple(x[0] for x in metadata)) def keys(self): """Return the current set of string keys for rows.""" diff --git a/lib/sqlalchemy/events.py b/lib/sqlalchemy/events.py index 908ce378f..e4bc48615 100644 --- a/lib/sqlalchemy/events.py +++ b/lib/sqlalchemy/events.py @@ -492,12 +492,12 @@ class ConnectionEvents(event.Events): parameters, context, executemany) return statement, parameters fn = wrap_before_cursor_execute - elif retval and \ - identifier not in ('before_execute', 'before_cursor_execute'): + identifier not in ('before_execute', + 'before_cursor_execute', 'handle_error'): raise exc.ArgumentError( - "Only the 'before_execute' and " - "'before_cursor_execute' engine " + "Only the 'before_execute', " + "'before_cursor_execute' and 'handle_error' engine " "event listeners accept the 'retval=True' " "argument.") event_key.with_wrapper(fn).base_listen() @@ -611,17 +611,22 @@ class ConnectionEvents(event.Events): This event is called with the DBAPI exception instance received from the DBAPI itself, *before* SQLAlchemy wraps the - exception with its own exception wrappers, and before any + exception with it's own exception wrappers, and before any other operations are performed on the DBAPI cursor; the existing transaction remains in effect as well as any state on the cursor. The use case here is to inject low-level exception handling into an :class:`.Engine`, typically for logging and - debugging purposes. In general, user code should **not** modify - any state or throw any exceptions here as this will - interfere with SQLAlchemy's cleanup and error handling - routines. + debugging purposes. + + .. warning:: + + Code should **not** modify + any state or throw any exceptions here as this will + interfere with SQLAlchemy's cleanup and error handling + routines. For exception modification, please refer to the + new :meth:`.ConnectionEvents.handle_error` event. Subsequent to this hook, SQLAlchemy may attempt any number of operations on the connection/cursor, including @@ -642,7 +647,90 @@ class ConnectionEvents(event.Events): :param exception: The **unwrapped** exception emitted directly from the DBAPI. The class here is specific to the DBAPI module in use. - .. versionadded:: 0.7.7 + .. deprecated:: 0.9.7 - replaced by + :meth:`.ConnectionEvents.handle_error` + + """ + + def handle_error(self, exception_context): + """Intercept all exceptions processed by the :class:`.Connection`. + + This includes all exceptions emitted by the DBAPI as well as + within SQLAlchemy's statement invocation process, including + encoding errors and other statement validation errors. Other areas + in which the event is invoked include transaction begin and end, + result row fetching, cursor creation. + + Note that :meth:`.handle_error` may support new kinds of exceptions + and new calling scenarios at *any time*. Code which uses this + event must expect new calling patterns to be present in minor + releases. + + To support the wide variety of members that correspond to an exception, + as well as to allow extensibility of the event without backwards + incompatibility, the sole argument received is an instance of + :class:`.ExceptionContext`. This object contains data members + representing detail about the exception. + + Use cases supported by this hook include: + + * read-only, low-level exception handling for logging and + debugging purposes + * exception re-writing + + The hook is called while the cursor from the failed operation + (if any) is still open and accessible. Special cleanup operations + can be called on this cursor; SQLAlchemy will attempt to close + this cursor subsequent to this hook being invoked. If the connection + is in "autocommit" mode, the transaction also remains open within + the scope of this hook; the rollback of the per-statement transaction + also occurs after the hook is called. + + The user-defined event handler has two options for replacing + the SQLAlchemy-constructed exception into one that is user + defined. It can either raise this new exception directly, in + which case all further event listeners are bypassed and the + exception will be raised, after appropriate cleanup as taken + place:: + + @event.listens_for(Engine, "handle_error") + def handle_exception(context): + if isinstance(context.original_exception, + psycopg2.OperationalError) and \\ + "failed" in str(context.original_exception): + raise MySpecialException("failed operation") + + Alternatively, a "chained" style of event handling can be + used, by configuring the handler with the ``retval=True`` + modifier and returning the new exception instance from the + function. In this case, event handling will continue onto the + next handler. The "chained" exception is available using + :attr:`.ExceptionContext.chained_exception`:: + + @event.listens_for(Engine, "handle_error", retval=True) + def handle_exception(context): + if context.chained_exception is not None and \\ + "special" in context.chained_exception.message: + return MySpecialException("failed", + cause=context.chained_exception) + + Handlers that return ``None`` may remain within this chain; the + last non-``None`` return value is the one that continues to be + passed to the next handler. + + When a custom exception is raised or returned, SQLAlchemy raises + this new exception as-is, it is not wrapped by any SQLAlchemy + object. If the exception is not a subclass of + :class:`sqlalchemy.exc.StatementError`, + certain features may not be available; currently this includes + the ORM's feature of adding a detail hint about "autoflush" to + exceptions raised within the autoflush process. + + :param context: an :class:`.ExceptionContext` object. See this + class for details on all available members. + + .. versionadded:: 0.9.7 Added the + :meth:`.ConnectionEvents.handle_error` hook. """ diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py index 0e68faa03..308c09691 100644 --- a/lib/sqlalchemy/ext/declarative/base.py +++ b/lib/sqlalchemy/ext/declarative/base.py @@ -56,7 +56,7 @@ def _as_declarative(cls, classname, dict_): @event.listens_for(mapper, "before_configured") def go(): cls.__declare_first__() - if '__abstract__' in base.__dict__: + if '__abstract__' in base.__dict__ and base.__abstract__: if (base is cls or (base in cls.__bases__ and not _is_declarative_inherits) ): diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index bae09d32d..82e0b0048 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -171,9 +171,10 @@ class DynamicAttributeImpl(attributes.AttributeImpl): c = self._get_collection_history(state, passive) return c.as_history() - def get_all_pending(self, state, dict_): + def get_all_pending(self, state, dict_, + passive=attributes.PASSIVE_NO_INITIALIZE): c = self._get_collection_history( - state, attributes.PASSIVE_NO_INITIALIZE) + state, passive) return [ (attributes.instance_state(x), x) for x in diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index b4c31a027..996cc8802 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -314,9 +314,8 @@ def _collect_update_commands(base_mapper, uowtransaction, col) prop = mapper._columntoproperty[col] - history = attributes.get_state_history( - state, prop.key, - attributes.PASSIVE_NO_INITIALIZE + history = state.manager[prop.key].impl.get_history( + state, state_dict, attributes.PASSIVE_NO_INITIALIZE ) if history.added: params[col.key] = history.added[0] @@ -331,15 +330,15 @@ def _collect_update_commands(base_mapper, uowtransaction, # in a different table than the one # where the version_id_col is. for prop in mapper._columntoproperty.values(): - history = attributes.get_state_history( - state, prop.key, + history = state.manager[prop.key].impl.get_history( + state, state_dict, attributes.PASSIVE_NO_INITIALIZE) if history.added: hasdata = True else: prop = mapper._columntoproperty[col] - history = attributes.get_state_history( - state, prop.key, + history = state.manager[prop.key].impl.get_history( + state, state_dict, attributes.PASSIVE_NO_INITIALIZE) if history.added: if isinstance(history.added[0], @@ -421,8 +420,8 @@ def _collect_post_update_commands(base_mapper, uowtransaction, table, elif col in post_update_cols: prop = mapper._columntoproperty[col] - history = attributes.get_state_history( - state, prop.key, + history = state.manager[prop.key].impl.get_history( + state, state_dict, attributes.PASSIVE_NO_INITIALIZE) if history.added: value = history.added[0] diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 5d60c4e29..728f7787a 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -941,7 +941,7 @@ class Query(object): """ fromclause = self.with_labels().enable_eagerloads(False).\ - _enable_single_crit(False).\ + _set_enable_single_crit(False).\ statement.correlate(None) q = self._from_selectable(fromclause) if entities: @@ -949,7 +949,7 @@ class Query(object): return q @_generative() - def _enable_single_crit(self, val): + def _set_enable_single_crit(self, val): self._enable_single_crit = val @_generative() @@ -2908,7 +2908,8 @@ class Query(object): subtypes are selected from the total results. """ - for (ext_info, adapter) in self._mapper_adapter_map.values(): + + for (ext_info, adapter) in set(self._mapper_adapter_map.values()): if ext_info in self._join_entities: continue single_crit = ext_info.mapper._single_table_criterion diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 3cc03a2d4..714c42a84 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -292,7 +292,10 @@ class SessionTransaction(object): for s in self._deleted: s.session_id = None self._deleted.clear() - + elif self.nested: + self._parent._new.update(self._new) + self._parent._deleted.update(self._deleted) + self._parent._key_switches.update(self._key_switches) def _connection_for_bind(self, bind): self._assert_active() diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 567c09fff..81860e045 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -713,7 +713,7 @@ class SubqueryLoader(AbstractRelationshipLoader): elif subq_path.contains_mapper(self.mapper): return - subq_mapper, leftmost_mapper, leftmost_attr, leftmost_relationship = \ + leftmost_mapper, leftmost_attr, leftmost_relationship = \ self._get_leftmost(subq_path) orig_query = context.attributes.get( @@ -725,7 +725,7 @@ class SubqueryLoader(AbstractRelationshipLoader): left_alias = self._generate_from_original_query( orig_query, leftmost_mapper, leftmost_attr, leftmost_relationship, - entity.mapper + entity.entity_zero ) # generate another Query that will join the @@ -738,13 +738,12 @@ class SubqueryLoader(AbstractRelationshipLoader): ("orig_query", SubqueryLoader): orig_query, ('subquery_path', None): subq_path } - q = q._enable_single_crit(False) + q = q._set_enable_single_crit(False) to_join, local_attr, parent_alias = \ self._prep_for_joins(left_alias, subq_path) q = q.order_by(*local_attr) q = q.add_columns(*local_attr) - q = self._apply_joins(q, to_join, left_alias, parent_alias, effective_entity) @@ -771,15 +770,17 @@ class SubqueryLoader(AbstractRelationshipLoader): leftmost_cols = leftmost_prop.local_columns leftmost_attr = [ - leftmost_mapper._columntoproperty[c].class_attribute + getattr(subq_path[0].entity, + leftmost_mapper._columntoproperty[c].key) for c in leftmost_cols ] - return subq_mapper, leftmost_mapper, leftmost_attr, leftmost_prop + + return leftmost_mapper, leftmost_attr, leftmost_prop def _generate_from_original_query(self, orig_query, leftmost_mapper, leftmost_attr, leftmost_relationship, - entity_mapper + orig_entity ): # reformat the original query # to look only for significant columns @@ -787,9 +788,8 @@ class SubqueryLoader(AbstractRelationshipLoader): # set a real "from" if not present, as this is more # accurate than just going off of the column expression - if not q._from_obj and entity_mapper.isa(leftmost_mapper): - q._set_select_from([entity_mapper], False) - + if not q._from_obj and orig_entity.mapper.isa(leftmost_mapper): + q._set_select_from([orig_entity], False) target_cols = q._adapt_col_list(leftmost_attr) # select from the identity columns of the outer diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 11608ef3b..010df5a16 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -170,6 +170,8 @@ class Compiled(object): defaults. """ + _cached_metadata = None + def __init__(self, dialect, statement, bind=None, compile_kwargs=util.immutabledict()): """Construct a new ``Compiled`` object. diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py index 1def809e0..4ed7d853d 100644 --- a/lib/sqlalchemy/sql/functions.py +++ b/lib/sqlalchemy/sql/functions.py @@ -267,6 +267,15 @@ func = _FunctionGenerator() calculate their return type automatically. For a listing of known generic functions, see :ref:`generic_functions`. + .. note:: + + The :data:`.func` construct has only limited support for calling + standalone "stored procedures", especially those with special parameterization + concerns. + + See the section :ref:`stored_procedures` for details on how to use + the DBAPI-level ``callproc()`` method for fully traditional stored procedures. + """ modifier = _FunctionGenerator(group=False) diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index 13461f3f3..f489a7b1d 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -840,10 +840,17 @@ class Column(SchemaItem, ColumnClause): * Part of the primary key - * Are not referenced by any foreign keys, unless - the value is specified as ``'ignore_fk'`` + * Not refering to another column via :class:`.ForeignKey`, unless + the value is specified as ``'ignore_fk'``:: - .. versionadded:: 0.7.4 + # turn on autoincrement for this column despite + # the ForeignKey() + Column('id', ForeignKey('other.id'), + primary_key=True, autoincrement='ignore_fk') + + It is typically not desirable to have "autoincrement" enabled + on such a column as its value intends to mirror that of a + primary key column elsewhere. * have no server side or client side defaults (with the exception of Postgresql SERIAL). @@ -857,8 +864,11 @@ class Column(SchemaItem, ColumnClause): SERIAL on Postgresql, and IDENTITY on MS-SQL. It does *not* issue AUTOINCREMENT for SQLite since this is a special SQLite flag that is not required for autoincrementing - behavior. See the SQLite dialect documentation for - information on SQLite's AUTOINCREMENT. + behavior. + + .. seealso:: + + :ref:`sqlite_autoincrement` * The column will be considered to be available as cursor.lastrowid or equivalent, for those dialects which diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py index 48b447b37..444366bc0 100644 --- a/lib/sqlalchemy/sql/type_api.py +++ b/lib/sqlalchemy/sql/type_api.py @@ -631,8 +631,8 @@ class TypeDecorator(TypeEngine): @property def comparator_factory(self): return type("TDComparator", - (TypeDecorator.Comparator, self.impl.comparator_factory), - {}) + (TypeDecorator.Comparator, self.impl.comparator_factory), + {}) def _gen_dialect_impl(self, dialect): """ @@ -1026,6 +1026,11 @@ class Variant(TypeDecorator): mapping[dialect_name] = type_ return Variant(self.impl, mapping) + @property + def comparator_factory(self): + """express comparison behavior in terms of the base type""" + return self.impl.comparator_factory + def _reconstitute_comparator(expression): return expression.comparator |
