This commit is contained in:
@@ -1,65 +1,46 @@
|
||||
# dialects/mysql/reflection.py
|
||||
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
|
||||
# mysql/reflection.py
|
||||
# Copyright (C) 2005-2023 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
|
||||
from __future__ import annotations
|
||||
# mypy: ignore-errors
|
||||
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from .enumerated import ENUM
|
||||
from .enumerated import SET
|
||||
from .types import DATETIME
|
||||
from .types import TIME
|
||||
from .types import TIMESTAMP
|
||||
from ... import log
|
||||
from ... import types as sqltypes
|
||||
from ... import util
|
||||
from ...util.typing import Literal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .base import MySQLDialect
|
||||
from .base import MySQLIdentifierPreparer
|
||||
from ...engine.interfaces import ReflectedColumn
|
||||
|
||||
|
||||
class ReflectedState:
|
||||
"""Stores raw information about a SHOW CREATE TABLE statement."""
|
||||
|
||||
charset: Optional[str]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.columns: List[ReflectedColumn] = []
|
||||
self.table_options: Dict[str, str] = {}
|
||||
self.table_name: Optional[str] = None
|
||||
self.keys: List[Dict[str, Any]] = []
|
||||
self.fk_constraints: List[Dict[str, Any]] = []
|
||||
self.ck_constraints: List[Dict[str, Any]] = []
|
||||
def __init__(self):
|
||||
self.columns = []
|
||||
self.table_options = {}
|
||||
self.table_name = None
|
||||
self.keys = []
|
||||
self.fk_constraints = []
|
||||
self.ck_constraints = []
|
||||
|
||||
|
||||
@log.class_logger
|
||||
class MySQLTableDefinitionParser:
|
||||
"""Parses the results of a SHOW CREATE TABLE statement."""
|
||||
|
||||
def __init__(
|
||||
self, dialect: MySQLDialect, preparer: MySQLIdentifierPreparer
|
||||
):
|
||||
def __init__(self, dialect, preparer):
|
||||
self.dialect = dialect
|
||||
self.preparer = preparer
|
||||
self._prep_regexes()
|
||||
|
||||
def parse(
|
||||
self, show_create: str, charset: Optional[str]
|
||||
) -> ReflectedState:
|
||||
def parse(self, show_create, charset):
|
||||
state = ReflectedState()
|
||||
state.charset = charset
|
||||
for line in re.split(r"\r?\n", show_create):
|
||||
@@ -84,11 +65,11 @@ class MySQLTableDefinitionParser:
|
||||
if type_ is None:
|
||||
util.warn("Unknown schema content: %r" % line)
|
||||
elif type_ == "key":
|
||||
state.keys.append(spec) # type: ignore[arg-type]
|
||||
state.keys.append(spec)
|
||||
elif type_ == "fk_constraint":
|
||||
state.fk_constraints.append(spec) # type: ignore[arg-type]
|
||||
state.fk_constraints.append(spec)
|
||||
elif type_ == "ck_constraint":
|
||||
state.ck_constraints.append(spec) # type: ignore[arg-type]
|
||||
state.ck_constraints.append(spec)
|
||||
else:
|
||||
pass
|
||||
return state
|
||||
@@ -96,13 +77,7 @@ class MySQLTableDefinitionParser:
|
||||
def _check_view(self, sql: str) -> bool:
|
||||
return bool(self._re_is_view.match(sql))
|
||||
|
||||
def _parse_constraints(self, line: str) -> Union[
|
||||
Tuple[None, str],
|
||||
Tuple[Literal["partition"], str],
|
||||
Tuple[
|
||||
Literal["ck_constraint", "fk_constraint", "key"], Dict[str, str]
|
||||
],
|
||||
]:
|
||||
def _parse_constraints(self, line):
|
||||
"""Parse a KEY or CONSTRAINT line.
|
||||
|
||||
:param line: A line of SHOW CREATE TABLE output
|
||||
@@ -152,7 +127,7 @@ class MySQLTableDefinitionParser:
|
||||
# No match.
|
||||
return (None, line)
|
||||
|
||||
def _parse_table_name(self, line: str, state: ReflectedState) -> None:
|
||||
def _parse_table_name(self, line, state):
|
||||
"""Extract the table name.
|
||||
|
||||
:param line: The first line of SHOW CREATE TABLE
|
||||
@@ -163,7 +138,7 @@ class MySQLTableDefinitionParser:
|
||||
if m:
|
||||
state.table_name = cleanup(m.group("name"))
|
||||
|
||||
def _parse_table_options(self, line: str, state: ReflectedState) -> None:
|
||||
def _parse_table_options(self, line, state):
|
||||
"""Build a dictionary of all reflected table-level options.
|
||||
|
||||
:param line: The final line of SHOW CREATE TABLE output.
|
||||
@@ -189,9 +164,7 @@ class MySQLTableDefinitionParser:
|
||||
for opt, val in options.items():
|
||||
state.table_options["%s_%s" % (self.dialect.name, opt)] = val
|
||||
|
||||
def _parse_partition_options(
|
||||
self, line: str, state: ReflectedState
|
||||
) -> None:
|
||||
def _parse_partition_options(self, line, state):
|
||||
options = {}
|
||||
new_line = line[:]
|
||||
|
||||
@@ -247,7 +220,7 @@ class MySQLTableDefinitionParser:
|
||||
else:
|
||||
state.table_options["%s_%s" % (self.dialect.name, opt)] = val
|
||||
|
||||
def _parse_column(self, line: str, state: ReflectedState) -> None:
|
||||
def _parse_column(self, line, state):
|
||||
"""Extract column details.
|
||||
|
||||
Falls back to a 'minimal support' variant if full parse fails.
|
||||
@@ -310,16 +283,13 @@ class MySQLTableDefinitionParser:
|
||||
|
||||
type_instance = col_type(*type_args, **type_kw)
|
||||
|
||||
col_kw: Dict[str, Any] = {}
|
||||
col_kw = {}
|
||||
|
||||
# NOT NULL
|
||||
col_kw["nullable"] = True
|
||||
# this can be "NULL" in the case of TIMESTAMP
|
||||
if spec.get("notnull", False) == "NOT NULL":
|
||||
col_kw["nullable"] = False
|
||||
# For generated columns, the nullability is marked in a different place
|
||||
if spec.get("notnull_generated", False) == "NOT NULL":
|
||||
col_kw["nullable"] = False
|
||||
|
||||
# AUTO_INCREMENT
|
||||
if spec.get("autoincr", False):
|
||||
@@ -351,13 +321,9 @@ class MySQLTableDefinitionParser:
|
||||
name=name, type=type_instance, default=default, comment=comment
|
||||
)
|
||||
col_d.update(col_kw)
|
||||
state.columns.append(col_d) # type: ignore[arg-type]
|
||||
state.columns.append(col_d)
|
||||
|
||||
def _describe_to_create(
|
||||
self,
|
||||
table_name: str,
|
||||
columns: Sequence[Tuple[str, str, str, str, str, str]],
|
||||
) -> str:
|
||||
def _describe_to_create(self, table_name, columns):
|
||||
"""Re-format DESCRIBE output as a SHOW CREATE TABLE string.
|
||||
|
||||
DESCRIBE is a much simpler reflection and is sufficient for
|
||||
@@ -410,9 +376,7 @@ class MySQLTableDefinitionParser:
|
||||
]
|
||||
)
|
||||
|
||||
def _parse_keyexprs(
|
||||
self, identifiers: str
|
||||
) -> List[Tuple[str, Optional[int], str]]:
|
||||
def _parse_keyexprs(self, identifiers):
|
||||
"""Unpack '"col"(2),"col" ASC'-ish strings into components."""
|
||||
|
||||
return [
|
||||
@@ -422,12 +386,11 @@ class MySQLTableDefinitionParser:
|
||||
)
|
||||
]
|
||||
|
||||
def _prep_regexes(self) -> None:
|
||||
def _prep_regexes(self):
|
||||
"""Pre-compile regular expressions."""
|
||||
|
||||
self._pr_options: List[
|
||||
Tuple[re.Pattern[Any], Optional[Callable[[str], str]]]
|
||||
] = []
|
||||
self._re_columns = []
|
||||
self._pr_options = []
|
||||
|
||||
_final = self.preparer.final_quote
|
||||
|
||||
@@ -485,13 +448,11 @@ class MySQLTableDefinitionParser:
|
||||
r"(?: +COLLATE +(?P<collate>[\w_]+))?"
|
||||
r"(?: +(?P<notnull>(?:NOT )?NULL))?"
|
||||
r"(?: +DEFAULT +(?P<default>"
|
||||
r"(?:NULL|'(?:''|[^'])*'|\(.+?\)|[\-\w\.\(\)]+"
|
||||
r"(?:NULL|'(?:''|[^'])*'|[\-\w\.\(\)]+"
|
||||
r"(?: +ON UPDATE [\-\w\.\(\)]+)?)"
|
||||
r"))?"
|
||||
r"(?: +(?:GENERATED ALWAYS)? ?AS +(?P<generated>\("
|
||||
r".*\))? ?(?P<persistence>VIRTUAL|STORED)?"
|
||||
r"(?: +(?P<notnull_generated>(?:NOT )?NULL))?"
|
||||
r")?"
|
||||
r".*\))? ?(?P<persistence>VIRTUAL|STORED)?)?"
|
||||
r"(?: +(?P<autoincr>AUTO_INCREMENT))?"
|
||||
r"(?: +COMMENT +'(?P<comment>(?:''|[^'])*)')?"
|
||||
r"(?: +COLUMN_FORMAT +(?P<colfmt>\w+))?"
|
||||
@@ -539,7 +500,7 @@ class MySQLTableDefinitionParser:
|
||||
#
|
||||
# unique constraints come back as KEYs
|
||||
kw = quotes.copy()
|
||||
kw["on"] = "RESTRICT|CASCADE|SET NULL|NO ACTION|SET DEFAULT"
|
||||
kw["on"] = "RESTRICT|CASCADE|SET NULL|NO ACTION"
|
||||
self._re_fk_constraint = _re_compile(
|
||||
r" "
|
||||
r"CONSTRAINT +"
|
||||
@@ -616,21 +577,21 @@ class MySQLTableDefinitionParser:
|
||||
|
||||
_optional_equals = r"(?:\s*(?:=\s*)|\s+)"
|
||||
|
||||
def _add_option_string(self, directive: str) -> None:
|
||||
def _add_option_string(self, directive):
|
||||
regex = r"(?P<directive>%s)%s" r"'(?P<val>(?:[^']|'')*?)'(?!')" % (
|
||||
re.escape(directive),
|
||||
self._optional_equals,
|
||||
)
|
||||
self._pr_options.append(_pr_compile(regex, cleanup_text))
|
||||
|
||||
def _add_option_word(self, directive: str) -> None:
|
||||
def _add_option_word(self, directive):
|
||||
regex = r"(?P<directive>%s)%s" r"(?P<val>\w+)" % (
|
||||
re.escape(directive),
|
||||
self._optional_equals,
|
||||
)
|
||||
self._pr_options.append(_pr_compile(regex))
|
||||
|
||||
def _add_partition_option_word(self, directive: str) -> None:
|
||||
def _add_partition_option_word(self, directive):
|
||||
if directive == "PARTITION BY" or directive == "SUBPARTITION BY":
|
||||
regex = r"(?<!\S)(?P<directive>%s)%s" r"(?P<val>\w+.*)" % (
|
||||
re.escape(directive),
|
||||
@@ -645,7 +606,7 @@ class MySQLTableDefinitionParser:
|
||||
regex = r"(?<!\S)(?P<directive>%s)(?!\S)" % (re.escape(directive),)
|
||||
self._pr_options.append(_pr_compile(regex))
|
||||
|
||||
def _add_option_regex(self, directive: str, regex: str) -> None:
|
||||
def _add_option_regex(self, directive, regex):
|
||||
regex = r"(?P<directive>%s)%s" r"(?P<val>%s)" % (
|
||||
re.escape(directive),
|
||||
self._optional_equals,
|
||||
@@ -663,35 +624,21 @@ _options_of_type_string = (
|
||||
)
|
||||
|
||||
|
||||
@overload
|
||||
def _pr_compile(
|
||||
regex: str, cleanup: Callable[[str], str]
|
||||
) -> Tuple[re.Pattern[Any], Callable[[str], str]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def _pr_compile(
|
||||
regex: str, cleanup: None = None
|
||||
) -> Tuple[re.Pattern[Any], None]: ...
|
||||
|
||||
|
||||
def _pr_compile(
|
||||
regex: str, cleanup: Optional[Callable[[str], str]] = None
|
||||
) -> Tuple[re.Pattern[Any], Optional[Callable[[str], str]]]:
|
||||
def _pr_compile(regex, cleanup=None):
|
||||
"""Prepare a 2-tuple of compiled regex and callable."""
|
||||
|
||||
return (_re_compile(regex), cleanup)
|
||||
|
||||
|
||||
def _re_compile(regex: str) -> re.Pattern[Any]:
|
||||
def _re_compile(regex):
|
||||
"""Compile a string to regex, I and UNICODE."""
|
||||
|
||||
return re.compile(regex, re.I | re.UNICODE)
|
||||
|
||||
|
||||
def _strip_values(values: Sequence[str]) -> List[str]:
|
||||
def _strip_values(values):
|
||||
"Strip reflected values quotes"
|
||||
strip_values: List[str] = []
|
||||
strip_values = []
|
||||
for a in values:
|
||||
if a[0:1] == '"' or a[0:1] == "'":
|
||||
# strip enclosing quotes and unquote interior
|
||||
@@ -703,9 +650,7 @@ def _strip_values(values: Sequence[str]) -> List[str]:
|
||||
def cleanup_text(raw_text: str) -> str:
|
||||
if "\\" in raw_text:
|
||||
raw_text = re.sub(
|
||||
_control_char_regexp,
|
||||
lambda s: _control_char_map[s[0]], # type: ignore[index]
|
||||
raw_text,
|
||||
_control_char_regexp, lambda s: _control_char_map[s[0]], raw_text
|
||||
)
|
||||
return raw_text.replace("''", "'")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user