///|
suberror LexerError {
  UnterminatedString
  UnknownCharacter(Char)
} derive(Show)

///|
pub(all) enum Token {
  Keyword(Keyword)
  Identifier(String)
  Number(String)
  StringLiteral(String)
  Boolean(Bool)
  Comma
  Semicolon
  Colon
  Eq
  DoubleEq
  Neq
  Lt
  Gt
  LtEq
  GtEq
  Spaceship
  Plus
  Minus
  Mul // Asterisk
  Div
  Mod
  LBracket
  RBracket
  LBrace
  RBrace
  LParen
  RParen
  Period
  Placeholder(String) // ?, $1, :name, @name
  // PostgreSQL JSON operators
  JsonExtract // ->
  JsonExtractText // ->>
  JsonExtractPath // #>
  JsonExtractPathText // #>>
  JsonContains // @>
  JsonContainedIn // <@
  Unknown(Char)
  Eof
} derive(Eq, Show)

///|
priv struct Lexer {
  dialect : &Dialect
}

///|
pub fn tokenize(
  dialect? : &Dialect = MySQL::{  },
  input : String,
) -> Array[Token] raise LexerError {
  let lexer = Lexer::{ dialect, }
  let tokens = []
  loop input[:] {
    [] => break
    input => {
      let (token, input) = lexer.next_token(input)
      tokens.push(token)
      continue input
    }
  }
  tokens
}

///|
fn Lexer::next_token(
  self : Lexer,
  input : StringView,
) -> (Token, StringView) raise LexerError {
  match skip_whitespace(input) {
    [',', .. rest] => (Comma, rest)
    // : followed by identifier is a named parameter, otherwise it's just Colon
    [':', c, ..] as code if c.is_ascii_alphabetic() || c == '_' =>
      self.read_colon_parameter(code)
    [':', .. rest] => (Colon, rest)
    ['?', .. rest] => (Placeholder("?"), rest)
    [';', .. rest] => (Semicolon, rest)
    ['*', .. rest] => (Mul, rest)
    [.. "==", .. rest] => (DoubleEq, rest)
    ['=', .. rest] => (Eq, rest)
    [.. "<=>", .. rest] => (Spaceship, rest)
    [.. "<>", .. rest] => (Neq, rest)
    [.. "<=", .. rest] => (LtEq, rest)
    [.. "<@", .. rest] => (JsonContainedIn, rest) // PostgreSQL JSON <@
    ['<', .. rest] => (Lt, rest)
    [.. ">=", .. rest] => (GtEq, rest)
    ['>', .. rest] => (Gt, rest)
    ['+', .. rest] => (Plus, rest)
    [.. "->>", .. rest] => (JsonExtractText, rest) // PostgreSQL JSON ->>
    [.. "->", .. rest] => (JsonExtract, rest) // PostgreSQL JSON ->
    [.. "--", .. rest] => {
      let rest = skip_line_comment(rest)
      self.next_token(rest)
    }
    ['-', .. rest] => (Minus, rest)
    ['/', .. rest] => (Div, rest)
    ['%', .. rest] => (Mod, rest)
    ['(', .. rest] => (LParen, rest)
    [')', .. rest] => (RParen, rest)
    ['[', .. rest] => (LBracket, rest)
    [']', .. rest] => (RBracket, rest)
    ['{', .. rest] => (LBrace, rest)
    ['}', .. rest] => (RBrace, rest)
    ['\'', .. rest] => self.read_single_quoted_string(rest)
    ['"', .. rest] => self.read_double_quoted_string(rest)
    ['`', .. rest] => self.read_backtick_quoted_identifier(rest)
    ['.', .. rest] => read_frac(rest)
    // PostgreSQL JSON operators (must come before general patterns and character matching)
    [.. "#>>", .. rest] => (JsonExtractPathText, rest) // PostgreSQL JSON #>>
    [.. "#>", .. rest] => (JsonExtractPath, rest) // PostgreSQL JSON #>
    [.. "@>", .. rest] => (JsonContains, rest) // PostgreSQL JSON @>
    // $ followed by digits is a PostgreSQL numbered placeholder
    ['$', c, ..] as code if c.is_ascii_digit() =>
      self.read_dollar_placeholder(code)
    // General patterns after specific operators
    [c, .. _rest] as code if c.is_ascii_digit() => read_number(code)
    ['_', .. _rest] as code => self.read_identifier_or_keyword(code)
    ['@', .. _rest] as code => self.read_at_parameter(code)
    [c, .. _rest] as code if c.is_ascii_alphabetic() =>
      self.read_identifier_or_keyword(code)
    [c, .. _rest] => raise UnknownCharacter(c)
    [] => (Eof, [])
  }
}

