From: Mike Bayer Date: Thu, 17 May 2012 15:45:05 +0000 (-0400) Subject: - [feature] The "deferred declarative X-Git-Tag: rel_0_8_0b1~423 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=29251675503271fc700a6f7655157850e2de426d;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - [feature] The "deferred declarative reflection" system has been moved into the declarative extension itself, using the new DeferredReflection class. This class is now tested with both single and joined table inheritance use cases. [ticket:2485] - [bug] The autoload_replace flag on Table, when False, will cause any reflected foreign key constraints which refer to already-declared columns to be skipped, assuming that the in-Python declared column will take over the task of specifying in-Python ForeignKey or ForeignKeyConstraint declarations. --- diff --git a/CHANGES b/CHANGES index 0be1dc951b..f00eff48ff 100644 --- a/CHANGES +++ b/CHANGES @@ -49,6 +49,14 @@ CHANGES of a join in place of the "of_type()" modifier. [ticket:2333] + - [feature] The "deferred declarative + reflection" system has been moved into the + declarative extension itself, using the + new DeferredReflection class. This + class is now tested with both single + and joined table inheritance use cases. + [ticket:2485] + - [feature] Added new core function "inspect()", which serves as a generic gateway to introspection into mappers, objects, @@ -174,6 +182,14 @@ CHANGES deprecated; use Inspector.get_pk_constraint(). Courtesy Diana Clarke. [ticket:2422] + - [bug] The autoload_replace flag on Table, + when False, will cause any reflected foreign key + constraints which refer to already-declared + columns to be skipped, assuming that the + in-Python declared column will take over + the task of specifying in-Python ForeignKey + or ForeignKeyConstraint declarations. + - sql - [feature] The Inspector object can now be acquired using the new inspect() service, diff --git a/doc/build/orm/examples.rst b/doc/build/orm/examples.rst index bd7fc5a943..fcee004347 100644 --- a/doc/build/orm/examples.rst +++ b/doc/build/orm/examples.rst @@ -44,15 +44,6 @@ Location: /examples/beaker_caching/ .. automodule:: beaker_caching -.. _examples_declarative_reflection: - -Declarative Reflection ----------------------- - -Location: /examples/declarative_reflection - -.. automodule:: declarative_reflection - Directed Graphs --------------- diff --git a/doc/build/orm/extensions/declarative.rst b/doc/build/orm/extensions/declarative.rst index aaa0261b6d..c2ca4eabca 100644 --- a/doc/build/orm/extensions/declarative.rst +++ b/doc/build/orm/extensions/declarative.rst @@ -26,3 +26,4 @@ API Reference .. autoclass:: ConcreteBase +.. autoclass:: DeferredReflection diff --git a/examples/declarative_reflection/__init__.py b/examples/declarative_reflection/__init__.py deleted file mode 100644 index cadd6ab241..0000000000 --- a/examples/declarative_reflection/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -Illustrates how to mix table reflection with Declarative, such that -the reflection process itself can take place **after** all classes -are defined. Declarative classes can also override column -definitions loaded from the database. - -At the core of this example is the ability to change how Declarative -assigns mappings to classes. The ``__mapper_cls__`` special attribute -is overridden to provide a function that gathers mapping requirements -as they are established, without actually creating the mapping. -Then, a second class-level method ``prepare()`` is used to iterate -through all mapping configurations collected, reflect the tables -named within and generate the actual mappers. - -The example is new in 0.7.5 and makes usage of the new -``autoload_replace`` flag on :class:`.Table` to allow declared -classes to override reflected columns. - -Usage example:: - - Base = declarative_base(cls=DeclarativeReflectedBase) - - class Foo(Base): - __tablename__ = 'foo' - bars = relationship("Bar") - - class Bar(Base): - __tablename__ = 'bar' - - # illustrate overriding of "bar.foo_id" to have - # a foreign key constraint otherwise not - # reflected, such as when using MySQL - foo_id = Column(Integer, ForeignKey('foo.id')) - - Base.prepare(e) - - s = Session(e) - - s.add_all([ - Foo(bars=[Bar(data='b1'), Bar(data='b2')], data='f1'), - Foo(bars=[Bar(data='b3'), Bar(data='b4')], data='f2') - ]) - s.commit() - - -""" diff --git a/examples/declarative_reflection/declarative_reflection.py b/examples/declarative_reflection/declarative_reflection.py deleted file mode 100644 index 3721493172..0000000000 --- a/examples/declarative_reflection/declarative_reflection.py +++ /dev/null @@ -1,98 +0,0 @@ -from sqlalchemy import * -from sqlalchemy.orm import * -from sqlalchemy.orm.util import _is_mapped_class -from sqlalchemy.ext.declarative import declarative_base, declared_attr - -class DeclarativeReflectedBase(object): - _mapper_args = [] - - @classmethod - def __mapper_cls__(cls, *args, **kw): - """Declarative will use this function in lieu of - calling mapper() directly. - - Collect each series of arguments and invoke - them when prepare() is called. - """ - - cls._mapper_args.append((args, kw)) - - @classmethod - def prepare(cls, engine): - """Reflect all the tables and map !""" - while cls._mapper_args: - args, kw = cls._mapper_args.pop() - klass = args[0] - # autoload Table, which is already - # present in the metadata. This - # will fill in db-loaded columns - # into the existing Table object. - if args[1] is not None: - table = args[1] - Table(table.name, - cls.metadata, - extend_existing=True, - autoload_replace=False, - autoload=True, - autoload_with=engine, - schema=table.schema) - - # see if we need 'inherits' in the - # mapper args. Declarative will have - # skipped this since mappings weren't - # available yet. - for c in klass.__bases__: - if _is_mapped_class(c): - kw['inherits'] = c - break - - klass.__mapper__ = mapper(*args, **kw) - -if __name__ == '__main__': - Base = declarative_base() - - # create a separate base so that we can - # define a subset of classes as "Reflected", - # instead of everything. - class Reflected(DeclarativeReflectedBase, Base): - __abstract__ = True - - class Foo(Reflected): - __tablename__ = 'foo' - bars = relationship("Bar") - - class Bar(Reflected): - __tablename__ = 'bar' - - # illustrate overriding of "bar.foo_id" to have - # a foreign key constraint otherwise not - # reflected, such as when using MySQL - foo_id = Column(Integer, ForeignKey('foo.id')) - - e = create_engine('sqlite://', echo=True) - e.execute(""" - create table foo( - id integer primary key, - data varchar(30) - ) - """) - - e.execute(""" - create table bar( - id integer primary key, - data varchar(30), - foo_id integer - ) - """) - - Reflected.prepare(e) - - s = Session(e) - - s.add_all([ - Foo(bars=[Bar(data='b1'), Bar(data='b2')], data='f1'), - Foo(bars=[Bar(data='b3'), Bar(data='b4')], data='f2') - ]) - s.commit() - for f in s.query(Foo): - print f.data, ",".join([b.data for b in f.bars]) \ No newline at end of file diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index 13a7e1b888..3a12819f12 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -431,6 +431,9 @@ class Inspector(object): for fkey_d in fkeys: conname = fkey_d['name'] constrained_columns = fkey_d['constrained_columns'] + if exclude_columns and set(constrained_columns).intersection( + exclude_columns): + continue referred_schema = fkey_d['referred_schema'] referred_table = fkey_d['referred_table'] referred_columns = fkey_d['referred_columns'] diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index 93df1c5935..2e9012384b 100755 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -325,12 +325,13 @@ in conjunction with a mapped class:: However, one improvement that can be made here is to not require the :class:`.Engine` to be available when classes are -being first declared. To achieve this, use the example -described at :ref:`examples_declarative_reflection` to build a -declarative base that sets up mappings only after a special -``prepare(engine)`` step is called:: +being first declared. To achieve this, use the +:class:`.DeferredReflection` mixin, which sets up mappings +only after a special ``prepare(engine)`` step is called:: - Base = declarative_base(cls=DeclarativeReflectedBase) + from sqlalchemy.ext.declarative import declarative_base, DeferredReflection + + Base = declarative_base(cls=DeferredReflection) class Foo(Base): __tablename__ = 'foo' @@ -346,7 +347,9 @@ declarative base that sets up mappings only after a special Base.prepare(e) - +.. versionadded:: 0.8 + Added :class:`.DeferredReflection`. + Mapper Configuration ==================== @@ -1871,8 +1874,8 @@ class DeferredReflection(object): def prepare(cls, engine): """Reflect all :class:`.Table` objects for all current :class:`.DeferredReflection` subclasses""" - to_map = set([m for m in _MapperConfig.configs.values() - if issubclass(m.cls, cls)]) + to_map = [m for m in _MapperConfig.configs.values() + if issubclass(m.cls, cls)] for thingy in to_map: cls.__prepare__(thingy.args, engine) thingy.map() diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py index 6bfc3246a1..be2acb1f36 100644 --- a/test/engine/test_reflection.py +++ b/test/engine/test_reflection.py @@ -201,7 +201,11 @@ class ReflectionTest(fixtures.TestBase, ComparesTables): assert len(t2.indexes) == 2 @testing.provide_metadata - def test_autoload_replace_foreign_key(self): + def test_autoload_replace_foreign_key_nonpresent(self): + """test autoload_replace=False with col plus FK + establishes the FK not present in the DB. + + """ a = Table('a', self.metadata, Column('id', Integer, primary_key=True)) b = Table('b', self.metadata, Column('id', Integer, primary_key=True), Column('a_id', Integer)) @@ -218,6 +222,51 @@ class ReflectionTest(fixtures.TestBase, ComparesTables): assert b2.c.a_id.references(a2.c.id) eq_(len(b2.constraints), 2) + @testing.provide_metadata + def test_autoload_replace_foreign_key_ispresent(self): + """test autoload_replace=False with col plus FK mirroring + DB-reflected FK skips the reflected FK and installs + the in-python one only. + + """ + a = Table('a', self.metadata, Column('id', Integer, primary_key=True)) + b = Table('b', self.metadata, Column('id', Integer, primary_key=True), + Column('a_id', Integer, sa.ForeignKey('a.id'))) + self.metadata.create_all() + + m2 = MetaData() + b2 = Table('b', m2, Column('a_id', Integer, sa.ForeignKey('a.id'))) + a2 = Table('a', m2, autoload=True, autoload_with=testing.db) + b2 = Table('b', m2, extend_existing=True, autoload=True, + autoload_with=testing.db, + autoload_replace=False) + + assert b2.c.id is not None + assert b2.c.a_id.references(a2.c.id) + eq_(len(b2.constraints), 2) + + @testing.provide_metadata + def test_autoload_replace_foreign_key_removed(self): + """test autoload_replace=False with col minus FK that's in the + DB means the FK is skipped and doesn't get installed at all. + + """ + a = Table('a', self.metadata, Column('id', Integer, primary_key=True)) + b = Table('b', self.metadata, Column('id', Integer, primary_key=True), + Column('a_id', Integer, sa.ForeignKey('a.id'))) + self.metadata.create_all() + + m2 = MetaData() + b2 = Table('b', m2, Column('a_id', Integer)) + a2 = Table('a', m2, autoload=True, autoload_with=testing.db) + b2 = Table('b', m2, extend_existing=True, autoload=True, + autoload_with=testing.db, + autoload_replace=False) + + assert b2.c.id is not None + assert not b2.c.a_id.references(a2.c.id) + eq_(len(b2.constraints), 1) + @testing.provide_metadata def test_autoload_replace_primary_key(self): a = Table('a', self.metadata, Column('id', Integer)) diff --git a/test/ext/test_declarative_reflection.py b/test/ext/test_declarative_reflection.py index 28aa47c927..b0aa148a97 100644 --- a/test/ext/test_declarative_reflection.py +++ b/test/ext/test_declarative_reflection.py @@ -13,6 +13,10 @@ class DeclarativeReflectionBase(fixtures.TablesTest): global Base Base = decl.declarative_base(testing.db) + def teardown(self): + super(DeclarativeReflectionBase, self).teardown() + clear_mappers() + class DeclarativeReflectionTest(DeclarativeReflectionBase): @classmethod @@ -216,3 +220,168 @@ class DeferredReflectionTest(DeferredReflectBase): DefBase.prepare(testing.db) self._roundtrip() + + def test_redefine_fk_double(self): + class User(decl.DeferredReflection, fixtures.ComparableEntity, + Base): + __tablename__ = 'users' + addresses = relationship("Address", backref="user") + + class Address(decl.DeferredReflection, fixtures.ComparableEntity, + Base): + __tablename__ = 'addresses' + user_id = Column(Integer, ForeignKey('users.id')) + + decl.DeferredReflection.prepare(testing.db) + self._roundtrip() + + +class DeferredInhReflectBase(DeferredReflectBase): + def _roundtrip(self): + Foo = Base._decl_class_registry['Foo'] + Bar = Base._decl_class_registry['Bar'] + + s = Session(testing.db) + + s.add_all([ + Bar(data='d1', bar_data='b1'), + Bar(data='d2', bar_data='b2'), + Bar(data='d3', bar_data='b3'), + Foo(data='d4') + ]) + s.commit() + + eq_( + s.query(Foo).order_by(Foo.id).all(), + [ + Bar(data='d1', bar_data='b1'), + Bar(data='d2', bar_data='b2'), + Bar(data='d3', bar_data='b3'), + Foo(data='d4') + ] + ) + +class DeferredSingleInhReflectionTest(DeferredInhReflectBase): + @classmethod + def define_tables(cls, metadata): + Table("foo", metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('type', String(32)), + Column('data', String(30)), + Column('bar_data', String(30)) + ) + + def test_basic(self): + class Foo(decl.DeferredReflection, fixtures.ComparableEntity, + Base): + __tablename__ = 'foo' + __mapper_args__ = {"polymorphic_on":"type", + "polymorphic_identity":"foo"} + + class Bar(Foo): + __mapper_args__ = {"polymorphic_identity":"bar"} + + decl.DeferredReflection.prepare(testing.db) + self._roundtrip() + + def test_add_subclass_column(self): + class Foo(decl.DeferredReflection, fixtures.ComparableEntity, + Base): + __tablename__ = 'foo' + __mapper_args__ = {"polymorphic_on":"type", + "polymorphic_identity":"foo"} + + class Bar(Foo): + __mapper_args__ = {"polymorphic_identity":"bar"} + bar_data = Column(String(30)) + + decl.DeferredReflection.prepare(testing.db) + self._roundtrip() + + def test_add_pk_column(self): + class Foo(decl.DeferredReflection, fixtures.ComparableEntity, + Base): + __tablename__ = 'foo' + __mapper_args__ = {"polymorphic_on":"type", + "polymorphic_identity":"foo"} + id = Column(Integer, primary_key=True) + + class Bar(Foo): + __mapper_args__ = {"polymorphic_identity":"bar"} + + decl.DeferredReflection.prepare(testing.db) + self._roundtrip() + +class DeferredJoinedInhReflectionTest(DeferredInhReflectBase): + @classmethod + def define_tables(cls, metadata): + Table("foo", metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('type', String(32)), + Column('data', String(30)), + ) + Table('bar', metadata, + Column('id', Integer, ForeignKey('foo.id'), primary_key=True), + Column('bar_data', String(30)) + ) + + def test_basic(self): + class Foo(decl.DeferredReflection, fixtures.ComparableEntity, + Base): + __tablename__ = 'foo' + __mapper_args__ = {"polymorphic_on":"type", + "polymorphic_identity":"foo"} + + class Bar(Foo): + __tablename__ = 'bar' + __mapper_args__ = {"polymorphic_identity":"bar"} + + decl.DeferredReflection.prepare(testing.db) + self._roundtrip() + + def test_add_subclass_column(self): + class Foo(decl.DeferredReflection, fixtures.ComparableEntity, + Base): + __tablename__ = 'foo' + __mapper_args__ = {"polymorphic_on":"type", + "polymorphic_identity":"foo"} + + class Bar(Foo): + __tablename__ = 'bar' + __mapper_args__ = {"polymorphic_identity":"bar"} + bar_data = Column(String(30)) + + decl.DeferredReflection.prepare(testing.db) + self._roundtrip() + + def test_add_pk_column(self): + class Foo(decl.DeferredReflection, fixtures.ComparableEntity, + Base): + __tablename__ = 'foo' + __mapper_args__ = {"polymorphic_on":"type", + "polymorphic_identity":"foo"} + id = Column(Integer, primary_key=True) + + class Bar(Foo): + __tablename__ = 'bar' + __mapper_args__ = {"polymorphic_identity":"bar"} + + decl.DeferredReflection.prepare(testing.db) + self._roundtrip() + + def test_add_fk_pk_column(self): + class Foo(decl.DeferredReflection, fixtures.ComparableEntity, + Base): + __tablename__ = 'foo' + __mapper_args__ = {"polymorphic_on":"type", + "polymorphic_identity":"foo"} + + class Bar(Foo): + __tablename__ = 'bar' + __mapper_args__ = {"polymorphic_identity":"bar"} + id = Column(Integer, ForeignKey('foo.id'), primary_key=True) + + decl.DeferredReflection.prepare(testing.db) + self._roundtrip()