]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
correct mariadb sequence behavior when cycle=False
authorrusher <diego.dupin@mariadb.com>
Wed, 14 Jan 2026 14:03:00 +0000 (09:03 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 16 Jan 2026 14:29:33 +0000 (09:29 -0500)
Fixed the SQL compilation for the mariadb sequence "NOCYCLE" keyword that
is to be emitted when the :paramref:`.Sequence.cycle` parameter is set to
False on a :class:`.Sequence`.  Pull request courtesy Diego Dupin.

Fixes: #13073
Closes: #13074
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/13074
Pull-request-sha: ead18a04018db6d574a3bc4bd71f21c23256737c

Change-Id: Ie1640c969aaa64e41da334fe0eff21e0d12a8bf0

doc/build/changelog/unreleased_20/13073.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/mysql/mariadb.py
lib/sqlalchemy/sql/compiler.py
test/dialect/mysql/test_compiler.py
test/dialect/mysql/test_query.py

diff --git a/doc/build/changelog/unreleased_20/13073.rst b/doc/build/changelog/unreleased_20/13073.rst
new file mode 100644 (file)
index 0000000..2716b81
--- /dev/null
@@ -0,0 +1,7 @@
+.. change::
+    :tags: bug, mariadb
+    :tickets: 13070
+
+    Fixed the SQL compilation for the mariadb sequence "NOCYCLE" keyword that
+    is to be emitted when the :paramref:`.Sequence.cycle` parameter is set to
+    False on a :class:`.Sequence`.  Pull request courtesy Diego Dupin.
index c47705b7301427ad24bb7c648e2820e6770bfe91..de9d290f2db869155528d2d8729a777db6f4bca9 100644 (file)
@@ -1180,6 +1180,7 @@ if TYPE_CHECKING:
     from ...sql.functions import random
     from ...sql.functions import rollup
     from ...sql.functions import sysdate
+    from ...sql.schema import IdentityOptions
     from ...sql.schema import Sequence as Sequence_SchemaItem
     from ...sql.type_api import TypeEngine
     from ...sql.visitors import ExternallyTraversible
@@ -2364,6 +2365,15 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
             self.get_column_specification(create.element),
         )
 
+    def get_identity_options(self, identity_options: IdentityOptions) -> str:
+        """mariadb-specific sequence option; this will move to a
+        mariadb-specific module in 2.1
+
+        """
+        text = super().get_identity_options(identity_options)
+        text = text.replace("NO CYCLE", "NOCYCLE")
+        return text
+
 
 class MySQLTypeCompiler(compiler.GenericTypeCompiler):
     def _extend_numeric(self, type_: _NumericCommonType, spec: str) -> str:
index 8b66531131c902f372218f2a53ff6e61ac93f062..65e533a1bbb87cced64e73c64302a9c9ace56c3f 100644 (file)
@@ -8,7 +8,6 @@
 from __future__ import annotations
 
 from typing import Any
-from typing import Callable
 from typing import Optional
 from typing import TYPE_CHECKING
 
@@ -101,7 +100,7 @@ class MariaDBDialect(MySQLDialect):
         )
 
 
-def loader(driver: str) -> Callable[[], type[MariaDBDialect]]:
+def loader(driver: str) -> type[MariaDBDialect]:
     dialect_mod = __import__(
         "sqlalchemy.dialects.mysql.%s" % driver
     ).dialects.mysql
index 53030dcf9ec6e3a0190459275cca9f47d0ad2dbf..8a86fab9301b5d0c95b794c91c6cf0a3941cbd84 100644 (file)
@@ -121,6 +121,7 @@ if typing.TYPE_CHECKING:
     from .schema import Column
     from .schema import Constraint
     from .schema import ForeignKeyConstraint
+    from .schema import IdentityOptions
     from .schema import Index
     from .schema import PrimaryKeyConstraint
     from .schema import Table
@@ -7255,7 +7256,7 @@ class DDLCompiler(Compiled):
     def visit_drop_constraint_comment(self, drop, **kw):
         raise exc.UnsupportedCompilationError(self, type(drop))
 
-    def get_identity_options(self, identity_options):
+    def get_identity_options(self, identity_options: IdentityOptions) -> str:
         text = []
         if identity_options.increment is not None:
             text.append("INCREMENT BY %d" % identity_options.increment)
index b0f933364c18c61251985f704582c93a8a69cc8e..265d08c11150c48019dd37e96e8b5db8ec323f1e 100644 (file)
@@ -38,6 +38,7 @@ from sqlalchemy import NVARCHAR
 from sqlalchemy import PrimaryKeyConstraint
 from sqlalchemy import schema
 from sqlalchemy import select
+from sqlalchemy import Sequence
 from sqlalchemy import SmallInteger
 from sqlalchemy import sql
 from sqlalchemy import String
@@ -64,6 +65,7 @@ from sqlalchemy.sql import column
 from sqlalchemy.sql import delete
 from sqlalchemy.sql import table
 from sqlalchemy.sql import update
+from sqlalchemy.sql.ddl import CreateSequence
 from sqlalchemy.sql.expression import bindparam
 from sqlalchemy.sql.expression import literal_column
 from sqlalchemy.testing import assert_raises_message
