from __future__ import annotations

import typing as t

from sqlglot import exp, generator, tokens, transforms
from sqlglot.dialects.dialect import (
    Dialect,
    NormalizationStrategy,
    arrow_json_extract_sql,
    build_date_delta,
    build_date_delta_with_interval,
    date_add_interval_sql,
    datestrtodate_sql,
    length_or_char_length_sql,
    max_or_greatest,
    min_or_least,
    no_ilike_sql,
    no_paren_current_date_sql,
    no_pivot_sql,
    no_tablesample_sql,
    no_trycast_sql,
    rename_func,
    strposition_sql,
    unit_to_var,
    trim_sql,
    timestrtotime_sql,
)
from sqlglot.generator import unsupported_args
from sqlglot.parsers.mysql import MySQLParser
from sqlglot.tokens import TokenType
from sqlglot.typing.mysql import EXPRESSION_METADATA


def _date_trunc_sql(self: MySQL.Generator, expression: exp.DateTrunc) -> str:
    expr = self.sql(expression, "this")
    unit = expression.text("unit").upper()

    if unit == "WEEK":
        concat = f"CONCAT(YEAR({expr}), ' ', WEEK({expr}, 1), ' 1')"
        date_format = "%Y %u %w"
    elif unit == "MONTH":
        concat = f"CONCAT(YEAR({expr}), ' ', MONTH({expr}), ' 1')"
        date_format = "%Y %c %e"
    elif unit == "QUARTER":
        concat = f"CONCAT(YEAR({expr}), ' ', QUARTER({expr}) * 3 - 2, ' 1')"
        date_format = "%Y %c %e"
    elif unit == "YEAR":
        concat = f"CONCAT(YEAR({expr}), ' 1 1')"
        date_format = "%Y %c %e"
    else:
        if unit != "DAY":
            self.unsupported(f"Unexpected interval unit: {unit}")
        return self.func("DATE", expr)

    return self.func("STR_TO_DATE", concat, f"'{date_format}'")


def _str_to_date_sql(
    self: MySQL.Generator, expression: exp.StrToDate | exp.StrToTime | exp.TsOrDsToDate
) -> str:
    return self.func("STR_TO_DATE", expression.this, self.format_time(expression))


def _unix_to_time_sql(self: MySQL.Generator, expression: exp.UnixToTime) -> str:
    scale = expression.args.get("scale")
    timestamp = expression.this

    if scale in (None, exp.UnixToTime.SECONDS):
        return self.func("FROM_UNIXTIME", timestamp, self.format_time(expression))

    return self.func(
        "FROM_UNIXTIME",
        exp.Div(this=timestamp, expression=exp.func("POW", 10, scale)),
        self.format_time(expression),
    )


def date_add_sql(
    kind: str,
) -> t.Callable[[generator.Generator, exp.Expr], str]:
    def func(self: generator.Generator, expression: exp.Expr) -> str:
        return self.func(
            f"DATE_{kind}",
            expression.this,
            exp.Interval(this=expression.expression, unit=unit_to_var(expression)),
        )

    return func


def _ts_or_ds_to_date_sql(self: MySQL.Generator, expression: exp.TsOrDsToDate) -> str:
    time_format = expression.args.get("format")
    return _str_to_date_sql(self, expression) if time_format else self.func("DATE", expression.this)


def _remove_ts_or_ds_to_date(
    to_sql: t.Optional[t.Callable[[MySQL.Generator, exp.Expr], str]] = None,
    args: t.Tuple[str, ...] = ("this",),
) -> t.Callable[[MySQL.Generator, exp.Func], str]:
    def func(self: MySQL.Generator, expression: exp.Func) -> str:
        for arg_key in args:
            arg = expression.args.get(arg_key)
            if isinstance(arg, (exp.TsOrDsToDate, exp.TsOrDsToTimestamp)) and not arg.args.get(
                "format"
            ):
                expression.set(arg_key, arg.this)

        return to_sql(self, expression) if to_sql else self.function_fallback_sql(expression)

    return func


