From: Mike Bayer Date: Tue, 10 Jul 2018 13:41:21 +0000 (-0400) Subject: Drop default-related structures after the Table is dropped. X-Git-Tag: rel_1_3_0b1~131^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=532566ba1f28ff8a6afa6eacc10c59eb918501f6;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Drop default-related structures after the Table is dropped. Fixed bug where a :class:`.Sequence` would be dropped explicitly before any :class:`.Table` that refers to it, which breaks in the case when the sequence is also involved in a server-side default for that table, when using :meth:`.MetaData.drop_all`. The step which processes sequences to be dropped via non server-side column default functions is now invoked after the table itself is dropped. Change-Id: I185f2cc76d2011ad4dd3ba9bde5d8aef0ec335ae Fixes: #4300 --- diff --git a/doc/build/changelog/unreleased_12/4300.rst b/doc/build/changelog/unreleased_12/4300.rst new file mode 100644 index 0000000000..b34742cc3c --- /dev/null +++ b/doc/build/changelog/unreleased_12/4300.rst @@ -0,0 +1,10 @@ +.. change:: + :tags: bug, sql + :tickets: 4300 + + Fixed bug where a :class:`.Sequence` would be dropped explicitly before any + :class:`.Table` that refers to it, which breaks in the case when the + sequence is also involved in a server-side default for that table, when + using :meth:`.MetaData.drop_all`. The step which processes sequences + to be dropped via non server-side column default functions is now invoked + after the table itself is dropped. diff --git a/doc/build/core/defaults.rst b/doc/build/core/defaults.rst index 05171d99de..4b52641384 100644 --- a/doc/build/core/defaults.rst +++ b/doc/build/core/defaults.rst @@ -440,6 +440,9 @@ and that options like default schema are propagated, setting the Associating a Sequence as the Server Side Default ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. note:: The following technique is known to work only with the Postgresql + database. It does not work with Oracle. + The preceding sections illustrate how to associate a :class:`.Sequence` with a :class:`.Column` as the **Python side default generator**:: diff --git a/lib/sqlalchemy/sql/ddl.py b/lib/sqlalchemy/sql/ddl.py index 8de6f9cdc1..91e93efe74 100644 --- a/lib/sqlalchemy/sql/ddl.py +++ b/lib/sqlalchemy/sql/ddl.py @@ -948,17 +948,22 @@ class SchemaDropper(DDLBase): _ddl_runner=self, _is_metadata_operation=_is_metadata_operation) + self.connection.execute(DropTable(table)) + + # traverse client side defaults which may refer to server-side + # sequences. noting that some of these client side defaults may also be + # set up as server side defaults (see http://docs.sqlalchemy.org/en/ + # latest/core/defaults.html#associating-a-sequence-as-the-server-side- + # default), so have to be dropped after the table is dropped. for column in table.columns: if column.default is not None: self.traverse_single(column.default) - self.connection.execute(DropTable(table)) - table.dispatch.after_drop( table, self.connection, - checkfirst=self.checkfirst, - _ddl_runner=self, - _is_metadata_operation=_is_metadata_operation) + checkfirst=self.checkfirst, + _ddl_runner=self, + _is_metadata_operation=_is_metadata_operation) def visit_foreign_key_constraint(self, constraint): if not self.dialect.supports_alter: @@ -966,6 +971,7 @@ class SchemaDropper(DDLBase): self.connection.execute(DropConstraint(constraint)) def visit_sequence(self, sequence, drop_ok=False): + if not drop_ok and not self._can_drop_sequence(sequence): return self.connection.execute(DropSequence(sequence)) diff --git a/test/engine/test_execute.py b/test/engine/test_execute.py index 30c41c6ee5..84262dab24 100644 --- a/test/engine/test_execute.py +++ b/test/engine/test_execute.py @@ -917,7 +917,7 @@ class MockStrategyTest(fixtures.TestBase): eq_( re.findall(r'DROP (\w+)', buf.getvalue()), - ["SEQUENCE", "TABLE"] + ["TABLE", "SEQUENCE"] ) diff --git a/test/requirements.py b/test/requirements.py index 57324a3f40..c6255eb8d7 100644 --- a/test/requirements.py +++ b/test/requirements.py @@ -329,6 +329,14 @@ class DefaultRequirements(SuiteRequirements): 'PG triggers need to be implemented for tests'), ]) + @property + def sequences_as_server_defaults(self): + """Target database must support SEQUENCE as a server side default.""" + + return only_on( + 'postgresql', + "doesn't support sequences as a server side default.") + @property def correlated_outer_joins(self): """Target must support an outer join to a subquery which diff --git a/test/sql/test_defaults.py b/test/sql/test_defaults.py index 0d4eecf6aa..42514a9276 100644 --- a/test/sql/test_defaults.py +++ b/test/sql/test_defaults.py @@ -17,6 +17,7 @@ from sqlalchemy.util import u, b from sqlalchemy import util from sqlalchemy.testing import mock import itertools +from sqlalchemy.testing.assertsql import CompiledSQL, AllOf, EachOf t = f = f2 = ts = currenttime = metadata = default_generator = None @@ -1312,6 +1313,85 @@ class TableBoundSequenceTest(fixtures.TestBase): (4, "name4", 4)]) +class SequenceAsServerDefaultTest( + testing.AssertsExecutionResults, fixtures.TablesTest): + __requires__ = ('sequences_as_server_defaults',) + __backend__ = True + + run_create_tables = 'each' + + @classmethod + def define_tables(cls, metadata): + m = metadata + + s = Sequence("t_seq", metadata=m) + Table( + "t_seq_test", m, + Column("id", Integer, s, server_default=s.next_value()), + Column("data", String(50)) + ) + + s2 = Sequence("t_seq_2", metadata=m) + Table( + "t_seq_test_2", m, + Column("id", Integer, server_default=s2.next_value()), + Column("data", String(50)) + ) + + def test_default_textual_w_default(self): + with testing.db.connect() as conn: + conn.execute("insert into t_seq_test (data) values ('some data')") + + eq_(conn.scalar("select id from t_seq_test"), 1) + + def test_default_core_w_default(self): + t_seq_test = self.tables.t_seq_test + with testing.db.connect() as conn: + conn.execute(t_seq_test.insert().values(data="some data")) + + eq_(conn.scalar(select([t_seq_test.c.id])), 1) + + def test_default_textual_server_only(self): + with testing.db.connect() as conn: + conn.execute( + "insert into t_seq_test_2 (data) values ('some data')") + + eq_(conn.scalar("select id from t_seq_test_2"), 1) + + def test_default_core_server_only(self): + t_seq_test = self.tables.t_seq_test_2 + with testing.db.connect() as conn: + conn.execute(t_seq_test.insert().values(data="some data")) + + eq_(conn.scalar(select([t_seq_test.c.id])), 1) + + def test_drop_ordering(self): + self.assert_sql_execution( + testing.db, + lambda: self.metadata.drop_all(checkfirst=False), + AllOf( + CompiledSQL( + "DROP TABLE t_seq_test_2", + {} + ), + EachOf( + CompiledSQL( + "DROP TABLE t_seq_test", + {} + ), + CompiledSQL( + "DROP SEQUENCE t_seq", # dropped as part of t_seq_test + {} + ), + ), + ), + CompiledSQL( + "DROP SEQUENCE t_seq_2", # dropped as part of metadata level + {} + ), + ) + + class SpecialTypePKTest(fixtures.TestBase): """test process_result_value in conjunction with primary key columns.