Repodb.Query

repodb · API reference

type join_kind = 
  | Inner
  | Left
  | Right
  | Full

Composable SQL query builder with phantom-typed query kinds.

('result, 'kind) t represents a SELECT, INSERT, UPDATE, or DELETE query. The phantom 'kind prevents passing an update query to a select executor and lets Repo.Make expose separate execution functions for each query type.

Query construction is pipeline-friendly:

let users_by_domain domain =
  Query.from User.table
  |> Query.select Expr.(column User.id ** column User.email)
  |> Query.where Expr.(Expr.like (column User.email) ("%@" ^ domain))
  |> Query.order_by Expr.(column User.inserted_at) ~direction:Query.Desc
  |> Query.limit 50

For inserts, use Query_values to build heterogeneous typed rows:

let q =
  Query.insert_into User.table
  |> Query.values
       [ User.email; User.name; User.active ]
       [ Query_values.values3 "ada@example.com" "Ada" true ]
  |> Query.returning Expr.(column User.id)

where clauses are combined with AND. Use or_where to combine the most recent predicate with OR. Repeated order_by, group_by, and join calls preserve the order in which they were added when rendered.

Beyond plain CRUD the builder covers the pieces a durable queue needs: lock for FOR UPDATE SKIP LOCKED, with_cte for common table expressions, update_from for UPDATE ... FROM, and as_scalar, in_subquery, and exists for typed subqueries. Each renders per dialect and raises Error.Unsupported_dialect_feature rather than emitting SQL a backend would reject.

to_sql_params is the preferred renderer for execution because it returns SQL plus driver parameters, collected strictly in the order their placeholders appear in the SQL text. to_sql is useful for debugging and test assertions. Dialect-specific rendering handles placeholder style, upsert syntax, JSON expressions, and RETURNING support checks.

type order_direction = 
  | Asc
  | Desc
type select_query
type insert_query
type update_query
type delete_query
type lock_strength = [ 
  | `Update
  | `No_key_update
  | `Share
  | `Key_share
 ]

Row-lock strength. No_key_update` and Key_share` are PostgreSQL-only.

