Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
reflection.py678 linesDownload Raw Back to mysql
1# dialects/mysql/reflection.py
2# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7# mypy: ignore-errors
8
9
10import re
11
12from .enumerated import ENUM
13from .enumerated import SET
14from .types import DATETIME
15from .types import TIME
16from .types import TIMESTAMP
17from ... import log
18from ... import types as sqltypes
19from ... import util
20
21
22class ReflectedState:
23    """Stores raw information about a SHOW CREATE TABLE statement."""
24
25    def __init__(self):
26        self.columns = []
27        self.table_options = {}
28        self.table_name = None
29        self.keys = []
30        self.fk_constraints = []
31        self.ck_constraints = []
32
33
34@log.class_logger
35class MySQLTableDefinitionParser:
36    """Parses the results of a SHOW CREATE TABLE statement."""
37
38    def __init__(self, dialect, preparer):
39        self.dialect = dialect
40        self.preparer = preparer
41        self._prep_regexes()
42
43    def parse(self, show_create, charset):
44        state = ReflectedState()
45        state.charset = charset
46        for line in re.split(r"\r?\n", show_create):
47            if line.startswith("  " + self.preparer.initial_quote):
48                self._parse_column(line, state)
49            # a regular table options line
50            elif line.startswith(") "):
51                self._parse_table_options(line, state)
52            # an ANSI-mode table options line
53            elif line == ")":
54                pass
55            elif line.startswith("CREATE "):
56                self._parse_table_name(line, state)
57            elif "PARTITION" in line:
58                self._parse_partition_options(line, state)
59            # Not present in real reflection, but may be if
60            # loading from a file.
61            elif not line:
62                pass
63            else:
64                type_, spec = self._parse_constraints(line)
65                if type_ is None:
66                    util.warn("Unknown schema content: %r" % line)
67                elif type_ == "key":
68                    state.keys.append(spec)
69                elif type_ == "fk_constraint":
70                    state.fk_constraints.append(spec)
71                elif type_ == "ck_constraint":
72                    state.ck_constraints.append(spec)
73                else:
74                    pass
75        return state
76
77    def _check_view(self, sql: str) -> bool:
78        return bool(self._re_is_view.match(sql))
79
80    def _parse_constraints(self, line):
81        """Parse a KEY or CONSTRAINT line.
82
83        :param line: A line of SHOW CREATE TABLE output
84        """
85
86        # KEY
87        m = self._re_key.match(line)
88        if m:
89            spec = m.groupdict()
90            # convert columns into name, length pairs
91            # NOTE: we may want to consider SHOW INDEX as the
92            # format of indexes in MySQL becomes more complex
93            spec["columns"] = self._parse_keyexprs(spec["columns"])
94            if spec["version_sql"]:
95                m2 = self._re_key_version_sql.match(spec["version_sql"])
96                if m2 and m2.groupdict()["parser"]:
97                    spec["parser"] = m2.groupdict()["parser"]
98            if spec["parser"]:
99                spec["parser"] = self.preparer.unformat_identifiers(
100                    spec["parser"]
101                )[0]
102            return "key", spec
103
104        # FOREIGN KEY CONSTRAINT
105        m = self._re_fk_constraint.match(line)
106        if m:
107            spec = m.groupdict()
108            spec["table"] = self.preparer.unformat_identifiers(spec["table"])
109            spec["local"] = [c[0] for c in self._parse_keyexprs(spec["local"])]
110            spec["foreign"] = [
111                c[0] for c in self._parse_keyexprs(spec["foreign"])
112            ]
113            return "fk_constraint", spec
114
115        # CHECK constraint
116        m = self._re_ck_constraint.match(line)
117        if m:
118            spec = m.groupdict()
119            return "ck_constraint", spec
120
121        # PARTITION and SUBPARTITION
122        m = self._re_partition.match(line)
123        if m:
124            # Punt!
125            return "partition", line
126
127        # No match.
128        return (None, line)
129
130    def _parse_table_name(self, line, state):
131        """Extract the table name.
132
133        :param line: The first line of SHOW CREATE TABLE
134        """
135
136        regex, cleanup = self._pr_name
137        m = regex.match(line)
138        if m:
139            state.table_name = cleanup(m.group("name"))
140
141    def _parse_table_options(self, line, state):
142        """Build a dictionary of all reflected table-level options.
143
144        :param line: The final line of SHOW CREATE TABLE output.
145        """
146
147        options = {}
148
149        if line and line != ")":
150            rest_of_line = line
151            for regex, cleanup in self._pr_options:
152                m = regex.search(rest_of_line)
153                if not m:
154                    continue
155                directive, value = m.group("directive"), m.group("val")
156                if cleanup:
157                    value = cleanup(value)
158                options[directive.lower()] = value
159                rest_of_line = regex.sub("", rest_of_line)
160
161        for nope in ("auto_increment", "data directory", "index directory"):
162            options.pop(nope, None)
163
164        for opt, val in options.items():
165            state.table_options["%s_%s" % (self.dialect.name, opt)] = val
166
167    def _parse_partition_options(self, line, state):
168        options = {}
169        new_line = line[:]
170
171        while new_line.startswith("(") or new_line.startswith(" "):
172            new_line = new_line[1:]
173
174        for regex, cleanup in self._pr_options:
175            m = regex.search(new_line)
176            if not m or "PARTITION" not in regex.pattern:
177                continue
178
179            directive = m.group("directive")
180            directive = directive.lower()
181            is_subpartition = directive == "subpartition"
182
183            if directive == "partition" or is_subpartition:
184                new_line = new_line.replace(") */", "")
185                new_line = new_line.replace(",", "")
186                if is_subpartition and new_line.endswith(")"):
187                    new_line = new_line[:-1]
188                if self.dialect.name == "mariadb" and new_line.endswith(")"):
189                    if (
190                        "MAXVALUE" in new_line
191                        or "MINVALUE" in new_line
192                        or "ENGINE" in new_line
193                    ):
194                        # final line of MariaDB partition endswith ")"
195                        new_line = new_line[:-1]
196
197                defs = "%s_%s_definitions" % (self.dialect.name, directive)
198                options[defs] = new_line
199
200            else:
201                directive = directive.replace(" ", "_")
202                value = m.group("val")
203                if cleanup:
204                    value = cleanup(value)
205                options[directive] = value
206            break
207
208        for opt, val in options.items():
209            part_def = "%s_partition_definitions" % (self.dialect.name)
210            subpart_def = "%s_subpartition_definitions" % (self.dialect.name)
211            if opt == part_def or opt == subpart_def:
212                # builds a string of definitions
213                if opt not in state.table_options:
214                    state.table_options[opt] = val
215                else:
216                    state.table_options[opt] = "%s, %s" % (
217                        state.table_options[opt],
218                        val,
219                    )
220            else:
221                state.table_options["%s_%s" % (self.dialect.name, opt)] = val
222
223    def _parse_column(self, line, state):
224        """Extract column details.
225
226        Falls back to a 'minimal support' variant if full parse fails.
227
228        :param line: Any column-bearing line from SHOW CREATE TABLE
229        """
230
231        spec = None
232        m = self._re_column.match(line)
233        if m:
234            spec = m.groupdict()
235            spec["full"] = True
236        else:
237            m = self._re_column_loose.match(line)
238            if m:
239                spec = m.groupdict()
240                spec["full"] = False
241        if not spec:
242            util.warn("Unknown column definition %r" % line)
243            return
244        if not spec["full"]:
245            util.warn("Incomplete reflection of column definition %r" % line)
246
247        name, type_, args = spec["name"], spec["coltype"], spec["arg"]
248
249        try:
250            col_type = self.dialect.ischema_names[type_]
251        except KeyError:
252            util.warn(
253                "Did not recognize type '%s' of column '%s'" % (type_, name)
254            )
255            col_type = sqltypes.NullType
256
257        # Column type positional arguments eg. varchar(32)
258        if args is None or args == "":
259            type_args = []
260        elif args[0] == "'" and args[-1] == "'":
261            type_args = self._re_csv_str.findall(args)
262        else:
263            type_args = [int(v) for v in self._re_csv_int.findall(args)]
264
265        # Column type keyword options
266        type_kw = {}
267
268        if issubclass(col_type, (DATETIME, TIME, TIMESTAMP)):
269            if type_args:
270                type_kw["fsp"] = type_args.pop(0)
271
272        for kw in ("unsigned", "zerofill"):
273            if spec.get(kw, False):
274                type_kw[kw] = True
275        for kw in ("charset", "collate"):
276            if spec.get(kw, False):
277                type_kw[kw] = spec[kw]
278        if issubclass(col_type, (ENUM, SET)):
279            type_args = _strip_values(type_args)
280
281            if issubclass(col_type, SET) and "" in type_args:
282                type_kw["retrieve_as_bitwise"] = True
283
284        type_instance = col_type(*type_args, **type_kw)
285
286        col_kw = {}
287
288        # NOT NULL
289        col_kw["nullable"] = True
290        # this can be "NULL" in the case of TIMESTAMP
291        if spec.get("notnull", False) == "NOT NULL":
292            col_kw["nullable"] = False
293        # For generated columns, the nullability is marked in a different place
294        if spec.get("notnull_generated", False) == "NOT NULL":
295            col_kw["nullable"] = False
296
297        # AUTO_INCREMENT
298        if spec.get("autoincr", False):
299            col_kw["autoincrement"] = True
300        elif issubclass(col_type, sqltypes.Integer):
301            col_kw["autoincrement"] = False
302
303        # DEFAULT
304        default = spec.get("default", None)
305
306        if default == "NULL":
307            # eliminates the need to deal with this later.
308            default = None
309
310        comment = spec.get("comment", None)
311
312        if comment is not None:
313            comment = cleanup_text(comment)
314
315        sqltext = spec.get("generated")
316        if sqltext is not None:
317            computed = dict(sqltext=sqltext)
318            persisted = spec.get("persistence")
319            if persisted is not None:
320                computed["persisted"] = persisted == "STORED"
321            col_kw["computed"] = computed
322
323        col_d = dict(
324            name=name, type=type_instance, default=default, comment=comment
325        )
326        col_d.update(col_kw)
327        state.columns.append(col_d)
328
329    def _describe_to_create(self, table_name, columns):
330        """Re-format DESCRIBE output as a SHOW CREATE TABLE string.
331
332        DESCRIBE is a much simpler reflection and is sufficient for
333        reflecting views for runtime use.  This method formats DDL
334        for columns only- keys are omitted.
335
336        :param columns: A sequence of DESCRIBE or SHOW COLUMNS 6-tuples.
337          SHOW FULL COLUMNS FROM rows must be rearranged for use with
338          this function.
339        """
340
341        buffer = []
342        for row in columns:
343            (name, col_type, nullable, default, extra) = (
344                row[i] for i in (0, 1, 2, 4, 5)
345            )
346
347            line = [" "]
348            line.append(self.preparer.quote_identifier(name))
349            line.append(col_type)
350            if not nullable:
351                line.append("NOT NULL")
352            if default:
353                if "auto_increment" in default:
354                    pass
355                elif col_type.startswith("timestamp") and default.startswith(
356                    "C"
357                ):
358                    line.append("DEFAULT")
359                    line.append(default)
360                elif default == "NULL":
361                    line.append("DEFAULT")
362                    line.append(default)
363                else:
364                    line.append("DEFAULT")
365                    line.append("'%s'" % default.replace("'", "''"))
366            if extra:
367                line.append(extra)
368
369            buffer.append(" ".join(line))
370
371        return "".join(
372            [
373                (
374                    "CREATE TABLE %s (\n"
375                    % self.preparer.quote_identifier(table_name)
376                ),
377                ",\n".join(buffer),
378                "\n) ",
379            ]
380        )
381
382    def _parse_keyexprs(self, identifiers):
383        """Unpack '"col"(2),"col" ASC'-ish strings into components."""
384
385        return [
386            (colname, int(length) if length else None, modifiers)
387            for colname, length, modifiers in self._re_keyexprs.findall(
388                identifiers
389            )
390        ]
391
392    def _prep_regexes(self):
393        """Pre-compile regular expressions."""
394
395        self._re_columns = []
396        self._pr_options = []
397
398        _final = self.preparer.final_quote
399
400        quotes = dict(
401            zip(
402                ("iq", "fq", "esc_fq"),
403                [
404                    re.escape(s)
405                    for s in (
406                        self.preparer.initial_quote,
407                        _final,
408                        self.preparer._escape_identifier(_final),
409                    )
410                ],
411            )
412        )
413
414        self._pr_name = _pr_compile(
415            r"^CREATE (?:\w+ +)?TABLE +"
416            r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +\($" % quotes,
417            self.preparer._unescape_identifier,
418        )
419
420        self._re_is_view = _re_compile(r"^CREATE(?! TABLE)(\s.*)?\sVIEW")
421
422        # `col`,`col2`(32),`col3`(15) DESC
423        #
424        self._re_keyexprs = _re_compile(
425            r"(?:"
426            r"(?:%(iq)s((?:%(esc_fq)s|[^%(fq)s])+)%(fq)s)"
427            r"(?:\((\d+)\))?(?: +(ASC|DESC))?(?=\,|$))+" % quotes
428        )
429
430        # 'foo' or 'foo','bar' or 'fo,o','ba''a''r'
431        self._re_csv_str = _re_compile(r"\x27(?:\x27\x27|[^\x27])*\x27")
432
433        # 123 or 123,456
434        self._re_csv_int = _re_compile(r"\d+")
435
436        # `colname` <type> [type opts]
437        #  (NOT NULL | NULL)
438        #   DEFAULT ('value' | CURRENT_TIMESTAMP...)
439        #   COMMENT 'comment'
440        #  COLUMN_FORMAT (FIXED|DYNAMIC|DEFAULT)
441        #  STORAGE (DISK|MEMORY)
442        self._re_column = _re_compile(
443            r"  "
444            r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +"
445            r"(?P<coltype>\w+)"
446            r"(?:\((?P<arg>(?:\d+|\d+,\d+|"
447            r"(?:'(?:''|[^'])*',?)+))\))?"
448            r"(?: +(?P<unsigned>UNSIGNED))?"
449            r"(?: +(?P<zerofill>ZEROFILL))?"
450            r"(?: +CHARACTER SET +(?P<charset>[\w_]+))?"
451            r"(?: +COLLATE +(?P<collate>[\w_]+))?"
452            r"(?: +(?P<notnull>(?:NOT )?NULL))?"
453            r"(?: +DEFAULT +(?P<default>"
454            r"(?:NULL|'(?:''|[^'])*'|[\-\w\.\(\)]+"
455            r"(?: +ON UPDATE [\-\w\.\(\)]+)?)"
456            r"))?"
457            r"(?: +(?:GENERATED ALWAYS)? ?AS +(?P<generated>\("
458            r".*\))? ?(?P<persistence>VIRTUAL|STORED)?"
459            r"(?: +(?P<notnull_generated>(?:NOT )?NULL))?"
460            r")?"
461            r"(?: +(?P<autoincr>AUTO_INCREMENT))?"
462            r"(?: +COMMENT +'(?P<comment>(?:''|[^'])*)')?"
463            r"(?: +COLUMN_FORMAT +(?P<colfmt>\w+))?"
464            r"(?: +STORAGE +(?P<storage>\w+))?"
465            r"(?: +(?P<extra>.*))?"
466            r",?$" % quotes
467        )
468
469        # Fallback, try to parse as little as possible
470        self._re_column_loose = _re_compile(
471            r"  "
472            r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +"
473            r"(?P<coltype>\w+)"
474            r"(?:\((?P<arg>(?:\d+|\d+,\d+|\x27(?:\x27\x27|[^\x27])+\x27))\))?"
475            r".*?(?P<notnull>(?:NOT )NULL)?" % quotes
476        )
477
478        # (PRIMARY|UNIQUE|FULLTEXT|SPATIAL) INDEX `name` (USING (BTREE|HASH))?
479        # (`col` (ASC|DESC)?, `col` (ASC|DESC)?)
480        # KEY_BLOCK_SIZE size | WITH PARSER name  /*!50100 WITH PARSER name */
481        self._re_key = _re_compile(
482            r"  "
483            r"(?:(?P<type>\S+) )?KEY"
484            r"(?: +%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s)?"
485            r"(?: +USING +(?P<using_pre>\S+))?"
486            r" +\((?P<columns>.+?)\)"
487            r"(?: +USING +(?P<using_post>\S+))?"
488            r"(?: +KEY_BLOCK_SIZE *[ =]? *(?P<keyblock>\S+))?"
489            r"(?: +WITH PARSER +(?P<parser>\S+))?"
490            r"(?: +COMMENT +(?P<comment>(\x27\x27|\x27([^\x27])*?\x27)+))?"
491            r"(?: +/\*(?P<version_sql>.+)\*/ *)?"
492            r",?$" % quotes
493        )
494
495        # https://forums.mysql.com/read.php?20,567102,567111#msg-567111
496        # It means if the MySQL version >= \d+, execute what's in the comment
497        self._re_key_version_sql = _re_compile(
498            r"\!\d+ " r"(?: *WITH PARSER +(?P<parser>\S+) *)?"
499        )
500
501        # CONSTRAINT `name` FOREIGN KEY (`local_col`)
502        # REFERENCES `remote` (`remote_col`)
503        # MATCH FULL | MATCH PARTIAL | MATCH SIMPLE
504        # ON DELETE CASCADE ON UPDATE RESTRICT
505        #
506        # unique constraints come back as KEYs
507        kw = quotes.copy()
508        kw["on"] = "RESTRICT|CASCADE|SET NULL|NO ACTION"
509        self._re_fk_constraint = _re_compile(
510            r"  "
511            r"CONSTRAINT +"
512            r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +"
513            r"FOREIGN KEY +"
514            r"\((?P<local>[^\)]+?)\) REFERENCES +"
515            r"(?P<table>%(iq)s[^%(fq)s]+%(fq)s"
516            r"(?:\.%(iq)s[^%(fq)s]+%(fq)s)?) +"
517            r"\((?P<foreign>(?:%(iq)s[^%(fq)s]+%(fq)s(?: *, *)?)+)\)"
518            r"(?: +(?P<match>MATCH \w+))?"
519            r"(?: +ON DELETE (?P<ondelete>%(on)s))?"
520            r"(?: +ON UPDATE (?P<onupdate>%(on)s))?" % kw
521        )
522
523        # CONSTRAINT `CONSTRAINT_1` CHECK (`x` > 5)'
524        # testing on MariaDB 10.2 shows that the CHECK constraint
525        # is returned on a line by itself, so to match without worrying
526        # about parenthesis in the expression we go to the end of the line
527        self._re_ck_constraint = _re_compile(
528            r"  "
529            r"CONSTRAINT +"
530            r"%(iq)s(?P<name>(?:%(esc_fq)s|[^%(fq)s])+)%(fq)s +"
531            r"CHECK +"
532            r"\((?P<sqltext>.+)\),?" % kw
533        )
534
535        # PARTITION
536        #
537        # punt!
538        self._re_partition = _re_compile(r"(?:.*)(?:SUB)?PARTITION(?:.*)")
539
540        # Table-level options (COLLATE, ENGINE, etc.)
541        # Do the string options first, since they have quoted
542        # strings we need to get rid of.
543        for option in _options_of_type_string:
544            self._add_option_string(option)
545
546        for option in (
547            "ENGINE",
548            "TYPE",
549            "AUTO_INCREMENT",
550            "AVG_ROW_LENGTH",
551            "CHARACTER SET",
552            "DEFAULT CHARSET",
553            "CHECKSUM",
554            "COLLATE",
555            "DELAY_KEY_WRITE",
556            "INSERT_METHOD",
557            "MAX_ROWS",
558            "MIN_ROWS",
559            "PACK_KEYS",
560            "ROW_FORMAT",
561            "KEY_BLOCK_SIZE",
562            "STATS_SAMPLE_PAGES",
563        ):
564            self._add_option_word(option)
565
566        for option in (
567            "PARTITION BY",
568            "SUBPARTITION BY",
569            "PARTITIONS",
570            "SUBPARTITIONS",
571            "PARTITION",
572            "SUBPARTITION",
573        ):
574            self._add_partition_option_word(option)
575
576        self._add_option_regex("UNION", r"\([^\)]+\)")
577        self._add_option_regex("TABLESPACE", r".*? STORAGE DISK")
578        self._add_option_regex(
579            "RAID_TYPE",
580            r"\w+\s+RAID_CHUNKS\s*\=\s*\w+RAID_CHUNKSIZE\s*=\s*\w+",
581        )
582
583    _optional_equals = r"(?:\s*(?:=\s*)|\s+)"
584
585    def _add_option_string(self, directive):
586        regex = r"(?P<directive>%s)%s" r"'(?P<val>(?:[^']|'')*?)'(?!')" % (
587            re.escape(directive),
588            self._optional_equals,
589        )
590        self._pr_options.append(_pr_compile(regex, cleanup_text))
591
592    def _add_option_word(self, directive):
593        regex = r"(?P<directive>%s)%s" r"(?P<val>\w+)" % (
594            re.escape(directive),
595            self._optional_equals,
596        )
597        self._pr_options.append(_pr_compile(regex))
598
599    def _add_partition_option_word(self, directive):
600        if directive == "PARTITION BY" or directive == "SUBPARTITION BY":
601            regex = r"(?<!\S)(?P<directive>%s)%s" r"(?P<val>\w+.*)" % (
602                re.escape(directive),
603                self._optional_equals,
604            )
605        elif directive == "SUBPARTITIONS" or directive == "PARTITIONS":
606            regex = r"(?<!\S)(?P<directive>%s)%s" r"(?P<val>\d+)" % (
607                re.escape(directive),
608                self._optional_equals,
609            )
610        else:
611            regex = r"(?<!\S)(?P<directive>%s)(?!\S)" % (re.escape(directive),)
612        self._pr_options.append(_pr_compile(regex))
613
614    def _add_option_regex(self, directive, regex):
615        regex = r"(?P<directive>%s)%s" r"(?P<val>%s)" % (
616            re.escape(directive),
617            self._optional_equals,
618            regex,
619        )
620        self._pr_options.append(_pr_compile(regex))
621
622
623_options_of_type_string = (
624    "COMMENT",
625    "DATA DIRECTORY",
626    "INDEX DIRECTORY",
627    "PASSWORD",
628    "CONNECTION",
629)
630
631
632def _pr_compile(regex, cleanup=None):
633    """Prepare a 2-tuple of compiled regex and callable."""
634
635    return (_re_compile(regex), cleanup)
636
637
638def _re_compile(regex):
639    """Compile a string to regex, I and UNICODE."""
640
641    return re.compile(regex, re.I | re.UNICODE)
642
643
644def _strip_values(values):
645    "Strip reflected values quotes"
646    strip_values = []
647    for a in values:
648        if a[0:1] == '"' or a[0:1] == "'":
649            # strip enclosing quotes and unquote interior
650            a = a[1:-1].replace(a[0] * 2, a[0])
651        strip_values.append(a)
652    return strip_values
653
654
655def cleanup_text(raw_text: str) -> str:
656    if "\\" in raw_text:
657        raw_text = re.sub(
658            _control_char_regexp, lambda s: _control_char_map[s[0]], raw_text
659        )
660    return raw_text.replace("''", "'")
661
662
663_control_char_map = {
664    "\\\\": "\\",
665    "\\0": "\0",
666    "\\a": "\a",
667    "\\b": "\b",
668    "\\t": "\t",
669    "\\n": "\n",
670    "\\v": "\v",
671    "\\f": "\f",
672    "\\r": "\r",
673    # '\\e':'\e',
674}
675_control_char_regexp = re.compile(
676    "|".join(re.escape(k) for k in _control_char_map)
677)
678 
codekingpro/portable-devtools · Team Ai