View Source Dequel.Semantic.Analyzer (Dequel v0.7.0)
Semantic analyzer that transforms parser AST into typed AST.
The parser produces syntax-only AST with :block nodes for relation filtering
and string field names. This analyzer walks the AST and:
- Converts string field names to atoms (safely via schema lookup)
- Resolves
:blocknodes to their semantic equivalents based on schema info:has_manyrelations →:existswith[cardinality: :many]belongs_to/has_onerelations →:joinwith[cardinality: :one]embeds_many→:embeddedwith[cardinality: :many]embeds_one→:embeddedwith[cardinality: :one]
- Coerces field values to their proper types based on schema info
Example
# Parser output (untyped AST with string field names)
{:block, [], ["items", {:==, [], ["name", "foo"]}]}
# After semantic analysis with has_many relation
{:exists, [cardinality: :many], [:items, {:==, [], [:name, "foo"]}]}
# After semantic analysis with belongs_to relation
{:join, [cardinality: :one], [:author, {:==, [], [:name, "foo"]}]}
# Type coercion
{:==, [], ["age", "25"]} → {:==, [], [:age, 25]}The semantic layer also enables LSP support by providing type information that can be used for validation, completion, and hover information.
Schema Resolver
The analyzer accepts a schema resolver function that returns field/relation info:
resolver = fn field ->
case field do
:items -> %{kind: :has_many, resolver: nested_resolver}
:author -> %{kind: :belongs_to, resolver: nested_resolver}
:address -> %{kind: :embeds_one, resolver: nested_resolver}
:age -> %{kind: :field, type: :integer}
_ -> nil
end
end
Analyzer.analyze(ast, resolver)For Ecto schemas, use Analyzer.ecto_resolver/1 to create a resolver.
Summary
Functions
Analyzes the AST and resolves block nodes to their typed equivalents.
Creates a resolver function for an Ecto schema module.
Types
@type field_info() :: %{kind: :field, type: atom()}
@type relation_info() :: %{kind: relation_kind(), resolver: resolver()}
@type relation_kind() ::
:has_many | :has_one | :belongs_to | :embeds_one | :embeds_many
@type resolver() :: (atom() -> field_info() | relation_info() | nil) | nil
Functions
Analyzes the AST and resolves block nodes to their typed equivalents.
Takes an optional resolver function that maps field names to field/relation info. The resolver should return one of:
%{kind: :has_many | :has_one | :belongs_to | :embeds_one | :embeds_many, resolver: fn}for relations%{kind: :field, type: atom}for typed fieldsnilfor unknown fields
If no resolver is provided, block nodes are left unchanged and no type coercion occurs.
Examples
Block nodes are transformed based on relation type (strings converted to atoms):
iex> ast = {:block, [], ["items", {:==, [], ["name", "foo"]}]}
iex> resolver = fn :items -> %{kind: :has_many, resolver: nil}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:exists, [cardinality: :many], [:items, {:==, [], [:name, "foo"]}]}
iex> ast = {:block, [], ["author", {:==, [], ["name", "Tolkien"]}]}
iex> resolver = fn :author -> %{kind: :belongs_to, resolver: nil}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:join, [cardinality: :one], [:author, {:==, [], [:name, "Tolkien"]}]}
iex> ast = {:block, [], ["address", {:==, [], ["city", "NYC"]}]}
iex> resolver = fn :address -> %{kind: :embeds_one, resolver: nil}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:embedded, [cardinality: :one], [:address, {:==, [], [:city, "NYC"]}]}Field values are coerced based on type:
iex> ast = {:==, [], ["age", "25"]}
iex> resolver = fn :age -> %{kind: :field, type: :integer}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:==, [], [:age, 25]}
iex> ast = {:==, [], ["active", "true"]}
iex> resolver = fn :active -> %{kind: :field, type: :boolean}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:==, [], [:active, true]}Comparison operators are coerced based on field type:
iex> ast = {:>, [], ["age", "18"]}
iex> resolver = fn :age -> %{kind: :field, type: :integer}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:>, [], [:age, 18]}
iex> ast = {:between, [], ["price", "10", "50"]}
iex> resolver = fn :price -> %{kind: :field, type: :integer}; _ -> nil end
iex> Dequel.Semantic.Analyzer.analyze(ast, resolver)
{:between, [], [:price, 10, 50]}Without a resolver, AST passes through unchanged (fields stay as strings):
iex> Dequel.Semantic.Analyzer.analyze({:==, [], ["name", "foo"]}, nil)
{:==, [], ["name", "foo"]}
iex> Dequel.Semantic.Analyzer.analyze({:block, [], ["items", {:==, [], ["name", "foo"]}]}, nil)
{:block, [], ["items", {:==, [], ["name", "foo"]}]}
Creates a resolver function for an Ecto schema module.
The schema must have __schema__/2 support for associations, embeds, and types.
Example
resolver = Analyzer.ecto_resolver(MyApp.ItemSchema)
Analyzer.analyze(ast, resolver)