From b81586ee4a0543a5f9866586c78dccdd6328934a Mon Sep 17 00:00:00 2001 From: Marcos Magueta Date: Mon, 22 Jun 2026 22:55:27 -0300 Subject: RNT ephemeral relations representation --- lib/dml/ast.ml | 8 +++++ lib/dml/executor.ml | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) (limited to 'lib/dml') 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 -- cgit v1.2.3