SqlParserEx
An Elixir library wrapping the Apache DataFusion SQL parser (sqlparser Rust crate v0.61) via Rustler NIFs. Precompiled binaries are provided — no Rust toolchain required for end users.
Installation
def deps do
[
{:sql_parser_ex, "~> 0.1"}
]
endUsage
# Parse a single statement
{:ok, ast} = SqlParserEx.parse("SELECT id, name FROM users WHERE id = 1")
# Parse with a specific dialect
{:ok, ast} = SqlParserEx.parse("SELECT $1::int", dialect: :postgres)
# Parse multiple statements
{:ok, [stmt1, stmt2]} = SqlParserEx.parse_many("SELECT 1; SELECT 2")
# Reconstruct SQL from AST (round-trip)
{:ok, sql} = SqlParserEx.to_sql(ast)
# List supported dialects
SqlParserEx.dialects()AST Output
The AST mirrors the full structure of the sqlparser Rust crate, including source position span fields for every token. The example below omits spans for readability.
sql = """
SELECT DISTINCT u.id, u.name, COUNT(o.id) AS order_count
FROM users AS u
JOIN orders AS o ON u.id = o.user_id
WHERE u.active = true
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 0
ORDER BY order_count DESC
LIMIT 10
"""
{:ok, ast} = SqlParserEx.parse(sql)Returns:
{
"Query": {
"body": {
"Select": {
"distinct": "Distinct",
"projection": [
{
"UnnamedExpr": {
"CompoundIdentifier": [
{ "value": "u", "quote_style": null },
{ "value": "id", "quote_style": null }
]
}
},
{
"UnnamedExpr": {
"CompoundIdentifier": [
{ "value": "u", "quote_style": null },
{ "value": "name", "quote_style": null }
]
}
},
{
"ExprWithAlias": {
"expr": {
"Function": {
"name": [{ "Identifier": { "value": "COUNT", "quote_style": null } }],
"args": {
"List": {
"args": [
{
"Unnamed": {
"Expr": {
"CompoundIdentifier": [
{ "value": "o", "quote_style": null },
{ "value": "id", "quote_style": null }
]
}
}
}
],
"clauses": [],
"duplicate_treatment": null
}
},
"filter": null,
"null_treatment": null,
"over": null,
"parameters": "None",
"within_group": [],
"uses_odbc_syntax": false
}
},
"alias": { "value": "order_count", "quote_style": null }
}
}
],
"from": [
{
"relation": {
"Table": {
"name": [{ "Identifier": { "value": "users", "quote_style": null } }],
"alias": {
"name": { "value": "u", "quote_style": null },
"columns": [],
"explicit": true
}
}
},
"joins": [
{
"relation": {
"Table": {
"name": [{ "Identifier": { "value": "orders", "quote_style": null } }],
"alias": {
"name": { "value": "o", "quote_style": null },
"columns": [],
"explicit": true
}
}
},
"join_operator": {
"Join": {
"On": {
"BinaryOp": {
"left": {
"CompoundIdentifier": [
{ "value": "u", "quote_style": null },
{ "value": "id", "quote_style": null }
]
},
"op": "Eq",
"right": {
"CompoundIdentifier": [
{ "value": "o", "quote_style": null },
{ "value": "user_id", "quote_style": null }
]
}
}
}
}
},
"global": false
}
]
}
],
"selection": {
"BinaryOp": {
"left": {
"CompoundIdentifier": [
{ "value": "u", "quote_style": null },
{ "value": "active", "quote_style": null }
]
},
"op": "Eq",
"right": { "Value": { "value": { "Boolean": true } } }
}
},
"group_by": {
"Expressions": [
[
{
"CompoundIdentifier": [
{ "value": "u", "quote_style": null },
{ "value": "id", "quote_style": null }
]
},
{
"CompoundIdentifier": [
{ "value": "u", "quote_style": null },
{ "value": "name", "quote_style": null }
]
}
],
[]
]
},
"having": {
"BinaryOp": {
"left": {
"Function": {
"name": [{ "Identifier": { "value": "COUNT", "quote_style": null } }],
"args": {
"List": {
"args": [
{
"Unnamed": {
"Expr": {
"CompoundIdentifier": [
{ "value": "o", "quote_style": null },
{ "value": "id", "quote_style": null }
]
}
}
}
],
"clauses": [],
"duplicate_treatment": null
}
}
}
},
"op": "Gt",
"right": { "Value": { "value": { "Number": ["0", false] } } }
}
},
"flavor": "Standard",
"cluster_by": [],
"distribute_by": [],
"sort_by": [],
"lateral_views": [],
"named_window": [],
"connect_by": []
}
},
"order_by": {
"kind": {
"Expressions": [
{
"expr": { "Identifier": { "value": "order_count", "quote_style": null } },
"options": { "asc": false, "nulls_first": null },
"with_fill": null
}
]
},
"interpolate": null
},
"limit_clause": {
"LimitOffset": {
"limit": { "Value": { "value": { "Number": ["10", false] } } },
"offset": null,
"limit_by": []
}
},
"with": null,
"locks": [],
"fetch": null,
"pipe_operators": [],
"for_clause": null,
"format_clause": null,
"settings": null
}
}Supported Dialects
| Atom | SQL Dialect |
|---|---|
:generic | Generic (permissive, default) |
:ansi | ANSI SQL:2011 |
:postgres | PostgreSQL |
:mysql | MySQL |
:sqlite | SQLite |
:mssql | Microsoft SQL Server |
:bigquery | Google BigQuery |
:clickhouse | ClickHouse |
:duckdb | DuckDB |
:databricks | Databricks |
:hive | Apache Hive |
:redshift | Amazon Redshift |
:snowflake | Snowflake |
Error Handling
All functions return {:ok, result} or {:error, reason}:
# Invalid SQL
{:error, "sql parser error: ..."} = SqlParserEx.parse("SELECT * FROM")
# Unknown dialect
{:error, {:unknown_dialect, :cobol}} = SqlParserEx.parse("SELECT 1", dialect: :cobol)
# Multiple statements passed to parse/2
{:error, "expected exactly one statement; use parse_many/2 for multiple"} =
SqlParserEx.parse("SELECT 1; SELECT 2")Building from Source
If you want to compile the NIF locally (requires Rust stable):
SQL_PARSER_EX_BUILD=1 mix deps.get
SQL_PARSER_EX_BUILD=1 mix compileNote for consumers: When forcing a local build, you must also add
{:rustler, "~> 0.37"}to your owndeps/0, as it is an optional dependency of this library:def deps do [ {:sql_parser_ex, "~> 0.1"}, {:rustler, "~> 0.37"} # only needed when SQL_PARSER_EX_BUILD=1 ] end
License
Copyright 2025 Andre Anastacio. Licensed under the Apache License, Version 2.0.