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.3 and < 0.5.0"},
    {:selecto_db_postgresql, ">= 0.4.2 and < 0.5.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

Related Repos