From: Mike Bayer Date: Sun, 4 Dec 2011 19:16:42 +0000 (-0500) Subject: - [feature] The "extend_existing" flag on Table X-Git-Tag: rel_0_7_4~31 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=81945d7a0c60cc898541189d52564df2010871c1;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - [feature] The "extend_existing" flag on Table now allows for the reflection process to take effect for a Table object that's already been defined; when autoload=True and extend_existing=True are both set, the full set of columns will be reflected from the Table which will then *overwrite* those columns already present, rather than no activity occurring. Columns that are present directly in the autoload run will be used as always, however. [ticket:1410] --- diff --git a/CHANGES b/CHANGES index b9bdfb1521..6d4870ab70 100644 --- a/CHANGES +++ b/CHANGES @@ -164,6 +164,18 @@ CHANGES on dialect, but only works on Postgresql so far. Courtesy Manlio Perillo, [ticket:1679] + - [feature] The "extend_existing" flag on Table + now allows for the reflection process to take + effect for a Table object that's already been + defined; when autoload=True and extend_existing=True + are both set, the full set of columns will be + reflected from the Table which will then + *overwrite* those columns already present, + rather than no activity occurring. Columns that + are present directly in the autoload run + will be used as always, however. + [ticket:1410] + - [bug] Fixed bug whereby TypeDecorator would return a stale value for _type_affinity, when using a TypeDecorator that "switches" types, diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index 093a456e22..b5b057625b 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -138,13 +138,34 @@ class Table(SchemaItem, expression.TableClause): or Connection instance to be used for the table reflection. If ``None``, the underlying MetaData's bound connectable will be used. - :param extend_existing: When ``True``, indicates that if this Table is already + :param extend_existing: When ``True``, indicates that if this :class:`.Table` is already present in the given :class:`.MetaData`, apply further arguments within the constructor to the existing :class:`.Table`. - If extend_existing or keep_existing are not set, an error is + If ``extend_existing`` or ``keep_existing`` are not set, an error is raised if additional table modifiers are specified when the given :class:`.Table` is already present in the :class:`.MetaData`. + + As of version 0.7.4, ``extend_existing`` will work in conjunction + with ``autoload=True`` to run a new reflection operation against + the database; new :class:`.Column` objects will be produced + from database metadata to replace those existing with the same + name, and additional :class:`.Column` objects not present + in the :class:`.Table` will be added. + As is always the case with ``autoload=True``, :class:`.Column` + objects can be specified in the same :class:`.Table` constructor, + which will take precedence. I.e.:: + + Table("mytable", metadata, + Column('y', Integer), + extend_existing=True, + autoload=True, + autoload_with=engine + ) + + The above will overwrite all columns within ``mytable`` which are present + in the database, except for ``y`` which will be used as is + from the above definition. :param implicit_returning: True by default - indicates that RETURNING can be used by default to fetch newly inserted primary key @@ -329,27 +350,30 @@ class Table(SchemaItem, expression.TableClause): # we do it after the table is in the singleton dictionary to support # circular foreign keys if autoload: - if autoload_with: - autoload_with.run_callable( - autoload_with.dialect.reflecttable, - self, include_columns - ) - else: - bind = _bind_or_error(metadata, - msg="No engine is bound to this Table's MetaData. " - "Pass an engine to the Table via " - "autoload_with=, " - "or associate the MetaData with an engine via " - "metadata.bind=") - bind.run_callable( - bind.dialect.reflecttable, - self, include_columns - ) + self._autoload(metadata, autoload_with, include_columns) # initialize all the column, etc. objects. done after reflection to # allow user-overrides self._init_items(*args) + def _autoload(self, metadata, autoload_with, include_columns): + if autoload_with: + autoload_with.run_callable( + autoload_with.dialect.reflecttable, + self, include_columns + ) + else: + bind = _bind_or_error(metadata, + msg="No engine is bound to this Table's MetaData. " + "Pass an engine to the Table via " + "autoload_with=, " + "or associate the MetaData with an engine via " + "metadata.bind=") + bind.run_callable( + bind.dialect.reflecttable, + self, include_columns + ) + @property def _sorted_constraints(self): """Return the set of constraints as a list, sorted by creation order.""" @@ -378,6 +402,9 @@ class Table(SchemaItem, expression.TableClause): if 'info' in kwargs: self.info = kwargs.pop('info') + if autoload: + self._autoload(self.metadata, autoload_with, include_columns) + self._extra_kwargs(**kwargs) self._init_items(*args) diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py index 4beeab14d7..4e80e71e4f 100644 --- a/test/engine/test_reflection.py +++ b/test/engine/test_reflection.py @@ -3,7 +3,7 @@ import StringIO, unicodedata from sqlalchemy import types as sql_types from sqlalchemy import schema, events, event from sqlalchemy.engine.reflection import Inspector -from sqlalchemy import MetaData, Integer +from sqlalchemy import MetaData, Integer, String from test.lib.schema import Table, Column import sqlalchemy as sa from test.lib import ComparesTables, \ @@ -118,6 +118,45 @@ class ReflectionTest(fixtures.TestBase, ComparesTables): for c in ('a', 'c', 'd'): assert c not in foo.c + @testing.provide_metadata + def test_extend_existing(self): + meta = self.metadata + + t1 = Table('t', meta, + Column('x', Integer), + Column('y', Integer), + Column('z', Integer, server_default="5"), + ) + meta.create_all() + + m2 = MetaData() + old_z = Column('z', String) + old_y = Column('y', String) + old_q = Column('q', Integer) + t2 = Table('t', m2, old_z, old_q) + t2 = Table('t', m2, old_y, + extend_existing=True, + autoload=True, + autoload_with=testing.db) + eq_( + set(t2.columns.keys()), + set(['x', 'y', 'z', 'q']) + ) + assert t2.c.z is not old_z + assert t2.c.y is old_y + assert t2.c.z.type._type_affinity is Integer + assert t2.c.q is old_q + + m3 = MetaData() + t3 = Table('t', m3, Column('z', Integer)) + t3 = Table('t', m3, extend_existing=False, + autoload=True, + autoload_with=testing.db) + eq_( + set(t3.columns.keys()), + set(['z']) + ) + @testing.emits_warning(r".*omitted columns") @testing.provide_metadata def test_include_columns_indexes(self):