defmodule ExSQL.Parser do @moduledoc """ Recursive-descent parser producing `ExSQL.AST` structs. SQLite generates its parser from a Lemon LALR grammar (`parse.y`); a hand-written recursive descent with precedence climbing is the idiomatic Elixir equivalent and keeps the grammar readable as plain functions, one per production. ## Expression representation Expressions are tagged tuples: * `{:literal, value}` — `nil`, integer, float, UTF-8 binary, `{:blob, bin}`, or boolean (`TRUE`/`FALSE` literals) * `{:column, table | nil, name}` — column reference * `{:binary, op, left, right}` — `op` in `:or | :and | :eq | :ne | :lt | :le | :gt | :ge | :add | :sub | :mul | :div | :mod | :concat` * `{:not, expr}` / `{:negate, expr}` * `{:is, expr, expr}` / `{:is_not, expr, expr}` — NULL-safe comparison * `{:in, expr, [expr] | {:select, query}, negated?}` * `{:between, expr, low, high, negated?}` * `{:like, expr, pattern, negated?}` / `{:glob, expr, pattern, negated?}` * `{:function, name, :star | [expr]}` — e.g. `count(*)` * `{:case, operand | nil, [{when_expr, then_expr}], else_expr | nil}` * `{:subquery, %Select{}}` — scalar subquery * `{:exists, %Select{}}` — `EXISTS (SELECT ...)` """ alias ExSQL.AST.{ AlterTable, ColumnDef, Compound, CreateIndex, CreateTable, CreateTrigger, CreateView, Delete, DropIndex, DropTable, DropView, Insert, Pragma, Select, Update, Values, With } alias ExSQL.{Error, Tokenizer} @type expr :: tuple() @type statement :: Select.t() | Compound.t() | Values.t() | With.t() | CreateTable.t() | CreateIndex.t() | CreateView.t() | AlterTable.t() | Insert.t() | Pragma.t() | Update.t() | Delete.t() | DropTable.t() | DropIndex.t() | DropView.t() | {:attach, expr(), String.t()} | {:detach, String.t()} | {:explain, statement()} | {:explain_query_plan, statement()} @doc """ Parses `sql` into a list of statements. Accepts multiple statements separated by `;`. Returns `{:ok, statements}` or `{:error, %ExSQL.Error{}}`. """ @spec parse(String.t()) :: {:ok, [statement()]} | {:error, Error.t()} def parse(sql) when is_binary(sql) do with {:ok, tokens} <- tokenize(sql) do {:ok, statements(tokens, [])} end rescue e in Error -> {:error, e} end defp tokenize(sql) do case Tokenizer.tokenize(sql) do {:ok, tokens} -> {:ok, tokens} {:error, reason} -> {:error, %Error{ message: "tokenizer error: #{inspect(reason)}", line: elem(reason, tuple_size(reason) - 1) }} end end defp statements([], acc), do: Enum.reverse(acc) defp statements([{:semicolon, _, _} | rest], acc), do: statements(rest, acc) defp statements(tokens, acc) do {stmt, rest} = statement(tokens) case rest do [] -> statements(rest, [stmt | acc]) [{:semicolon, _, _} | rest] -> statements(rest, [stmt | acc]) [{_, value, line} | _] -> fail(~s(near "#{token_text(value)}": syntax error), line) end end # -- statement dispatch --------------------------------------------------- defp statement([{:keyword, :select, _} | _] = tokens), do: select(tokens) defp statement([{:keyword, :values, _} | _] = tokens), do: select(tokens) defp statement([{:keyword, :with, _} | _] = tokens), do: with_statement(tokens) defp statement([{:keyword, :create, _} | _] = tokens), do: create_stmt(tokens) defp statement([{:keyword, :insert, _} | _] = tokens), do: insert(tokens) defp statement([{:keyword, :replace, _} | _] = tokens), do: insert(tokens) defp statement([{:keyword, :update, _} | _] = tokens), do: update(tokens) defp statement([{:keyword, :delete, _} | _] = tokens), do: delete(tokens) defp statement([{:keyword, :drop, _} | _] = tokens), do: drop_stmt(tokens) defp statement([{:keyword, :alter, _} | _] = tokens), do: alter_table(tokens) defp statement([{:keyword, :pragma, _} | _] = tokens), do: pragma(tokens) defp statement([{:keyword, :explain, line} | rest]), do: explain_statement(rest, line) defp statement([{:keyword, :begin, _} | rest]), do: begin_statement(rest) defp statement([{:keyword, :commit, _} | rest]), do: commit_statement(rest) defp statement([{:keyword, :end, _} | rest]), do: commit_statement(rest) defp statement([{:keyword, :rollback, _} | rest]), do: rollback_statement(rest) defp statement([{:keyword, :savepoint, _} | rest]), do: savepoint_statement(rest) defp statement([{:keyword, :release, _} | rest]), do: release_statement(rest) defp statement([{:keyword, :analyze, _} | rest]), do: operational_statement(:analyze, rest) defp statement([{:keyword, :vacuum, _} | rest]), do: operational_statement(:vacuum, rest) defp statement([{:keyword, :reindex, _} | rest]), do: operational_statement(:reindex, rest) defp statement([{:keyword, :attach, line} | rest]), do: attach_statement(rest, line) defp statement([{:keyword, :detach, line} | rest]), do: detach_statement(rest, line) defp statement([{_, value, line} | _]), do: fail("expected a statement, got #{inspect(value)}", line) defp operational_statement(kind, tokens) do {name, rest} = optional_qualified_identifier(tokens) {{kind, name}, rest} end defp attach_statement(tokens, line) do tokens = skip_optional_database_keyword(tokens) {filename_expr, rest} = expression(tokens) rest = expect_keyword(rest, :as, line) {name, rest} = identifier(rest) {{:attach, filename_expr, name}, rest} end defp detach_statement(tokens, _line) do tokens = skip_optional_database_keyword(tokens) {name, rest} = identifier(tokens) {{:detach, name}, rest} end defp skip_optional_database_keyword([{:keyword, :database, _} | rest]), do: rest defp skip_optional_database_keyword(tokens), do: tokens defp optional_qualified_identifier([]), do: {nil, []} defp optional_qualified_identifier([{:semicolon, _, _} | _] = tokens), do: {nil, tokens} defp optional_qualified_identifier(tokens) do case tokens do [{:keyword, kw, _} | _] when kw in [:where, :order, :limit, :returning] -> {nil, tokens} _ -> {schema, name, rest} = qualified_schema_identifier(tokens) {if(schema, do: schema <> "." <> name, else: name), rest} end end defp explain_statement( [{:keyword, :query, _}, {:keyword, :plan, _} | rest], _line ) do {stmt, rest} = statement(rest) {{:explain_query_plan, stmt}, rest} end defp explain_statement(tokens, _line) do {stmt, rest} = statement(tokens) {{:explain, stmt}, rest} end defp pragma([{:keyword, :pragma, line} | rest]) do {schema, name, rest} = pragma_name(rest, line) name = String.downcase(name) {stmt, rest} = case name do "database_list" -> {arg, rest} = optional_pragma_arg(rest) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} "collation_list" -> {arg, rest} = optional_pragma_arg(rest) {%Pragma{name: name, arg: arg}, rest} "compile_options" -> {arg, rest} = optional_pragma_arg(rest) {%Pragma{name: name, arg: arg}, rest} "function_list" -> {arg, rest} = optional_pragma_arg(rest) {%Pragma{name: name, arg: arg}, rest} "module_list" -> {arg, rest} = optional_pragma_arg(rest) {%Pragma{name: name, arg: arg}, rest} "pragma_list" -> {arg, rest} = optional_pragma_arg(rest) {%Pragma{name: name, arg: arg}, rest} "encoding" -> {arg, rest} = optional_pragma_setting_arg(rest) {%Pragma{name: name, arg: arg}, rest} "foreign_keys" -> {arg, rest} = optional_pragma_setting_arg(rest) {%Pragma{name: name, arg: arg}, rest} "defer_foreign_keys" -> {arg, rest} = optional_pragma_setting_arg(rest) {%Pragma{name: name, arg: arg}, rest} "recursive_triggers" -> {arg, rest} = optional_pragma_setting_arg(rest) {%Pragma{name: name, arg: arg}, rest} "ignore_check_constraints" -> {arg, rest} = optional_pragma_setting_arg(rest) {%Pragma{name: name, arg: arg}, rest} name when name in [ "count_changes", "case_sensitive_like", "short_column_names", "full_column_names", "reverse_unordered_selects", "read_uncommitted", "query_only", "cell_size_check", "checkpoint_fullfsync", "empty_result_callbacks", "fullfsync", "trusted_schema" ] -> {arg, rest} = optional_pragma_setting_arg(rest) {%Pragma{name: name, arg: arg}, rest} "automatic_index" -> {arg, rest} = optional_pragma_setting_arg(rest) {%Pragma{name: name, arg: arg}, rest} "auto_vacuum" -> {arg, rest} = optional_pragma_setting_arg(rest) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} "incremental_vacuum" -> {arg, rest} = optional_pragma_arg(rest) {%Pragma{name: name, arg: arg}, rest} "shrink_memory" -> {arg, rest} = optional_pragma_arg(rest) {%Pragma{name: name, arg: arg}, rest} "mmap_size" -> {arg, rest} = optional_pragma_setting_arg(rest) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} name when name in [ "analysis_limit", "busy_timeout", "hard_heap_limit", "soft_heap_limit", "threads" ] -> {arg, rest} = optional_pragma_setting_arg(rest) {%Pragma{name: name, arg: arg}, rest} name when name in [ "cache_spill", "default_cache_size", "journal_size_limit", "max_page_count", "secure_delete", "wal_autocheckpoint" ] -> {arg, rest} = optional_pragma_setting_arg(rest) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} "wal_checkpoint" -> {arg, rest} = optional_pragma_arg(rest) {%Pragma{name: name, arg: arg}, rest} "optimize" -> {arg, rest} = optional_pragma_arg(rest) {%Pragma{name: name, arg: arg}, rest} "stats" -> {arg, rest} = optional_pragma_arg(rest) {%Pragma{name: name, arg: arg}, rest} "data_version" -> {arg, rest} = optional_pragma_setting_arg(rest) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} "page_count" -> {arg, rest} = optional_pragma_setting_arg(rest) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} name when name in ["page_size", "cache_size"] -> {arg, rest} = optional_pragma_setting_arg(rest) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} name when name in ["journal_mode", "locking_mode", "synchronous", "temp_store"] -> {arg, rest} = optional_pragma_setting_arg(rest) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} name when name in ["schema_version", "user_version", "application_id"] -> {arg, rest} = optional_pragma_setting_arg(rest) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} "table_list" -> {arg, rest} = optional_pragma_identifier_arg(rest, line, name) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} "freelist_count" -> {arg, rest} = optional_pragma_setting_arg(rest) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} "foreign_key_check" -> {arg, rest} = optional_pragma_identifier_arg(rest, line, name) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} name when name in ["integrity_check", "quick_check"] -> {arg, rest} = optional_pragma_integrity_arg(rest, line, name) {%Pragma{name: name, arg: arg}, rest} name when name in [ "table_info", "table_xinfo", "foreign_key_list", "index_list", "index_info", "index_xinfo" ] -> {arg, rest} = optional_pragma_identifier_arg(rest, line, name) arg = pragma_schema_arg(schema, arg) {%Pragma{name: name, arg: arg}, rest} _ -> fail("unsupported PRAGMA: #{name}", line) end {schema_pragma(stmt, schema), rest} end defp pragma_name(tokens, _line) do {first, rest} = identifier(tokens) case rest do [{:dot, _, _} | rest] -> schema = String.downcase(first) {name, rest} = identifier(rest) {schema, name, rest} rest -> {nil, first, rest} end end defp pragma_schema_arg(nil, arg), do: arg defp pragma_schema_arg(schema, arg), do: {:schema, schema, arg} defp schema_pragma(stmt, nil), do: stmt defp schema_pragma(%Pragma{arg: {:schema, _schema, _arg}} = stmt, _outer_schema), do: stmt defp schema_pragma(%Pragma{arg: arg} = stmt, schema), do: %{stmt | arg: {:schema, schema, arg}} defp pragma_arg([{:lparen, _, _}, {:string, name, _}, {:rparen, _, _} | rest], _line, _name), do: {name, rest} defp pragma_arg([{:lparen, _, _} | rest], _line, _name) do {name, rest} = identifier(rest) {name, expect(rest, :rparen)} end defp pragma_arg([{:eq, _, _}, {:string, name, _} | rest], _line, _name), do: {name, rest} defp pragma_arg([{:eq, _, _} | rest], _line, _name), do: identifier(rest) defp pragma_arg(tokens, line, name), do: fail("PRAGMA #{name} requires an argument", line_of(tokens) || line) defp optional_pragma_arg([{:lparen, _, _} | rest]), do: {nil, skip_until_rparen(rest)} defp optional_pragma_arg([{:eq, _, _} | rest]), do: {nil, elem(expression(rest), 1)} defp optional_pragma_arg(tokens), do: {nil, tokens} defp optional_pragma_identifier_arg([{:lparen, _, _} | _] = tokens, line, name), do: pragma_arg(tokens, line, name) defp optional_pragma_identifier_arg([{:eq, _, _} | _] = tokens, line, name), do: pragma_arg(tokens, line, name) defp optional_pragma_identifier_arg(tokens, _line, _name), do: {nil, tokens} defp optional_pragma_integrity_arg( [{:lparen, _, _}, {:int, value, _}, {:rparen, _, _} | rest], _line, _name ), do: {value, rest} defp optional_pragma_integrity_arg( [{:lparen, _, _}, {:minus, _, _}, {:int, value, _}, {:rparen, _, _} | rest], _line, _name ), do: {-value, rest} defp optional_pragma_integrity_arg( [{:lparen, _, _}, {:string, name, _}, {:rparen, _, _} | rest], _line, _name ), do: {name, rest} defp optional_pragma_integrity_arg([{:lparen, _, _} | _] = tokens, line, name), do: pragma_arg(tokens, line, name) defp optional_pragma_integrity_arg([{:eq, _, _} | rest], _line, _name), do: pragma_setting_value(rest) defp optional_pragma_integrity_arg(tokens, _line, _name), do: {nil, tokens} defp optional_pragma_setting_arg([{:lparen, _, _} | rest]) do {value, rest} = pragma_setting_value(rest) {value, expect(rest, :rparen)} end defp optional_pragma_setting_arg([{:eq, _, _} | rest]), do: pragma_setting_value(rest) defp optional_pragma_setting_arg(tokens), do: {nil, tokens} defp pragma_setting_value([{:int, value, _} | rest]), do: {value, rest} defp pragma_setting_value([{:minus, _, _}, {:int, value, _} | rest]), do: {-value, rest} defp pragma_setting_value([{:string, value, _} | rest]), do: {value, rest} defp pragma_setting_value([{:id, value, _} | rest]), do: {value, rest} defp pragma_setting_value([{:keyword, value, _} | rest]), do: {Atom.to_string(value), rest} defp pragma_setting_value(tokens) do {expr, rest} = expression(tokens) {expr, rest} end # -- transactions ------------------------------------------------------------ # # BEGIN [DEFERRED|IMMEDIATE|EXCLUSIVE] [TRANSACTION [name]] — the locking # mode and transaction name are accepted and ignored, as in SQLite. defp begin_statement(tokens) do tokens = case tokens do [{:id, mode, _} | rest] -> if String.downcase(mode) in ~w(deferred immediate exclusive), do: rest, else: tokens tokens -> tokens end {{:begin}, tokens |> skip_transaction_keyword() |> skip_transaction_name()} end defp commit_statement(tokens), do: {{:commit}, tokens |> skip_transaction_keyword() |> skip_transaction_name()} defp rollback_statement(tokens) do tokens = skip_transaction_keyword(tokens) case tokens do [{:keyword, :to, _} | rest] -> rest = case rest do [{:keyword, :savepoint, _} | rest] -> rest rest -> rest end {name, rest} = savepoint_name(rest) {{:rollback_to, name}, rest} tokens -> {{:rollback}, skip_transaction_name(tokens)} end end defp savepoint_statement(tokens) do {name, rest} = savepoint_name(tokens) {{:savepoint, name}, rest} end defp release_statement(tokens) do tokens = case tokens do [{:keyword, :savepoint, _} | rest] -> rest tokens -> tokens end {name, rest} = savepoint_name(tokens) {{:release, name}, rest} end defp skip_transaction_keyword([{:keyword, :transaction, _} | rest]), do: rest defp skip_transaction_keyword(tokens), do: tokens defp skip_transaction_name([{:id, _, _} | rest]), do: rest defp skip_transaction_name([{:string, _, _} | rest]), do: rest defp skip_transaction_name(tokens), do: tokens defp savepoint_name([{:id, name, _} | rest]), do: {name, rest} defp savepoint_name([{:string, name, _} | rest]), do: {name, rest} defp savepoint_name([{_, value, line} | _]), do: fail(~s(near "#{token_text(value)}": syntax error), line) defp savepoint_name([]), do: fail("expected a savepoint name, got end of input", nil) # -- WITH (CTEs) ----------------------------------------------------------- # # WITH [RECURSIVE] name [(cols)] AS (query) [, ...]