Packages
SQL provides state-of-the-art, high-performance SQL integration for Elixir, built to handle extreme concurrency with unmatched expressiveness and ergonomic query composition. Write safe, composable, parameterized queries directly, without translating to Ecto or any ORM.
Current section
Files
Jump to
Current section
Files
lib/sql.ex
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 DBVisor
defmodule SQL do
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)
@moduledoc since: "0.1.0"
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@doc false
@behaviour SQL
import SQL
def token_to_sql(token), do: token_to_sql(token)
defoverridable token_to_sql: 1
end
end
@optional_callbacks token_to_sql: 1
@doc """
Returns a SQL string for a given token.
"""
@doc since: "0.1.0"
@callback token_to_sql(token :: {atom, keyword, list}) :: String.t()
defstruct [:tokens, :query, :params, :module]
@doc """
Returns a parameterized SQL.
## Examples
iex(1)> email = "john@example.com"
iex(2)> SQL.to_sql(~SQL"select id, email from users where email = \#{email}")
{"select id, email from users where email = $0", ["john@example.com"]}
"""
@doc since: "0.1.0"
def to_sql(sql), do: {"#{sql}", sql.params}
@doc false
def build(right, {:<<>>, meta, _} = left, _modifiers, env) do
quote bind_quoted: [right: Macro.unpipe(right), left: left, meta: Macro.escape({meta[:line], meta[:column] || 0, env.file})] do
{t, p} = Enum.reduce(right, {[], []}, fn
{[], 0}, acc -> acc
{v, 0}, {tokens, params} -> {tokens ++ v.tokens, params ++ v.params}
end)
{:ok, tokens, params} = SQL.Parser.parse(left, binding(), meta, p)
t ++ tokens
|> SQL.to_query()
|> Map.merge(%{module: __MODULE__, params: params})
end
end
@doc """
Handles the sigil `~SQL` for SQL.
It returns a `%SQL{}` struct that can be transformed to a parameterized query.
## Examples
iex(1)> ~SQL"from users select id, email"
"select id, email from users"
"""
@doc since: "0.1.0"
defmacro sigil_SQL(left \\ [], right, modifiers) do
SQL.build(left, right, modifiers, __CALLER__)
end
@doc false
def parse(binary) do
{:ok, tokens, []} = SQL.Parser.parse(binary, false, {1, 0, nil})
to_query(tokens)
end
@doc false
@acc ~w[; with update delete select fetch from join where group having window except intersect union order limit offset lock]a
def to_query([value | _] = tokens) when is_tuple(value) and elem(value, 0) in @acc do
struct(SQL, tokens: tokens, query: Enum.reduce(@acc, [], fn key, acc -> acc ++ for {k, meta, v} <- Enum.filter(tokens, &(elem(&1, 0) == key)), do: {k, meta, Enum.map(v, &to_query/1)} end))
end
def to_query({tag, meta, values}) do
{tag, meta, Enum.map(values, &to_query/1)}
end
def to_query(tokens) when is_list(tokens) do
Enum.map(tokens, &to_query/1)
end
def to_query(token) do
token
end
@doc false
def __token_to_sql__(sql) do
if Kernel.function_exported?(sql.module, :token_to_sql, 1) do
&sql.module.token_to_sql/1
else
&SQL.String.token_to_sql/1
end
end
defimpl Inspect, for: SQL do
def inspect(sql, _opts) do
fun = SQL.__token_to_sql__(sql)
Enum.reduce(0..length(sql.params), to_string(sql), &String.replace(&2, fun.({:binding, [], [&1]}), fun.(Enum.at(sql.params, &1))))
end
end
defimpl String.Chars, for: SQL do
def to_string(sql) do
Enum.map_join(sql.query, " ", &SQL.__token_to_sql__(sql).(&1))
end
end
end