diff options
Diffstat (limited to 'lib/sqlalchemy')
33 files changed, 492 insertions, 93 deletions
diff --git a/lib/sqlalchemy/cextension/immutabledict.c b/lib/sqlalchemy/cextension/immutabledict.c index 0b1003d63..1188dcd2b 100644 --- a/lib/sqlalchemy/cextension/immutabledict.c +++ b/lib/sqlalchemy/cextension/immutabledict.c @@ -173,7 +173,7 @@ ImmutableDict_union(PyObject *self, PyObject *args, PyObject *kw) } if (!PyDict_CheckExact(arg_dict)) { - // if we didnt get a dict, and got lists of tuples or + // if we didn't get a dict, and got lists of tuples or // keyword args, make a dict arg_dict = PyObject_Call((PyObject *) &PyDict_Type, args, kw); if (arg_dict == NULL) { diff --git a/lib/sqlalchemy/cextension/resultproxy.c b/lib/sqlalchemy/cextension/resultproxy.c index dc828698c..2de672f22 100644 --- a/lib/sqlalchemy/cextension/resultproxy.c +++ b/lib/sqlalchemy/cextension/resultproxy.c @@ -442,7 +442,7 @@ BaseRow_subscript_impl(BaseRow *self, PyObject *key, int asmapping) // support negative indexes. We can also call PySequence_GetItem, // but here we can stay with the simpler tuple protocol - // rather than the seqeunce protocol which has to check for + // rather than the sequence protocol which has to check for // __getitem__ methods etc. if (index < 0) index += (long)BaseRow_length(self); @@ -467,7 +467,7 @@ BaseRow_subscript_impl(BaseRow *self, PyObject *key, int asmapping) // support negative indexes. We can also call PySequence_GetItem, // but here we can stay with the simpler tuple protocol - // rather than the seqeunce protocol which has to check for + // rather than the sequence protocol which has to check for // __getitem__ methods etc. if (index < 0) index += (long)BaseRow_length(self); diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 8607edeca..7946633eb 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -514,7 +514,7 @@ or embedded dots, use two sets of brackets:: .. versionchanged:: 1.2 the SQL Server dialect now treats brackets as - identifier delimeters splitting the schema into separate database + identifier delimiters splitting the schema into separate database and owner tokens, to allow dots within either name itself. .. _legacy_schema_rendering: diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py index 219ba82e4..aab2018bf 100644 --- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py @@ -108,7 +108,7 @@ Any cx_Oracle parameter value and/or constant may be passed, such as:: ) Note that the default value for ``encoding`` and ``nencoding`` was changed to -"UTF-8" in cx_Oracle 8.0 so these parameters can be ommitted when using that +"UTF-8" in cx_Oracle 8.0 so these parameters can be omitted when using that version, or later. Options consumed by the SQLAlchemy cx_Oracle dialect outside of the driver diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index d8e4d5d20..f33542ee8 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -1868,6 +1868,14 @@ class ENUM(sqltypes.NativeForEmulated, sqltypes.Enum): be used to emit SQL to a target bind. """ + native_enum = kw.pop("native_enum", None) + if native_enum is False: + util.warn( + "the native_enum flag does not apply to the " + "sqlalchemy.dialects.postgresql.ENUM datatype; this type " + "always refers to ENUM. Use sqlalchemy.types.Enum for " + "non-native enum." + ) self.create_type = kw.pop("create_type", True) super(ENUM, self).__init__(*enums, **kw) @@ -3425,7 +3433,7 @@ class PGDialect(default.DefaultDialect): return bool(cursor.scalar()) def _get_server_version_info(self, connection): - v = connection.exec_driver_sql("select version()").scalar() + v = connection.exec_driver_sql("select pg_catalog.version()").scalar() m = re.match( r".*(?:PostgreSQL|EnterpriseDB) " r"(\d+)\.?(\d+)?(?:\.(\d+))?(?:\.\d+)?(?:devel|beta)?", diff --git a/lib/sqlalchemy/dialects/postgresql/psycopg2.py b/lib/sqlalchemy/dialects/postgresql/psycopg2.py index e28c01f11..c80198825 100644 --- a/lib/sqlalchemy/dialects/postgresql/psycopg2.py +++ b/lib/sqlalchemy/dialects/postgresql/psycopg2.py @@ -50,7 +50,7 @@ may be passed to :func:`_sa.create_engine()`, and include the following: * ``executemany_mode``, ``executemany_batch_page_size``, ``executemany_values_page_size``: Allows use of psycopg2 - extensions for optimizing "executemany"-stye queries. See the referenced + extensions for optimizing "executemany"-style queries. See the referenced section below for details. .. seealso:: @@ -1037,7 +1037,7 @@ class PGDialect_psycopg2(PGDialect): "connection not open", "could not receive data from server", "could not send data to server", - # psycopg2 client errors, psycopg2/conenction.h, + # psycopg2 client errors, psycopg2/connection.h, # psycopg2/cursor.h "connection already closed", "cursor already closed", diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index dc5ebc3f0..c4a6bf8e9 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -1915,7 +1915,9 @@ class SQLiteDialect(default.DefaultDialect): 14, ) - _isolation_lookup = {"READ UNCOMMITTED": 1, "SERIALIZABLE": 0} + _isolation_lookup = util.immutabledict( + {"READ UNCOMMITTED": 1, "SERIALIZABLE": 0} + ) def set_isolation_level(self, connection, level): try: @@ -1925,7 +1927,11 @@ class SQLiteDialect(default.DefaultDialect): exc.ArgumentError( "Invalid value '%s' for isolation_level. " "Valid isolation levels for %s are %s" - % (level, self.name, ", ".join(self._isolation_lookup)) + % ( + level, + self.name, + ", ".join(self._isolation_lookup), + ) ), replace_context=err, ) diff --git a/lib/sqlalchemy/dialects/sqlite/pysqlite.py b/lib/sqlalchemy/dialects/sqlite/pysqlite.py index 96a5351da..0f96e8830 100644 --- a/lib/sqlalchemy/dialects/sqlite/pysqlite.py +++ b/lib/sqlalchemy/dialects/sqlite/pysqlite.py @@ -492,6 +492,12 @@ class SQLiteDialect_pysqlite(SQLiteDialect): def _get_server_version_info(self, connection): return self.dbapi.sqlite_version_info + _isolation_lookup = SQLiteDialect._isolation_lookup.union( + { + "AUTOCOMMIT": None, + } + ) + def set_isolation_level(self, connection, level): if hasattr(connection, "connection"): dbapi_connection = connection.connection diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index a316f904f..25ced0343 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1157,10 +1157,28 @@ class Connection(Connectable): """Executes and returns the first column of the first row. The underlying result/cursor is closed after execution. + """ return self.execute(object_, *multiparams, **params).scalar() + def scalars(self, object_, *multiparams, **params): + """Executes and returns a scalar result set, which yields scalar values + from the first column of each row. + + This method is equivalent to calling :meth:`_engine.Connection.execute` + to receive a :class:`_result.Result` object, then invoking the + :meth:`_result.Result.scalars` method to produce a + :class:`_result.ScalarResult` instance. + + :return: a :class:`_result.ScalarResult` + + .. versionadded:: 1.4.24 + + """ + + return self.execute(object_, *multiparams, **params).scalars() + def execute(self, statement, *multiparams, **params): r"""Executes a SQL statement construct and returns a :class:`_engine.CursorResult`. diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 8d6f40ff6..8bd8a121b 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -1835,8 +1835,9 @@ class DefaultExecutionContext(interfaces.ExecutionContext): # to avoid many calls of get_insert_default()/ # get_update_default() for c in insert_prefetch: - if c.default and c.default.is_scalar: + if c.default and not c.default.is_sequence and c.default.is_scalar: scalar_defaults[c] = c.default.arg + for c in update_prefetch: if c.onupdate and c.onupdate.is_scalar: scalar_defaults[c] = c.onupdate.arg diff --git a/lib/sqlalchemy/engine/events.py b/lib/sqlalchemy/engine/events.py index f3775aed7..f091c7733 100644 --- a/lib/sqlalchemy/engine/events.py +++ b/lib/sqlalchemy/engine/events.py @@ -722,15 +722,27 @@ class DialectEvents(event.Events): def do_connect(self, dialect, conn_rec, cargs, cparams): """Receive connection arguments before a connection is made. - Return a DBAPI connection to halt further events from invoking; - the returned connection will be used. - - Alternatively, the event can manipulate the cargs and/or cparams - collections; cargs will always be a Python list that can be mutated - in-place and cparams a Python dictionary. Return None to - allow control to pass to the next event handler and ultimately - to allow the dialect to connect normally, given the updated - arguments. + This event is useful in that it allows the handler to manipulate the + cargs and/or cparams collections that control how the DBAPI + ``connect()`` function will be called. ``cargs`` will always be a + Python list that can be mutated in-place, and ``cparams`` a Python + dictionary that may also be mutated:: + + e = create_engine("postgresql+psycopg2://user@host/dbname") + + @event.listens_for(e, 'do_connect') + def receive_do_connect(dialect, conn_rec, cargs, cparams): + cparams["password"] = "some_password" + + The event hook may also be used to override the call to ``connect()`` + entirely, by returning a non-``None`` DBAPI connection object:: + + e = create_engine("postgresql+psycopg2://user@host/dbname") + + @event.listens_for(e, 'do_connect') + def receive_do_connect(dialect, conn_rec, cargs, cparams): + return psycopg2.connect(*cargs, **cparams) + .. versionadded:: 1.0.3 diff --git a/lib/sqlalchemy/engine/url.py b/lib/sqlalchemy/engine/url.py index d72654c73..d91f06011 100644 --- a/lib/sqlalchemy/engine/url.py +++ b/lib/sqlalchemy/engine/url.py @@ -67,8 +67,13 @@ class URL( * :attr:`_engine.URL.drivername`: database backend and driver name, such as ``postgresql+psycopg2`` * :attr:`_engine.URL.username`: username string - * :attr:`_engine.URL.password`: password, which is normally a string but - may also be any object that has a ``__str__()`` method. + * :attr:`_engine.URL.password`: password string, or object that includes + a ``__str__()`` method that produces a password. + + .. note:: A password-producing object will be stringified only + **once** per :class:`_engine.Engine` object. For dynamic password + generation per connect, see :ref:`engines_dynamic_tokens`. + * :attr:`_engine.URL.host`: string hostname * :attr:`_engine.URL.port`: integer port number * :attr:`_engine.URL.database`: string database name @@ -108,8 +113,13 @@ class URL( correspond to a module in sqlalchemy/databases or a third party plug-in. :param username: The user name. - :param password: database password. May be a string or an object that - can be stringified with ``str()``. + :param password: database password. Is typically a string, but may + also be an object that can be stringified with ``str()``. + + .. note:: A password-producing object will be stringified only + **once** per :class:`_engine.Engine` object. For dynamic password + generation per connect, see :ref:`engines_dynamic_tokens`. + :param host: The name of the host. :param port: The port number. :param database: The database name. @@ -154,9 +164,6 @@ class URL( @classmethod def _assert_str(cls, v, paramname): - if v is None: - return v - if not isinstance(v, compat.string_types): raise TypeError("%s must be a string" % paramname) return v @@ -655,7 +662,7 @@ class URL( dialect_cls = entrypoint.get_dialect_cls(self) return dialect_cls - def translate_connect_args(self, names=[], **kw): + def translate_connect_args(self, names=None, **kw): r"""Translate url attributes into a dictionary of connection arguments. Returns attributes of this url (`host`, `database`, `username`, @@ -669,6 +676,14 @@ class URL( names, but correlates the name to the original positionally. """ + if names is not None: + util.warn_deprecated( + "The `URL.translate_connect_args.name`s parameter is " + "deprecated. Please pass the " + "alternate names as kw arguments.", + "1.4", + ) + translated = {} attribute_names = ["host", "database", "username", "password", "port"] for sname in attribute_names: @@ -679,7 +694,11 @@ class URL( else: name = sname if name is not None and getattr(self, sname, False): - translated[name] = getattr(self, sname) + if sname == "password": + translated[name] = str(getattr(self, sname)) + else: + translated[name] = getattr(self, sname) + return translated diff --git a/lib/sqlalchemy/ext/asyncio/base.py b/lib/sqlalchemy/ext/asyncio/base.py index 3f2c084f4..3f77f5500 100644 --- a/lib/sqlalchemy/ext/asyncio/base.py +++ b/lib/sqlalchemy/ext/asyncio/base.py @@ -8,6 +8,7 @@ from . import exc as async_exc class ReversibleProxy: # weakref.ref(async proxy object) -> weakref.ref(sync proxied object) _proxy_objects = {} + __slots__ = ("__weakref__",) def _assign_proxied(self, target): if target is not None: @@ -46,6 +47,8 @@ class ReversibleProxy: class StartableContext(abc.ABC): + __slots__ = () + @abc.abstractmethod async def start(self, is_ctxmanager=False): pass @@ -68,6 +71,8 @@ class StartableContext(abc.ABC): class ProxyComparable(ReversibleProxy): + __slots__ = () + def __hash__(self): return id(self) diff --git a/lib/sqlalchemy/ext/asyncio/engine.py b/lib/sqlalchemy/ext/asyncio/engine.py index f5c3bdca4..ab29438ed 100644 --- a/lib/sqlalchemy/ext/asyncio/engine.py +++ b/lib/sqlalchemy/ext/asyncio/engine.py @@ -9,6 +9,7 @@ from .base import ProxyComparable from .base import StartableContext from .result import AsyncResult from ... import exc +from ... import inspection from ... import util from ...engine import create_engine as _create_engine from ...engine.base import NestedTransaction @@ -80,6 +81,7 @@ class AsyncConnection(ProxyComparable, StartableContext, AsyncConnectable): # create a new AsyncConnection that matches this one given only the # "sync" elements. __slots__ = ( + "engine", "sync_engine", "sync_connection", ) @@ -437,6 +439,47 @@ class AsyncConnection(ProxyComparable, StartableContext, AsyncConnectable): result = await self.execute(statement, parameters, execution_options) return result.scalar() + async def scalars( + self, + statement, + parameters=None, + execution_options=util.EMPTY_DICT, + ): + r"""Executes a SQL statement construct and returns a scalar objects. + + This method is shorthand for invoking the + :meth:`_engine.Result.scalars` method after invoking the + :meth:`_future.Connection.execute` method. Parameters are equivalent. + + :return: a :class:`_engine.ScalarResult` object. + + .. versionadded:: 1.4.24 + + """ + result = await self.execute(statement, parameters, execution_options) + return result.scalars() + + async def stream_scalars( + self, + statement, + parameters=None, + execution_options=util.EMPTY_DICT, + ): + r"""Executes a SQL statement and returns a streaming scalar result + object. + + This method is shorthand for invoking the + :meth:`_engine.AsyncResult.scalars` method after invoking the + :meth:`_future.Connection.stream` method. Parameters are equivalent. + + :return: an :class:`_asyncio.AsyncScalarResult` object. + + .. versionadded:: 1.4.24 + + """ + result = await self.stream(statement, parameters, execution_options) + return result.scalars() + async def run_sync(self, fn, *arg, **kw): """Invoke the given sync callable passing self as the first argument. @@ -709,3 +752,24 @@ def _get_sync_engine_or_connection(async_engine): raise exc.ArgumentError( "AsyncEngine expected, got %r" % async_engine ) from e + + +@inspection._inspects(AsyncConnection) +def _no_insp_for_async_conn_yet(subject): + raise exc.NoInspectionAvailable( + "Inspection on an AsyncConnection is currently not supported. " + "Please use ``run_sync`` to pass a callable where it's possible " + "to call ``inspect`` on the passed connection.", + code="xd3s", + ) + + +@inspection._inspects(AsyncEngine) +def _no_insp_for_async_engine_xyet(subject): + raise exc.NoInspectionAvailable( + "Inspection on an AsyncEngine is currently not supported. " + "Please obtain a connection then use ``conn.run_sync`` to pass a " + "callable where it's possible to call ``inspect`` on the " + "passed connection.", + code="xd3s", + ) diff --git a/lib/sqlalchemy/ext/asyncio/session.py b/lib/sqlalchemy/ext/asyncio/session.py index a10621eef..6e3ac5a90 100644 --- a/lib/sqlalchemy/ext/asyncio/session.py +++ b/lib/sqlalchemy/ext/asyncio/session.py @@ -14,6 +14,9 @@ from ...orm import Session from ...orm import state as _instance_state from ...util.concurrency import greenlet_spawn +_EXECUTE_OPTIONS = util.immutabledict({"prebuffer_rows": True}) +_STREAM_OPTIONS = util.immutabledict({"stream_results": True}) + @util.create_proxy_methods( Session, @@ -48,24 +51,41 @@ from ...util.concurrency import greenlet_spawn class AsyncSession(ReversibleProxy): """Asyncio version of :class:`_orm.Session`. + The :class:`_asyncio.AsyncSession` is a proxy for a traditional + :class:`_orm.Session` instance. .. versionadded:: 1.4 + To use an :class:`_asyncio.AsyncSession` with custom :class:`_orm.Session` + implementations, see the + :paramref:`_asyncio.AsyncSession.sync_session_class` parameter. + + """ _is_asyncio = True - __slots__ = ( - "binds", - "bind", - "sync_session", - "_proxied", - "_slots_dispatch", - ) - dispatch = None - def __init__(self, bind=None, binds=None, **kw): + def __init__(self, bind=None, binds=None, sync_session_class=None, **kw): + r"""Construct a new :class:`_asyncio.AsyncSession`. + + All parameters other than ``sync_session_class`` are passed to the + ``sync_session_class`` callable directly to instantiate a new + :class:`_orm.Session`. Refer to :meth:`_orm.Session.__init__` for + parameter documentation. + + :param sync_session_class: + A :class:`_orm.Session` subclass or other callable which will be used + to construct the :class:`_orm.Session` which will be proxied. This + parameter may be used to provide custom :class:`_orm.Session` + subclasses. Defaults to the + :attr:`_asyncio.AsyncSession.sync_session_class` class-level + attribute. + + .. versionadded:: 1.4.24 + + """ kw["future"] = True if bind: self.bind = bind @@ -78,10 +98,30 @@ class AsyncSession(ReversibleProxy): for key, b in binds.items() } + if sync_session_class: + self.sync_session_class = sync_session_class + self.sync_session = self._proxied = self._assign_proxied( - Session(bind=bind, binds=binds, **kw) + self.sync_session_class(bind=bind, binds=binds, **kw) ) + sync_session_class = Session + """The class or callable that provides the + underlying :class:`_orm.Session` instance for a particular + :class:`_asyncio.AsyncSession`. + + At the class level, this attribute is the default value for the + :paramref:`_asyncio.AsyncSession.sync_session_class` parameter. Custom + subclasses of :class:`_asyncio.AsyncSession` can override this. + + At the instance level, this attribute indicates the current class or + callable that was used to provide the :class:`_orm.Session` instance for + this :class:`_asyncio.AsyncSession` instance. + + .. versionadded:: 1.4.24 + + """ + async def refresh( self, instance, attribute_names=None, with_for_update=None ): @@ -93,6 +133,10 @@ class AsyncSession(ReversibleProxy): This is the async version of the :meth:`_orm.Session.refresh` method. See that method for a complete description of all options. + .. seealso:: + + :meth:`_orm.Session.refresh` - main documentation for refresh + """ return await greenlet_spawn( @@ -138,9 +182,20 @@ class AsyncSession(ReversibleProxy): **kw ): """Execute a statement and return a buffered - :class:`_engine.Result` object.""" + :class:`_engine.Result` object. + + .. seealso:: + + :meth:`_orm.Session.execute` - main documentation for execute + + """ - execution_options = execution_options.union({"prebuffer_rows": True}) + if execution_options: + execution_options = util.immutabledict(execution_options).union( + _EXECUTE_OPTIONS + ) + else: + execution_options = _EXECUTE_OPTIONS return await greenlet_spawn( self.sync_session.execute, @@ -159,7 +214,13 @@ class AsyncSession(ReversibleProxy): bind_arguments=None, **kw ): - """Execute a statement and return a scalar result.""" + """Execute a statement and return a scalar result. + + .. seealso:: + + :meth:`_orm.Session.scalar` - main documentation for scalar + + """ result = await self.execute( statement, @@ -170,6 +231,37 @@ class AsyncSession(ReversibleProxy): ) return result.scalar() + async def scalars( + self, + statement, + params=None, + execution_options=util.EMPTY_DICT, + bind_arguments=None, + **kw + ): + """Execute a statement and return scalar results. + + :return: a :class:`_result.ScalarResult` object + + .. versionadded:: 1.4.24 + + .. seealso:: + + :meth:`_orm.Session.scalars` - main documentation for scalars + + :meth:`_asyncio.AsyncSession.stream_scalars` - streaming version + + """ + + result = await self.execute( + statement, + params=params, + execution_options=execution_options, + bind_arguments=bind_arguments, + **kw + ) + return result.scalars() + async def get( self, entity, @@ -182,6 +274,10 @@ class AsyncSession(ReversibleProxy): """Return an instance based on the given primary key identifier, or ``None`` if not found. + .. seealso:: + + :meth:`_orm.Session.get` - main documentation for get + """ return await greenlet_spawn( @@ -205,7 +301,12 @@ class AsyncSession(ReversibleProxy): """Execute a statement and return a streaming :class:`_asyncio.AsyncResult` object.""" - execution_options = execution_options.union({"stream_results": True}) + if execution_options: + execution_options = util.immutabledict(execution_options).union( + _STREAM_OPTIONS + ) + else: + execution_options = _STREAM_OPTIONS result = await greenlet_spawn( self.sync_session.execute, @@ -217,6 +318,37 @@ class AsyncSession(ReversibleProxy): ) return _result.AsyncResult(result) + async def stream_scalars( + self, + statement, + params=None, + execution_options=util.EMPTY_DICT, + bind_arguments=None, + **kw + ): + """Execute a statement and return a stream of scalar results. + + :return: an :class:`_asyncio.AsyncScalarResult` object + + .. versionadded:: 1.4.24 + + .. seealso:: + + :meth:`_orm.Session.scalars` - main documentation for scalars + + :meth:`_asyncio.AsyncSession.scalars` - non streaming version + + """ + + result = await self.stream( + statement, + params=params, + execution_options=execution_options, + bind_arguments=bind_arguments, + **kw + ) + return result.scalars() + async def delete(self, instance): """Mark an instance as deleted. @@ -225,17 +357,24 @@ class AsyncSession(ReversibleProxy): As this operation may need to cascade along unloaded relationships, it is awaitable to allow for those queries to take place. + .. seealso:: + + :meth:`_orm.Session.delete` - main documentation for delete """ return await greenlet_spawn(self.sync_session.delete, instance) - async def merge(self, instance, load=True): + async def merge(self, instance, load=True, options=None): """Copy the state of a given instance into a corresponding instance within this :class:`_asyncio.AsyncSession`. + .. seealso:: + + :meth:`_orm.Session.merge` - main documentation for merge + """ return await greenlet_spawn( - self.sync_session.merge, instance, load=load + self.sync_session.merge, instance, load=load, options=options ) async def flush(self, objects=None): @@ -243,7 +382,7 @@ class AsyncSession(ReversibleProxy): .. seealso:: - :meth:`_orm.Session.flush` + :meth:`_orm.Session.flush` - main documentation for flush """ await greenlet_spawn(self.sync_session.flush, objects=objects) @@ -279,13 +418,26 @@ class AsyncSession(ReversibleProxy): else: return None - async def connection(self): + async def connection(self, **kw): r"""Return a :class:`_asyncio.AsyncConnection` object corresponding to this :class:`.Session` object's transactional state. + This method may also be used to establish execution options for the + database connection used by the current transaction. + + .. versionadded:: 1.4.24 Added **kw arguments which are passed through + to the underlying :meth:`_orm.Session.connection` method. + + .. seealso:: + + :meth:`_orm.Session.connection` - main documentation for + "connection" + """ - sync_connection = await greenlet_spawn(self.sync_session.connection) + sync_connection = await greenlet_spawn( + self.sync_session.connection, **kw + ) return engine.AsyncConnection._retrieve_proxy_for_target( sync_connection ) diff --git a/lib/sqlalchemy/ext/mypy/decl_class.py b/lib/sqlalchemy/ext/mypy/decl_class.py index 23c78aa51..b85ec0f69 100644 --- a/lib/sqlalchemy/ext/mypy/decl_class.py +++ b/lib/sqlalchemy/ext/mypy/decl_class.py @@ -61,6 +61,9 @@ def scan_declarative_assignments_and_apply_types( List[util.SQLAlchemyAttribute] ] = util.get_mapped_attributes(info, api) + # used by assign.add_additional_orm_attributes among others + util.establish_as_sqlalchemy(info) + if mapped_attributes is not None: # ensure that a class that's mapped is always picked up by # its mapped() decorator or declarative metaclass before diff --git a/lib/sqlalchemy/ext/mypy/infer.py b/lib/sqlalchemy/ext/mypy/infer.py index 85a94bba6..52570f772 100644 --- a/lib/sqlalchemy/ext/mypy/infer.py +++ b/lib/sqlalchemy/ext/mypy/infer.py @@ -284,20 +284,35 @@ def _infer_type_from_decl_column_property( """ assert isinstance(stmt.rvalue, CallExpr) - first_prop_arg = stmt.rvalue.args[0] - if isinstance(first_prop_arg, CallExpr): - type_id = names.type_id_for_callee(first_prop_arg.callee) + if stmt.rvalue.args: + first_prop_arg = stmt.rvalue.args[0] + + if isinstance(first_prop_arg, CallExpr): + type_id = names.type_id_for_callee(first_prop_arg.callee) + + # look for column_property() / deferred() etc with Column as first + # argument + if type_id is names.COLUMN: + return _infer_type_from_decl_column( + api, + stmt, + node, + left_hand_explicit_type, + right_hand_expression=first_prop_arg, + ) - # look for column_property() / deferred() etc with Column as first - # argument - if type_id is names.COLUMN: + if isinstance(stmt.rvalue, CallExpr): + type_id = names.type_id_for_callee(stmt.rvalue.callee) + # this is probably not strictly necessary as we have to use the left + # hand type for query expression in any case. any other no-arg + # column prop objects would go here also + if type_id is names.QUERY_EXPRESSION: return _infer_type_from_decl_column( api, stmt, node, left_hand_explicit_type, - right_hand_expression=first_prop_arg, ) return infer_type_from_left_hand_type_only( diff --git a/lib/sqlalchemy/ext/mypy/names.py b/lib/sqlalchemy/ext/mypy/names.py index 22a79e29b..3dbfcc770 100644 --- a/lib/sqlalchemy/ext/mypy/names.py +++ b/lib/sqlalchemy/ext/mypy/names.py @@ -45,6 +45,7 @@ MAPPER_PROPERTY: int = util.symbol("MAPPER_PROPERTY") # type: ignore AS_DECLARATIVE: int = util.symbol("AS_DECLARATIVE") # type: ignore AS_DECLARATIVE_BASE: int = util.symbol("AS_DECLARATIVE_BASE") # type: ignore DECLARATIVE_MIXIN: int = util.symbol("DECLARATIVE_MIXIN") # type: ignore +QUERY_EXPRESSION: int = util.symbol("QUERY_EXPRESSION") # type: ignore _lookup: Dict[str, Tuple[int, Set[str]]] = { "Column": ( @@ -150,6 +151,10 @@ _lookup: Dict[str, Tuple[int, Set[str]]] = { "sqlalchemy.orm.declarative_mixin", }, ), + "query_expression": ( + QUERY_EXPRESSION, + {"sqlalchemy.orm.query_expression"}, + ), } diff --git a/lib/sqlalchemy/ext/mypy/util.py b/lib/sqlalchemy/ext/mypy/util.py index 614805d77..a3825f175 100644 --- a/lib/sqlalchemy/ext/mypy/util.py +++ b/lib/sqlalchemy/ext/mypy/util.py @@ -99,6 +99,10 @@ def _get_info_mro_metadata(info: TypeInfo, key: str) -> Optional[Any]: return None +def establish_as_sqlalchemy(info: TypeInfo) -> None: + info.metadata.setdefault("sqlalchemy", {}) + + def set_is_base(info: TypeInfo) -> None: _set_info_metadata(info, "is_base", True) diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py index 9318bb163..85c736e12 100644 --- a/lib/sqlalchemy/orm/context.py +++ b/lib/sqlalchemy/orm/context.py @@ -187,6 +187,12 @@ class ORMCompileState(CompileState): def __init__(self, *arg, **kw): raise NotImplementedError() + def _append_dedupe_col_collection(self, obj, col_collection): + dedupe = self.dedupe_columns + if obj not in dedupe: + dedupe.add(obj) + col_collection.append(obj) + @classmethod def _column_naming_convention(cls, label_style, legacy): @@ -443,6 +449,7 @@ class ORMFromStatementCompileState(ORMCompileState): self.primary_columns = [] self.secondary_columns = [] + self.dedupe_columns = set() self.create_eager_joins = [] self._fallback_from_clauses = [] @@ -645,6 +652,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): self.primary_columns = [] self.secondary_columns = [] + self.dedupe_columns = set() self.eager_joins = {} self.extra_criteria_entities = {} self.create_eager_joins = [] @@ -765,8 +773,6 @@ class ORMSelectCompileState(ORMCompileState, SelectState): # PART II - self.dedupe_cols = True - self._for_update_arg = query._for_update_arg for entity in self._entities: @@ -1036,9 +1042,8 @@ class ORMSelectCompileState(ORMCompileState, SelectState): # put FOR UPDATE on the inner query, where MySQL will honor it, # as well as if it has an OF so PostgreSQL can use it. inner = self._select_statement( - util.unique_list(self.primary_columns + order_by_col_expr) - if self.dedupe_cols - else (self.primary_columns + order_by_col_expr), + self.primary_columns + + [c for c in order_by_col_expr if c not in self.dedupe_columns], self.from_clauses, self._where_criteria, self._having_criteria, @@ -1116,9 +1121,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): self.primary_columns += to_add statement = self._select_statement( - util.unique_list(self.primary_columns + self.secondary_columns) - if self.dedupe_cols - else (self.primary_columns + self.secondary_columns), + self.primary_columns + self.secondary_columns, tuple(self.from_clauses) + tuple(self.eager_joins.values()), self._where_criteria, self._having_criteria, @@ -2822,6 +2825,7 @@ class _RawColumnEntity(_ColumnEntity): # result due to the __eq__() method, so use deannotated column = column._deannotate() + compile_state.dedupe_columns.add(column) compile_state.primary_columns.append(column) self._fetch_column = column @@ -2949,6 +2953,7 @@ class _ORMColumnEntity(_ColumnEntity): ): compile_state._fallback_from_clauses.append(ezero.selectable) + compile_state.dedupe_columns.add(column) compile_state.primary_columns.append(column) self._fetch_column = column diff --git a/lib/sqlalchemy/orm/decl_api.py b/lib/sqlalchemy/orm/decl_api.py index 23b89a543..94cda236d 100644 --- a/lib/sqlalchemy/orm/decl_api.py +++ b/lib/sqlalchemy/orm/decl_api.py @@ -126,7 +126,7 @@ class declared_attr(interfaces._MappedAttribute, property): :class:`_orm.declared_attr` is typically applied as a decorator to a class level method, turning the attribute into a scalar-like property that can be invoked from the uninstantiated class. The Declarative mapping process - looks for these :class:`_orm.declared_attr` callables as it scans classe, + looks for these :class:`_orm.declared_attr` callables as it scans classes, and assumes any attribute marked with :class:`_orm.declared_attr` will be a callable that will produce an object specific to the Declarative mapping or table configuration. @@ -761,6 +761,8 @@ class registry(object): registry = mapper_registry metadata = mapper_registry.metadata + __init__ = mapper_registry.constructor + The :meth:`_orm.registry.generate_base` method provides the implementation for the :func:`_orm.declarative_base` function, which creates the :class:`_orm.registry` and base class all at once. diff --git a/lib/sqlalchemy/orm/path_registry.py b/lib/sqlalchemy/orm/path_registry.py index d50a242ee..0327605d5 100644 --- a/lib/sqlalchemy/orm/path_registry.py +++ b/lib/sqlalchemy/orm/path_registry.py @@ -420,7 +420,7 @@ class AbstractEntityRegistry(PathRegistry): ) # it seems to make sense that since these paths get mixed up # with statements that are cached or not, we should make - # sure the natural path is cachable across different occurrences + # sure the natural path is cacheable across different occurrences # of equivalent AliasedClass objects. however, so far this # does not seem to be needed for whatever reason. # elif not parent.path and self.is_aliased_class: diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index 4747d0bba..fd484b52b 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -1833,7 +1833,7 @@ class BulkUDCompileState(CompileState): return ( statement, util.immutabledict(execution_options).union( - dict(_sa_orm_update_options=update_options) + {"_sa_orm_update_options": update_options} ), ) diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index b44a16102..e7999521a 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -3431,7 +3431,7 @@ class JoinCondition(object): and pr.key not in self.prop._overlaps and self.prop.key not in pr._overlaps # note: the "__*" symbol is used internally by - # SQLAlchemy as a general means of supressing the + # SQLAlchemy as a general means of suppressing the # overlaps warning for some extension cases, however # this is not currently # a publicly supported symbol and may change at diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 37c94e231..f051d8df2 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -1309,6 +1309,7 @@ class Session(_SessionClassMethods): "subtransactions are not implemented in future " "Session objects." ) + if self._autobegin(): if not subtransactions and not nested and not _subtrans: return self._transaction @@ -1326,6 +1327,7 @@ class Session(_SessionClassMethods): elif not self.autocommit: # outermost transaction. must be a not nested and not # a subtransaction + assert not nested and not _subtrans and not subtransactions trans = SessionTransaction(self) assert self._transaction is trans @@ -1579,7 +1581,7 @@ class Session(_SessionClassMethods): :param execution_options: optional dictionary of execution options, which will be associated with the statement execution. This dictionary can provide a subset of the options that are accepted - by :meth:`_future.Connection.execution_options`, and may also + by :meth:`_engine.Connection.execution_options`, and may also provide additional options understood only in an ORM context. :param bind_arguments: dictionary of additional arguments to determine @@ -1722,6 +1724,35 @@ class Session(_SessionClassMethods): **kw ).scalar() + def scalars( + self, + statement, + params=None, + execution_options=util.EMPTY_DICT, + bind_arguments=None, + **kw + ): + """Execute a statement and return the results as scalars. + + Usage and parameters are the same as that of + :meth:`_orm.Session.execute`; the return result is a + :class:`_result.ScalarResult` filtering object which + will return single elements rather than :class:`_row.Row` objects. + + :return: a :class:`_result.ScalarResult` object + + .. versionadded:: 1.4.24 + + """ + + return self.execute( + statement, + params=params, + execution_options=execution_options, + bind_arguments=bind_arguments, + **kw + ).scalars() + def close(self): """Close out the transactional resources and ORM objects used by this :class:`_orm.Session`. @@ -2841,7 +2872,7 @@ class Session(_SessionClassMethods): load_options=load_options, ) - def merge(self, instance, load=True): + def merge(self, instance, load=True, options=None): """Copy the state of a given instance into a corresponding instance within this :class:`.Session`. @@ -2887,6 +2918,11 @@ class Session(_SessionClassMethods): produced as "clean", so it is only appropriate that the given objects should be "clean" as well, else this suggests a mis-use of the method. + :param options: optional sequence of loader options which will be + applied to the :meth:`_orm.Session.get` method when the merge + operation loads the existing version of the object from the database. + + .. versionadded:: 1.4.24 .. seealso:: @@ -2914,6 +2950,7 @@ class Session(_SessionClassMethods): attributes.instance_state(instance), attributes.instance_dict(instance), load=load, + options=options, _recursive=_recursive, _resolve_conflict_map=_resolve_conflict_map, ) @@ -2925,6 +2962,7 @@ class Session(_SessionClassMethods): state, state_dict, load=True, + options=None, _recursive=None, _resolve_conflict_map=None, ): @@ -2988,7 +3026,12 @@ class Session(_SessionClassMethods): new_instance = True elif key_is_persistent: - merged = self.get(mapper.class_, key[1], identity_token=key[2]) + merged = self.get( + mapper.class_, + key[1], + identity_token=key[2], + options=options, + ) if merged is None: merged = mapper.class_manager.new_instance() @@ -3453,11 +3496,14 @@ class Session(_SessionClassMethods): SQL expressions. The objects as given are not added to the session and no additional - state is established on them, unless the ``return_defaults`` flag - is also set, in which case primary key attributes and server-side - default values will be populated. - - .. versionadded:: 1.0.0 + state is established on them. If the + :paramref:`_orm.Session.bulk_save_objects.return_defaults` flag is set, + then server-generated primary key values will be assigned to the + returned objects, but **not server side defaults**; this is a + limitation in the implementation. If stateful objects are desired, + please use the standard :meth:`_orm.Session.add_all` approach or + as an alternative newer mass-insert features such as + :ref:`orm_dml_returning_objects`. .. warning:: @@ -3467,6 +3513,14 @@ class Session(_SessionClassMethods): and SQL clause support are **silently omitted** in favor of raw INSERT/UPDATES of records. + Please note that newer versions of SQLAlchemy are **greatly + improving the efficiency** of the standard flush process. It is + **strongly recommended** to not use the bulk methods as they + represent a forking of SQLAlchemy's functionality and are slowly + being moved into legacy status. New features such as + :ref:`orm_dml_returning_objects` are both more efficient than + the "bulk" methods and provide more predictable functionality. + **Please read the list of caveats at** :ref:`bulk_operations_caveats` **before using this method, and fully test and confirm the functionality of all code developed @@ -3498,7 +3552,9 @@ class Session(_SessionClassMethods): and other multi-table mappings to insert correctly without the need to provide primary key values ahead of time; however, :paramref:`.Session.bulk_save_objects.return_defaults` **greatly - reduces the performance gains** of the method overall. + reduces the performance gains** of the method overall. It is strongly + advised to please use the standard :meth:`_orm.Session.add_all` + approach. :param update_changed_only: when True, UPDATE statements are rendered based on those attributes in each state that have logged changes. @@ -3568,6 +3624,14 @@ class Session(_SessionClassMethods): and SQL clause support are **silently omitted** in favor of raw INSERT of records. + Please note that newer versions of SQLAlchemy are **greatly + improving the efficiency** of the standard flush process. It is + **strongly recommended** to not use the bulk methods as they + represent a forking of SQLAlchemy's functionality and are slowly + being moved into legacy status. New features such as + :ref:`orm_dml_returning_objects` are both more efficient than + the "bulk" methods and provide more predictable functionality. + **Please read the list of caveats at** :ref:`bulk_operations_caveats` **before using this method, and fully test and confirm the functionality of all code developed @@ -3661,6 +3725,14 @@ class Session(_SessionClassMethods): and SQL clause support are **silently omitted** in favor of raw UPDATES of records. + Please note that newer versions of SQLAlchemy are **greatly + improving the efficiency** of the standard flush process. It is + **strongly recommended** to not use the bulk methods as they + represent a forking of SQLAlchemy's functionality and are slowly + being moved into legacy status. New features such as + :ref:`orm_dml_returning_objects` are both more efficient than + the "bulk" methods and provide more predictable functionality. + **Please read the list of caveats at** :ref:`bulk_operations_caveats` **before using this method, and fully test and confirm the functionality of all code developed diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 069e5e667..4f361be2c 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -157,7 +157,7 @@ class UninstrumentedColumnLoader(LoaderStrategy): for c in self.columns: if adapter: c = adapter.columns[c] - column_collection.append(c) + compile_state._append_dedupe_col_collection(c, column_collection) def create_row_processor( self, @@ -206,7 +206,7 @@ class ColumnLoader(LoaderStrategy): else: c = adapter.columns[c] - column_collection.append(c) + compile_state._append_dedupe_col_collection(c, column_collection) fetch = self.columns[0] if adapter: @@ -296,7 +296,7 @@ class ExpressionColumnLoader(ColumnLoader): for c in columns: if adapter: c = adapter.columns[c] - column_collection.append(c) + compile_state._append_dedupe_col_collection(c, column_collection) fetch = columns[0] if adapter: @@ -2335,7 +2335,9 @@ class JoinedLoader(AbstractRelationshipLoader): if localparent.persist_selectable.c.contains_column(col): if adapter: col = adapter.columns[col] - compile_state.primary_columns.append(col) + compile_state._append_dedupe_col_collection( + col, compile_state.primary_columns + ) if self.parent_property.order_by: compile_state.eager_order_by += tuple( diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 94a0b4201..9af82823a 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -610,7 +610,7 @@ class SQLCompiler(Compiled): """ _loose_column_name_matching = False - """tell the result object that the SQL staement is textual, wants to match + """tell the result object that the SQL statement is textual, wants to match up to Column objects, and may be using the ._tq_label in the SELECT rather than the base name. diff --git a/lib/sqlalchemy/sql/ddl.py b/lib/sqlalchemy/sql/ddl.py index 233e79f7c..f8985548e 100644 --- a/lib/sqlalchemy/sql/ddl.py +++ b/lib/sqlalchemy/sql/ddl.py @@ -1081,7 +1081,7 @@ class SchemaDropper(DDLBase): table, drop_ok=False, _is_metadata_operation=False, - _ignore_sequences=[], + _ignore_sequences=(), ): if not drop_ok and not self._can_drop_table(table): return diff --git a/lib/sqlalchemy/sql/lambdas.py b/lib/sqlalchemy/sql/lambdas.py index 36e470ce7..03cd05f02 100644 --- a/lib/sqlalchemy/sql/lambdas.py +++ b/lib/sqlalchemy/sql/lambdas.py @@ -905,7 +905,7 @@ class AnalyzedCode(object): util.raise_( exc.InvalidRequestError( "Closure variable named '%s' inside of lambda callable %s " - "does not refer to a cachable SQL element, and also does not " + "does not refer to a cacheable SQL element, and also does not " "appear to be serving as a SQL literal bound value based on " "the default " "SQL expression returned by the function. This variable " diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index e530beef2..c6d997649 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -809,7 +809,7 @@ class FromClause(roles.AnonymizedFromClauseRole, Selectable): objects maintained by this :class:`_expression.FromClause`. The :attr:`_sql.FromClause.c` attribute is an alias for the - :attr:`_sql.FromClause.columns` atttribute. + :attr:`_sql.FromClause.columns` attribute. :return: a :class:`.ColumnCollection` diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py index f660556eb..40127addf 100644 --- a/lib/sqlalchemy/testing/requirements.py +++ b/lib/sqlalchemy/testing/requirements.py @@ -500,7 +500,7 @@ class SuiteRequirements(Requirements): def foreign_key_constraint_name_reflection(self): """Target supports refleciton of FOREIGN KEY constraints and will return the name of the constraint that was used in the - "CONSTRANT <name> FOREIGN KEY" DDL. + "CONSTRAINT <name> FOREIGN KEY" DDL. MySQL prior to version 8 and MariaDB prior to version 10.5 don't support this. @@ -857,7 +857,7 @@ class SuiteRequirements(Requirements): >>> testing.requirements.get_isolation_levels() { - "default": "READ_COMMITED", + "default": "READ_COMMITTED", "supported": [ "SERIALIZABLE", "READ UNCOMMITTED", "READ COMMITTED", "REPEATABLE READ", @@ -1227,6 +1227,10 @@ class SuiteRequirements(Requirements): return self.python36 @property + def insert_order_dicts(self): + return self.python37 + + @property def python36(self): return exclusions.skip_if( lambda: sys.version_info < (3, 6), diff --git a/lib/sqlalchemy/testing/warnings.py b/lib/sqlalchemy/testing/warnings.py index 31834cebe..d22fb175e 100644 --- a/lib/sqlalchemy/testing/warnings.py +++ b/lib/sqlalchemy/testing/warnings.py @@ -66,13 +66,6 @@ def setup_filters(): # we are moving one at a time for msg in [ # - # DML - # - r"The update.preserve_parameter_order parameter will be removed in " - "SQLAlchemy 2.0.", - r"Passing dialect keyword arguments directly to the " - "(?:Insert|Update|Delete) constructor", - # # ORM configuration # r"Calling the mapper\(\) function directly outside of a " diff --git a/lib/sqlalchemy/util/compat.py b/lib/sqlalchemy/util/compat.py index 5d52f740f..5914e8681 100644 --- a/lib/sqlalchemy/util/compat.py +++ b/lib/sqlalchemy/util/compat.py @@ -392,6 +392,9 @@ if py3k: """ + kwonlydefaults = kwonlydefaults or {} + annotations = annotations or {} + def formatargandannotation(arg): result = formatarg(arg) if arg in annotations: |
