--- /dev/null
+.. change::
+ :tags: bug, mysql, mariadb
+ :tickets: 7167
+
+ Reorganized the list of reserved words into two separate lists, one for
+ MySQL and one for MariaDB, so that these diverging sets of words can be
+ managed more accurately; adjusted the MySQL/MariaDB dialect to switch among
+ these lists based on either explicitly configured or
+ server-version-detected "MySQL" or "MariaDB" backend. Added all current
+ reserved words through MySQL 8 and current MariaDB versions including
+ recently added keywords like "lead" . Pull request courtesy Kevin Kirsche.
from .json import JSON
from .json import JSONIndexType
from .json import JSONPathType
+from .reserved_words import RESERVED_WORDS_MARIADB
+from .reserved_words import RESERVED_WORDS_MYSQL
from .types import _FloatType
from .types import _IntegerType
from .types import _MatchType
from ...types import VARBINARY
from ...util import topological
-
-RESERVED_WORDS = set(
- [
- "accessible",
- "action",
- "add",
- "admin",
- "all",
- "alter",
- "analyze",
- "and",
- "array", # 8.0
- "as",
- "asc",
- "asensitive",
- "before",
- "between",
- "bigint",
- "binary",
- "blob",
- "both",
- "by",
- "call",
- "cascade",
- "case",
- "change",
- "char",
- "character",
- "check",
- "collate",
- "column",
- "columns",
- "condition",
- "constraint",
- "continue",
- "convert",
- "create",
- "cross",
- "cube",
- "cume_dist",
- "current_date",
- "current_time",
- "current_timestamp",
- "current_user",
- "cursor",
- "database",
- "databases",
- "day_hour",
- "day_microsecond",
- "day_minute",
- "day_second",
- "dec",
- "decimal",
- "declare",
- "default",
- "delayed",
- "delete",
- "desc",
- "describe",
- "deterministic",
- "distinct",
- "distinctrow",
- "div",
- "double",
- "drop",
- "dual",
- "each",
- "else",
- "elseif",
- "empty",
- "enclosed",
- "escaped",
- "except",
- "exists",
- "exit",
- "explain",
- "false",
- "fetch",
- "fields",
- "first_value",
- "float",
- "float4",
- "float8",
- "for",
- "force",
- "foreign",
- "from",
- "fulltext",
- "function",
- "general",
- "generated",
- "get",
- "grant",
- "group",
- "grouping",
- "groups",
- "having",
- "high_priority",
- "hour_microsecond",
- "hour_minute",
- "hour_second",
- "if",
- "ignore",
- "ignore_server_ids",
- "in",
- "index",
- "infile",
- "inner",
- "inout",
- "insensitive",
- "insert",
- "int",
- "int1",
- "int2",
- "int3",
- "int4",
- "int8",
- "integer",
- "interval",
- "into",
- "io_after_gtids",
- "io_before_gtids",
- "is",
- "iterate",
- "join",
- "json_table",
- "key",
- "keys",
- "kill",
- "last_value",
- "lateral",
- "leading",
- "leave",
- "left",
- "level",
- "like",
- "limit",
- "linear",
- "linear",
- "lines",
- "load",
- "localtime",
- "localtimestamp",
- "lock",
- "long",
- "longblob",
- "longtext",
- "loop",
- "low_priority",
- "master_bind",
- "master_heartbeat_period",
- "master_ssl_verify_server_cert",
- "master_ssl_verify_server_cert",
- "match",
- "maxvalue",
- "mediumblob",
- "mediumint",
- "mediumtext",
- "member", # 8.0
- "middleint",
- "minute_microsecond",
- "minute_second",
- "mod",
- "mode",
- "modifies",
- "natural",
- "no_write_to_binlog",
- "not",
- "nth_value",
- "ntile",
- "null",
- "numeric",
- "of",
- "on",
- "one_shot",
- "optimize",
- "optimizer_costs",
- "option",
- "optionally",
- "or",
- "order",
- "out",
- "outer",
- "outfile",
- "over",
- "partition",
- "percent_rank",
- "persist",
- "persist_only",
- "precision",
- "primary",
- "privileges",
- "procedure",
- "purge",
- "range",
- "range",
- "rank",
- "read",
- "read_only",
- "read_only",
- "read_write",
- "read_write", # 5.1
- "reads",
- "real",
- "recursive",
- "references",
- "regexp",
- "release",
- "rename",
- "repeat",
- "replace",
- "require",
- "resignal",
- "restrict",
- "return",
- "revoke",
- "right",
- "rlike",
- "role",
- "row",
- "row_number",
- "rows",
- "schema",
- "schemas",
- "second_microsecond",
- "select",
- "sensitive",
- "separator",
- "set",
- "show",
- "signal",
- "slow", # 5.5
- "smallint",
- "soname",
- "spatial",
- "specific",
- "sql",
- "sql_after_gtids",
- "sql_before_gtids", # 5.6
- "sql_big_result",
- "sql_calc_found_rows",
- "sql_small_result",
- "sqlexception",
- "sqlstate",
- "sqlwarning",
- "ssl",
- "starting",
- "status",
- "stored",
- "straight_join",
- "system",
- "table",
- "tables", # 4.1
- "terminated",
- "text",
- "then",
- "time",
- "tinyblob",
- "tinyint",
- "tinytext",
- "to",
- "trailing",
- "trigger",
- "true",
- "undo",
- "union",
- "unique",
- "unlock",
- "unsigned",
- "update",
- "usage",
- "use",
- "using",
- "utc_date",
- "utc_time",
- "utc_timestamp",
- "values",
- "varbinary",
- "varchar",
- "varcharacter",
- "varying",
- "virtual", # 5.7
- "when",
- "where",
- "while",
- "window", # 8.0
- "with",
- "write",
- "x509",
- "xor",
- "year_month",
- "zerofill", # 5.0
- ]
-)
-
AUTOCOMMIT_RE = re.compile(
r"\s*(?:UPDATE|INSERT|CREATE|DELETE|DROP|ALTER|LOAD +DATA|REPLACE)",
re.I | re.UNICODE,
class MySQLIdentifierPreparer(compiler.IdentifierPreparer):
-
- reserved_words = RESERVED_WORDS
+ reserved_words = RESERVED_WORDS_MYSQL
def __init__(self, dialect, server_ansiquotes=False, **kw):
if not server_ansiquotes:
return tuple([self.quote_identifier(i) for i in ids if i is not None])
+class MariaDBIdentifierPreparer(MySQLIdentifierPreparer):
+ reserved_words = RESERVED_WORDS_MARIADB
+
+
@log.class_logger
class MySQLDialect(default.DefaultDialect):
"""Details of the MySQL dialect.
"MySQL version %s is not a MariaDB variant."
% (server_version_info,)
)
+ if is_mariadb:
+ self.preparer = MariaDBIdentifierPreparer
+ # this would have been set by the default dialect already,
+ # so set it again
+ self.identifier_preparer = self.preparer(self)
self.is_mariadb = is_mariadb
def do_begin_twophase(self, connection, xid):
+from .base import MariaDBIdentifierPreparer
from .base import MySQLDialect
is_mariadb = True
supports_statement_cache = True
name = "mariadb"
+ preparer = MariaDBIdentifierPreparer
def loader(driver):
from .base import MySQLCompiler
from .base import MySQLDialect
from .base import MySQLExecutionContext
-from .base import MySQLIdentifierPreparer
from ... import sql
from ... import util
pass
-class MySQLIdentifierPreparer_mariadbconnector(MySQLIdentifierPreparer):
- pass
-
-
class MySQLDialect_mariadbconnector(MySQLDialect):
driver = "mariadbconnector"
supports_statement_cache = True
default_paramstyle = "qmark"
execution_ctx_cls = MySQLExecutionContext_mariadbconnector
statement_compiler = MySQLCompiler_mariadbconnector
- preparer = MySQLIdentifierPreparer_mariadbconnector
supports_server_side_cursors = True
pass
-class MySQLIdentifierPreparer_mysqldb(MySQLIdentifierPreparer):
- pass
-
-
class MySQLDialect_mysqldb(MySQLDialect):
driver = "mysqldb"
supports_statement_cache = True
default_paramstyle = "format"
execution_ctx_cls = MySQLExecutionContext_mysqldb
statement_compiler = MySQLCompiler_mysqldb
- preparer = MySQLIdentifierPreparer_mysqldb
+ preparer = MySQLIdentifierPreparer
def __init__(self, **kwargs):
super(MySQLDialect_mysqldb, self).__init__(**kwargs)
--- /dev/null
+# mysql/reserved_words.py
+# Copyright (C) 2005-2021 the SQLAlchemy authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: https://www.opensource.org/licenses/mit-license.php
+
+# generated using:
+# https://gist.github.com/kkirsche/4f31f2153ed7a3248be1ec44ca6ddbc9
+#
+# https://mariadb.com/kb/en/reserved-words/
+# includes: Reserved Words
+# excludes: Exceptions, Oracle Mode, Function Names
+RESERVED_WORDS_MARIADB = {
+ "accessible",
+ "add",
+ "all",
+ "alter",
+ "analyze",
+ "and",
+ "as",
+ "asc",
+ "asensitive",
+ "before",
+ "between",
+ "bigint",
+ "binary",
+ "blob",
+ "both",
+ "by",
+ "call",
+ "cascade",
+ "case",
+ "change",
+ "char",
+ "character",
+ "check",
+ "collate",
+ "column",
+ "condition",
+ "constraint",
+ "continue",
+ "convert",
+ "create",
+ "cross",
+ "current_date",
+ "current_role",
+ "current_time",
+ "current_timestamp",
+ "current_user",
+ "cursor",
+ "database",
+ "databases",
+ "day_hour",
+ "day_microsecond",
+ "day_minute",
+ "day_second",
+ "dec",
+ "decimal",
+ "declare",
+ "default",
+ "delayed",
+ "delete",
+ "desc",
+ "describe",
+ "deterministic",
+ "distinct",
+ "distinctrow",
+ "div",
+ "do_domain_ids",
+ "double",
+ "drop",
+ "dual",
+ "each",
+ "else",
+ "elseif",
+ "enclosed",
+ "escaped",
+ "except",
+ "exists",
+ "exit",
+ "explain",
+ "false",
+ "fetch",
+ "float",
+ "float4",
+ "float8",
+ "for",
+ "force",
+ "foreign",
+ "from",
+ "fulltext",
+ "general",
+ "grant",
+ "group",
+ "having",
+ "high_priority",
+ "hour_microsecond",
+ "hour_minute",
+ "hour_second",
+ "if",
+ "ignore",
+ "ignore_domain_ids",
+ "ignore_server_ids",
+ "in",
+ "index",
+ "infile",
+ "inner",
+ "inout",
+ "insensitive",
+ "insert",
+ "int",
+ "int1",
+ "int2",
+ "int3",
+ "int4",
+ "int8",
+ "integer",
+ "intersect",
+ "interval",
+ "into",
+ "is",
+ "iterate",
+ "join",
+ "key",
+ "keys",
+ "kill",
+ "leading",
+ "leave",
+ "left",
+ "like",
+ "limit",
+ "linear",
+ "lines",
+ "load",
+ "localtime",
+ "localtimestamp",
+ "lock",
+ "long",
+ "longblob",
+ "longtext",
+ "loop",
+ "low_priority",
+ "master_heartbeat_period",
+ "master_ssl_verify_server_cert",
+ "match",
+ "maxvalue",
+ "mediumblob",
+ "mediumint",
+ "mediumtext",
+ "middleint",
+ "minute_microsecond",
+ "minute_second",
+ "mod",
+ "modifies",
+ "natural",
+ "no_write_to_binlog",
+ "not",
+ "null",
+ "numeric",
+ "offset",
+ "on",
+ "optimize",
+ "option",
+ "optionally",
+ "or",
+ "order",
+ "out",
+ "outer",
+ "outfile",
+ "over",
+ "page_checksum",
+ "parse_vcol_expr",
+ "partition",
+ "position",
+ "precision",
+ "primary",
+ "procedure",
+ "purge",
+ "range",
+ "read",
+ "read_write",
+ "reads",
+ "real",
+ "recursive",
+ "ref_system_id",
+ "references",
+ "regexp",
+ "release",
+ "rename",
+ "repeat",
+ "replace",
+ "require",
+ "resignal",
+ "restrict",
+ "return",
+ "returning",
+ "revoke",
+ "right",
+ "rlike",
+ "rows",
+ "schema",
+ "schemas",
+ "second_microsecond",
+ "select",
+ "sensitive",
+ "separator",
+ "set",
+ "show",
+ "signal",
+ "slow",
+ "smallint",
+ "spatial",
+ "specific",
+ "sql",
+ "sql_big_result",
+ "sql_calc_found_rows",
+ "sql_small_result",
+ "sqlexception",
+ "sqlstate",
+ "sqlwarning",
+ "ssl",
+ "starting",
+ "stats_auto_recalc",
+ "stats_persistent",
+ "stats_sample_pages",
+ "straight_join",
+ "table",
+ "terminated",
+ "then",
+ "tinyblob",
+ "tinyint",
+ "tinytext",
+ "to",
+ "trailing",
+ "trigger",
+ "true",
+ "undo",
+ "union",
+ "unique",
+ "unlock",
+ "unsigned",
+ "update",
+ "usage",
+ "use",
+ "using",
+ "utc_date",
+ "utc_time",
+ "utc_timestamp",
+ "values",
+ "varbinary",
+ "varchar",
+ "varcharacter",
+ "varying",
+ "when",
+ "where",
+ "while",
+ "window",
+ "with",
+ "write",
+ "xor",
+ "year_month",
+ "zerofill",
+}
+
+# https://dev.mysql.com/doc/refman/8.0/en/keywords.html
+# https://dev.mysql.com/doc/refman/5.7/en/keywords.html
+# https://dev.mysql.com/doc/refman/5.6/en/keywords.html
+# includes: MySQL x.0 Keywords and Reserved Words
+# excludes: MySQL x.0 New Keywords and Reserved Words,
+# MySQL x.0 Removed Keywords and Reserved Words
+RESERVED_WORDS_MYSQL = {
+ "accessible",
+ "add",
+ "admin",
+ "all",
+ "alter",
+ "analyze",
+ "and",
+ "array",
+ "as",
+ "asc",
+ "asensitive",
+ "before",
+ "between",
+ "bigint",
+ "binary",
+ "blob",
+ "both",
+ "by",
+ "call",
+ "cascade",
+ "case",
+ "change",
+ "char",
+ "character",
+ "check",
+ "collate",
+ "column",
+ "condition",
+ "constraint",
+ "continue",
+ "convert",
+ "create",
+ "cross",
+ "cube",
+ "cume_dist",
+ "current_date",
+ "current_time",
+ "current_timestamp",
+ "current_user",
+ "cursor",
+ "database",
+ "databases",
+ "day_hour",
+ "day_microsecond",
+ "day_minute",
+ "day_second",
+ "dec",
+ "decimal",
+ "declare",
+ "default",
+ "delayed",
+ "delete",
+ "dense_rank",
+ "desc",
+ "describe",
+ "deterministic",
+ "distinct",
+ "distinctrow",
+ "div",
+ "double",
+ "drop",
+ "dual",
+ "each",
+ "else",
+ "elseif",
+ "empty",
+ "enclosed",
+ "escaped",
+ "except",
+ "exists",
+ "exit",
+ "explain",
+ "false",
+ "fetch",
+ "first_value",
+ "float",
+ "float4",
+ "float8",
+ "for",
+ "force",
+ "foreign",
+ "from",
+ "fulltext",
+ "function",
+ "general",
+ "generated",
+ "get",
+ "get_master_public_key",
+ "grant",
+ "group",
+ "grouping",
+ "groups",
+ "having",
+ "high_priority",
+ "hour_microsecond",
+ "hour_minute",
+ "hour_second",
+ "if",
+ "ignore",
+ "ignore_server_ids",
+ "in",
+ "index",
+ "infile",
+ "inner",
+ "inout",
+ "insensitive",
+ "insert",
+ "int",
+ "int1",
+ "int2",
+ "int3",
+ "int4",
+ "int8",
+ "integer",
+ "interval",
+ "into",
+ "io_after_gtids",
+ "io_before_gtids",
+ "is",
+ "iterate",
+ "join",
+ "json_table",
+ "key",
+ "keys",
+ "kill",
+ "lag",
+ "last_value",
+ "lateral",
+ "lead",
+ "leading",
+ "leave",
+ "left",
+ "like",
+ "limit",
+ "linear",
+ "lines",
+ "load",
+ "localtime",
+ "localtimestamp",
+ "lock",
+ "long",
+ "longblob",
+ "longtext",
+ "loop",
+ "low_priority",
+ "master_bind",
+ "master_heartbeat_period",
+ "master_ssl_verify_server_cert",
+ "match",
+ "maxvalue",
+ "mediumblob",
+ "mediumint",
+ "mediumtext",
+ "member",
+ "middleint",
+ "minute_microsecond",
+ "minute_second",
+ "mod",
+ "modifies",
+ "natural",
+ "no_write_to_binlog",
+ "not",
+ "nth_value",
+ "ntile",
+ "null",
+ "numeric",
+ "of",
+ "on",
+ "optimize",
+ "optimizer_costs",
+ "option",
+ "optionally",
+ "or",
+ "order",
+ "out",
+ "outer",
+ "outfile",
+ "over",
+ "parse_gcol_expr",
+ "partition",
+ "percent_rank",
+ "persist",
+ "persist_only",
+ "precision",
+ "primary",
+ "procedure",
+ "purge",
+ "range",
+ "rank",
+ "read",
+ "read_write",
+ "reads",
+ "real",
+ "recursive",
+ "references",
+ "regexp",
+ "release",
+ "rename",
+ "repeat",
+ "replace",
+ "require",
+ "resignal",
+ "restrict",
+ "return",
+ "revoke",
+ "right",
+ "rlike",
+ "role",
+ "row",
+ "row_number",
+ "rows",
+ "schema",
+ "schemas",
+ "second_microsecond",
+ "select",
+ "sensitive",
+ "separator",
+ "set",
+ "show",
+ "signal",
+ "slow",
+ "smallint",
+ "spatial",
+ "specific",
+ "sql",
+ "sql_after_gtids",
+ "sql_before_gtids",
+ "sql_big_result",
+ "sql_calc_found_rows",
+ "sql_small_result",
+ "sqlexception",
+ "sqlstate",
+ "sqlwarning",
+ "ssl",
+ "starting",
+ "stored",
+ "straight_join",
+ "system",
+ "table",
+ "terminated",
+ "then",
+ "tinyblob",
+ "tinyint",
+ "tinytext",
+ "to",
+ "trailing",
+ "trigger",
+ "true",
+ "undo",
+ "union",
+ "unique",
+ "unlock",
+ "unsigned",
+ "update",
+ "usage",
+ "use",
+ "using",
+ "utc_date",
+ "utc_time",
+ "utc_timestamp",
+ "values",
+ "varbinary",
+ "varchar",
+ "varcharacter",
+ "varying",
+ "virtual",
+ "when",
+ "where",
+ "while",
+ "window",
+ "with",
+ "write",
+ "xor",
+ "year_month",
+ "zerofill",
+}
from sqlalchemy import CLOB
from sqlalchemy import Column
from sqlalchemy import Computed
+from sqlalchemy import create_engine
from sqlalchemy import DATE
from sqlalchemy import Date
from sqlalchemy import DATETIME
from sqlalchemy.testing import eq_
from sqlalchemy.testing import expect_warnings
from sqlalchemy.testing import fixtures
+from sqlalchemy.testing import mock
-class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
-
- __dialect__ = mysql.dialect()
-
- def test_reserved_words(self):
+class ReservedWordFixture(AssertsCompiledSQL):
+ @testing.fixture()
+ def mysql_mariadb_reserved_words(self):
table = Table(
- "mysql_table",
+ "rw_table",
MetaData(),
- Column("col1", Integer),
- Column("master_ssl_verify_server_cert", Integer),
+ Column("mysql_reserved", Integer),
+ Column("mdb_mysql_reserved", Integer),
+ Column("mdb_reserved", Integer),
)
- x = select(table.c.col1, table.c.master_ssl_verify_server_cert)
+ expected_mysql = (
+ "SELECT rw_table.`mysql_reserved`, "
+ "rw_table.`mdb_mysql_reserved`, "
+ "rw_table.mdb_reserved FROM rw_table"
+ )
+ expected_mdb = (
+ "SELECT rw_table.mysql_reserved, "
+ "rw_table.`mdb_mysql_reserved`, "
+ "rw_table.`mdb_reserved` FROM rw_table"
+ )
+
+ from sqlalchemy.dialects.mysql import reserved_words
+
+ reserved_words.RESERVED_WORDS_MARIADB.add("mdb_reserved")
+ reserved_words.RESERVED_WORDS_MYSQL.add("mysql_reserved")
+ reserved_words.RESERVED_WORDS_MYSQL.add("mdb_mysql_reserved")
+ reserved_words.RESERVED_WORDS_MARIADB.add("mdb_mysql_reserved")
+
+ try:
+ yield table, expected_mysql, expected_mdb
+ finally:
+
+ reserved_words.RESERVED_WORDS_MARIADB.discard("mdb_reserved")
+ reserved_words.RESERVED_WORDS_MYSQL.discard("mysql_reserved")
+ reserved_words.RESERVED_WORDS_MYSQL.discard("mdb_mysql_reserved")
+ reserved_words.RESERVED_WORDS_MARIADB.discard("mdb_mysql_reserved")
+
+
+class CompileTest(ReservedWordFixture, fixtures.TestBase, AssertsCompiledSQL):
+
+ __dialect__ = mysql.dialect()
+
+ @testing.combinations(
+ ("mariadb", True),
+ ("mysql", False),
+ (mysql.dialect(), False),
+ (mysql.dialect(is_mariadb=True), True),
+ (
+ create_engine(
+ "mysql+pymysql://", module=mock.Mock(paramstyle="format")
+ ).dialect,
+ False,
+ ),
+ (
+ create_engine(
+ "mariadb+pymysql://", module=mock.Mock(paramstyle="format")
+ ).dialect,
+ True,
+ ),
+ argnames="dialect, expect_mariadb",
+ )
+ def test_reserved_words_mysql_vs_mariadb(
+ self, dialect, expect_mariadb, mysql_mariadb_reserved_words
+ ):
+ """test #7167 - compiler level
+
+ We want to make sure that the "is mariadb" flag as well as the
+ correct identifier preparer are set up for dialects no matter how they
+ determine their "is_mariadb" flag.
+
+ """
+
+ table, expected_mysql, expected_mdb = mysql_mariadb_reserved_words
self.assert_compile(
- x,
- "SELECT mysql_table.col1, "
- "mysql_table.`master_ssl_verify_server_cert` FROM mysql_table",
+ select(table),
+ expected_mdb if expect_mariadb else expected_mysql,
+ dialect=dialect,
)
def test_create_index_simple(self):
from sqlalchemy import func
from sqlalchemy import Integer
from sqlalchemy import MetaData
+from sqlalchemy import select
from sqlalchemy import Table
from sqlalchemy import testing
from sqlalchemy.dialects import mysql
from sqlalchemy.testing import in_
from sqlalchemy.testing import is_
from sqlalchemy.testing import mock
+from sqlalchemy.testing.assertions import AssertsCompiledSQL
+from .test_compiler import ReservedWordFixture
from ...engine import test_deprecations
-class BackendDialectTest(fixtures.TestBase):
+class BackendDialectTest(
+ ReservedWordFixture, fixtures.TestBase, AssertsCompiledSQL
+):
__backend__ = True
__only_on__ = "mysql", "mariadb"
+ def test_reserved_words_mysql_vs_mariadb(
+ self, mysql_mariadb_reserved_words
+ ):
+ """test #7167 - real backend level
+
+ We want to make sure that the "is mariadb" flag as well as the
+ correct identifier preparer are set up for dialects no matter how they
+ determine their "is_mariadb" flag.
+
+ """
+
+ dialect = testing.db.dialect
+ expect_mariadb = testing.only_on("mariadb").enabled
+
+ table, expected_mysql, expected_mdb = mysql_mariadb_reserved_words
+ self.assert_compile(
+ select(table),
+ expected_mdb if expect_mariadb else expected_mysql,
+ dialect=dialect,
+ )
+
def test_no_show_variables(self):
from sqlalchemy.testing import mock