Selecto

Alpha software. Expect API churn and breaking changes while the core package is still being hardened.

selecto is the core query engine in the Selecto ecosystem.

It gives you:

Ecosystem

Use selecto with companion packages when you need more than the core engine:

Installation

Add selecto and the adapter package your app uses:

def deps do
[
{:selecto, ">= 0.4.6 and < 0.6.0"},
{:selecto_db_postgresql, ">= 0.4.4 and < 0.6.0"}
]
end

Quick Start

Define a domain:

domain = %{
name: "Orders",
source: %{
source_table: "orders",
primary_key: :id,
fields: [:id, :total, :customer_id, :created_at],
columns: %{
id: %{type: :integer},
total: %{type: :decimal},
customer_id: %{type: :integer},
created_at: %{type: :utc_datetime}
},
associations: %{
customer: %{queryable: :customers, field: :customer, owner_key: :customer_id, related_key: :id}
}
},
schemas: %{
customers: %{
source_table: "customers",
fields: [:id, :name],
columns: %{
id: %{type: :integer},
name: %{type: :string}
}
}
},
joins: %{
customer: %{type: :star_dimension, display_field: :name}
}
}

Build and run a query:

selecto = Selecto.configure(domain, Repo)
{:ok, {rows, columns, aliases}} =
selecto
|> Selecto.select(["id", "total", "customer.name"])
|> Selecto.filter([{"total", {:gt, 100}}])
|> Selecto.order_by(["created_at"])
|> Selecto.execute()

Expression Helpers

Use Selecto.Expr when query structure is assembled dynamically in Elixir:

alias Selecto.Expr, as: X
query =
selecto
|> Selecto.filter(
X.compact_and([
X.eq("status", "active"),
X.when_present(search, &X.ilike("customer.name", "%#{&1}%")),
X.gte("total", 100)
])
)
|> Selecto.select([
X.field("id"),
X.field("customer.name"),
X.as(X.count("*"), "row_count")
])

For lighter authoring, Selecto also ships macro and sigil support. See docs/expression_dsl.md for the detailed guide.

Named Functions (UDF Registry)

Selecto domains can register named database functions under domain[:functions].

That lets you reuse typed scalar, predicate, and table-function definitions instead of scattering raw SQL fragments through your code.

domain = %{
# ...
functions: %{
"name_lower" => %{
kind: :scalar,
sql_name: "lower",
returns: :string,
allowed_in: [:select, :order_by],
args: [%{name: :value, type: :string, source: :selector}]
}
}
}
query =
selecto
|> Selecto.select([
"name",
Selecto.Expr.as(Selecto.udf("name_lower", ["name"]), "normalized_name")
])

UDF-backed custom columns are also supported through custom_columns[*].select.

Extensions

Selecto supports extension packages through the :extensions key on domains.

Example with PostGIS:

domain = %{
# ...
extensions: [Selecto.Extensions.PostGIS]
}

Use extensions when a package needs to contribute domain metadata, overlay DSL, adapter type mapping, or companion-package integrations.

Status

Current 0.4.x scope:

Demos And Tutorials

Developer Guides