Packages

Elixir NIF wrapper for the sqlparser Rust crate — parse and reconstruct SQL across multiple dialects

Current section

Files

Jump to
sql_parser_ex lib sql_parser_ex.ex
Raw

lib/sql_parser_ex.ex

defmodule SqlParserEx do
@moduledoc """
Elixir NIF wrapper for the `sqlparser` Rust crate.
Parses SQL into an AST (as a decoded map) and reconstructs SQL from an AST.
## Dialects
Pass a dialect atom as the `dialect:` option. Defaults to `:generic`.
## Limitations
`to_sql/2` accepts a `dialect:` option for API consistency, but the underlying Rust
serializer uses a dialect-agnostic Display implementation. The dialect argument is
validated but does not affect the reconstructed SQL output.
## Examples
iex> {:ok, ast} = SqlParserEx.parse("SELECT 1")
iex> is_map(ast) and map_size(ast) > 0
true
"""
alias SqlParserEx.Native
@valid_dialects ~w(
generic ansi postgres mysql sqlite mssql bigquery
clickhouse duckdb databricks hive redshift snowflake
)a
@type dialect ::
:generic
| :ansi
| :postgres
| :mysql
| :sqlite
| :mssql
| :bigquery
| :clickhouse
| :duckdb
| :databricks
| :hive
| :redshift
| :snowflake
@type sql_opt :: {:dialect, dialect()}
@type sql_string :: String.t()
@type parse_error ::
String.t()
| {:unknown_dialect, atom()}
| {:encode_error, Jason.EncodeError.t() | String.t()}
@doc """
Parses a SQL string and returns the single statement as an AST map.
Returns `{:error, reason}` if zero or multiple statements are found.
Use `parse_many/2` for multi-statement SQL.
"""
@spec parse(sql_string(), [sql_opt()]) :: {:ok, map()} | {:error, parse_error()}
def parse(sql, opts \\ []) do
case parse_many(sql, opts) do
{:ok, [statement]} -> {:ok, statement}
{:ok, []} -> {:error, "no statements found in SQL input"}
{:ok, _many} -> {:error, "expected exactly one statement; use parse_many/2 for multiple"}
{:error, _} = error -> error
end
end
@doc """
Parses a SQL string and returns all statements as a list of AST maps.
"""
@spec parse_many(sql_string(), [sql_opt()]) :: {:ok, [map()]} | {:error, parse_error()}
def parse_many(sql, opts \\ []) do
dialect = Keyword.get(opts, :dialect, :generic)
with :ok <- validate_dialect(dialect),
{:ok, json} <- Native.parse_sql(sql, dialect_to_string(dialect)),
{:ok, statements} <- Jason.decode(json),
:ok <- validate_statements(statements) do
{:ok, statements}
end
end
@doc """
Reconstructs a SQL string from an AST map returned by `parse/2` or `parse_many/2`.
Note: the `dialect:` option is validated but does not affect output. The underlying
Rust serializer uses a dialect-agnostic Display implementation.
"""
@spec to_sql(map(), [sql_opt()]) :: {:ok, sql_string()} | {:error, parse_error()}
def to_sql(ast, opts \\ []) do
dialect = Keyword.get(opts, :dialect, :generic)
with :ok <- validate_dialect(dialect),
{:ok, json} <- encode_ast(ast) do
Native.to_sql(json, dialect_to_string(dialect))
end
end
@doc "Returns all supported dialect atoms."
@spec dialects() :: [dialect()]
def dialects, do: @valid_dialects
defp validate_dialect(d) when d in @valid_dialects, do: :ok
defp validate_dialect(d), do: {:error, {:unknown_dialect, d}}
defp validate_statements(stmts) when is_list(stmts) do
if Enum.all?(stmts, &is_map/1),
do: :ok,
else: {:error, "unexpected NIF output: statements must be a list of maps"}
end
defp validate_statements(_), do: {:error, "unexpected NIF output: expected a JSON array"}
defp encode_ast(ast) when is_map(ast) do
case Jason.encode([ast]) do
{:ok, _} = ok -> ok
{:error, reason} -> {:error, {:encode_error, reason}}
end
end
defp encode_ast(ast), do: {:error, {:encode_error, "expected a map, got: #{inspect(ast)}"}}
defp dialect_to_string(dialect), do: Atom.to_string(dialect)
end