NamedSQL

Hex.pmLicense

NamedSQL

NamedSQL is a small Elixir library for writing plain SQL with named parameters, with compile-time validation and no DSL.

You write SQL. NamedSQL checks your parameters. You get a nicely mapped output. That’s it.


Why NamedSQL?

It was born out of reoccurring frustration of having to write complex SQL queries using Ecto’s DSL. While there are advantages to having composable queries, SQL in itself often the perfect language to describe complex relations, especially when using a number of CTEs in your queries.

Ecto allows you to use raw sql (MyApp.Repo.query/3) as an esacpe hatch, but it is not treated as first class citizen. Also, the code tends to be hard to maintain, as you write queries with numbered parameters ($1, $2, etc…).

The NamedSQL approach:

If you like writing SQL and want it to be safer and cleaner in Elixir, this library is for you.


Installation

Add named_sql to your dependencies:

defp deps do
  [
    {:named_sql, "~> 0.1.0"}
  ]
end

Setup

Use NamedSQL inside your Repo module:

defmodule MyApp.Repo do
  use Ecto.Repo,
    otp_app: :my_app,
    adapter: Ecto.Adapters.Postgres

  use NamedSQL, repo: __MODULE__
end

Usage

Because named_sql/2 is a macro (to enable compile-time checks), you must require the repo at the callsite:

alias MyApp.Repo
require Repo

Basic example

Repo.named_sql("""
  SELECT name, birth
  FROM users
  WHERE name = $name
  """,
  name: "Jürgen"
)

Result

[
  %{"name" => "Jürgen", "birth" => ~N[2025-12-09 13:03:12]}
]

Results are string-keyed maps by default, to avoid runtime atom creation.


Named Parameters

SQL placeholders use $ followed by an identifier:

WHERE user_id = $user_id AND created_at > $since

Parameters are passed as a keyword list:

Repo.named_sql(sql,
  user_id: 42,
  since: ~N[2025-01-01 00:00:00]
)

Compile-time vs runtime validation

NamedSQL provides two explicit execution paths, depending on how parameters are supplied.

named_sql/2 — compile-time validated via macro

Repo.named_sql("SELECT * FROM users WHERE id = $id", id: 1)

This is the recommended path.

When the parameter list is a literal keyword list, NamedSQL performs validation at compile time:

Passing a variable or dynamically constructed keyword list will result in a compile-time error.

named_sql_dynamic/2 — runtime validated (function)

params = [id: user_id]
Repo.named_sql_dynamic("SELECT * FROM users WHERE id = $id", params)

This function exists as an explicit escape hatch for dynamic scenarios

Parameters are validated at runtime only and the same checks apply as in the compile-time version.


Result Mapping

You can use the :result_mapper option to control how result rows are formatted, to avoid the intermediate map format in case it’s not desired.

The mapper receives each row as a list, in column order.

Repo.named_sql("""
  SELECT name, birth
  FROM users
  """,
  result_mapper: fn [name, birth] ->
    %{name: name, birth: birth}
  end
)

This is the recommended way to return structs or atom-keyed maps.


Reserved options

The following option keys are reserved and cannot be used as SQL parameters:

Using them in the SQL query will raise an error.

Design principles

NamedSQL does not try to replace Ecto or build a query language. It simply makes raw SQL safer and nicer to use.


License MIT


Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/named_sql.