]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Support mariadb 10.2
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 19 Aug 2017 22:39:08 +0000 (18:39 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 29 Sep 2017 17:21:16 +0000 (13:21 -0400)
Fixed issue where CURRENT_TIMESTAMP would not reflect correctly
in the MariaDB 10.2 series due to a syntax change, where the function
is now represented as ``current_timestamp()``.

Fixes: #4096
MariaDB 10.2 now supports CHECK constraints (warning: use version 10.2.9
or greater due to upstream issues noted in :ticket:`4097`).  Reflection
now takes these CHECK constraints into account when they are present in
the ``SHOW CREATE TABLE`` output.

Fixes: #4098
Change-Id: I8666d61814e8145ca12cbecad94019b44af868e3
(cherry picked from commit 29b752f8b24909c9c715e1b2c5e01119d1f46aa7)

doc/build/changelog/unreleased_11/4096.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/mysql/reflection.py
test/dialect/mysql/test_reflection.py
test/engine/test_reflection.py
test/requirements.py
test/sql/test_types.py

diff --git a/doc/build/changelog/unreleased_11/4096.rst b/doc/build/changelog/unreleased_11/4096.rst
new file mode 100644 (file)
index 0000000..1ded9d5
--- /dev/null
@@ -0,0 +1,18 @@
+.. change:
+    :tags: bug, mysql
+    :tickets: 4096
+    :versions: 1.2.0b3
+
+    Fixed issue where CURRENT_TIMESTAMP would not reflect correctly
+    in the MariaDB 10.2 series due to a syntax change, where the function
+    is now represented as ``current_timestamp()``.
+
+.. change:
+    :tags: bug, mysql
+    :tickets: 4098
+    :versions: 1.2.0b3
+
+    MariaDB 10.2 now supports CHECK constraints (warning: use version 10.2.9
+    or greater due to upstream issues noted in :ticket:`4097`).  Reflection
+    now takes these CHECK constraints into account when they are present in
+    the ``SHOW CREATE TABLE`` output.
index 5e76960decf7e3df0f286895ee0f3aab8134f635..70d4cdf106d0a557328b242336d3249f64d69545 100644 (file)
@@ -1740,6 +1740,12 @@ class MySQLDialect(default.DefaultDialect):
     def _is_mariadb(self):
         return 'MariaDB' in self.server_version_info
 
+    @property
+    def _is_mariadb_102(self):
+        return self._is_mariadb and \
+            self._mariadb_normalized_version_info > (10, 2)
+
+    @property
     def _mariadb_normalized_version_info(self):
         if len(self.server_version_info) > 5:
             return self.server_version_info[3:]
@@ -1829,8 +1835,7 @@ class MySQLDialect(default.DefaultDialect):
 
         fkeys = []
 
-        for spec in parsed_state.constraints:
-            # only FOREIGN KEYs
+        for spec in parsed_state.fk_constraints:
             ref_name = spec['table'][-1]
             ref_schema = len(spec['table']) > 1 and \
                 spec['table'][-2] or schema
@@ -1861,6 +1866,24 @@ class MySQLDialect(default.DefaultDialect):
             fkeys.append(fkey_d)
         return fkeys
 
+    @reflection.cache
+    def get_check_constraints(
+            self, connection, table_name, schema=None, **kw):
+
+        parsed_state = self._parsed_state_or_create(
+            connection, table_name, schema, **kw)
+
+        return [
+            {"name": spec['name'], "sqltext": spec['sqltext']}
+            for spec in parsed_state.ck_constraints
+        ]
+
+    @reflection.cache
+    def get_table_comment(self, connection, table_name, schema=None, **kw):
+        parsed_state = self._parsed_state_or_create(
+            connection, table_name, schema, **kw)
+        return {"text": parsed_state.table_options.get('mysql_comment', None)}
+
     @reflection.cache
     def get_indexes(self, connection, table_name, schema=None, **kw):
 
index f5f09b80b38265d4a9cf95154b31063eb5b30ba7..5c2d22cd119cf35b31a0feaed5c8e6106a8fc614 100644 (file)
@@ -20,7 +20,8 @@ class ReflectedState(object):
         self.table_options = {}
         self.table_name = None
         self.keys = []
-        self.constraints = []
+        self.fk_constraints = []
+        self.ck_constraints = []
 
 
 @log.class_logger
@@ -56,8 +57,10 @@ class MySQLTableDefinitionParser(object):
                     util.warn("Unknown schema content: %r" % line)
                 elif type_ == 'key':
                     state.keys.append(spec)
-                elif type_ == 'constraint':
-                    state.constraints.append(spec)
+                elif type_ == 'fk_constraint':
+                    state.fk_constraints.append(spec)
+                elif type_ == 'ck_constraint':
+                    state.ck_constraints.append(spec)
                 else:
                     pass
         return state
@@ -76,8 +79,8 @@ class MySQLTableDefinitionParser(object):
             spec['columns'] = self._parse_keyexprs(spec['columns'])
             return 'key', spec
 
-        # CONSTRAINT
-        m = self._re_constraint.match(line)
+        # FOREIGN KEY CONSTRAINT
+        m = self._re_fk_constraint.match(line)
         if m:
             spec = m.groupdict()
             spec['table'] = \
@@ -86,7 +89,13 @@ class MySQLTableDefinitionParser(object):
                              for c in self._parse_keyexprs(spec['local'])]
             spec['foreign'] = [c[0]
                                for c in self._parse_keyexprs(spec['foreign'])]
