The SQL-specific half of an Ecto-backed data source.
Lotus.Source.Adapter is the adapter-agnostic contract every data source
implements. A dialect is narrower: it supplies the parts that differ between
SQL engines behind an Ecto.Repo — identifier quoting, parameter
placeholders, EXPLAIN syntax, catalogue introspection queries, and the
built-in deny rules for each engine's system tables.
Pair a dialect with the Lotus.Source.Adapters.Ecto macro and the adapter
is a one-liner:
defmodule LotusMSSql.Dialect do
@behaviour Lotus.Source.Adapters.Ecto.Dialect
# ... callbacks ...
end
defmodule LotusMSSql.Adapter do
use Lotus.Source.Adapters.Ecto, dialect: LotusMSSql.Dialect
endThe macro injects every Lotus.Source.Adapter callback, routing the shared
Ecto plumbing through Lotus.Source.Adapters.Ecto and the engine-specific
parts through your dialect. All of them are defoverridable, so a dialect
that needs to escape the macro's assumptions can implement the adapter
callback directly.
When not to write a dialect
A dialect is for SQL over Ecto. A data source that is not SQL, or not
reached through an Ecto.Repo, implements Lotus.Source.Adapter directly
instead — see the Source Adapters guide. Elasticsearch
is the worked example of that path.
Statement shape
Pipeline callbacks take and return %Lotus.Query.Statement{}. For a
dialect, statement.body is always SQL text and statement.params the
bound values in the order the driver expects.
Optional callbacks
Everything in @optional_callbacks has a safe default, so a minimal
dialect only implements what its engine actually needs. Notably
set_statement_timeout/2 and set_search_path/2 are optional: engines
with no session-level timeout or schema search path simply omit them
rather than defining no-op clauses.
Summary
Callbacks
Return the adapter-shaped AI context for this dialect.
Map a database-specific column type string to a Lotus internal type atom.
Functions a read-only statement may not call: those with effects that outlast the transaction or reach other sessions (locks, session settings, signalling backends, files, other connections), or that run SQL text preflight can't see.
Extract the set of tables/relations a statement will access.
Whether the statement needs the visibility preflight check before execution.
Apply a session-level schema search path.
Apply a session-level statement timeout.
Row count estimates for the relations in schemas, from the engine's
statistics (see Lotus.Source.Adapter.table_row_estimates/2).
Rewrite the statement before variables are extracted and bound.
Types
@type repo() :: Ecto.Repo.t()
Callbacks
@callback ai_context() :: {:ok, Lotus.Source.Adapter.ai_context_map()} | {:error, term()}
Return the adapter-shaped AI context for this dialect.
Supplies the four Lotus.Source.Adapter.ai_context_map keys —
:language, :example_query, :syntax_notes, :error_patterns —
with dialect-specific content. The built-in SQL dialects populate
Postgres / MySQL / SQLite quirks here.
Default (when not implemented): generic-SQL context synthesized from
query_language/0, with an empty error-pattern list.
@callback apply_filters( statement :: Lotus.Query.Statement.t(), filters :: [Lotus.Query.Filter.t()] ) :: Lotus.Query.Statement.t()
@callback apply_sorts( statement :: Lotus.Query.Statement.t(), sorts :: [Lotus.Query.Sort.t()] ) :: Lotus.Query.Statement.t()
Map a database-specific column type string to a Lotus internal type atom.
Each dialect knows its own type system (e.g. Postgres uuid, MySQL char(36),
SQLite INTEGER).
Default (when not implemented): :text.
@callback denied_functions() :: [String.t()]
Functions a read-only statement may not call: those with effects that outlast the transaction or reach other sessions (locks, session settings, signalling backends, files, other connections), or that run SQL text preflight can't see.
Names are lowercase; * matches any run of word characters
("pg_advisory_*"). Sanitization refuses a call to any of them, with or
without a schema qualifier. A deny list is never complete: it is defense
in depth behind a database role that can only read.
Default (when not implemented): [].
@callback ecto_adapter() :: module() | nil
@callback editor_config() :: %{ :language => String.t(), :keywords => [String.t()], :types => [String.t()], :functions => [%{name: String.t(), detail: String.t(), args: String.t()}], :context_boundaries => [String.t()], optional(:dialect_spec) => Lotus.Source.Adapter.dialect_spec(), optional(:context_schema) => Lotus.Source.Adapter.context_schema() }
@callback extract_accessed_resources(repo(), statement :: Lotus.Query.Statement.t()) :: {:ok, MapSet.t({String.t() | nil, String.t()})} | {:error, term()} | {:unrestricted, String.t()}
Extract the set of tables/relations a statement will access.
Used by Lotus.Preflight to check visibility rules before execution.
Return {:ok, MapSet} with {schema, table} tuples, {:error, reason},
or {:unrestricted, reason} when the dialect cannot enforce visibility
at this layer.
Default (when not implemented): {:unrestricted, ...}.
@callback hierarchy_label() :: String.t()
@callback limit_offset_placeholders( limit_index :: pos_integer(), offset_index :: pos_integer() ) :: {limit_placeholder :: String.t(), offset_placeholder :: String.t()}
@callback limit_query(statement :: Lotus.Query.Statement.t(), limit :: pos_integer()) :: Lotus.Query.Statement.t()
@callback needs_preflight?(statement :: Lotus.Query.Statement.t()) :: boolean()
Whether the statement needs the visibility preflight check before execution.
Used by the Ecto adapter's built-in needs_preflight?/2 default to skip
introspection statements (EXPLAIN, SHOW, PRAGMA) that don't access
visible relations. Dialects may override with a language-specific check.
Default (when not implemented): true (always preflight).
@callback param_placeholder( index :: pos_integer(), var :: String.t(), type :: atom() | nil ) :: String.t()
@callback query_language() :: String.t()
@callback query_plan(repo(), statement :: Lotus.Query.Statement.t(), opts :: keyword()) :: {:ok, String.t() | nil} | {:error, term()}
Apply a session-level schema search path.
Optional. Called inside the query transaction when the caller supplies
:search_path. Engines without a search path omit it, and Lotus ignores
the option for that source.
@callback set_statement_timeout(repo(), non_neg_integer()) :: :ok | no_return()
Apply a session-level statement timeout.
Optional. Engines with no such notion omit it and Lotus relies on the
driver-level :timeout passed to execute_query/4 instead.
@callback source_type() :: :postgres | :mysql | :sqlite | :other | atom()
@callback table_row_estimates(repo(), schemas :: [String.t()]) :: %{ required({String.t() | nil, String.t()}) => non_neg_integer() }
Row count estimates for the relations in schemas, from the engine's
statistics (see Lotus.Source.Adapter.table_row_estimates/2).
@callback transform_statement(statement :: Lotus.Query.Statement.t()) :: Lotus.Query.Statement.t()
Rewrite the statement before variables are extracted and bound.
Fires in Lotus.Storage.Query.compile/2 before {{var}} placeholders
are resolved. Use for dialect-specific syntax rewrites (wildcard handling,
INTERVAL, CONCAT vs ||, etc.). Return the rewritten statement.
Default (when not implemented): statement unchanged.