///|
fn skip_whitespace(input : StringView) -> StringView {
  loop input {
    [c, .. rest] if c.is_ascii_whitespace() => continue rest
    input => break input
  }
}

///|
fn read_frac(input : StringView) -> (Token, StringView) {
  let (idx, rest) = loop (input, 0) {
    ([c, .. rest], n) if c.is_ascii_digit() => continue (rest, n + 1)
    (rest, n) => break (n, rest)
  }
  if idx == 0 {
    (Period, rest)
  } else {
    (Number("." + input.view(start_offset=0, end_offset=idx).to_string()), rest)
  }
}

///|
fn read_number(input : StringView) -> (Token, StringView) {
  let (idx, rest) = loop (input, 0) {
    ([c, .. rest], n) if c.is_ascii_digit() => continue (rest, n + 1)
    (rest, n) => break (n, rest)
  }
  let (idx, rest) = match rest {
    ['.', .. rest] =>
      loop (rest, idx + 1) {
        ([c, .. rest], n) if c.is_ascii_digit() => continue (rest, n + 1)
        (rest, n) => break (n, rest)
      }
    rest => (idx, rest)
  }
  (Number(input.view(start_offset=0, end_offset=idx).to_string()), rest)
}

///|
fn Lexer::read_identifier_or_keyword(
  self : Lexer,
  input : StringView,
) -> (Token, StringView) {
  let (idx, rest) = loop (input, 0) {
    ([c, .. rest], n) if is_ascii_alphabetic_or_digit(c) =>
      continue (rest, n + 1)
    (rest, n) => break (n, rest)
  }
  let word = input.view(start_offset=0, end_offset=idx).to_lower()

  // First try dialect-specific keywords
  let token = match self.dialect.read_keyword(word.to_string()) {
    Some(keyword) => Keyword(keyword)
    None =>
      // Then try common keywords and boolean literals
      match word {
        "true" if self.dialect.supports_boolean_literals() =>
          Token::Boolean(true)
        "false" if self.dialect.supports_boolean_literals() =>
          Token::Boolean(false)
        "select" => Keyword(Select)
        "from" => Keyword(From)
        "where" => Keyword(Where)
        "as" => Keyword(As)
        "group" => Keyword(Group)
        "order" => Keyword(Order)
        "by" => Keyword(By)
        "asc" => Keyword(Asc)
        "desc" => Keyword(Desc)
        "nulls" => Keyword(Nulls)
        "first" => Keyword(First)
        "last" => Keyword(Last)
        "year" => Keyword(Year)
        "month" => Keyword(Month)
        "day" => Keyword(Day)
        "hour" => Keyword(Hour)
        "minute" => Keyword(Minute)
        "second" => Keyword(Second)
        "date" => Keyword(Date)
        "interval" => Keyword(Interval)
        "to" => Keyword(To)
        "like" => Keyword(Like)
        "not" => Keyword(Not)
        "ilike" => Keyword(ILike)
        "and" => Keyword(And)
        "or" => Keyword(Or)
        "exists" => Keyword(Exists)
        "between" => Keyword(Between)
        "extract" => Keyword(Extract)
        "filter" => Keyword(Filter)
        "case" => Keyword(Case)
        "when" => Keyword(When)
        "matched" => Keyword(Matched)
        "target" => Keyword(Target)
        "then" => Keyword(Then)
        "else" => Keyword(Else)
        "end" => Keyword(End)
        "having" => Keyword(Having)
        "in" => Keyword(In)
        "join" => Keyword(Join)
        "left" => Keyword(Left)
        "right" => Keyword(Right)
        "full" => Keyword(Full)
        "outer" => Keyword(Outer)
        "inner" => Keyword(Inner)
        "cross" => Keyword(Cross)
        "on" => Keyword(On)
        "using" => Keyword(Using)
        "limit" => Keyword(Limit)
        "offset" => Keyword(Offset)
        "create" => Keyword(Create)
        "table" => Keyword(Table)
        "integer" => Keyword(Integer)
        "int" => Keyword(Int)
        "smallint" => Keyword(Smallint)
        "bigint" => Keyword(Bigint)
        "real" => Keyword(Real)
        "double" => Keyword(Double)
        "precision" => Keyword(Precision)
        "char" => Keyword(Char)
        "character" => Keyword(Character)
        "collate" => Keyword(Collate)
        "authorization" => Keyword(Authorization)
        "varchar" => Keyword(Varchar)
        "varing" => Keyword(Varing)
        "text" => Keyword(Text)
        "time" => Keyword(Time)
        "boolean" => Keyword(Boolean)
        "float" => Keyword(Float)
        "timestamp" => Keyword(Timestamp)
        "blob" => Keyword(Blob)
        "null" => Keyword(Null)
        "default" => Keyword(Default)
        "unique" => Keyword(Unique)
        "view" => Keyword(View)
        "drop" => Keyword(Drop)
        "distinct" => Keyword(Distinct)
        "all" => Keyword(All)
        "substring" => Keyword(Substring)
        "for" => Keyword(For)
        "primary" => Keyword(Primary)
        "key" => Keyword(Key)
        "foreign" => Keyword(Foreign)
        "references" => Keyword(References)
        "check" => Keyword(Check)
        "union" => Keyword(Union)
        "intersect" => Keyword(Intersect)
        "except" => Keyword(Except)
        "top" => Keyword(Top)
        "insert" => Keyword(Insert)
        "into" => Keyword(Into)
        "values" => Keyword(Values)
        "merge" => Keyword(Merge)
        "if" => Keyword(If)
        "delete" => Keyword(Delete)
        "update" => Keyword(Update)
        "set" => Keyword(Set)
        "replace" => Keyword(Replace)
        "rollback" => Keyword(Rollback)
        "abort" => Keyword(Abort)
        "fail" => Keyword(Fail)
        "ignore" => Keyword(Ignore)
        "truncate" => Keyword(Truncate)
        "alter" => Keyword(Alter)
        "column" => Keyword(Column)
        "show" => Keyword(Show)
        "tables" => Keyword(Tables)
        "columns" => Keyword(Columns)
        "status" => Keyword(Status)
        "databases" => Keyword(Databases)
        "database" => Keyword(Database)
        "div" => Keyword(Div)
        "lock" => Keyword(Lock)
        "unlock" => Keyword(Unlock)
        "listen" => Keyword(Listen)
        "notify" => Keyword(Notify)
        "schemas" => Keyword(Schemas)
        "schema" => Keyword(Schema)
        "variables" => Keyword(Variables)
        "processlist" => Keyword(Processlist)
        "grants" => Keyword(Grants)
        "functions" => Keyword(Functions)
        "function" => Keyword(Function)
        "extended" => Keyword(Extended)
        "global" => Keyword(Global)
        "session" => Keyword(Session)
        "procedure" => Keyword(Procedure)
        "returns" => Keyword(Returns)
        "language" => Keyword(Language)
        "deterministic" => Keyword(Deterministic)
        "event" => Keyword(Event)
        "trigger" => Keyword(Trigger)
        "duplicate" => Keyword(Duplicate)
        "conflict" => Keyword(Conflict)
        "do" => Keyword(Do)
        "nothing" => Keyword(Nothing)
        "constraint" => Keyword(Constraint)
        "array" => Keyword(Array)
        "with" => Keyword(With)
        "begin" => Keyword(Begin)
        "start" => Keyword(Start)
        "transaction" => Keyword(Transaction)
        "commit" => Keyword(Commit)
        "savepoint" => Keyword(Savepoint)
        "release" => Keyword(Release)
        "work" => Keyword(Work)
        "grant" => Keyword(Grant)
        "revoke" => Keyword(Revoke)
        "privileges" => Keyword(Privileges)
        "usage" => Keyword(Usage)
        "use" => Keyword(Use)
        "execute" => Keyword(Execute)
        "prepare" => Keyword(Prepare)
        "deallocate" => Keyword(Deallocate)
        "immediate" => Keyword(Immediate)
        "connect" => Keyword(Connect)
        "temporary" => Keyword(Temporary)
        "temp" => Keyword(Temp)
        "option" => Keyword(Option)
        "restrict" => Keyword(Restrict)
        "cascade" => Keyword(Cascade)
        "index" => Keyword(Index)
        // Index types and options
        "btree" => Keyword(Btree)
        "hash" => Keyword(Hash)
        "gin" => Keyword(Gin)
        "gist" => Keyword(Gist)
        "spgist" => Keyword(Spgist)
        "brin" => Keyword(Brin)
        "concurrently" => Keyword(Concurrently)
        // Window function keywords
        "over" => Keyword(Over)
        "window" => Keyword(Window)
        "partition" => Keyword(Partition)
        "rows" => Keyword(Rows)
        "range" => Keyword(Range)
        "preceding" => Keyword(Preceding)
        "following" => Keyword(Following)
        "current" => Keyword(Current)
        "unbounded" => Keyword(Unbounded)
        "row" => Keyword(Row)
        // Function/procedure parameter modes
        "out" => Keyword(Out)
        "inout" => Keyword(InOut)
        // Sequence keywords
        "sequence" => Keyword(Sequence)
        "increment" => Keyword(Increment)
        "minvalue" => Keyword(Minvalue)
        "maxvalue" => Keyword(Maxvalue)
        "cache" => Keyword(Cache)
        "cycle" => Keyword(Cycle)
        "owned" => Keyword(Owned)
        "no" => Keyword(No)
        // ALTER INDEX keywords
        "rename" => Keyword(Rename)
        "tablespace" => Keyword(Tablespace)
        "reset" => Keyword(Reset)
        "copy" => Keyword(Copy)
        // COPY statement keywords
        "format" => Keyword(Format)
        "stdin" => Keyword(Stdin)
        "stdout" => Keyword(Stdout)
        "program" => Keyword(Program)
        "header" => Keyword(Header)
        "force_quote" => Keyword(ForceQuote)
        "force_not_null" => Keyword(ForceNotNull)
        "force_null" => Keyword(ForceNull)
        "encoding" => Keyword(Encoding)
        // NOTE: Removed LOAD DATA keywords from common keywords - they are now MySQL-specific
        word => Token::Identifier(word.to_string())
      }
  }
  (token, rest)
}

///|
fn is_ascii_alphabetic_or_digit(ch : Char) -> Bool {
  ch.is_ascii_alphabetic() || ch.is_ascii_digit() || ch == '_'
}

///|
/// Read PostgreSQL numbered placeholder: $1, $2, $3, etc.
fn Lexer::read_dollar_placeholder(
  _self : Lexer,
  input : StringView,
) -> (Token, StringView) {
  // Skip the $ character
  let input = match input {
    ['$', .. rest] => rest
    _ => input
  }

  // Read the number part
  let (idx, rest) = loop (input, 0) {
    ([c, .. rest], n) if c.is_ascii_digit() => continue (rest, n + 1)
    (rest, n) => break (n, rest)
  }
  let placeholder = "$" + input.view(start_offset=0, end_offset=idx).to_string()
  (Token::Placeholder(placeholder), rest)
}

///|
/// Read colon-prefixed named parameter: :name, :param1, etc.
fn Lexer::read_colon_parameter(
  _self : Lexer,
  input : StringView,
) -> (Token, StringView) {
  // Skip the : character
  let input = match input {
    [':', .. rest] => rest
    _ => input
  }

  // Read the parameter name part
  let (idx, rest) = loop (input, 0) {
    ([c, .. rest], n) if is_ascii_alphabetic_or_digit(c) =>
      continue (rest, n + 1)
    (rest, n) => break (n, rest)
  }
  let placeholder = ":" + input.view(start_offset=0, end_offset=idx).to_string()
  (Token::Placeholder(placeholder), rest)
}

///|
/// Read at-sign parameter: @name, @param1, etc.
/// This can be either a placeholder or a user variable depending on context
fn Lexer::read_at_parameter(
  _self : Lexer,
  input : StringView,
) -> (Token, StringView) {
  // Skip the @ character
  let input = match input {
    ['@', .. rest] => rest
    _ => input
  }

  // Read the parameter name part
  let (idx, rest) = loop (input, 0) {
    ([c, .. rest], n) if is_ascii_alphabetic_or_digit(c) =>
      continue (rest, n + 1)
    (rest, n) => break (n, rest)
  }
  let placeholder = "@" + input.view(start_offset=0, end_offset=idx).to_string()
  (Token::Placeholder(placeholder), rest)
}

///|
fn Lexer::read_single_quoted_string(
  self : Lexer,
  input : StringView,
) -> (Token, StringView) raise LexerError {
  let result = StringBuilder::new()
  let rest = loop input {
    [.. "''", .. rest] => {
      result.write_char('\'')
      continue rest
    }
    ['\'', .. rest] => break rest
    [.. "\\'", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\'')
      continue rest
    }
    [.. "\\\"", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\"')
      continue rest
    }
    [.. "\\\\", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\\')
      continue rest
    }
    [.. "\\n", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\n')
      continue rest
    }
    [.. "\\t", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\t')
      continue rest
    }
    [.. "\\r", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\r')
      continue rest
    }
    [c, .. rest] => {
      result.write_char(c)
      continue rest
    }
    [] => raise LexerError::UnterminatedString
  }
  (StringLiteral(result.to_string()), rest)
}

///|
fn Lexer::read_double_quoted_string(
  self : Lexer,
  input : StringView,
) -> (Token, StringView) raise LexerError {
  let result = StringBuilder::new()
  let rest = loop input {
    [.. "\\\"", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\"')
      continue rest
    }
    [.. "\\'", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\'')
      continue rest
    }
    [.. "\\\\", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\\')
      continue rest
    }
    [.. "\\n", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\n')
      continue rest
    }
    [.. "\\t", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\t')
      continue rest
    }
    [.. "\\r", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\r')
      continue rest
    }
    ['"', .. rest] => break rest
    [c, .. rest] => {
      result.write_char(c)
      continue rest
    }
    [] => raise LexerError::UnterminatedString
  }
  (StringLiteral(result.to_string()), rest)
}

///|
fn Lexer::read_backtick_quoted_identifier(
  self : Lexer,
  input : StringView,
) -> (Token, StringView) raise LexerError {
  let result = StringBuilder::new()
  let rest = loop input {
    [.. "``", .. rest] => {
      result.write_char('`')
      continue rest
    }
    [.. "\\`", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('`')
      continue rest
    }
    [.. "\\\\", .. rest] if self.dialect.supports_string_literal_backslash_escape() => {
      result.write_char('\\')
      continue rest
    }
    ['`', .. rest] => break rest
    [c, .. rest] => {
      result.write_char(c)
      continue rest
    }
    [] => raise LexerError::UnterminatedString
  }
  (Identifier(result.to_string()), rest)
}

///|
test "Double quoted string" {
  let input = "\"Hello,\\\" World!\""
  let tokens = tokenize(input)
  inspect(
    tokens,
    content=(
      #|[StringLiteral("Hello,\" World!")]
    ),
  )
}

///|
fn skip_line_comment(input : StringView) -> StringView {
  loop input {
    ['\r' | '\n', .. rest] => break rest
    [_c, .. rest] => continue rest
    [] => break []
  }
}

///|
test "Lexing one keyword" {
  let input = "SELECT"
  let tokens = tokenize(input)
  inspect(tokens, content="[Keyword(Select)]")
}

///|
test "Lexing two keywords" {
  let input = "SELECT FROM"
  let tokens = tokenize(input)
  inspect(tokens, content="[Keyword(Select), Keyword(From)]")
}

///|
test "Lexing simple query" {
  let input = "SELECT * FROM table WHERE id = 1;"
  let tokens = tokenize(input)
  inspect(
    tokens,
    content=(
      #|[Keyword(Select), Mul, Keyword(From), Keyword(Table), Keyword(Where), Identifier("id"), Eq, Number("1"), Semicolon]
    ),
  )
}

///|
test "Lexing single quoted string" {
  let input = "SELECT 'Hello, World!' FROM t;"
  let tokens = tokenize(input)
  inspect(
    tokens,
    content=(
      #|[Keyword(Select), StringLiteral("Hello, World!"), Keyword(From), Identifier("t"), Semicolon]
    ),
  )
}

///|
test "Lexing single quoted string with escaped quotes" {
  let input = "SELECT 'Hello, ''World!''!' FROM t;"
  let tokens = tokenize(input)
  inspect(
    tokens,
    content=(
      #|[Keyword(Select), StringLiteral("Hello, 'World!'!"), Keyword(From), Identifier("t"), Semicolon]
    ),
  )
}

///|
test "Lexing binary operators" {
  let input = "< = > == <> <= >= <=>"
  let tokens = tokenize(input)
  inspect(tokens, content="[Lt, Eq, Gt, DoubleEq, Neq, LtEq, GtEq, Spaceship]")
}

///|
test "Skip line comment" {
  let input = "SELECT * FROM t -- This is a comment\n WHERE id = 1;\n --x \r -- Another comment \n"
  let tokens = tokenize(input)
  inspect(
    tokens,
    content=(
      #|[Keyword(Select), Mul, Keyword(From), Identifier("t"), Keyword(Where), Identifier("id"), Eq, Number("1"), Semicolon, Eof]
    ),
  )
}

///|
test "Skip line comment 2" {
  let input = "-- License: Apache-2.0\nSELECT * FROM t -- This is a comment\n WHERE id = 1;\n --x \r -- Another comment \n 123"
  let tokens = tokenize(input)
  inspect(
    tokens,
    content=(
      #|[Keyword(Select), Mul, Keyword(From), Identifier("t"), Keyword(Where), Identifier("id"), Eq, Number("1"), Semicolon, Number("123")]
    ),
  )
}

///|
test "Decimal point only" {
  let input = "123 . 123 .123 123.123 .1 1.1 22.22;"
  let tokens = tokenize(input)
  inspect(
    tokens,
    content=(
      #|[Number("123"), Period, Number("123"), Number(".123"), Number("123.123"), Number(".1"), Number("1.1"), Number("22.22"), Semicolon]
    ),
  )
}

///|
test "Decimal point" {
  let input = "SELECT 3.14, .11 FROM t;"
  let tokens = tokenize(input)
  inspect(
    tokens,
    content=(
      #|[Keyword(Select), Number("3.14"), Comma, Number(".11"), Keyword(From), Identifier("t"), Semicolon]
    ),
  )
}

///|
test "Complicated" {
  let input = "l_linestatus;\n"
  let tokens = tokenize(input)
  inspect(
    tokens,
    content=(
      #|[Identifier("l_linestatus"), Semicolon, Eof]
    ),
  )
}

///|
test "Single quoted string with escaped backslash" {
  let input = "SELECT 'Hello, \\'World!\\'' FROM t;"
  let tokens = tokenize(dialect=MySQL::{  }, input)
  inspect(
    tokens,
    content=(
      #|[Keyword(Select), StringLiteral("Hello, 'World!'"), Keyword(From), Identifier("t"), Semicolon]
    ),
  )
}

///|
test "MySQL backtick quoted identifiers" {
  let input = "SELECT `column name` FROM `table name`;"
  let tokens = tokenize(dialect=MySQL::{  }, input)
  inspect(
    tokens,
    content=(
      #|[Keyword(Select), Identifier("column name"), Keyword(From), Identifier("table name"), Semicolon]
    ),
  )
}

///|
test "MySQL backtick with escaped backticks" {
  let input = "SELECT `column``with``backticks` FROM t;"
  let tokens = tokenize(dialect=MySQL::{  }, input)
  inspect(
    tokens,
    content=(
      #|[Keyword(Select), Identifier("column`with`backticks"), Keyword(From), Identifier("t"), Semicolon]
    ),
  )
}