-            return 'constraint', spec
+            return 'fk_constraint', spec
+
+        # CHECK constraint
+        m = self._re_ck_constraint.match(line)
+        if m:
+            spec = m.groupdict()
+            return 'ck_constraint', spec
 
         # PARTITION and SUBPARTITION
         m = self._re_partition.match(line)
@@ -314,26 +323,26 @@ class MySQLTableDefinitionParser(object):
         #  COLUMN_FORMAT (FIXED|DYNAMIC|DEFAULT)
         #  STORAGE (DISK|MEMORY)
         self._re_column = _re_compile(
-            r'  '
-            r'%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +'
-            r'(?P<coltype>\w+)'
-            r'(?:\((?P<arg>(?:\d+|\d+,\d+|'
-            r'(?:\x27(?:\x27\x27|[^\x27])*\x27,?)+))\))?'
-            r'(?: +(?P<unsigned>UNSIGNED))?'
-            r'(?: +(?P<zerofill>ZEROFILL))?'
-            r'(?: +CHARACTER SET +(?P<charset>[\w_]+))?'
-            r'(?: +COLLATE +(?P<collate>[\w_]+))?'
-            r'(?: +(?P<notnull>(?:NOT )?NULL))?'
-            r'(?: +DEFAULT +(?P<default>'
-            r'(?:NULL|\x27(?:\x27\x27|[^\x27])*\x27|\w+'
-            r'(?: +ON UPDATE \w+)?)'
-            r'))?'
-            r'(?: +(?P<autoincr>AUTO_INCREMENT))?'
-            r'(?: +COMMENT +(P<comment>(?:\x27\x27|[^\x27])+))?'
-            r'(?: +COLUMN_FORMAT +(?P<colfmt>\w+))?'
-            r'(?: +STORAGE +(?P<storage>\w+))?'
-            r'(?: +(?P<extra>.*))?'
-            r',?$'
+            r"  "
+            r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +"
+            r"(?P<coltype>\w+)"
+            r"(?:\((?P<arg>(?:\d+|\d+,\d+|"
+            r"(?:'(?:''|[^'])*',?)+))\))?"
+            r"(?: +(?P<unsigned>UNSIGNED))?"
+            r"(?: +(?P<zerofill>ZEROFILL))?"
+            r"(?: +CHARACTER SET +(?P<charset>[\w_]+))?"
+            r"(?: +COLLATE +(?P<collate>[\w_]+))?"
+            r"(?: +(?P<notnull>(?:NOT )?NULL))?"
+            r"(?: +DEFAULT +(?P<default>"
+            r"(?:NULL|'(?:''|[^'])*'|[\w\(\)]+"
+            r"(?: +ON UPDATE [\w\(\)]+)?)"
+            r"))?"
+            r"(?: +(?P<autoincr>AUTO_INCREMENT))?"
+            r"(?: +COMMENT +'(?P<comment>(?:''|[^'])*)')?"
+            r"(?: +COLUMN_FORMAT +(?P<colfmt>\w+))?"
+            r"(?: +STORAGE +(?P<storage>\w+))?"
+            r"(?: +(?P<extra>.*))?"
+            r",?$"
             % quotes
         )
 
