diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-02-02 16:33:54 -0500 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2014-02-02 16:33:54 -0500 |
commit | 0326f3cf014ffb4928b4c6051d2fca13cb6945d7 (patch) | |
tree | 0f3585321eccf6ba5b7cd97f38490f8c2650ccd5 /lib/sqlalchemy | |
parent | 4ed4266803cbba480e5785103302eba5b5a86652 (diff) | |
download | sqlalchemy-0326f3cf014ffb4928b4c6051d2fca13cb6945d7.tar.gz |
- Added :paramref:`.MetaData.reflect.**dialect_kwargs`
to support dialect-level reflection options for all :class:`.Table`
objects reflected.
- Added a new dialect-level argument ``postgresql_ignore_search_path``;
this argument is accepted by both the :class:`.Table` constructor
as well as by the :meth:`.MetaData.reflect` method. When in use
against Postgresql, a foreign-key referenced table which specifies
a remote schema name will retain that schema name even if the name
is present in the ``search_path``; the default behavior since 0.7.3
has been that schemas present in ``search_path`` would not be copied
to reflected :class:`.ForeignKey` objects. The documentation has been
updated to describe in detail the behavior of the ``pg_get_constraintdef()``
function and how the ``postgresql_ignore_search_path`` feature essentially
determines if we will honor the schema qualification reported by
this function or not. [ticket:2922]
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/base.py | 187 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/base.py | 23 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/schema.py | 13 |
3 files changed, 177 insertions, 46 deletions
diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 11bd3830d..29584d1eb 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -76,36 +76,132 @@ Valid values for ``isolation_level`` include: The :mod:`~sqlalchemy.dialects.postgresql.psycopg2` dialect also offers the special level ``AUTOCOMMIT``. See :ref:`psycopg2_isolation_level` for details. - -Remote / Cross-Schema Table Introspection ------------------------------------------ - -Tables can be introspected from any accessible schema, including -inter-schema foreign key relationships. However, care must be taken -when specifying the "schema" argument for a given :class:`.Table`, when -the given schema is also present in PostgreSQL's ``search_path`` variable -for the current connection. - -If a FOREIGN KEY constraint reports that the remote table's schema is within -the current ``search_path``, the "schema" attribute of the resulting -:class:`.Table` will be set to ``None``, unless the actual schema of the -remote table matches that of the referencing table, and the "schema" argument -was explicitly stated on the referencing table. - -The best practice here is to not use the ``schema`` argument -on :class:`.Table` for any schemas that are present in ``search_path``. -``search_path`` defaults to "public", but care should be taken -to inspect the actual value using:: - - SHOW search_path; - -.. versionchanged:: 0.7.3 - Prior to this version, cross-schema foreign keys when the schemas - were also in the ``search_path`` could make an incorrect assumption - if the schemas were explicitly stated on each :class:`.Table`. - -Background on PG's ``search_path`` is at: -http://www.postgresql.org/docs/9.0/static/ddl-schemas.html#DDL-SCHEMAS-PATH +.. _postgresql_schema_reflection: + +Remote-Schema Table Introspection and Postgresql search_path +------------------------------------------------------------ + +The Postgresql dialect can reflect tables from any schema. The +:paramref:`.Table.schema` argument, or alternatively the +:paramref:`.MetaData.reflect.schema` argument determines which schema will +be searched for the table or tables. The reflected :class:`.Table` objects +will in all cases retain this ``.schema`` attribute as was specified. However, +with regards to tables which these :class:`.Table` objects refer to via +foreign key constraint, a decision must be made as to how the ``.schema`` +is represented in those remote tables, in the case where that remote +schema name is also a member of the current +`Postgresql search path <http://www.postgresql.org/docs/9.0/static/ddl-schemas.html#DDL-SCHEMAS-PATH>`_. + +By default, the Postgresql dialect mimics the behavior encouraged by +Postgresql's own ``pg_get_constraintdef()`` builtin procedure. This function +returns a sample definition for a particular foreign key constraint, +omitting the referenced schema name from that definition when the name is +also in the Postgresql schema search path. The interaction below +illustrates this behavior:: + + test=> CREATE TABLE test_schema.referred(id INTEGER PRIMARY KEY); + CREATE TABLE + test=> CREATE TABLE referring( + test(> id INTEGER PRIMARY KEY, + test(> referred_id INTEGER REFERENCES test_schema.referred(id)); + CREATE TABLE + test=> SET search_path TO public, test_schema; + test=> SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM + test-> pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + test-> JOIN pg_catalog.pg_constraint r ON c.oid = r.conrelid + test-> WHERE c.relname='referring' AND r.contype = 'f' + test-> ; + pg_get_constraintdef + --------------------------------------------------- + FOREIGN KEY (referred_id) REFERENCES referred(id) + (1 row) + +Above, we created a table ``referred`` as a member of the remote schema ``test_schema``, however +when we added ``test_schema`` to the PG ``search_path`` and then asked ``pg_get_constraintdef()`` +for the ``FOREIGN KEY`` syntax, ``test_schema`` was not included in the +output of the function. + +On the other hand, if we set the search path back to the typical default +of ``public``:: + + test=> SET search_path TO public; + SET + +The same query against ``pg_get_constraintdef()`` now returns the fully +schema-qualified name for us:: + + test=> SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM + test-> pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + test-> JOIN pg_catalog.pg_constraint r ON c.oid = r.conrelid + test-> WHERE c.relname='referring' AND r.contype = 'f'; + pg_get_constraintdef + --------------------------------------------------------------- + FOREIGN KEY (referred_id) REFERENCES test_schema.referred(id) + (1 row) + +SQLAlchemy will by default use the return value of ``pg_get_constraintdef()`` +in order to determine the remote schema name. That is, if our ``search_path`` +were set to include ``test_schema``, and we invoked a table +reflection process as follows:: + + >>> from sqlalchemy import Table, MetaData, create_engine + >>> engine = create_engine("postgresql://scott:tiger@localhost/test") + >>> with engine.connect() as conn: + ... conn.execute("SET search_path TO test_schema, public") + ... meta = MetaData() + ... referring = Table('referring', meta, autoload=True, autoload_with=conn) + ... + <sqlalchemy.engine.result.ResultProxy object at 0x101612ed0> + +The above process would deliver to the :attr:`.MetaData.tables` collection +``referred`` table named **without** the schema:: + + >>> meta.tables['referred'].schema is None + True + +To alter the behavior of reflection such that the referred schema is maintained +regardless of the ``search_path`` setting, use the ``postgresql_ignore_search_path`` +option, which can be specified as a dialect-specific argument to both +:class:`.Table` as well as :meth:`.MetaData.reflect`:: + + >>> with engine.connect() as conn: + ... conn.execute("SET search_path TO test_schema, public") + ... meta = MetaData() + ... referring = Table('referring', meta, autoload=True, autoload_with=conn, + ... postgresql_ignore_search_path=True) + ... + <sqlalchemy.engine.result.ResultProxy object at 0x1016126d0> + +We will now have ``test_schema.referred`` stored as schema-qualified:: + + >>> meta.tables['test_schema.referred'].schema + 'test_schema' + +.. sidebar:: Best Practices for Postgresql Schema reflection + + The description of Postgresql schema reflection behavior is complex, and is + the product of many years of dealing with widely varied use cases and user preferences. + But in fact, there's no need to understand any of it if you just stick to the simplest + use pattern: leave the ``search_path`` set to its default of ``public`` only, never refer + to the name ``public`` as an explicit schema name otherwise, and + refer to all other schema names explicitly when building + up a :class:`.Table` object. The options described here are only for those users + who can't, or prefer not to, stay within these guidelines. + +Note that **in all cases**, the "default" schema is always reflected as ``None``. +The "default" schema on Postgresql is that which is returned by the +Postgresql ``current_schema()`` function. On a typical Postgresql installation, +this is the name ``public``. So a table that refers to another which is +in the ``public`` (i.e. default) schema will always have the ``.schema`` attribute +set to ``None``. + +.. versionadded:: 0.9.2 Added the ``postgresql_ignore_search_path`` + dialect-level option accepted by :class:`.Table` and :meth:`.MetaData.reflect`. + + +.. seealso:: + + `The Schema Search Path <http://www.postgresql.org/docs/9.0/static/ddl-schemas.html#DDL-SCHEMAS-PATH>`_ - on the Postgresql website. INSERT/UPDATE...RETURNING ------------------------- @@ -1439,9 +1535,14 @@ class PGDialect(default.DefaultDialect): "using": False, "where": None, "ops": {} + }), + (schema.Table, { + "ignore_search_path": False }) ] + reflection_options = ('postgresql_ignore_search_path', ) + _backslash_escapes = True def __init__(self, isolation_level=None, json_serializer=None, @@ -1968,7 +2069,8 @@ class PGDialect(default.DefaultDialect): return {'constrained_columns': cols, 'name': name} @reflection.cache - def get_foreign_keys(self, connection, table_name, schema=None, **kw): + def get_foreign_keys(self, connection, table_name, schema=None, + postgresql_ignore_search_path=False, **kw): preparer = self.identifier_preparer table_oid = self.get_table_oid(connection, table_name, schema, info_cache=kw.get('info_cache')) @@ -2004,26 +2106,35 @@ class PGDialect(default.DefaultDialect): fkeys = [] for conname, condef, conschema in c.fetchall(): m = re.search(FK_REGEX, condef).groups() + constrained_columns, referred_schema, \ referred_table, referred_columns, \ _, match, _, onupdate, _, ondelete, \ deferrable, _, initially = m + if deferrable is not None: deferrable = True if deferrable == 'DEFERRABLE' else False constrained_columns = [preparer._unquote_identifier(x) for x in re.split(r'\s*,\s*', constrained_columns)] - if referred_schema: + if postgresql_ignore_search_path: + # when ignoring search path, we use the actual schema + # provided it isn't the "default" schema + if conschema != self.default_schema_name: + referred_schema = conschema + else: + referred_schema = schema + elif referred_schema: + # referred_schema is the schema that we regexp'ed from + # pg_get_constraintdef(). If the schema is in the search + # path, pg_get_constraintdef() will give us None. referred_schema = \ preparer._unquote_identifier(referred_schema) elif schema is not None and schema == conschema: - # no schema was returned by pg_get_constraintdef(). This - # means the schema is in the search path. We will leave - # it as None, unless the actual schema, which we pull out - # from pg_namespace even though pg_get_constraintdef() doesn't - # want to give it to us, matches that of the referencing table, - # and an explicit schema was given for the referencing table. + # If the actual schema matches the schema of the table + # we're reflecting, then we will use that. referred_schema = schema + referred_table = preparer._unquote_identifier(referred_table) referred_columns = [preparer._unquote_identifier(x) for x in re.split(r'\s*,\s', referred_columns)] diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 1f2b7a3e5..888a15fee 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -353,17 +353,26 @@ class Connection(Connectable): def detach(self): """Detach the underlying DB-API connection from its connection pool. - This Connection instance will remain usable. When closed, + E.g.:: + + with engine.connect() as conn: + conn.detach() + conn.execute("SET search_path TO schema1, schema2") + + # work with connection + + # connection is fully closed (since we used "with:", can + # also call .close()) + + This :class:`.Connection` instance will remain usable. When closed + (or exited from a context manager context as above), the DB-API connection will be literally closed and not - returned to its pool. The pool will typically lazily create a - new connection to replace the detached connection. + returned to its originating pool. This method can be used to insulate the rest of an application from a modified state on a connection (such as a transaction - isolation level or similar). Also see - :class:`~sqlalchemy.interfaces.PoolListener` for a mechanism to modify - connection state when connections leave and return to their - connection pool. + isolation level or similar). + """ self.__connection.detach() diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index ef4b4cfa5..a9d5a69b1 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -3153,7 +3153,8 @@ class MetaData(SchemaItem): def reflect(self, bind=None, schema=None, views=False, only=None, extend_existing=False, - autoload_replace=True): + autoload_replace=True, + **dialect_kwargs): """Load all available table definitions from the database. Automatically creates ``Table`` entries in this ``MetaData`` for any @@ -3198,6 +3199,14 @@ class MetaData(SchemaItem): .. versionadded:: 0.9.1 + :param \**dialect_kwargs: Additional keyword arguments not mentioned above are + dialect specific, and passed in the form ``<dialectname>_<argname>``. + See the documentation regarding an individual dialect at + :ref:`dialect_toplevel` for detail on documented arguments. + + .. versionadded:: 0.9.2 - Added :paramref:`.MetaData.reflect.**dialect_kwargs` + to support dialect-level reflection options for all :class:`.Table` + objects reflected. """ if bind is None: @@ -3212,6 +3221,8 @@ class MetaData(SchemaItem): 'autoload_replace': autoload_replace } + reflect_opts.update(dialect_kwargs) + if schema is None: schema = self.schema |