aboutsummaryrefslogtreecommitdiffstats
path: root/lib/dml
diff options
context:
space:
mode:
authorMarcos Magueta <maguetamarcos@gmail.com>2026-06-22 22:55:27 -0300
committerGitHub <noreply@github.com>2026-06-22 22:55:27 -0300
commitb81586ee4a0543a5f9866586c78dccdd6328934a (patch)
tree4cd2f26e8e57e7e8d852fb7deddd0818c482dc8b /lib/dml
parent57b55b51ee6a1d4678ba37e4631d61a248411ff2 (diff)
downloadSakura-b81586ee4a0543a5f9866586c78dccdd6328934a.tar.gz
Sakura-b81586ee4a0543a5f9866586c78dccdd6328934a.zip
RNT ephemeral relations representation
Diffstat (limited to '')
-rw-r--r--lib/dml/ast.ml8
-rw-r--r--lib/dml/executor.ml87
2 files changed, 94 insertions, 1 deletions
diff --git a/lib/dml/ast.ml b/lib/dml/ast.ml
index d8defe6..03e68ac 100644
--- a/lib/dml/ast.ml
+++ b/lib/dml/ast.ml
@@ -9,4 +9,12 @@ type statement =
| Assign of { target : string; body : Drl.Ast.query }
| InsertFrom of { target : string; source : Drl.Ast.query }
| DeleteWhere of { target : string; predicate : Drl.Ast.query }
+ (** Binds an unqualified name to a session-scoped ephemeral relation.
+ The body is recomputed on each scan and never materialized;
+ the binding lives until [DropDefine] or a session close.
+ In contrast, [Assign] drains the body into an existing
+ stored relation. *)
+ | Define of { target : string; body : Drl.Ast.query }
+ (** Drops a [Define] binding, releasing its session-ownership pin. *)
+ | DropDefine of { target : string }
[@@deriving sexp]
diff --git a/lib/dml/executor.ml b/lib/dml/executor.ml
index c35f128..d5c55f8 100644
--- a/lib/dml/executor.ml
+++ b/lib/dml/executor.ml
@@ -43,7 +43,7 @@ module Make (NT : Nt.S) = struct
(* Drain all tuples from a DRL query using ctx.resolve for path resolution.
Cross-multigroup reads work transparently here. *)
let drain_query (ctx : Sublanguage_context.t) query =
- let* plan = DrlExec.compile ctx.resolve query in
+ let* plan = DrlExec.compile ctx query in
let* stream =
NT.execute_query plan ~rel_name:"dml_drain"
in
@@ -57,6 +57,58 @@ module Make (NT : Nt.S) = struct
ignore (NT.stream_close stream);
Ok tuples
+ (* Stored-relation paths referenced by a query, for ephemeral dependency
+ pinning. A qualified leaf resolves through ctx; an unqualified one is a
+ previously Defined session ephemeral (views compose). *)
+ let rec base_dep_paths (ctx : Sublanguage_context.t) (q : Drl.Ast.query) :
+ string list =
+ match q with
+ | Drl.Ast.Base name ->
+ (match Qualified_name.try_parse name with
+ | Ok fqn -> [ ctx.resolve fqn ]
+ | Error _ -> [ Sublanguage_context.ephemeral_path ctx name ])
+ | Drl.Ast.Const _ -> []
+ | Drl.Ast.Select (a, b)
+ | Drl.Ast.Join (_, a, b)
+ | Drl.Ast.Cartesian (a, b)
+ | Drl.Ast.Union (a, b)
+ | Drl.Ast.Diff (a, b) -> base_dep_paths ctx a @ base_dep_paths ctx b
+ | Drl.Ast.Project (_, s)
+ | Drl.Ast.Rename (_, s)
+ | Drl.Ast.Take (_, s) -> base_dep_paths ctx s
+
+ (* Schema of a query result, walked structurally from the base relations'
+ schemas. Mirrors the operator semantics in docs/relational-algebra.org. *)
+ let rec infer_schema (ctx : Sublanguage_context.t) (q : Drl.Ast.query) :
+ (Schema.t, Condition.t) result =
+ match q with
+ | Drl.Ast.Base name ->
+ (match Qualified_name.try_parse name with
+ | Error _ ->
+ (* Session ephemeral: RNT owns its schema; nothing to derive
+ on the OCaml-side, so treated as contributing no attributes. *)
+ Ok []
+ | Ok fqn ->
+ let* rel = get_rel ctx fqn in
+ Ok rel#schema)
+ | Drl.Ast.Const attrs -> Ok (List.map (fun (n, _) -> (n, "abstract")) attrs)
+ | Drl.Ast.Select (_, src) -> infer_schema ctx src
+ | Drl.Ast.Join (_, l, r) | Drl.Ast.Cartesian (l, r) ->
+ let* ls = infer_schema ctx l in
+ let* rs = infer_schema ctx r in
+ Ok (ls @ List.filter (fun (n, _) -> not (List.mem_assoc n ls)) rs)
+ | Drl.Ast.Project (attrs, src) ->
+ let* s = infer_schema ctx src in
+ Ok (List.filter (fun (n, _) -> List.mem n attrs) s)
+ | Drl.Ast.Rename (renames, src) ->
+ let* s = infer_schema ctx src in
+ Ok (List.map (fun (n, d) ->
+ match List.assoc_opt n renames with
+ | Some n' -> (n', d)
+ | None -> (n, d)) s)
+ | Drl.Ast.Union (l, _) | Drl.Ast.Diff (l, _) -> infer_schema ctx l
+ | Drl.Ast.Take (_, src) -> infer_schema ctx src
+
let attr_val_eq a b =
Stdlib.( = ) a.Attribute.value b.Attribute.value
@@ -136,6 +188,39 @@ module Make (NT : Nt.S) = struct
(List.map (retarget rel#name) tuples)
in
after fqn
+ | Ast.Define { target; body } ->
+ let* () =
+ if String.contains target ':' then
+ Error (Error.parse_error
+ ("Define target must be unqualified (session-scoped): " ^ target))
+ else Ok ()
+ in
+ (* TODO: redefining an existing name isn't guarded. Right now we lean on
+ Hashtbl.replace and whatever RNT does on collision. Pick replace or
+ error and enforce it here. *)
+ let* schema = infer_schema ctx body in
+ (* TODO: every page drains the whole query and then filters down to the
+ window, so paging costs O(total * pages) and reruns the VM plan each
+ time. Push the offset/limit down or cache the result. *)
+ let generator ~offset ~limit =
+ match drain_query ctx body with
+ | Error _ -> []
+ | Ok tuples ->
+ List.filteri (fun i _ -> i >= offset && i < offset + limit) tuples
+ in
+ let* _path =
+ NT.register_ephemeral ~session:ctx.session_hash ~named:true
+ ~name:target ~generator
+ ~cardinality:Conventions.Cardinality.ConstrainedFinite
+ ~identity:(Drl.Parser.to_string body) ~schema
+ ~deps:(base_dep_paths ctx body)
+ in
+ Ok []
+ | Ast.DropDefine { target } ->
+ let* () =
+ NT.drop_ephemeral ~session:ctx.session_hash ~name:target
+ in
+ Ok []
| Ast.DeleteWhere { target; predicate } ->
let* fqn = parse_fqn target in
let* rel = get_rel ctx fqn in