@@ -372,7 +381,7 @@ class MySQLTableDefinitionParser(object):
         # unique constraints come back as KEYs
         kw = quotes.copy()
         kw['on'] = 'RESTRICT|CASCADE|SET NULL|NOACTION'
-        self._re_constraint = _re_compile(
+        self._re_fk_constraint = _re_compile(
             r'  '
             r'CONSTRAINT +'
             r'%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +'
@@ -387,6 +396,19 @@ class MySQLTableDefinitionParser(object):
             % kw
         )
 
+        # CONSTRAINT `CONSTRAINT_1` CHECK (`x` > 5)'
+        # testing on MariaDB 10.2 shows that the CHECK constraint
+        # is returned on a line by itself, so to match without worrying
+        # about parenthesis in the expresion we go to the end of the line
+        self._re_ck_constraint = _re_compile(
+            r'  '
+            r'CONSTRAINT +'
+            r'%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +'
+            r'CHECK +'
+            r'\((?P<sqltext>.+)\),?'
+            % kw
+        )
+
         # PARTITION
         #
         # punt!
index 9e122e680ff44dd64cfd6755960d31baf5f63eee..5caec72c97ffe232c3b706887062db6501ac412c 100644 (file)
@@ -12,6 +12,8 @@ from sqlalchemy.dialects.mysql import base as mysql
 from sqlalchemy.dialects.mysql import reflection as _reflection
 from sqlalchemy.testing import fixtures, AssertsExecutionResults
 from sqlalchemy import testing
+from sqlalchemy.testing import assert_raises_message, expect_warnings
+import re
 
 
 class TypeReflectionTest(fixtures.TestBase):
@@ -231,9 +233,9 @@ class ReflectionTest(fixtures.TestBase, AssertsExecutionResults):
         assert reflected.c.c5.default is None
         assert reflected.c.c5.server_default is None
         assert reflected.c.c6.default is None
-        eq_(
-            str(reflected.c.c6.server_default.arg).upper(),
-            "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
+        assert re.match(
+            r"CURRENT_TIMESTAMP(\(\))? ON UPDATE CURRENT_TIMESTAMP(\(\))?",
+            str(reflected.c.c6.server_default.arg).upper()
         )
         reflected.create()
         try:
@@ -249,9 +251,9 @@ class ReflectionTest(fixtures.TestBase, AssertsExecutionResults):
         assert reflected.c.c5.default is None
         assert reflected.c.c5.server_default is None
         assert reflected.c.c6.default is None
-        eq_(
-            str(reflected.c.c6.server_default.arg).upper(),
-            "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
+        assert re.match(
+            r"CURRENT_TIMESTAMP(\(\))? ON UPDATE CURRENT_TIMESTAMP(\(\))?",
+            str(reflected.c.c6.server_default.arg).upper()
         )
 
     def test_reflection_with_table_options(self):
@@ -478,6 +480,11 @@ class ReflectionTest(fixtures.TestBase, AssertsExecutionResults):
                 for d in inspect(testing.db).get_columns("nn_t%d" % idx)
             )
 
+        if testing.db.dialect._is_mariadb_102:
+            current_timestamp = "current_timestamp()"
+        else:
+            current_timestamp = "CURRENT_TIMESTAMP"
+
         eq_(
             reflected,
             [
@@ -486,15 +493,19 @@ class ReflectionTest(fixtures.TestBase, AssertsExecutionResults):
                 {'name': 'z', 'nullable': True, 'default': None},
                 {'name': 'q', 'nullable': True, 'default': None},
                 {'name': 'p', 'nullable': True,
-                 'default': 'CURRENT_TIMESTAMP'},
+                 'default': current_timestamp},
                 {'name': 'r', 'nullable': False,
-                 'default': "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"},
+                 'default':
+                 "%(current_timestamp)s ON UPDATE %(current_timestamp)s" %
+                 {"current_timestamp": current_timestamp}},
                 {'name': 's', 'nullable': False,
-                 'default': 'CURRENT_TIMESTAMP'},
+                 'default': current_timestamp},
                 {'name': 't', 'nullable': False,
-                 'default': "CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"},
+                 'default':
+                 "%(current_timestamp)s ON UPDATE %(current_timestamp)s" %
+                 {"current_timestamp": current_timestamp}},
                 {'name': 'u', 'nullable': False,
-                 'default': 'CURRENT_TIMESTAMP'},
+                 'default': current_timestamp},
             ]
         )
 
