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.
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.
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 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 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.