Selecto
Copy MarkdownAlpha 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:
- domain-driven query configuration
- safe select/filter/group/order composition
- automatic join resolution from configured relationships
- aggregate and OLAP-style query support
- CTEs, lateral joins, and other advanced SQL shapes
- expression helpers and a named-function registry for reusable query AST
Ecosystem
Use selecto with companion packages when you need more than the core engine:
selecto_componentsfor Phoenix LiveView query UIselecto_mixfor domain generation and installation tasksselecto_updatofor write operations over Selecto domains- adapter packages such as
selecto_db_postgresql,selecto_db_mysql,selecto_db_sqlite, and others selecto_postgisfor spatial/map extension support
Installation
Add selecto and the adapter package your app uses:
def deps do
[
{:selecto, ">= 0.4.3 and < 0.5.0"},
{:selecto_db_postgresql, ">= 0.4.2 and < 0.5.0"}
]
endQuick 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:
- core query building is usable but not stable
- advanced subfilter internals are still high-risk/experimental
- adapter support exists across multiple databases, but PostgreSQL remains the most complete path
- schema/domain generation and UI are intentionally outside this package and live in companion repos
Demos And Tutorials
selecto_livebooksfor guided notebooksselecto_northwindfor tutorial-style examplestestselecto.fly.devfor a hosted demo app
Related Repos
selecto_componentsselecto_mixselecto_updatoselecto_postgisselecto_test