type lock_wait = [ 
  | `Wait
  | `Nowait
  | `Skip_locked
 ]

What to do when a wanted row is already locked: block, fail immediately, or skip it. A queue claim wants ``Skip_locked`.

type materialization = [ 
  | `Default
  | `Materialized
  | `Not_materialized
 ]

Optional MATERIALIZED hint on a CTE. PostgreSQL 12+ and SQLite 3.35+ accept it; MySQL does not.

type ('result, 'kind) t = {
  query_type : query_type;
  table : Schema.table;
  select : Expr.wrapped_expr list option;
  wheres : bool Expr.t list;
  joins : join list;
  order_by : (Expr.wrapped_expr * order_direction) list;
  group_by : Expr.wrapped_expr list;
  having : bool Expr.t list;
  limit : limit_spec option;
  offset : limit_spec option;
  distinct : bool;
  returning : Expr.wrapped_expr list option;
  set_values : (string * Expr.wrapped_expr) list;
  insert_columns : string list;
  insert_values : Expr.wrapped_expr list list;
  conflict_target : string list option;
  conflict_action : conflict_action option;
  ctes : cte list;
  update_from : source list;
  lock : row_lock option;
}
and query_type = 
  | Select
  | Insert
  | Update
  | Delete
and limit_spec = 
  | Limit_const of int
  | Limit_expr of Expr.wrapped_expr
and join = {
  kind : join_kind;
  table : Schema.table;
  on : Expr.wrapped_expr;
}
and conflict_action = 
  | DoNothing
  | DoUpdate of (string * Expr.wrapped_expr) list
and cte = {
  cte_name : string;
  cte_columns : string list;
  cte_materialized : materialization;
  cte_body : cte_body;
}
and cte_body = 
  | Cte_select : ('r, select_query) t -> cte_body
and source = 
  | Source_table of Schema.table
  | Source_name of string
and row_lock = {
  lock_strength : lock_strength;
  lock_wait : lock_wait;
  lock_of : string list;
}
val empty_query : Schema.table -> query_type -> ('a, 'b) t
val from : Schema.table -> ('a, 'b) t
val insert_into : Schema.table -> ('a, 'b) t
val update : Schema.table -> ('a, 'b) t
val delete_from : Schema.table -> ('a, 'b) t
val select : 'a Expr.expr_list -> ('b, 'c) t -> ('d, 'e) t
val select_all : ('a, 'b) t -> ('c, 'd) t
val where : bool Expr.t -> ('a, 'b) t -> ('c, 'd) t
val and_where : bool Expr.t -> ('a, 'b) t -> ('c, 'd) t
val or_where : bool Expr.t -> ('a, 'b) t -> ('c, 'd) t
val join : 
  ?kind:join_kind ->
  on:'a Expr.t ->
  Schema.table ->
  ('b, 'c) t ->
  ('d, 'e) t
val left_join : on:'a Expr.t -> Schema.table -> ('b, 'c) t -> ('d, 'e) t
val right_join : on:'a Expr.t -> Schema.table -> ('b, 'c) t -> ('d, 'e) t
val inner_join : on:'a Expr.t -> Schema.table -> ('b, 'c) t -> ('d, 'e) t
val full_join : on:'a Expr.t -> Schema.table -> ('b, 'c) t -> ('d, 'e) t
val order_by : 
  ?direction:order_direction ->
  'a Expr.t ->
  ('b, 'c) t ->
  ('d, 'e) t
val asc : 'a Expr.t -> ('b, 'c) t -> ('d, 'e) t
val desc : 'a Expr.t -> ('b, 'c) t -> ('d, 'e) t
val group_by : 'a Expr.expr_list -> ('b, 'c) t -> ('d, 'e) t
val having : bool Expr.t -> ('a, 'b) t -> ('c, 'd) t
val limit : int -> ('a, 'b) t -> ('c, 'd) t
val offset : int -> ('a, 'b) t -> ('c, 'd) t
val limit_expr : 'a Expr.t -> ('b, 'c) t -> ('d, 'e) t

Parameterized row counts: limit_expr Expr.(int n) renders LIMIT ? and binds n instead of inlining it.

val offset_expr : 'a Expr.t -> ('b, 'c) t -> ('d, 'e) t
val distinct : ('a, 'b) t -> ('c, 'd) t
val returning : 'a Expr.expr_list -> ('b, 'c) t -> ('d, 'e) t
val returning_all : Schema.table -> ('a, 'b) t -> ('c, 'd) t

returning_all table renders RETURNING <table>.*, which PostgreSQL needs to disambiguate a RETURNING over an UPDATE ... FROM.

val returning_star : ('a, 'b) t -> ('c, 'd) t

returning_star renders a bare RETURNING *.

val set : ('a, 'b) Field.t -> 'c Expr.t -> ('d, 'e) t -> ('f, 'g) t
val inc : ('a, 'b) Field.t -> int -> ('c, 'd) t -> ('e, 'f) t

inc col n renders col = col + n, the arithmetic increment an attempt counter needs.

val values : 
  ('a, 'b) Field.t list ->
  'c Expr.t list list ->
  ('d, 'e) t ->
  ('f, 'g) t
val on_conflict_do_nothing : 
  ?target:('a, 'b) Field.t list ->
  ('c, 'd) t ->
  ('e, 'f) t
val on_conflict_do_update : 
  target:('a, 'b) Field.t list ->
  set:(('c, 'd) Field.t * 'e Expr.t) list ->
  ('f, 'g) t ->
  ('h, 'i) t

Row locking

val lock : 
  strength:lock_strength ->
  ?wait:lock_wait ->
  ?of_:Schema.table list ->
  ('r, select_query) t ->
  ('r, select_query) t

Common table expressions

val cte : 
  ?columns:string list ->
  ?materialized:materialization ->
  name:string ->
  ('a, select_query) t ->
  cte
val with_cte : cte -> ('a, 'b) t -> ('c, 'd) t
val cte_column : cte -> String.t -> 'a Types.t -> 'a Expr.t

cte_column c "id" Types.int64 is a typed reference to a column of the CTE. When the CTE declares an explicit column list the name is checked against it.

UPDATE ... FROM

val update_from : cte -> ('a, 'b) t -> ('c, 'd) t
val update_from_table : Schema.table -> ('a, 'b) t -> ('c, 'd) t

Rendering

val join_kind_to_sql : join_kind -> string
val direction_to_sql : order_direction -> string
val source_name : source -> string
val unsupported : 
  feature:string ->
  dialect:Driver.dialect ->
  ?suggestion:string ->
  unit ->
  'a
type writer = {
  dialect : Driver.dialect;
  emit : Buffer.t -> Expr.wrapped_expr -> unit;
}
val literal_writer : Driver.dialect -> writer
val param_writer : Expr.param_ctx -> writer
val emit_expr : writer -> Buffer.t -> 'a Expr.t -> unit
val emit_sep : Buffer.t -> sep:string -> f:('a -> unit) -> 'a list -> unit
val add : Buffer.t -> string -> unit
val render_query : 'r 'k. writer -> Buffer.t -> ('r, 'k) t -> unit
val render_ctes : writer -> Buffer.t -> cte list -> unit
val render_select : 'r 'k. writer -> Buffer.t -> ('r, 'k) t -> unit
val render_insert : 'r 'k. writer -> Buffer.t -> ('r, 'k) t -> unit
val render_conflict : 'r 'k. writer -> Buffer.t -> ('r, 'k) t -> unit
val render_update : 'r 'k. writer -> Buffer.t -> ('r, 'k) t -> unit
val render_delete : 'r 'k. writer -> Buffer.t -> ('r, 'k) t -> unit
val render_where : writer -> Buffer.t -> bool Expr.t list -> unit
val render_group_by : writer -> Buffer.t -> Expr.wrapped_expr list -> unit
val render_having : writer -> Buffer.t -> bool Expr.t list -> unit
val render_order_by : 
  writer ->
  Buffer.t ->
  (Expr.wrapped_expr * order_direction) list ->
  unit
val render_row_count : 
  writer ->
  Buffer.t ->
  string ->
  limit_spec option ->
  unit
val render_returning : 'r 'k. writer -> Buffer.t -> ('r, 'k) t -> unit
val render_lock : writer -> Buffer.t -> row_lock option -> unit
val to_sql : ?dialect:Driver.dialect -> ('a, 'b) t -> string
val to_sql_params : 
  ?dialect:Driver.dialect ->
  ('a, 'b) t ->
  string * Driver.Value.t array

Subqueries

val renderer_of_query : ('a, 'b) t -> Expr.renderer
val as_scalar : ('a, 'b) t -> 'c Types.t -> 'c Expr.t

as_scalar q ty uses q as a typed scalar subexpression.

val in_subquery : 'a Expr.t -> ('b, 'c) t -> bool Expr.t
val not_in_subquery : 'a Expr.t -> ('b, 'c) t -> bool Expr.t
val exists : ('a, 'b) t -> bool Expr.t
val not_exists : ('a, 'b) t -> bool Expr.t