]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Drop default-related structures after the Table is dropped.
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 10 Jul 2018 13:41:21 +0000 (09:41 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 11 Jul 2018 02:38:52 +0000 (22:38 -0400)
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
doc/build/changelog/unreleased_12/4300.rst [new file with mode: 0644]
doc/build/core/defaults.rst
lib/sqlalchemy/sql/ddl.py
test/engine/test_execute.py
test/requirements.py
test/sql/test_defaults.py

diff --git a/doc/build/changelog/unreleased_12/4300.rst b/doc/build/changelog/unreleased_12/4300.rst
new file mode 100644 (file)
index 0000000..b34742c
--- /dev/null
@@ -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.
index 05171d99dec0e3a4b41e3d1236938ce3e6e761cf..4b52641384fa863261617b1fc9c7420b03144952 100644 (file)
@@ -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**::
 
index 8de6f9cdc17456d52ac32eb5cedd967fd9ba0517..91e93efe74958b760139b48d3e0067f5d25e1178 100644 (file)
@@ -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))
index 30c41c6ee5d14dcf8cfb44576593e579bb9de3da..84262dab24495f3c444c73bc640de2804cb144e9 100644 (file)
@@ -917,7 +917,7 @@ class MockStrategyTest(fixtures.TestBase):
 
         eq_(
             re.findall(r'DROP (\w+)', buf.getvalue()),
-            ["SEQUENCE", "TABLE"]
+            ["TABLE", "SEQUENCE"]
         )
 
 
index 57324a3f40431c5c05d0dbd373e9d47efdd32d06..c6255eb8d74580ca3f811892b969f4d69f90f553 100644 (file)
@@ -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
index 0d4eecf6aae4bb83a0d669ccd24b7dde137ccb94..42514a9276094203336b16c803c8b44a13c10ff0 100644 (file)
@@ -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.