@@ -1234,6 +1236,36 @@ class SQLTest(fixtures.TestBase, AssertsCompiledSQL, CacheKeyFixture):
             ")ENGINE=InnoDB",
         )
 
+    @testing.combinations(
+        (Sequence("foo_seq"), "CREATE SEQUENCE foo_seq"),
+        (Sequence("foo_seq", cycle=True), "CREATE SEQUENCE foo_seq CYCLE"),
+        (Sequence("foo_seq", cycle=False), "CREATE SEQUENCE foo_seq NOCYCLE"),
+        (
+            Sequence(
+                "foo_seq",
+                start=1,
+                increment=2,
+                nominvalue=True,
+                nomaxvalue=True,
+                cycle=False,
+                cache=100,
+            ),
+            (
+                "CREATE SEQUENCE foo_seq INCREMENT BY 2 START WITH 1 NO"
+                " MINVALUE NO MAXVALUE CACHE 100 NOCYCLE"
+            ),
+        ),
+        argnames="seq, expected",
+    )
+    @testing.variation("use_mariadb", [True, False])
+    def test_mariadb_sequence_behaviors(self, seq, expected, use_mariadb):
+        """test #13073"""
+        self.assert_compile(
+            CreateSequence(seq),
+            expected,
+            dialect="mariadb" if use_mariadb else "mysql",
+        )
+
     def test_create_table_with_partition(self):
         t1 = Table(
             "testtable",
index a27993d38972f3af1230d2db9a92f0f94dc6573e..06626b579323ea389586b25c44a0ee61d5fdd34d 100644 (file)
@@ -11,12 +11,14 @@ from sqlalchemy import exc
 from sqlalchemy import false
 from sqlalchemy import ForeignKey
 from sqlalchemy import func
+from sqlalchemy import inspect
 from sqlalchemy import Integer
 from sqlalchemy import literal_column
 from sqlalchemy import MetaData
 from sqlalchemy import or_
 from sqlalchemy import schema
 from sqlalchemy import select
+from sqlalchemy import Sequence
 from sqlalchemy import String
 from sqlalchemy import Table
 from sqlalchemy import testing
@@ -25,12 +27,17 @@ from sqlalchemy import true
 from sqlalchemy import update
 from sqlalchemy.dialects.mysql import limit
 from sqlalchemy.dialects.mysql import TIMESTAMP
+from sqlalchemy.sql.ddl import CreateSequence
+from sqlalchemy.sql.ddl import DropSequence
 from sqlalchemy.testing import assert_raises
 from sqlalchemy.testing import combinations
 from sqlalchemy.testing import eq_
+from sqlalchemy.testing import expect_raises_message
 from sqlalchemy.testing import expect_warnings
 from sqlalchemy.testing import fixtures
 from sqlalchemy.testing import is_
+from sqlalchemy.testing import is_false
+from sqlalchemy.testing import is_true
 from sqlalchemy.testing.assertsql import CompiledSQL
 from sqlalchemy.testing.fixtures import fixture_session
 
@@ -115,6 +122,68 @@ class ServerDefaultCreateTest(fixtures.TestBase):
         t.create(connection)
 
 
+class MariaDBSequenceTest(fixtures.TestBase):
+    __only_on__ = "mariadb"
+    __backend__ = True
+
+    __requires__ = ("sequences",)
+
+    @testing.fixture
+    def create_seq(self, connection):
+        seqs = set()
+
+        def go(seq):
+            seqs.add(seq)
+            connection.execute(CreateSequence(seq))
+
+        yield go
+
+        for seq in seqs:
+            connection.execute(DropSequence(seq, if_exists=True))
+
+    def test_has_sequence_and_exists_flag(self, connection, create_seq):
+        seq = Sequence("has_seq_test")
+        is_false(inspect(connection).has_sequence("has_seq_test"))
+
+        create_seq(seq)
+        is_true(inspect(connection).has_sequence("has_seq_test"))
+
+        connection.execute(CreateSequence(seq, if_not_exists=True))
+
+        connection.execute(DropSequence(seq))
+        is_false(inspect(connection).has_sequence("has_seq_test"))
+        connection.execute(DropSequence(seq, if_exists=True))
+
+    @testing.combinations(
+        (Sequence("foo_seq"), (1, 2, 3, 4, 5, 6, 7), False),
+        (
+            Sequence("foo_seq", maxvalue=3, cycle=True),
+            (1, 2, 3, 1, 2, 3, 1),
+            False,
+        ),
+        (Sequence("foo_seq", maxvalue=3, cycle=False), (1, 2, 3), True),
+        argnames="seq, expected, runout",
+    )
+    def test_sequence_roundtrip(
+        self, connection, create_seq, seq, expected, runout
+    ):
+        """tests related to #13073"""
+
+        create_seq(seq)
+
+        eq_(
+            [
+                connection.scalar(seq.next_value())
+                for i in range(len(expected))
+            ],
+            list(expected),
+        )
+
+        if runout:
+            with expect_raises_message(exc.DBAPIError, ".*has run out"):
+                connection.scalar(seq.next_value())
+
+
 class MatchTest(fixtures.TablesTest):
     __only_on__ = "mysql", "mariadb"
     __backend__ = True