1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
|
module Make (NT : Nt.S) = struct
module DrlExec = Drl.Executor.Make (NT)
module Error = struct
open Condition
(* TODO: more structure *)
let parse_error msg = condition "parse-error" msg empty
let relation_not_found name = condition "relation-not-found" "Relation not found" ("name" |=| (of_string name))
let multigroup_not_found name = condition "multigroup-not-found" "Multigroup not found" ("name" |=| (of_string name))
end
let ( let* ) = Result.bind
let parse_fqn (s : string) : (Qualified_name.t, Condition.t) result =
Qualified_name.try_parse s
let lookup_mg (ctx : Sublanguage_context.t) (mg_name : string) :
(Management.Multigroup.multigroup, Condition.t) result =
match ctx.branch#mg_of mg_name with
| Some mg -> Ok mg
| None -> Error (Error.multigroup_not_found mg_name)
let get_rel (ctx : Sublanguage_context.t) (fqn : Qualified_name.t) =
let* mg = lookup_mg ctx fqn.mg in
match NT.get_relation mg fqn.name with
| Some r -> Ok r
| None -> Error (Error.relation_not_found (Qualified_name.to_string fqn))
let retarget target (t : Tuple.materialized) =
{ t with Tuple.relation = target }
let build_tuple ~relation (attributes : Ast.attr_value list) : Tuple.materialized =
let attr_map =
List.fold_left
(fun acc (name, value) ->
Tuple.AttributeMap.add name
{ Attribute.value = Drl.Ast.value_to_abstract value }
acc)
Tuple.AttributeMap.empty attributes
in
{ Tuple.relation; attributes = attr_map }
(* 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 query in
let* stream =
NT.execute_query plan ~rel_name:"dml_drain"
in
let rec drain acc =
match NT.stream_next stream with
| Error _ -> List.rev acc
| Ok None -> List.rev acc
| Ok (Some t) -> drain (t :: acc)
in
let tuples = drain [] in
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
let tuple_matches_pred common (target_t : Tuple.materialized)
(pred_t : Tuple.materialized) =
List.for_all (fun attr ->
match
( Tuple.AttributeMap.find_opt attr target_t.Tuple.attributes,
Tuple.AttributeMap.find_opt attr pred_t.Tuple.attributes )
with
| Some a, Some b -> attr_val_eq a b
| _ -> false)
common
let semijoin common target_tuples pred_tuples =
List.filter
(fun t -> List.exists (tuple_matches_pred common t) pred_tuples)
target_tuples
(* DML mutates tuples in a single mg per statement. Returns a
single-element transition delta keyed by that mg's name. *)
let execute (ctx : Sublanguage_context.t) (stmt : Ast.statement) :
(Sublanguage_types.transition_delta, Condition.t) result =
let bh = ctx.write_handle in
let branch_name = ctx.branch#name in
let after fqn = Result.bind (lookup_mg ctx fqn.Qualified_name.mg)
(fun mg -> Ok [ (fqn.Qualified_name.mg, mg) ])
in
match stmt with
| Ast.InsertTuple { relation; attributes } ->
let* fqn = parse_fqn relation in
let* rel = get_rel ctx fqn in
let tuple = build_tuple ~relation:rel#name attributes in
let* _ =
NT.create_tuple ~branch_name ~mg_name:fqn.mg ~rel_name:rel#name tuple
in
after fqn
| Ast.InsertTuples { relation; tuples } ->
let* fqn = parse_fqn relation in
let* rel = get_rel ctx fqn in
let tuple_list = List.map (build_tuple ~relation:rel#name) tuples in
let* _ =
NT.create_tuples ~branch_name ~mg_name:fqn.mg ~rel_name:rel#name
tuple_list
in
after fqn
| Ast.DeleteTuple { relation; attributes } ->
let* fqn = parse_fqn relation in
let* rel = get_rel ctx fqn in
let tuple = build_tuple ~relation:rel#name attributes in
let hash = Hashing.hash_tuple tuple in
let* () =
NT.retract_tuple ~branch_name ~mg_name:fqn.mg ~rel_name:rel#name hash
in
after fqn
| Ast.Assign { target; body } ->
let* fqn = parse_fqn target in
let* mg = lookup_mg ctx fqn.mg in
let* rel = get_rel ctx fqn in
let* tuples = drain_query ctx body in
let* _bh, new_mg =
NT.clear_relation bh mg ~branch_name ~mg_name:fqn.mg
(rel :> Relation.relation)
in
ctx.branch#set_mg ~name:fqn.mg new_mg;
let* _ =
NT.create_tuples ~branch_name ~mg_name:fqn.mg ~rel_name:rel#name
(List.map (retarget rel#name) tuples)
in
Ok [ (fqn.mg, new_mg) ]
| Ast.InsertFrom { target; source } ->
let* fqn = parse_fqn target in
let* rel = get_rel ctx fqn in
let* tuples = drain_query ctx source in
let* _ =
NT.create_tuples ~branch_name ~mg_name:fqn.mg ~rel_name:rel#name
(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
let* target_tuples = drain_query ctx (Drl.Ast.Base target) in
let* pred_tuples = drain_query ctx predicate in
let attr_names_of = function
| [] -> []
| t :: _ ->
List.map fst (Tuple.AttributeMap.bindings t.Tuple.attributes)
in
let target_attrs = attr_names_of target_tuples in
let pred_attrs = attr_names_of pred_tuples in
let common = List.filter (fun n -> List.mem n pred_attrs) target_attrs in
let to_delete = semijoin common target_tuples pred_tuples in
let* () =
List.fold_left
(fun acc t ->
let* () = acc in
let hash = Hashing.hash_tuple (retarget rel#name t) in
NT.retract_tuple ~branch_name ~mg_name:fqn.mg ~rel_name:rel#name
hash)
(Ok ()) to_delete
in
after fqn
end
module Memory = Make (Nt.Memory)
|