]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Support "blank" schema when MetaData.schema is set
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 18 May 2016 15:07:02 +0000 (11:07 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 18 May 2016 15:43:38 +0000 (11:43 -0400)
Previously, it was impossible to have a Table that has
None for a schema name when the "schema" parameter on
MetaData was set.  A new symbol sqlalchemy.schema.BLANK_SCHEMA
is added which indicates that the schema name should unconditionally
be set to None.  In particular, this value must be passed within
cross-schema foreign key reflection, so that a Table which
is in the "default" schema can be represented properly.

Fixes: #3716
Change-Id: I3d24f99c22cded206c5379fd32a225e74edb7a8e

doc/build/changelog/changelog_10.rst
doc/build/core/metadata.rst
lib/sqlalchemy/__init__.py
lib/sqlalchemy/engine/reflection.py
lib/sqlalchemy/schema.py
lib/sqlalchemy/sql/schema.py
test/dialect/postgresql/test_reflection.py
test/engine/test_reflection.py
test/sql/test_metadata.py

index 39670b0d8ddc13c06f06ce3eb440bfb5829dfa64..1b2414f2ad741c30aac299bc38432b49ee69e9ce 100644 (file)
     .. include:: changelog_07.rst
         :start-line: 5
 
+.. changelog::
+    :version: 1.0.14
+
+    .. change::
+        :tags: bug, engine, postgresql
+        :tickets: 3716
+
+        Fixed bug in cross-schema foreign key reflection in conjunction
+        with the :paramref:`.MetaData.schema` argument, where a referenced
+        table that is present in the "default" schema would fail since there
+        would be no way to indicate a :class:`.Table` that has "blank" for
+        a schema.  The special symbol :attr:`.schema.BLANK_SCHEMA` has been
+        added as an available value for :paramref:`.Table.schema` and
+        :paramref:`.Sequence.schema`, indicating that the schema name
+        should be forced to be ``None`` even if :paramref:`.MetaData.schema`
+        is specified.
+
 .. changelog::
     :version: 1.0.13
     :released: May 16, 2016
index 5052e0e7fcfe77fdbffc88af9cdd901ea9e654b7..b37d579f1d7ac7574c63b370df67067344c54675 100644 (file)
@@ -303,6 +303,23 @@ described in the individual documentation sections for each dialect.
 Column, Table, MetaData API
 ---------------------------
 
+.. attribute:: sqlalchemy.schema.BLANK_SCHEMA
+
+    Symbol indicating that a :class:`.Table` or :class:`.Sequence`
+    should have 'None' for its schema, even if the parent
+    :class:`.MetaData` has specified a schema.
+
+    .. seealso::
+
+        :paramref:`.MetaData.schema`
+
+        :paramref:`.Table.schema`
+
+        :paramref:`.Sequence.schema`
+
+    .. versionadded:: 1.0.14
+
+
 .. autoclass:: Column
     :members:
     :inherited-members:
index 1193a1b0b001e2f0998582162882bed15867bfd6..b1d240edf92bf2990ac925d00feb1f9f034c8fab 100644 (file)
@@ -120,6 +120,7 @@ from .schema import (
     ThreadLocalMetaData,
     UniqueConstraint,
     DDL,
+    BLANK_SCHEMA
 )
 
 
index eaa5e2e487364570831d8e4fe82b009000530710..2d524978da09eb5c2511c175ddbf6dddd3a15466 100644 (file)
@@ -693,6 +693,7 @@ class Inspector(object):
             else:
                 sa_schema.Table(referred_table, table.metadata, autoload=True,
                                 autoload_with=self.bind,
+                                schema=sa_schema.BLANK_SCHEMA,
                                 **reflection_options
                                 )
                 for column in referred_columns:
index 5b703f7b6a152db64909afa871b5f7ee2b0705e0..bd0cbe54e6908f926110a46bfdd671c6b26a3fd0 100644 (file)
@@ -15,6 +15,7 @@ from .sql.base import (
 
 
 from .sql.schema import (
+    BLANK_SCHEMA,
     CheckConstraint,
     Column,
     ColumnDefault,
index 5e709b1e3a6daafcaf74aef1911a444666e11eb8..64692644cf4ef023fa8183449e951ca9f156b3a7 100644 (file)
@@ -46,6 +46,17 @@ from . import ddl
 
 RETAIN_SCHEMA = util.symbol('retain_schema')
 
+BLANK_SCHEMA = util.symbol(
+    'blank_schema',
+    """Symbol indicating that a :class:`.Table` or :class:`.Sequence`
+    should have 'None' for its schema, even if the parent
+    :class:`.MetaData` has specified a schema.
+
+    .. versionadded:: 1.0.14
+
+    """
+)
+
 
 def _get_table_key(name, schema):
     if schema is None:
@@ -340,6 +351,17 @@ class Table(DialectKWArgs, SchemaItem, TableClause):
         the table resides in a schema other than the default selected schema
         for the engine's database connection.  Defaults to ``None``.
 
+        If the owning :class:`.MetaData` of this :class:`.Table` specifies
+        its own :paramref:`.MetaData.schema` parameter, then that schema
+        name will be applied to this :class:`.Table` if the schema parameter
+        here is set to ``None``.  To set a blank schema name on a :class:`.Table`
+        that would otherwise use the schema set on the owning :class:`.MetaData`,
+        specify the special symbol :attr:`.BLANK_SCHEMA`.
+
+        .. versionadded:: 1.0.14  Added the :attr:`.BLANK_SCHEMA` symbol to
+           allow a :class:`.Table` to have a blank schema name even when the
+           parent :class:`.MetaData` specifies :paramref:`.MetaData.schema`.
+
         The quoting rules for the schema name are the same as those for the
         ``name`` parameter, in that quoting is applied for reserved words or
         case-sensitive names; to enable unconditional quoting for the
@@ -371,6 +393,8 @@ class Table(DialectKWArgs, SchemaItem, TableClause):
         schema = kw.get('schema', None)
         if schema is None:
             schema = metadata.schema
+        elif schema is BLANK_SCHEMA:
+            schema = None
         keep_existing = kw.pop('keep_existing', False)
         extend_existing = kw.pop('extend_existing', False)
         if 'useexisting' in kw:
@@ -442,6 +466,8 @@ class Table(DialectKWArgs, SchemaItem, TableClause):
         self.schema = kwargs.pop('schema', None)
         if self.schema is None:
             self.schema = metadata.schema
+        elif self.schema is BLANK_SCHEMA:
+            self.schema = None
         else:
             quote_schema = kwargs.pop('quote_schema', None)
             self.schema = quoted_name(self.schema, quote_schema)
@@ -2120,7 +2146,10 @@ class Sequence(DefaultGenerator):
          .. versionadded:: 1.0.7
 
         :param schema: Optional schema name for the sequence, if located
-         in a schema other than the default.
+         in a schema other than the default.  The rules for selecting the
+         schema name when a :class:`.MetaData` is also present are the same
+         as that of :paramref:`.Table.schema`.
+
         :param optional: boolean value, when ``True``, indicates that this
          :class:`.Sequence` object only needs to be explicitly generated
          on backends that don't provide another way to generate primary
@@ -2169,7 +2198,9 @@ class Sequence(DefaultGenerator):
         self.nomaxvalue = nomaxvalue
         self.cycle = cycle
         self.optional = optional
-        if metadata is not None and schema is None and metadata.schema:
+        if schema is BLANK_SCHEMA:
+            self.schema = schema = None
+        elif metadata is not None and schema is None and metadata.schema:
             self.schema = schema = metadata.schema
         else:
             self.schema = quoted_name(schema, quote_schema)
@@ -3372,8 +3403,21 @@ class MetaData(SchemaItem):
 
         :param schema:
            The default schema to use for the :class:`.Table`,
-           :class:`.Sequence`, and other objects associated with this
-           :class:`.MetaData`. Defaults to ``None``.
+           :class:`.Sequence`, and potentially other objects associated with
+           this :class:`.MetaData`. Defaults to ``None``.
+
+           When this value is set, any :class:`.Table` or :class:`.Sequence`
+           which specifies ``None`` for the schema parameter will instead
+           have this schema name defined.  To build a :class:`.Table`
+           or :class:`.Sequence` that still has ``None`` for the schema
+           even when this parameter is present, use the :attr:`.BLANK_SCHEMA`
+           symbol.
+
+           .. seealso::
+
+                :paramref:`.Table.schema`
+
+                :paramref:`.Sequence.schema`
 
         :param quote_schema:
             Sets the ``quote_schema`` flag for those :class:`.Table`,
index 8da18108f907acda206163efbb222ef7794815c2..4897c4a7e2803583bc5edfba529a38030268a22b 100644 (file)
@@ -581,6 +581,29 @@ class ReflectionTest(fixtures.TestBase):
             eq_(set(meta3.tables), set(
                 ['test_schema_2.some_other_table', 'test_schema.some_table']))
 
+    @testing.provide_metadata
+    def test_cross_schema_reflection_metadata_uses_schema(self):
+        # test [ticket:3716]
+
+        metadata = self.metadata
+
+        Table('some_table', metadata,
+              Column('id', Integer, primary_key=True),
+              Column('sid', Integer, ForeignKey('some_other_table.id')),
+              schema='test_schema'
+              )
+        Table('some_other_table', metadata,
+              Column('id', Integer, primary_key=True),
+              schema=None
+              )
+        metadata.create_all()
+        with testing.db.connect() as conn:
+            meta2 = MetaData(conn, schema="test_schema")
+            meta2.reflect()
+
+            eq_(set(meta2.tables), set(
+                ['some_other_table', 'test_schema.some_table']))
+
     @testing.provide_metadata
     def test_uppercase_lowercase_table(self):
         metadata = self.metadata
index f9799fda0febe54cb4f2bd2501110e8250c889f3..0d98147cb62fc7fa2cf9d9f5caaa16bc0fd42650 100644 (file)
@@ -1303,6 +1303,31 @@ class SchemaTest(fixtures.TestBase):
         eq_(testing.db.dialect.has_schema(testing.db,
             'sa_fake_schema_123'), False)
 
+    @testing.requires.schemas
+    @testing.requires.cross_schema_fk_reflection
+    @testing.provide_metadata
+    def test_blank_schema_arg(self):
+        metadata = self.metadata
+
+        Table('some_table', metadata,
+              Column('id', Integer, primary_key=True),
+              Column('sid', Integer, sa.ForeignKey('some_other_table.id')),
+              schema=testing.config.test_schema
+              )
+        Table('some_other_table', metadata,
+              Column('id', Integer, primary_key=True),
+              schema=None
+              )
+        metadata.create_all()
+        with testing.db.connect() as conn:
+            meta2 = MetaData(conn, schema=testing.config.test_schema)
+            meta2.reflect()
+
+            eq_(set(meta2.tables), set(
+                [
+                    'some_other_table',
+                    '%s.some_table' % testing.config.test_schema]))
+
     @testing.requires.schemas
     @testing.fails_on('sqlite', 'FIXME: unknown')
     @testing.fails_on('sybase', 'FIXME: unknown')
index 050929d3db57e1b393856feec30a9e8086f16a99..449956fcd46f889f98c56a7d38a2ec0f731a33d7 100644 (file)
@@ -7,7 +7,8 @@ from sqlalchemy import Integer, String, UniqueConstraint, \
     CheckConstraint, ForeignKey, MetaData, Sequence, \
     ForeignKeyConstraint, PrimaryKeyConstraint, ColumnDefault, Index, event,\
     events, Unicode, types as sqltypes, bindparam, \
-    Table, Column, Boolean, Enum, func, text, TypeDecorator
+    Table, Column, Boolean, Enum, func, text, TypeDecorator, \
+    BLANK_SCHEMA
 from sqlalchemy import schema, exc
 from sqlalchemy.engine import default
 from sqlalchemy.sql import elements, naming
@@ -446,6 +447,7 @@ class MetaDataTest(fixtures.TestBase, ComparesTables):
                     ('t2', m1, 'sch2', None, 'sch2', None),
                     ('t3', m1, 'sch2', True, 'sch2', True),
                     ('t4', m1, 'sch1', None, 'sch1', None),
+                    ('t5', m1, BLANK_SCHEMA, None, None, None),
                     ('t1', m2, None, None, 'sch1', True),
                     ('t2', m2, 'sch2', None, 'sch2', None),
                     ('t3', m2, 'sch2', True, 'sch2', True),
@@ -458,6 +460,7 @@ class MetaDataTest(fixtures.TestBase, ComparesTables):
                     ('t2', m4, 'sch2', None, 'sch2', None),
                     ('t3', m4, 'sch2', True, 'sch2', True),
                     ('t4', m4, 'sch1', None, 'sch1', None),
+                    ('t5', m4, BLANK_SCHEMA, None, None, None),
                 ]):
             kw = {}
             if schema is not None: