# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: 2025 DBVisor # https://standards.iso.org/iso-iec/9075/-2/ed-6/en/ # https://standards.iso.org/ittf/PubliclyAvailableStandards/ISO_IEC_9075-1_2023_ed_6_-_id_76583_Publication_PDF_(en).zip # "standard/ISO_IEC_9075-2(E)_Foundation.bnf.txt" defmodule SQL.BNF do @moduledoc false @doc false def parse(opts \\ %{}) do case opts do %{path: path} -> File.cwd!() |> Path.join(path) |> File.read!() |> parse(opts) %{download: true} -> {:ok, {_, _, body}} = :httpc.request(:get, {~c"https://standards.iso.org/iso-iec/9075/-2/ed-6/en/ISO_IEC_9075-2(E)_Foundation.bnf.txt", []}, [], [body_format: :binary]) parse(body, opts) opts when is_binary(opts) -> parse(opts, %{}) end end defp parse(binary, opts, type \\ :symbol, data \\ [], acc \\ [], symbol \\ [], expr \\ [], rules \\ {[], []}) defp parse(<<>>, opts, type, data, acc, symbol, expr, rules) do {terminals, non_terminals} = merge(type, rules, symbol, [expr | [acc | data]]) {terminals, non_terminals} = Enum.reduce(non_terminals, {terminals, []}, fn {r, [v]} = rule, {terminals, non_terminals} -> value = Enum.find(terminals, &(elem(&1, 0) == v)) if value do {[{r, elem(value, 1)} | terminals], non_terminals} else {terminals, [rule | non_terminals]} end rule, {terminals, non_terminals} -> {terminals, [rule | non_terminals]} end) exprs = Enum.flat_map(non_terminals, &elem(&1, 1)) %{true: non_terminals, false: root} = Enum.group_by(non_terminals, &(elem(&1, 0) in exprs)) {keywords, operators, letters, digits, terminals} = Enum.reduce(terminals, {[], [], [], [], []}, fn {r, e} = rule, {keywords, operators, letters, digits, terminals} -> cond do String.ends_with?(r, "word>") == true -> e = if is_map_key(opts, r), do: e ++ opts[r], else: e {[{r, (for v <- e, v not in ["|", "AS"], do: {atom(v), quoted_inline_match(v)})} | keywords], operators, letters, digits, terminals} String.ends_with?(r, "letter>") == true -> {keywords, operators, [{r, Enum.reject(e, &(&1 == "|"))}|letters], digits, terminals} String.ends_with?(r, "digit>") == true -> {keywords, operators, letters, [{r, Enum.reject(e, &(&1 == "|"))}|digits], terminals} String.ends_with?(r, "operator>") == true -> {keywords, [rule | operators], letters, digits, terminals} r in ~w[ ] -> {keywords, [rule | operators], letters, digits, terminals} true -> {keywords, operators, letters, digits, [rule | terminals]} end end) symbols = terminals ++ operators operators = opts |> Map.get(:operators, []) |> Kernel.++(operators) |> Enum.sort_by(fn {_, [b]} -> byte_size(b) {_, _, [b |_]} -> byte_size(b) end, :desc) |> Enum.map(fn {r, e} -> {r, to_quoted_operator(e)} {r, _, e} -> {r, to_quoted_operator(e)} end) special_characters = Enum.filter(non_terminals ++ root, &(String.ends_with?(elem(&1, 0), "special character>") || String.ends_with?(elem(&1, 0), "special symbol>"))) special_characters = for {_, e} <- special_characters, v <- e, v != "|", do: {String.to_atom(v), elem(Enum.find(symbols, {v, v}, &elem(&1, 0) == v), 1)} special_characters = special_characters ++ [tilde: ["~"], number_sign: ["#"]] special_characters = Enum.filter(special_characters, &byte_size(hd(elem(&1, 1))) == 1) %{count: {length(terminals), length(non_terminals), length(root)}, letters: letters, digits: Map.new(digits), special_characters: special_characters, keywords: Map.new(keywords), operators: operators, root: root, terminals: Map.new(terminals), non_terminals: Map.new(non_terminals)} end defp parse(<>, opts, :symbol = type, symbol, _acc, _data, _expr, rules) do parse(rest, opts, type, [], [], symbol, [], rules) end defp parse(<>, opts, type, data, acc, symbol, expr, rules) when type in [:non_terminal, :terminal] and (b in ?a..?z or b in ?A..?Z) do parse(<>, opts, :symbol, [], [], [], [], merge(type, rules, symbol, [expr | [acc | data]])) end defp parse(<>, opts, type, data, acc, symbol, expr, rules) when b in [?\s, ?\t, ?\r, ?\n, ?\f] do parse(rest, opts, :terminal, [], [], "#{data}", [], merge(type, rules, symbol, [expr | acc])) end defp parse(<>, opts, type, [?!, ?! | _] = data, acc, symbol, expr, rules) do parse(rest, opts, type, [], [acc, [data | ~c"."]], symbol, expr, rules) end defp parse(<>, opts, type, data, acc, symbol, expr, rules) do parse(rest, opts, type, [data | ~c" ..."], acc, symbol, expr, rules) end defp parse(<>, opts, type, data, acc, symbol, expr, rules) do parse(rest, opts, type, [data | ~c"|"], acc, symbol, expr, rules) end defp parse(<>, opts, type, data, acc, symbol, expr, rules) when b in [?\s, ?\t, ?\r, ?\f] and type != :symbol and (c in ?a..?z or c in ?A..?Z) do parse(rest, opts, :non_terminal, [data | [?<, c]], acc, symbol, expr, rules) end defp parse(<>, opts, type, data, acc, symbol, expr, rules) when b in [?\s, ?\t, ?\r, ?\f] and c not in [?\s, ?\t, ?\r, ?\f, ?\n] do parse(rest, opts, type, [data | [b, c]], acc, symbol, expr, rules) end defp parse(<>, opts, type, data, acc, symbol, expr, rules) when b in [?\n, ?\s, ?\t, ?\r, ?\n, ?\f] do parse(rest, opts, type, data, acc, symbol, expr, rules) end defp parse(<>, opts, type, data, acc, symbol, expr, rules) do parse(rest, opts, type, [data | [b]], acc, symbol, expr, rules) end @syntax_rules "!! See the Syntax Rules." defp merge(_type, rules, [], expr) when expr in [[], ""], do: rules defp merge(type, rules, rule, expr) when is_list(expr), do: merge(type, rules, rule, String.trim("#{expr}")) defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {[{symbol, ["\u0020"]} | terminals], non_terminals} # 32 \u0020 defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {[{symbol, ["[[:Lu:], [:Ll:], [:Lt:], [:Lm:], [:Lo:], [:Nl:]]"]} | terminals], non_terminals} # "Lu", "Ll", "Lt", "Lm", "Lo", or "Nl" Unicode.Set.match?(<>, "[[:Lu:], [:Ll:], [:Lt:], [:Lm:], [:Lo:], [:Nl:]]") defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {[{symbol, ["[[:Mn:], [:Mc:], [:Nd:], [:Pc:], [:Cf:]]"]} | terminals], non_terminals} # 183 \u00B7 or "Mn", "Mc", "Nd", "Pc", or "Cf" Unicode.Set.match?(<>, "[[:Mn:], [:Mc:], [:Nd:], [:Pc:], [:Cf:]]") defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {[{symbol, ["\\u"]} | terminals], non_terminals} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {[{symbol, [{:non_double_quote_character, [], "b != ?\""}]} | terminals], non_terminals} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {[{symbol, ["\u0009", "\u000D", "\u00A0", "\u00A0", "\u1680", "\u2000", "\u2001", "\u2002", "\u2003", "\u2004", "\u2005", "\u2006", "\u2007", "\u2008", "\u2009", "\u200A", "\u202F", "\u205F", "\u3000", "\u180E", "\u200B", "\u200C", "\u200D", "\u2060", "\uFEFF"]} | terminals], non_terminals} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, expr), do: {terminals, [map(symbol, String.replace(expr, @syntax_rules, "")) | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {[{symbol, ["\u000A", "\u000B", "\u000C", "\u000D", "\u0085", "\u2028", "\u2029"]} | terminals], non_terminals} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [{:not_qoute, "b != ?'", ""}]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [{:not_escape, "b != ?\\", ""}]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [{:escape, "b == ?\\", ""}]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, ["", "", ""]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, ["", "", ""]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, ["","", ""]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, ["","", ""]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [""]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} # would be dialect specific defp merge(_type, {terminals, non_terminals}, symbol, expr) when symbol in ["", ""], do: {terminals, [map(symbol, String.replace(expr, @syntax_rules, "")) | non_terminals]} # no between s defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} # would be dialect specific defp merge(_type, {terminals, non_terminals}, "" = symbol, @syntax_rules), do: {terminals, [{symbol, [:ignore]} | non_terminals]} # would be dialect specific defp merge(_type, _rules, symbol, @syntax_rules), do: raise "Please apply rules for #{symbol} by referencing the PDF or https://github.com/ronsavage/SQL/blob/master/Syntax.rules.txt" defp merge(:terminal, {terminals, non_terminals}, symbol, expr), do: {[map(symbol, expr) | terminals], non_terminals} defp merge(:non_terminal, {terminals, non_terminals}, symbol, expr), do: {terminals, [map(symbol, expr) | non_terminals]} @doc false def choice(value, group \\ [], acc \\ []) def choice([], group, []), do: Enum.reverse(group) def choice([], [], acc), do: Enum.reverse(acc) def choice([], group, acc), do: Enum.reverse([Enum.reverse(group) | acc]) def choice(["|" | rest], [] = group, acc), do: choice(rest, group, acc) def choice(["|" | rest], group, acc), do: choice(rest, [], [Enum.reverse(group) | acc]) def choice([k | rest], group, acc), do: choice(rest, [k | group], acc) @doc false def optional(value, acc \\ []) def optional([], acc), do: choice(Enum.reverse(acc)) def optional(["]" | rest], acc), do: {rest, optional([], acc)} def optional(["[" | rest], acc) do case optional(rest, []) do {rest, optional} -> optional(rest, [{:optional, optional} | acc]) optional -> optional(rest, [{:optional, optional} | acc]) end end def optional([node | rest], acc), do: optional(rest, [node | acc]) @doc false def group(value, acc \\ []) def group([], acc), do: optional(repeat(Enum.reverse(acc))) def group(["}" | rest], acc), do: {rest, {:group, group([], acc)}} def group(["{" | rest], acc) do case group(rest, []) do {rest, group} -> group(rest, [group | acc]) group -> group(rest, [group | acc]) end end def group([node | rest], acc), do: group(rest, [node | acc]) def group(expr, acc), do: group([], [expr | acc]) @doc false def repeat(value, acc \\ []) def repeat([], acc), do: Enum.reverse(acc) def repeat([node, "..." | rest], acc), do: repeat(rest, [{:repeat, node} | acc]) def repeat([node | rest], acc), do: repeat(rest, [node | acc]) defp map(symbol, [v] = expr) when is_tuple(v), do: {symbol, expr} defp map(symbol, expr) when expr in ["|", "[", "]", [:ignore], ["\u0020"], ["\u0009", "\u000D", "\u00A0", "\u00A0", "\u1680", "\u2000", "\u2001", "\u2002", "\u2003", "\u2004", "\u2005", "\u2006", "\u2007", "\u2008", "\u2009", "\u200A", "\u202F", "\u205F", "\u3000", "\u180E", "\u200B", "\u200C", "\u200D", "\u2060", "\uFEFF"], ["\u000A", "\u000B", "\u000C", "\u000D", "\u0085", "\u2028", "\u2029"]], do: {symbol, List.wrap(expr)} defp map(symbol, expr) when is_list(expr), do: {symbol, expr} defp map(symbol, expr) when is_binary(expr) do {symbol, ~r"<[^>]*>" |> Regex.split(expr, include_captures: true, trim: true) |> Enum.reject(&(&1 == " ")) |> Enum.flat_map(fn <> = nt -> [nt] expr -> String.split(String.trim(expr), " ", trim: true) end)} end @doc false def cast(<> = expr) when b in ?a..?z or b in ?A..?Z, do: expr def cast(expr) when expr in ["|", "{", "}", "[", "]", :ignore, :self, "\u0020", "\u0009", "\u000D", "\u00A0", "\u00A0", "\u1680", "\u2000", "\u2001", "\u2002", "\u2003", "\u2004", "\u2005", "\u2006", "\u2007", "\u2008", "\u2009", "\u200A", "\u202F", "\u205F", "\u3000", "\u180E", "\u200B", "\u200C", "\u200D", "\u2060", "\uFEFF", "\u000A", "\u000B", "\u000C", "\u000D", "\u0085", "\u2028", "\u2029"], do: expr def cast(expr) when is_binary(expr), do: {atom(expr), quoted_inline_match(expr)} def cast(expr) when is_tuple(expr) or is_list(expr) or is_atom(expr), do: expr @doc false def atom(value), do: String.to_atom(String.replace(String.replace(String.downcase(value), ["<", ">"], ""), ["/", " "], "_")) @doc false def to_quoted_operator(v) do for e <- v do if String.match?(e, ~r/[^A-Za-z]/) do {String.to_atom(e), quoted_inline_match(e)} else {atom(e), quoted_inline_match(e)} end end end @doc false def quoted_inline_match(value) do for <>, reduce: [] do acc -> [k|acc] end end @doc false def get_rules() do operators = [ {:asterisk, [type: :mysql], ["*"]}, {:solidus, [type: :mysql], ["/"]}, {:plus, [type: :mysql], ["+"]}, {:minus, [type: :mysql], ["-"]}, {:not, [type: :mysql], ["!"]}, {:logical_and, [type: :mysql], ["&&"]}, {:logical_or, [type: :mysql], ["||"]}, {:bitwise_and, [type: :mysql], ["&"]}, {:bitwise_and_assignment, [type: :tds], ["&="]}, {:bitwise_xor, [type: :mysql], ["^"]}, {:bitwise_xor_assignment, [type: :tds], ["^="]}, {:bitwise_or, [type: :mysql], ["|"]}, {:bitwise_or_assignment, [type: :tds], ["|=", "|*="]}, {:bitwise_exclusive_assignment, [type: :sql], ["^-="]}, {:bitwise_invasion, [type: :mysql], ["~"]}, {:right_shift, [type: :mysql], [">>"]}, {:left_shift, [type: :mysql], ["<<"]}, {:null_safe_equals_operator, [type: :mysql], ["<=>"]}, {:mod, [type: :mysql], ["%"]}, {:json_extract, [type: :mysql], ["->"]}, {:json_unquote_json_extract, [type: :mysql], ["->>"]}, {:assign_value, [type: :mysql], [":="]}, {:add_assignment, [type: :tds], ["+="]}, {:subtract_assignment, [type: :tds], ["-="]}, {:multiply_assignment, [type: :tds], ["*="]}, {:divide_assignment, [type: :tds], ["/="]}, {:mod_assignment, [type: :tds], ["%="]}, {:not_greater_then, [type: :tds], ["!>"]}, {:not_less_then, [type: :tds], ["!<"]}, {:right_containment, [type: :postgres], ["@>"]}, {:left_containment, [type: :postgres], ["<@"]}, {:sqaure_root, [type: :postgres], ["|/"]}, {:cube_root, [type: :postgres], ["||/"]}, {:abs, [type: :postgres], ["@"]}, {:bitwise_exclusive_or, [type: :postgres], ["#"]}, {:starts_with, [type: :postgres], ["^@"]}, {:regex_match, [type: :postgres], ["~*"]}, {:not_regex_match_case, [type: :postgres], ["!~"]}, {:not_regex_match, [type: :postgres], ["!~*"]}, {:bitwise_exclusive_or, [type: :postgres], ["##"]}, {:right_extend, [type: :postgres], ["&<"]}, {:left_extend, [type: :postgres], ["&>"]}, {:right_below, [type: :postgres], ["<<|"]}, {:left_below, [type: :postgres], ["|>>"]}, {:not_right_extend, [type: :postgres], ["&<|"]}, {:not_left_extend, [type: :postgres], ["|&>"]}, {:right_below, [type: :postgres], ["<^"]}, {:left_below, [type: :postgres], [">^"]}, {:object_intersect, [type: :postgres], ["?#"]}, {:horizontal_line, [type: :postgres], ["?-"]}, {:vertical_line, [type: :postgres], ["?|"]}, {:perpendicular_line, [type: :postgres], ["?-|"]}, {:parallel_line, [type: :postgres], ["?||"]}, {:regex_match, [type: :postgres], ["~="]}, {:right_containment, [type: :postgres], ["<<="]}, {:left_containment, [type: :postgres], [">>="]}, {:tsvector_match, [type: :postgres], ["@@"]}, {:negate_tsquery, [type: :postgres], ["!!"]}, {:json_extract, [type: :postgres], ["#>"]}, {:json_extract_text, [type: :postgres], ["#>>"]}, {:json_array_containment, [type: :postgres], ["?&"]}, {:json_delete, [type: :postgres], ["#-"]}, {:json_path_return, [type: :postgres], ["@?"]}, {:ranges_adjacent, [type: :postgres], ["-|-"]}, {:cast, [type: :postgres], ["::"]}, {:not_equal, [type: :postgres], ["!="]}, {:as, [type: :postgres], ["as"]}, {:ilike, [type: :postgres], ["ilike"]}, {:like, [type: :postgres], ["like"]}, {:in, [type: :postgres], ["in"]}, {:and, [type: :postgres], ["and"]}, {:or, [type: :postgres], ["or"]}, {:is, [type: :postgres], ["is"]}, {:not, [type: :postgres], ["not"]}, {:then, [type: :postgres], ["then"]}, {:escape, [type: :postgres], ["escape"]}, ] rules = SQL.BNF.parse(%{ "" => ~w[| LIMIT | ILIKE | BACKWARD | FORWARD | ISNULL | NOTNULL | HSTORE], operators: operators, download: true }) {Enum.uniq(rules.keywords[""] ++ rules.keywords[""]), Enum.uniq(rules.keywords[""]), Enum.uniq(Enum.flat_map(rules.operators, &elem(&1, 1)))} end end