# `Dllb.Query`
[🔗](https://github.com/Oeditus/dllb_ex/blob/v0.9.0/lib/dllb/query.ex#L1)

Query builder that generates dllb SQL strings.

Provides functions to construct CREATE, SELECT (with ORDER BY), UPDATE,
DELETE (point and `DELETE ... WHERE`), RELATE, COUNT (with GROUP BY),
DEFINE (secondary, full-text, and vector index), REMOVE INDEX, SEARCH /
VECTOR SEARCH / HYBRID SEARCH (with optional WHERE scoping), and the graph
analytics verbs (COMMUNITIES, COMPONENTS, PAGERANK, CENTRALITY, PATH, EDGES)
for the dllb query language. All functions return plain query strings ready
to be sent over the wire.

# `fields`

```elixir
@type fields() :: %{optional(atom()) =&gt; term()}
```

# `select_opts`

```elixir
@type select_opts() :: [
  fields: [String.t()],
  where: String.t(),
  order: String.t(),
  limit: non_neg_integer(),
  fetch: String.t()
]
```

# `count`

```elixir
@spec count(
  String.t(),
  keyword()
) :: String.t()
```

Builds a `COUNT <table> [WHERE <clause>] [GROUP BY <field>]` statement.

Without `:group_by` the engine returns a single total count. With
`:group_by`, it returns one row per distinct value of the field, each with a
`count`, sorted by count descending (a `Dllb.Result.Rows`, not a
`Dllb.Result.Count`).

## Options

  * `:where` - optional WHERE clause string
  * `:group_by` - optional field name to group counts by

## Examples

    iex> Dllb.Query.count("user")
    "COUNT user"

    iex> Dllb.Query.count("user", where: "age = 30")
    "COUNT user WHERE age = 30"

    iex> Dllb.Query.count("ast_node", group_by: "kind")
    "COUNT ast_node GROUP BY kind"

# `create`

```elixir
@spec create(String.t(), fields()) :: String.t()
```

Builds a CREATE statement for inserting a new record.

## Examples

    iex> Dllb.Query.create("user", %{name: "Alice", age: 30})
    "CREATE user SET age = 30, name = 'Alice'"

# `create_with_id`

```elixir
@spec create_with_id(String.t(), String.t(), fields()) :: String.t()
```

Builds a CREATE statement with an explicit record ID.

## Examples

    iex> Dllb.Query.create_with_id("user", "u1", %{name: "Alice"})
    "CREATE user:u1 SET name = 'Alice'"

# `define_field`

```elixir
@spec define_field(String.t(), String.t(), String.t(), keyword()) :: String.t()
```

Builds a DEFINE FIELD statement.

## Options

  * `:required` - if `true`, appends ASSERT $value IS NOT NONE

## Examples

    iex> Dllb.Query.define_field("user", "name", "string", required: true)
    "DEFINE FIELD name ON user TYPE string ASSERT $value IS NOT NONE"

# `define_fulltext_index`

```elixir
@spec define_fulltext_index(String.t(), String.t(), String.t(), keyword()) ::
  String.t()
```

Builds a `DEFINE FULLTEXT INDEX` statement that registers a BM25 full-text
(Tantivy) index over a single text `field`.

Requires a dllb server with full-text/vector services enabled (the default
server build).

## Options

  * `:analyzer` - tokenizer/stemmer to apply. One of `"default"`, `"simple"`,
    or a language (`"english"`, `"spanish"`, `"french"`, `"german"`,
    `"italian"`, `"portuguese"`, `"russian"`). Defaults to the engine's
    `default` analyzer when omitted.

## Examples

    iex> Dllb.Query.define_fulltext_index("article", "ft_body", "body")
    "DEFINE FULLTEXT INDEX ft_body ON TABLE article FIELDS body"

    iex> Dllb.Query.define_fulltext_index("article", "ft_body", "body", analyzer: "english")
    "DEFINE FULLTEXT INDEX ft_body ON TABLE article FIELDS body ANALYZER english"

# `define_index`

```elixir
@spec define_index(String.t(), String.t(), [String.t()], keyword()) :: String.t()
```

Builds a `DEFINE INDEX` statement that registers a persisted secondary
index in the engine's catalog and backfills entries for existing rows.

The index covers one or more `fields`. Composite (multi-field) indexes are
matched by the engine using leftmost-prefix planning, so list the most
selective leading field first. Once defined, equality and range
(`>`, `>=`, `<`, `<=`) predicates on the indexed fields are transparently
accelerated in `SELECT`/`COUNT`/`UPDATE` `WHERE` clauses — no change to the
query strings is required.

## Options

  * `:unique` - when `true`, enforces uniqueness over the full indexed
    tuple; defining the index fails if existing rows already hold duplicate
    values (default `false`)

## Examples

    iex> Dllb.Query.define_index("user", "by_age", ["age"])
    "DEFINE INDEX by_age ON TABLE user FIELDS age"

    iex> Dllb.Query.define_index("user", "by_email", ["email"], unique: true)
    "DEFINE INDEX by_email ON TABLE user FIELDS email UNIQUE"

    iex> Dllb.Query.define_index("ast_node", "idx_file_kind", ["file_path", "kind"])
    "DEFINE INDEX idx_file_kind ON TABLE ast_node FIELDS file_path, kind"

# `define_table`

```elixir
@spec define_table(String.t(), :schemafull | :schemaless) :: String.t()
```

Builds a DEFINE TABLE statement.

Mode can be `:schemafull` or `:schemaless`.

## Examples

    iex> Dllb.Query.define_table("user", :schemafull)
    "DEFINE TABLE user SCHEMAFULL"

# `define_vector_index`

```elixir
@spec define_vector_index(
  String.t(),
  String.t(),
  String.t(),
  pos_integer(),
  keyword()
) :: String.t()
```

Builds a `DEFINE VECTOR INDEX` statement that registers an approximate
nearest-neighbour (HNSW) index over a dense-embedding `field`.

`dimension` is the (positive) length of the vectors to be indexed. Requires
a dllb server with full-text/vector services enabled (the default server
build).

## Options

  * `:metric` - distance metric: `"cosine"` (default), `"euclidean"` (alias
    `"l2"`), or `"dot"` (alias `"dotproduct"`/`"dot_product"`). Defaults to
    the engine's `cosine` metric when omitted.

## Examples

    iex> Dllb.Query.define_vector_index("ast_node", "vec_src", "source_embedding", 768)
    "DEFINE VECTOR INDEX vec_src ON TABLE ast_node FIELDS source_embedding DIMENSION 768"

    iex> Dllb.Query.define_vector_index("doc", "vec_emb", "embedding", 8, metric: "euclidean")
    "DEFINE VECTOR INDEX vec_emb ON TABLE doc FIELDS embedding DIMENSION 8 METRIC euclidean"

# `delete`

```elixir
@spec delete(String.t()) :: String.t()
```

Builds a DELETE statement for a single record.

## Examples

    iex> Dllb.Query.delete("user:u1")
    "DELETE user:u1"

# `delete_where`

```elixir
@spec delete_where(String.t(), String.t() | nil) :: String.t()
```

Builds a `DELETE <table> [WHERE <clause>]` statement that removes every row
matching the (already-built) WHERE clause in a single server-side operation.

Secondary, full-text, and vector indexes are maintained by the engine. The
result is a `Dllb.Result.DeletedMany` reporting how many rows were removed.
An empty (or omitted) WHERE deletes every row in the table.

## Examples

    iex> Dllb.Query.delete_where("ast_node", "file_path = '/a.ex'")
    "DELETE ast_node WHERE file_path = '/a.ex'"

    iex> Dllb.Query.delete_where("ast_node")
    "DELETE ast_node"

# `graph_centrality`

```elixir
@spec graph_centrality(
  String.t(),
  keyword()
) :: String.t()
```

Builds a `GRAPH CENTRALITY <edge_table>` statement: degree centrality over
the edge table, returning `{id, score}` rows ranked by score descending.

## Options

  * `:mode` - `:degree` (default, in + out), `:indegree`, or `:outdegree`
  * `:limit` - return only the top-N nodes

## Examples

    iex> Dllb.Query.graph_centrality("calls")
    "GRAPH CENTRALITY calls"

    iex> Dllb.Query.graph_centrality("calls", mode: :indegree, limit: 10)
    "GRAPH CENTRALITY calls INDEGREE LIMIT 10"

# `graph_communities`

```elixir
@spec graph_communities(
  String.t(),
  keyword()
) :: String.t()
```

Builds a `GRAPH COMMUNITIES` statement for native community detection.

Delegates computation to the dllb engine (Rust Louvain / Label Propagation),
which runs in O(E) per iteration — orders of magnitude faster than the
equivalent pure-Elixir implementation on large graphs.

## Options

  * `:algorithm` - `:louvain` (default) or `:lp` (label propagation)
  * `:max_iter`  - maximum optimisation passes (default: 10)
  * `:resolution` - Louvain resolution γ; values < 1.0 → fewer, larger
    communities; > 1.0 → more, smaller communities (default: 1.0)

## Examples

    iex> Dllb.Query.graph_communities("calls")
    "GRAPH COMMUNITIES calls"

    iex> Dllb.Query.graph_communities("calls", algorithm: :lp, max_iter: 20)
    "GRAPH COMMUNITIES calls ALGORITHM lp MAX_ITER 20"

    iex> Dllb.Query.graph_communities("calls", algorithm: :louvain, resolution: 0.5)
    "GRAPH COMMUNITIES calls ALGORITHM louvain RESOLUTION 0.5"

# `graph_components`

```elixir
@spec graph_components(String.t()) :: String.t()
```

Builds a `GRAPH COMPONENTS <edge_table>` statement for native connected-
components detection.

Delegates computation to the dllb engine (Rust union-find over the edge
table, treated as undirected). The server returns a compact summary
(`component_count`, `largest`, `nodes`) rather than full membership.

## Examples

    iex> Dllb.Query.graph_components("calls")
    "GRAPH COMPONENTS calls"

# `graph_edges`

```elixir
@spec graph_edges(
  String.t(),
  keyword()
) :: String.t()
```

Builds a `GRAPH EDGES <edge_table> [WHERE <clause>]` statement: lists the
edges of an edge table as rows of `{src, dst, edge_type, weight, ...props}`,
surfacing the real stored `weight` (default `1.0`). The optional WHERE
filters the synthetic edge rows (e.g. by `src`, `dst`, or `weight`).

## Options

  * `:where` - optional WHERE clause string

## Examples

    iex> Dllb.Query.graph_edges("calls")
    "GRAPH EDGES calls"

    iex> Dllb.Query.graph_edges("calls", where: "weight > 0.5")
    "GRAPH EDGES calls WHERE weight > 0.5"

# `graph_pagerank`

```elixir
@spec graph_pagerank(
  String.t(),
  keyword()
) :: String.t()
```

Builds a `GRAPH PAGERANK <edge_table>` statement: weighted PageRank over the
edge table, returning `{id, score}` rows ranked by score descending.

## Options

  * `:damping` - damping factor (default the engine's `0.85`)
  * `:max_iter` - maximum power-iteration steps (default the engine's `100`)
  * `:limit` - return only the top-N nodes

## Examples

    iex> Dllb.Query.graph_pagerank("calls")
    "GRAPH PAGERANK calls"

    iex> Dllb.Query.graph_pagerank("calls", damping: 0.9, max_iter: 50, limit: 10)
    "GRAPH PAGERANK calls DAMPING 0.9 MAX_ITER 50 LIMIT 10"

# `graph_path`

```elixir
@spec graph_path(String.t(), String.t(), String.t(), keyword()) :: String.t()
```

Builds a `GRAPH PATH <src> -> <dst> ON <edge_table>` statement: the shortest
directed path between two vertices. `src` and `dst` are bare vertex ids (the
id part, not `table:id`). The result is a single row with `found`, `length`,
and `path` fields.

## Options

  * `:max_depth` - bound the search to at most this many edges

## Examples

    iex> Dllb.Query.graph_path("a", "b", "calls")
    "GRAPH PATH a -> b ON calls"

    iex> Dllb.Query.graph_path("a", "b", "calls", max_depth: 4)
    "GRAPH PATH a -> b ON calls MAX_DEPTH 4"

# `hybrid_search`

```elixir
@spec hybrid_search(
  String.t(),
  String.t(),
  String.t(),
  String.t(),
  [number()],
  keyword()
) :: String.t()
```

Builds a `HYBRID SEARCH` statement that fuses a BM25 full-text query on
`text_field` with an approximate nearest-neighbour query on `vector_field`.

The engine normalises each modality's scores independently and blends them
as `alpha * text_score + (1 - alpha) * vector_score`, then returns rows
ranked best-first, each carrying `score`, `text_score`, and `vector_score`
fields. Requires a full-text index on `text_field` and a vector index on
`vector_field`.

## Options

  * `:alpha` - weight on the (normalised) text score, in `0.0..1.0`; the
    vector score gets `1 - alpha`. Defaults to the engine's `0.5`.
  * `:where` - optional WHERE clause to scope results server-side
  * `:limit` - maximum number of hits to return

## Examples

    iex> Dllb.Query.hybrid_search("doc", "body", "graph db", "embedding", [0.1, 0.2])
    "HYBRID SEARCH doc TEXT body 'graph db' VECTOR embedding [0.1, 0.2]"

    iex> Dllb.Query.hybrid_search("doc", "body", "graph", "embedding", [0.1, 0.2], alpha: 0.7, limit: 5)
    "HYBRID SEARCH doc TEXT body 'graph' VECTOR embedding [0.1, 0.2] ALPHA 0.7 LIMIT 5"

# `raw`

```elixir
@spec raw(String.t()) :: String.t()
```

Passes through a raw query string without modification.

## Examples

    iex> Dllb.Query.raw("INFO FOR DB")
    "INFO FOR DB"

# `relate`

```elixir
@spec relate(String.t(), String.t(), String.t(), fields()) :: String.t()
```

Builds a RELATE statement to create a graph edge between two records.

## Examples

    iex> Dllb.Query.relate("user:a", "follows", "user:b", %{since: "2024"})
    "RELATE user:a->follows->user:b SET since = '2024'"

# `remove_index`

```elixir
@spec remove_index(String.t(), String.t()) :: String.t()
```

Builds a `REMOVE INDEX` statement that drops a secondary index and all of
its catalog entries. Subsequent queries fall back to full scans.

## Examples

    iex> Dllb.Query.remove_index("user", "by_age")
    "REMOVE INDEX by_age ON TABLE user"

# `search`

```elixir
@spec search(String.t(), String.t(), String.t(), keyword()) :: String.t()
```

Builds a `SEARCH` statement: a BM25 full-text query against a `field` that
has a full-text index. Results come back as rows ranked best-first, each
carrying an extra `score` field.

## Options

  * `:where` - optional WHERE clause to scope results server-side (applied
    to the ranked hits before `:limit`)
  * `:limit` - maximum number of hits to return

## Examples

    iex> Dllb.Query.search("article", "body", "graph database")
    "SEARCH article body 'graph database'"

    iex> Dllb.Query.search("article", "body", "graph database", limit: 5)
    "SEARCH article body 'graph database' LIMIT 5"

    iex> Dllb.Query.search("article", "body", "graph", where: "lang = 'en'", limit: 5)
    "SEARCH article body 'graph' WHERE lang = 'en' LIMIT 5"

# `select`

```elixir
@spec select(String.t(), select_opts()) :: String.t()
```

Builds a SELECT statement with optional clauses.

## Options

  * `:fields` - list of field names to select (default `["*"]`)
  * `:where` - WHERE clause string
  * `:order` - ORDER BY clause string
  * `:limit` - LIMIT value
  * `:fetch` - graph traversal fetch clause (e.g. `"->calls->fn_node"`)

## Examples

    iex> Dllb.Query.select("user", where: "age > 25", limit: 10)
    "SELECT * FROM user WHERE age > 25 LIMIT 10"

# `update`

```elixir
@spec update(String.t(), fields()) :: String.t()
```

Builds an UPDATE statement for an existing record.

## Examples

    iex> Dllb.Query.update("user:u1", %{name: "Bob"})
    "UPDATE user:u1 SET name = 'Bob'"

# `update_where`

```elixir
@spec update_where(String.t(), fields(), String.t()) :: String.t()
```

Builds an `UPDATE <table> SET ... WHERE <clause>` statement that updates
every row matching the (already-built) WHERE clause.

Only the listed fields are changed (partial-update semantics). An empty
WHERE string updates all rows in the table.

## Examples

    iex> Dllb.Query.update_where("ast_node", %{arity: 1}, "kind = 'function_def'")
    "UPDATE ast_node SET arity = 1 WHERE kind = 'function_def'"

# `upsert`

```elixir
@spec upsert(String.t(), String.t(), fields(), fields()) :: String.t()
```

Builds a `CREATE ... ON CONFLICT UPDATE` statement for idempotent upserts.

If the record already exists (by table + id), the conflict is resolved by
updating it instead of failing:

  * with no `update_fields` (or an empty map), the engine merges the same
    `fields` from the CREATE into the existing record (`ON CONFLICT UPDATE`);
  * with a non-empty `update_fields` map, the engine applies those explicit
    fields to the existing record instead (`ON CONFLICT UPDATE SET ...`).

## Examples

    iex> Dllb.Query.upsert("user", "u1", %{name: "Alice", age: 30})
    "CREATE user:u1 SET age = 30, name = 'Alice' ON CONFLICT UPDATE"

    iex> Dllb.Query.upsert("user", "u1", %{name: "Alice", age: 30}, %{age: 31})
    "CREATE user:u1 SET age = 30, name = 'Alice' ON CONFLICT UPDATE SET age = 31"

# `vector_info`

```elixir
@spec vector_info(String.t()) :: String.t()
```

Builds a `VECTOR INFO <index_name>` statement that returns metadata about
the HNSW index: node count, layer distribution, snapshot size, config.

## Examples

    iex> Dllb.Query.vector_info("vec_src")
    "VECTOR INFO vec_src"

# `vector_restore`

```elixir
@spec vector_restore(String.t()) :: String.t()
```

Builds a `VECTOR RESTORE <index_name>` statement that triggers the server
to reload the HNSW index from its latest persisted snapshot.

## Examples

    iex> Dllb.Query.vector_restore("vec_src")
    "VECTOR RESTORE vec_src"

# `vector_search`

```elixir
@spec vector_search(String.t(), String.t(), [number()], keyword()) :: String.t()
```

Builds a `VECTOR SEARCH` statement: an approximate nearest-neighbour query
against a `field` that has a vector index. `vector` is the query embedding
(a list of numbers). Results come back as rows ordered nearest-first, each
carrying an extra `distance` field.

## Options

  * `:where` - optional WHERE clause to scope results server-side (applied
    to the ranked hits before `:k`)
  * `:k` - number of nearest neighbours to return

## Examples

    iex> Dllb.Query.vector_search("doc", "embedding", [0.1, 0.2, 0.3])
    "VECTOR SEARCH doc embedding [0.1, 0.2, 0.3]"

    iex> Dllb.Query.vector_search("doc", "embedding", [0.1, 0.2, 0.3], k: 5)
    "VECTOR SEARCH doc embedding [0.1, 0.2, 0.3] K 5"

    iex> Dllb.Query.vector_search("doc", "embedding", [0.1, 0.2], where: "project = 'p1'", k: 5)
    "VECTOR SEARCH doc embedding [0.1, 0.2] WHERE project = 'p1' K 5"

# `vector_snapshot`

```elixir
@spec vector_snapshot(String.t()) :: String.t()
```

Builds a `VECTOR SNAPSHOT <index_name>` statement that triggers the server
to serialize the in-memory HNSW index to persistent storage.

The snapshot is atomic — readers are not blocked. On restart, the server
reloads from the latest snapshot rather than rebuilding from scratch.

## Examples

    iex> Dllb.Query.vector_snapshot("vec_src")
    "VECTOR SNAPSHOT vec_src"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