@@ -532,6 +543,8 @@ class ReflectionTest(fixtures.TestBase, AssertsExecutionResults):
 
 
 class RawReflectionTest(fixtures.TestBase):
+    __backend__ = True
+
     def setup(self):
         dialect = mysql.dialect()
         self.parser = _reflection.MySQLTableDefinitionParser(
@@ -568,7 +581,7 @@ class RawReflectionTest(fixtures.TestBase):
             "  KEY (`id`) USING BTREE COMMENT 'prefix''text''suffix'")
 
     def test_fk_reflection(self):
-        regex = self.parser._re_constraint
+        regex = self.parser._re_fk_constraint
 
         m = regex.match('  CONSTRAINT `addresses_user_id_fkey` '
                         'FOREIGN KEY (`user_id`) '
index e2a204e7b192659e59d60903f306d801ed3f861b..79c370b8ca3e5d33ede9ce578c4cae5a7d861f97 100644 (file)
@@ -7,7 +7,7 @@ from sqlalchemy.testing import (
     ComparesTables, engines, AssertsCompiledSQL,
     fixtures, skip)
 from sqlalchemy.testing.schema import Table, Column
-from sqlalchemy.testing import eq_, is_true, assert_raises, \
+from sqlalchemy.testing import eq_, eq_regex, is_true, assert_raises, \
     assert_raises_message
 from sqlalchemy import testing
 from sqlalchemy.util import ue
@@ -1027,7 +1027,7 @@ class ReflectionTest(fixtures.TestBase, ComparesTables):
             const for const in
             t2.constraints if isinstance(const, sa.CheckConstraint)][0]
 
-        eq_(ck.sqltext.text, "q > 10")
+        eq_regex(ck.sqltext.text, r".?q.? > 10")
         eq_(ck.name, "ck1")
 
     @testing.provide_metadata
index 58ba0bc18429dfdbbba8bad0b3da0daaf08e81e8..52fe37f9f933d5311f37b84d4c65d9e4e2abbdf0 100644 (file)
@@ -51,14 +51,8 @@ class DefaultRequirements(SuiteRequirements):
     def enforces_check_constraints(self):
         """Target database must also enforce check constraints."""
 
-        def mysql_not_mariadb_102(config):
-            return against(config, "mysql") and (
-                not config.db.dialect._is_mariadb or
-                config.db.dialect.server_version_info < (5, 5, 5, 10, 2)
-            )
-
         return self.check_constraints + fails_on(
-            mysql_not_mariadb_102,
+            self._mysql_not_mariadb_102,
             "check constraints don't enforce on MySQL, MariaDB<10.2"
         )
 
@@ -337,9 +331,8 @@ class DefaultRequirements(SuiteRequirements):
     @property
     def check_constraint_reflection(self):
         return fails_on_everything_except(
-                    "postgresql",
-                    "sqlite"
-                )
+            "postgresql", "sqlite", self._mariadb_102
+        )
 
     @property
     def temp_table_names(self):
@@ -930,6 +923,17 @@ class DefaultRequirements(SuiteRequirements):
 
         return only_if(check)
 
+    def _mariadb_102(self, config):
+        return against(config, "mysql") and \
+                config.db.dialect._is_mariadb and \
+                config.db.dialect._mariadb_normalized_version_info > (10, 2)
+
+    def _mysql_not_mariadb_102(self, config):
+        return against(config, "mysql") and (
+            not config.db.dialect._is_mariadb or
+            config.db.dialect._mariadb_normalized_version_info < (10, 2)
+        )
+
     def _has_mysql_on_windows(self, config):
         return against(config, 'mysql') and \
                 config.db.dialect._detect_casing(config.db) == 1
index 50d6ddf92342da6be87a0ad087d505d13e30d296..2509b8cf886ac8f7e37b6f8ed5b06fd101617773 100644 (file)
@@ -1367,7 +1367,12 @@ class EnumTest(AssertsCompiledSQL, fixtures.TablesTest):
     @testing.requires.enforces_check_constraints
     def test_check_constraint(self):
         assert_raises(
-            (exc.IntegrityError, exc.ProgrammingError),
+            (
+                exc.IntegrityError, exc.ProgrammingError,
+                exc.OperationalError,
+                # PyMySQL raising InternalError until
+                # https://github.com/PyMySQL/PyMySQL/issues/607 is resolved
+                exc.InternalError),
             testing.db.execute,
             "insert into non_native_enum_table "
             "(id, someenum) values(1, 'four')")