class MySQL(Dialect):
    PROMOTE_TO_INFERRED_DATETIME_TYPE = True

    # https://dev.mysql.com/doc/refman/8.0/en/identifiers.html
    IDENTIFIERS_CAN_START_WITH_DIGIT = True

    # We default to treating all identifiers as case-sensitive, since it matches MySQL's
    # behavior on Linux systems. For MacOS and Windows systems, one can override this
    # setting by specifying `dialect="mysql, normalization_strategy = lowercase"`.
    #
    # See also https://dev.mysql.com/doc/refman/8.2/en/identifier-case-sensitivity.html
    NORMALIZATION_STRATEGY = NormalizationStrategy.CASE_SENSITIVE

    TIME_FORMAT = "'%Y-%m-%d %T'"
    DPIPE_IS_STRING_CONCAT = False
    SUPPORTS_USER_DEFINED_TYPES = False
    SAFE_DIVISION = True
    SAFE_TO_ELIMINATE_DOUBLE_NEGATION = False
    LEAST_GREATEST_IGNORES_NULLS = False

    EXPRESSION_METADATA = EXPRESSION_METADATA.copy()

    # https://prestodb.io/docs/current/functions/datetime.html#mysql-date-functions
    TIME_MAPPING = {
        "%M": "%B",
        "%c": "%-m",
        "%e": "%-d",
        "%h": "%I",
        "%i": "%M",
        "%s": "%S",
        "%u": "%W",
        "%k": "%-H",
        "%l": "%-I",
        "%T": "%H:%M:%S",
        "%W": "%A",
    }

    VALID_INTERVAL_UNITS = {
        *Dialect.VALID_INTERVAL_UNITS,
        "SECOND_MICROSECOND",
        "MINUTE_MICROSECOND",
        "MINUTE_SECOND",
        "HOUR_MICROSECOND",
        "HOUR_SECOND",
        "HOUR_MINUTE",
        "DAY_MICROSECOND",
        "DAY_SECOND",
        "DAY_MINUTE",
        "DAY_HOUR",
        "YEAR_MONTH",
    }

    class Tokenizer(tokens.Tokenizer):
        QUOTES = ["'", '"']
        COMMENTS = ["--", "#", ("/*", "*/")]
        IDENTIFIERS = ["`"]
        STRING_ESCAPES = ["'", '"', "\\"]
        BIT_STRINGS = [("b'", "'"), ("B'", "'"), ("0b", "")]
        HEX_STRINGS = [("x'", "'"), ("X'", "'"), ("0x", "")]
        # https://dev.mysql.com/doc/refman/8.4/en/string-literals.html
        ESCAPE_FOLLOW_CHARS = ["0", "b", "n", "r", "t", "Z", "%", "_"]

        NESTED_COMMENTS = False

        KEYWORDS = {
            **tokens.Tokenizer.KEYWORDS,
            "BLOB": TokenType.BLOB,
            "CHARSET": TokenType.CHARACTER_SET,
            "DISTINCTROW": TokenType.DISTINCT,
            "EXPLAIN": TokenType.DESCRIBE,
            "FORCE": TokenType.FORCE,
            "IGNORE": TokenType.IGNORE,
            "KEY": TokenType.KEY,
            "LOCK TABLES": TokenType.COMMAND,
            "LONGBLOB": TokenType.LONGBLOB,
            "LONGTEXT": TokenType.LONGTEXT,
            "MEDIUMBLOB": TokenType.MEDIUMBLOB,
            "MEDIUMINT": TokenType.MEDIUMINT,
            "MEDIUMTEXT": TokenType.MEDIUMTEXT,
            "MEMBER OF": TokenType.MEMBER_OF,
            "MOD": TokenType.MOD,
            "SEPARATOR": TokenType.SEPARATOR,
            "SERIAL": TokenType.SERIAL,
            "SIGNED": TokenType.BIGINT,
            "SIGNED INTEGER": TokenType.BIGINT,
            "SOUNDS LIKE": TokenType.SOUNDS_LIKE,
            "START": TokenType.BEGIN,
            "TIMESTAMP": TokenType.TIMESTAMPTZ,
            "TINYBLOB": TokenType.TINYBLOB,
            "TINYTEXT": TokenType.TINYTEXT,
            "UNLOCK TABLES": TokenType.COMMAND,
            "UNSIGNED": TokenType.UBIGINT,
            "UNSIGNED INTEGER": TokenType.UBIGINT,
            "YEAR": TokenType.YEAR,
            "_ARMSCII8": TokenType.INTRODUCER,
            "_ASCII": TokenType.INTRODUCER,
            "_BIG5": TokenType.INTRODUCER,
            "_BINARY": TokenType.INTRODUCER,
            "_CP1250": TokenType.INTRODUCER,
            "_CP1251": TokenType.INTRODUCER,
            "_CP1256": TokenType.INTRODUCER,
            "_CP1257": TokenType.INTRODUCER,
            "_CP850": TokenType.INTRODUCER,
            "_CP852": TokenType.INTRODUCER,
            "_CP866": TokenType.INTRODUCER,
            "_CP932": TokenType.INTRODUCER,
            "_DEC8": TokenType.INTRODUCER,
            "_EUCJPMS": TokenType.INTRODUCER,
            "_EUCKR": TokenType.INTRODUCER,
            "_GB18030": TokenType.INTRODUCER,
            "_GB2312": TokenType.INTRODUCER,
            "_GBK": TokenType.INTRODUCER,
            "_GEOSTD8": TokenType.INTRODUCER,
            "_GREEK": TokenType.INTRODUCER,
            "_HEBREW": TokenType.INTRODUCER,
            "_HP8": TokenType.INTRODUCER,
            "_KEYBCS2": TokenType.INTRODUCER,
            "_KOI8R": TokenType.INTRODUCER,
            "_KOI8U": TokenType.INTRODUCER,
            "_LATIN1": TokenType.INTRODUCER,
            "_LATIN2": TokenType.INTRODUCER,
            "_LATIN5": TokenType.INTRODUCER,
            "_LATIN7": TokenType.INTRODUCER,
            "_MACCE": TokenType.INTRODUCER,
            "_MACROMAN": TokenType.INTRODUCER,
            "_SJIS": TokenType.INTRODUCER,
            "_SWE7": TokenType.INTRODUCER,
            "_TIS620": TokenType.INTRODUCER,
            "_UCS2": TokenType.INTRODUCER,
            "_UJIS": TokenType.INTRODUCER,
            # https://dev.mysql.com/doc/refman/8.0/en/string-literals.html
            "_UTF8": TokenType.INTRODUCER,
            "_UTF16": TokenType.INTRODUCER,
            "_UTF16LE": TokenType.INTRODUCER,
            "_UTF32": TokenType.INTRODUCER,
            "_UTF8MB3": TokenType.INTRODUCER,
            "_UTF8MB4": TokenType.INTRODUCER,
            "@@": TokenType.SESSION_PARAMETER,
        }

        COMMANDS = {*tokens.Tokenizer.COMMANDS, TokenType.REPLACE} - {TokenType.SHOW}

    Parser = MySQLParser

    class Generator(generator.Generator):
        INTERVAL_ALLOWS_PLURAL_FORM = False
        LOCKING_READS_SUPPORTED = True
        NULL_ORDERING_SUPPORTED: t.Optional[bool] = None
        JOIN_HINTS = False
        TABLE_HINTS = True
        DUPLICATE_KEY_UPDATE_WITH_SET = False
        QUERY_HINT_SEP = " "
        VALUES_AS_TABLE = False
        NVL2_SUPPORTED = False
        LAST_DAY_SUPPORTS_DATE_PART = False
        JSON_TYPE_REQUIRED_FOR_EXTRACTION = True
        JSON_PATH_BRACKETED_KEY_SUPPORTED = False
        JSON_KEY_VALUE_PAIR_SEP = ","
        SUPPORTS_TO_NUMBER = False
        PARSE_JSON_NAME: t.Optional[str] = None
        PAD_FILL_PATTERN_IS_REQUIRED = True
        WRAP_DERIVED_VALUES = False
        VARCHAR_REQUIRES_SIZE = True
        SUPPORTS_MEDIAN = False
        UPDATE_STATEMENT_SUPPORTS_FROM = False

        TRANSFORMS = {
            **generator.Generator.TRANSFORMS,
            exp.ArrayAgg: rename_func("GROUP_CONCAT"),
            exp.BitwiseAndAgg: rename_func("BIT_AND"),
            exp.BitwiseOrAgg: rename_func("BIT_OR"),
            exp.BitwiseXorAgg: rename_func("BIT_XOR"),
            exp.BitwiseCount: rename_func("BIT_COUNT"),
            exp.Chr: lambda self, e: self.chr_sql(e, "CHAR"),
            exp.CurrentDate: no_paren_current_date_sql,
            exp.CurrentVersion: rename_func("VERSION"),
            exp.DateDiff: _remove_ts_or_ds_to_date(
                lambda self, e: self.func("DATEDIFF", e.this, e.expression), ("this", "expression")
            ),
            exp.DateAdd: _remove_ts_or_ds_to_date(date_add_sql("ADD")),
            exp.DateStrToDate: datestrtodate_sql,
            exp.DateSub: _remove_ts_or_ds_to_date(date_add_sql("SUB")),
            exp.DateTrunc: _date_trunc_sql,
            exp.Day: _remove_ts_or_ds_to_date(),
            exp.DayOfMonth: _remove_ts_or_ds_to_date(rename_func("DAYOFMONTH")),
            exp.DayOfWeek: _remove_ts_or_ds_to_date(rename_func("DAYOFWEEK")),
            exp.DayOfYear: _remove_ts_or_ds_to_date(rename_func("DAYOFYEAR")),
            exp.GroupConcat: lambda self, e: (
                f"""GROUP_CONCAT({self.sql(e, "this")} SEPARATOR {self.sql(e, "separator") or "','"})"""
            ),
            exp.ILike: no_ilike_sql,
            exp.JSONExtractScalar: arrow_json_extract_sql,
            exp.Length: length_or_char_length_sql,
            exp.LogicalOr: rename_func("MAX"),
            exp.LogicalAnd: rename_func("MIN"),
            exp.Max: max_or_greatest,
            exp.Min: min_or_least,
            exp.Month: _remove_ts_or_ds_to_date(),
            exp.NullSafeEQ: lambda self, e: self.binary(e, "<=>"),
            exp.NullSafeNEQ: lambda self, e: f"NOT {self.binary(e, '<=>')}",
            exp.NumberToStr: rename_func("FORMAT"),
            exp.Pivot: no_pivot_sql,
            exp.Select: transforms.preprocess(
                [
                    transforms.eliminate_distinct_on,
                    transforms.eliminate_semi_and_anti_joins,
                    transforms.eliminate_qualify,
                    transforms.eliminate_full_outer_join,
                    transforms.unnest_generate_date_array_using_recursive_cte,
                ]
            ),
            exp.StrPosition: lambda self, e: strposition_sql(
                self, e, func_name="LOCATE", supports_position=True
            ),
            exp.StrToDate: _str_to_date_sql,
            exp.StrToTime: _str_to_date_sql,
            exp.Stuff: rename_func("INSERT"),
            exp.SessionUser: lambda *_: "SESSION_USER()",
            exp.TableSample: no_tablesample_sql,
            exp.TimeFromParts: rename_func("MAKETIME"),
            exp.TimestampAdd: date_add_interval_sql("DATE", "ADD"),
            exp.TimestampDiff: lambda self, e: self.func(
                "TIMESTAMPDIFF", unit_to_var(e), e.expression, e.this
            ),
            exp.TimestampSub: date_add_interval_sql("DATE", "SUB"),
            exp.TimeStrToUnix: rename_func("UNIX_TIMESTAMP"),
            exp.TimeStrToTime: lambda self, e: timestrtotime_sql(
                self,
                e,
                include_precision=not e.args.get("zone"),
            ),
            exp.TimeToStr: _remove_ts_or_ds_to_date(
                lambda self, e: self.func("DATE_FORMAT", e.this, self.format_time(e))
            ),
            exp.Trim: trim_sql,
            exp.Trunc: rename_func("TRUNCATE"),
            exp.TryCast: no_trycast_sql,
            exp.TsOrDsAdd: date_add_sql("ADD"),
            exp.TsOrDsDiff: lambda self, e: self.func("DATEDIFF", e.this, e.expression),
            exp.TsOrDsToDate: _ts_or_ds_to_date_sql,
            exp.Unicode: lambda self, e: f"ORD(CONVERT({self.sql(e.this)} USING utf32))",
            exp.UnixToTime: _unix_to_time_sql,
            exp.Week: _remove_ts_or_ds_to_date(),
            exp.WeekOfYear: _remove_ts_or_ds_to_date(rename_func("WEEKOFYEAR")),
            exp.Year: _remove_ts_or_ds_to_date(),
            exp.UtcTimestamp: rename_func("UTC_TIMESTAMP"),
            exp.UtcTime: rename_func("UTC_TIME"),
        }

        UNSIGNED_TYPE_MAPPING = {
            exp.DType.UBIGINT: "BIGINT",
            exp.DType.UINT: "INT",
            exp.DType.UMEDIUMINT: "MEDIUMINT",
            exp.DType.USMALLINT: "SMALLINT",
            exp.DType.UTINYINT: "TINYINT",
            exp.DType.UDECIMAL: "DECIMAL",
            exp.DType.UDOUBLE: "DOUBLE",
        }

        TIMESTAMP_TYPE_MAPPING = {
            exp.DType.DATETIME2: "DATETIME",
            exp.DType.SMALLDATETIME: "DATETIME",
            exp.DType.TIMESTAMP: "DATETIME",
            exp.DType.TIMESTAMPNTZ: "DATETIME",
            exp.DType.TIMESTAMPTZ: "TIMESTAMP",
            exp.DType.TIMESTAMPLTZ: "TIMESTAMP",
        }

        TYPE_MAPPING = {
            **generator.Generator.TYPE_MAPPING,
            **UNSIGNED_TYPE_MAPPING,
            **TIMESTAMP_TYPE_MAPPING,
        }

        TYPE_MAPPING.pop(exp.DType.MEDIUMTEXT)
        TYPE_MAPPING.pop(exp.DType.LONGTEXT)
        TYPE_MAPPING.pop(exp.DType.TINYTEXT)
        TYPE_MAPPING.pop(exp.DType.BLOB)
        TYPE_MAPPING.pop(exp.DType.MEDIUMBLOB)
        TYPE_MAPPING.pop(exp.DType.LONGBLOB)
        TYPE_MAPPING.pop(exp.DType.TINYBLOB)

        PROPERTIES_LOCATION = {
            **generator.Generator.PROPERTIES_LOCATION,
            exp.TransientProperty: exp.Properties.Location.UNSUPPORTED,
            exp.VolatileProperty: exp.Properties.Location.UNSUPPORTED,
            exp.PartitionedByProperty: exp.Properties.Location.UNSUPPORTED,
            exp.PartitionByRangeProperty: exp.Properties.Location.POST_SCHEMA,
            exp.PartitionByListProperty: exp.Properties.Location.POST_SCHEMA,
        }

        LIMIT_FETCH = "LIMIT"

        LIMIT_ONLY_LITERALS = True

        CHAR_CAST_MAPPING = dict.fromkeys(
            (
                exp.DType.LONGTEXT,
                exp.DType.LONGBLOB,
                exp.DType.MEDIUMBLOB,
                exp.DType.MEDIUMTEXT,
                exp.DType.TEXT,
                exp.DType.TINYBLOB,
                exp.DType.TINYTEXT,
                exp.DType.VARCHAR,
            ),
            "CHAR",
        )
        SIGNED_CAST_MAPPING = dict.fromkeys(
            (
                exp.DType.BIGINT,
                exp.DType.BOOLEAN,
                exp.DType.INT,
                exp.DType.SMALLINT,
                exp.DType.TINYINT,
                exp.DType.MEDIUMINT,
            ),
            "SIGNED",
        )

        # MySQL doesn't support many datatypes in cast.
        # https://dev.mysql.com/doc/refman/8.0/en/cast-functions.html#function_cast
        CAST_MAPPING = {
            **CHAR_CAST_MAPPING,
            **SIGNED_CAST_MAPPING,
            exp.DType.UBIGINT: "UNSIGNED",
        }

        TIMESTAMP_FUNC_TYPES = {
            exp.DType.TIMESTAMPTZ,
            exp.DType.TIMESTAMPLTZ,
        }

        # https://dev.mysql.com/doc/refman/8.0/en/keywords.html
        RESERVED_KEYWORDS = {
            "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",
            "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",
            "generated",
            "get",
            "grant",
            "group",
            "grouping",
            "groups",
            "having",
            "high_priority",
            "hour_microsecond",
            "hour_minute",
            "hour_second",
            "if",
            "ignore",
            "in",
            "index",
            "infile",
            "inner",
            "inout",
            "insensitive",
            "insert",
            "int",
            "int1",
            "int2",
            "int3",
            "int4",
            "int8",
            "integer",
            "intersect",
            "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_ssl_verify_server_cert",
            "match",
            "maxvalue",
            "mediumblob",
            "mediumint",
            "mediumtext",
            "middleint",
            "minute_microsecond",
            "minute_second",
            "mod",
            "modifies",
            "natural",
            "not",
            "no_write_to_binlog",
            "nth_value",
            "ntile",
            "null",
            "numeric",
            "of",
            "on",
            "optimize",
            "optimizer_costs",
            "option",
            "optionally",
            "or",
            "order",
            "out",
            "outer",
            "outfile",
            "over",
            "partition",
            "percent_rank",
            "precision",
            "primary",
            "procedure",
            "purge",
            "range",
            "rank",
            "read",
            "reads",
            "read_write",
            "real",
            "recursive",
            "references",
            "regexp",
            "release",
            "rename",
            "repeat",
            "replace",
            "require",
            "resignal",
            "restrict",
            "return",
            "revoke",
            "right",
            "rlike",
            "row",
            "rows",
            "row_number",
            "schema",
            "schemas",
            "second_microsecond",
            "select",
            "sensitive",
            "separator",
            "set",
            "show",
            "signal",
            "smallint",
            "spatial",
            "specific",
            "sql",
            "sqlexception",
            "sqlstate",
            "sqlwarning",
            "sql_big_result",
            "sql_calc_found_rows",
            "sql_small_result",
            "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",
        }

        SQL_SECURITY_VIEW_LOCATION = exp.Properties.Location.POST_CREATE

        def locate_properties(self, properties: exp.Properties) -> t.DefaultDict:
            locations = super().locate_properties(properties)

            # MySQL puts SQL SECURITY before VIEW but after the schema for functions/procedures
            if isinstance(create := properties.parent, exp.Create) and create.kind == "VIEW":
                post_schema = locations[exp.Properties.Location.POST_SCHEMA]
                for i, p in enumerate(post_schema):
                    if isinstance(p, exp.SqlSecurityProperty):
                        post_schema.pop(i)
                        locations[self.SQL_SECURITY_VIEW_LOCATION].append(p)
                        break

            return locations

        def computedcolumnconstraint_sql(self, expression: exp.ComputedColumnConstraint) -> str:
            persisted = "STORED" if expression.args.get("persisted") else "VIRTUAL"
            return f"GENERATED ALWAYS AS ({self.sql(expression.this.unnest())}) {persisted}"

        def array_sql(self, expression: exp.Array) -> str:
            self.unsupported("Arrays are not supported by MySQL")
            return self.function_fallback_sql(expression)

        def arraycontainsall_sql(self, expression: exp.ArrayContainsAll) -> str:
            self.unsupported("Array operations are not supported by MySQL")
            return self.function_fallback_sql(expression)

        def dpipe_sql(self, expression: exp.DPipe) -> str:
            return self.func("CONCAT", *expression.flatten())

        def extract_sql(self, expression: exp.Extract) -> str:
            unit = expression.name
            if unit and unit.lower() == "epoch":
                return self.func("UNIX_TIMESTAMP", expression.expression)

            return super().extract_sql(expression)

        def datatype_sql(self, expression: exp.DataType) -> str:
            if (
                self.VARCHAR_REQUIRES_SIZE
                and expression.is_type(exp.DType.VARCHAR)
                and not expression.expressions
            ):
                # `VARCHAR` must always have a size - if it doesn't, we always generate `TEXT`
                return "TEXT"

            # https://dev.mysql.com/doc/refman/8.0/en/numeric-type-syntax.html
            result = super().datatype_sql(expression)
            if expression.this in self.UNSIGNED_TYPE_MAPPING:
                result = f"{result} UNSIGNED"

            return result

        def jsonarraycontains_sql(self, expression: exp.JSONArrayContains) -> str:
            return f"{self.sql(expression, 'this')} MEMBER OF({self.sql(expression, 'expression')})"

        def cast_sql(self, expression: exp.Cast, safe_prefix: t.Optional[str] = None) -> str:
            if expression.to.this in self.TIMESTAMP_FUNC_TYPES:
                return self.func("TIMESTAMP", expression.this)

            to = self.CAST_MAPPING.get(expression.to.this)

            if to:
                expression.to.set("this", to)
            return super().cast_sql(expression)

        def show_sql(self, expression: exp.Show) -> str:
            this = f" {expression.name}"
            full = " FULL" if expression.args.get("full") else ""
            global_ = " GLOBAL" if expression.args.get("global_") else ""

            target = self.sql(expression, "target")
            target = f" {target}" if target else ""
            if expression.name in ("COLUMNS", "INDEX"):
                target = f" FROM{target}"
            elif expression.name == "GRANTS":
                target = f" FOR{target}"
            elif expression.name in ("LINKS", "PARTITIONS"):
                target = f" ON{target}" if target else ""
            elif expression.name == "PROJECTIONS":
                target = f" ON TABLE{target}" if target else ""

            db = self._prefixed_sql("FROM", expression, "db")

            like = self._prefixed_sql("LIKE", expression, "like")
            where = self.sql(expression, "where")

            types = self.expressions(expression, key="types")
            types = f" {types}" if types else types
            query = self._prefixed_sql("FOR QUERY", expression, "query")

            if expression.name == "PROFILE":
                offset = self._prefixed_sql("OFFSET", expression, "offset")
                limit = self._prefixed_sql("LIMIT", expression, "limit")
            else:
                offset = ""
                limit = self._oldstyle_limit_sql(expression)

            log = self._prefixed_sql("IN", expression, "log")
            position = self._prefixed_sql("FROM", expression, "position")

            channel = self._prefixed_sql("FOR CHANNEL", expression, "channel")

            if expression.name == "ENGINE":
                mutex_or_status = " MUTEX" if expression.args.get("mutex") else " STATUS"
            else:
                mutex_or_status = ""

            for_table = self._prefixed_sql("FOR TABLE", expression, "for_table")
            for_group = self._prefixed_sql("FOR GROUP", expression, "for_group")
            for_user = self._prefixed_sql("FOR USER", expression, "for_user")
            for_role = self._prefixed_sql("FOR ROLE", expression, "for_role")
            into_outfile = self._prefixed_sql("INTO OUTFILE", expression, "into_outfile")
            json = " JSON" if expression.args.get("json") else ""

            return f"SHOW{full}{global_}{this}{json}{target}{for_table}{types}{db}{query}{log}{position}{channel}{mutex_or_status}{like}{where}{offset}{limit}{for_group}{for_user}{for_role}{into_outfile}"

        def alterrename_sql(self, expression: exp.AlterRename, include_to: bool = True) -> str:
            """To avoid TO keyword in ALTER ... RENAME statements.
            It's moved from Doris, because it's the same for all MySQL, Doris, and StarRocks.
            """
            return super().alterrename_sql(expression, include_to=False)

        def altercolumn_sql(self, expression: exp.AlterColumn) -> str:
            dtype = self.sql(expression, "dtype")
            if not dtype:
                return super().altercolumn_sql(expression)

            this = self.sql(expression, "this")
            return f"MODIFY COLUMN {this} {dtype}"

        def _prefixed_sql(self, prefix: str, expression: exp.Expr, arg: str) -> str:
            sql = self.sql(expression, arg)
            return f" {prefix} {sql}" if sql else ""

        def _oldstyle_limit_sql(self, expression: exp.Show) -> str:
            limit = self.sql(expression, "limit")
            offset = self.sql(expression, "offset")
            if limit:
                limit_offset = f"{offset}, {limit}" if offset else limit
                return f" LIMIT {limit_offset}"
            return ""

        def timestamptrunc_sql(self, expression: exp.TimestampTrunc) -> str:
            unit = expression.args.get("unit")

            # Pick an old-enough date to avoid negative timestamp diffs
            start_ts = "'0000-01-01 00:00:00'"

            # Source: https://stackoverflow.com/a/32955740
            timestamp_diff = build_date_delta(exp.TimestampDiff)([unit, start_ts, expression.this])
            interval = exp.Interval(this=timestamp_diff, unit=unit)
            dateadd = build_date_delta_with_interval(exp.DateAdd)([start_ts, interval])

            return self.sql(dateadd)

        def converttimezone_sql(self, expression: exp.ConvertTimezone) -> str:
            from_tz = expression.args.get("source_tz")
            to_tz = expression.args.get("target_tz")
            dt = expression.args.get("timestamp")

            return self.func("CONVERT_TZ", dt, from_tz, to_tz)

        def attimezone_sql(self, expression: exp.AtTimeZone) -> str:
            self.unsupported("AT TIME ZONE is not supported by MySQL")
            return self.sql(expression.this)

        def isascii_sql(self, expression: exp.IsAscii) -> str:
            return f"REGEXP_LIKE({self.sql(expression.this)}, '^[[:ascii:]]*$')"

        def ignorenulls_sql(self, expression: exp.IgnoreNulls) -> str:
            # https://dev.mysql.com/doc/refman/8.4/en/window-function-descriptions.html
            self.unsupported("MySQL does not support IGNORE NULLS.")
            return self.sql(expression.this)

        @unsupported_args("this")
        def currentschema_sql(self, expression: exp.CurrentSchema) -> str:
            return self.func("SCHEMA")

        def partition_sql(self, expression: exp.Partition) -> str:
            parent = expression.parent
            if isinstance(parent, (exp.PartitionByRangeProperty, exp.PartitionByListProperty)):
                return self.expressions(expression, flat=True)
            return super().partition_sql(expression)

        def _partition_by_sql(
            self, expression: exp.PartitionByRangeProperty | exp.PartitionByListProperty, kind: str
        ) -> str:
            partitions = self.expressions(expression, key="partition_expressions", flat=True)
            create = self.expressions(expression, key="create_expressions", flat=True)
            return f"PARTITION BY {kind} ({partitions}) ({create})"

        def partitionbyrangeproperty_sql(self, expression: exp.PartitionByRangeProperty) -> str:
            return self._partition_by_sql(expression, "RANGE")

        def partitionbylistproperty_sql(self, expression: exp.PartitionByListProperty) -> str:
            return self._partition_by_sql(expression, "LIST")

        def partitionlist_sql(self, expression: exp.PartitionList) -> str:
            name = self.sql(expression, "this")
            values = self.expressions(expression, flat=True)
            return f"PARTITION {name} VALUES IN ({values})"

        def partitionrange_sql(self, expression: exp.PartitionRange) -> str:
            name = self.sql(expression, "this")
            values = self.expressions(expression, flat=True)
            return f"PARTITION {name} VALUES LESS THAN ({values})"
