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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
|
(* Interface to the RNT kernel *)
open Ctypes
(* Shared types independent of backend, shared across all Make instances *)
type auth_method = Certificate | PlainText
(** Opaque claim token returned by [authenticate]. *)
type claims = string
(** Opaque handle to an open branch (multigroup head) in the NT registry. *)
type branch_handle = nativeint
(** Opaque handle to an open relation object in the NT registry. *)
type relation_handle = nativeint
(** Distinguishes a simple single-relation cursor from a full VM plan cursor. *)
type cursor_kind = Simple | Vm
(** A streaming cursor over tuples produced either by a simple scan or the VM. *)
type tuple_stream = {
ts_cursor : nativeint;
ts_relation : string;
ts_kind : cursor_kind;
}
type path_arg = Var of string | Const of string
type plan_node =
| Scan of { path : string; args : (string * path_arg) list }
(* Given this is the query plan, this should probably specify a
concrete join algorithm (e.g. HashJoin, NestedLoop, LeapFrog, &c) *)
| Join of { left : plan_node; right : plan_node; on_attrs : string list }
| Take of { limit : int; source : plan_node }
| Project of { attrs : string list; source : plan_node }
module Error = struct
open Condition
(* TODO: better error structure *)
let auth_failed msg = condition "auth-failed" msg empty
let handle_error rc msg = condition "handle-error" msg ("return-code" |=| (of_int rc))
let object_not_found kind id = condition "object-not-found" "Object not found"
("kind" |=| (of_string kind) &
"identifier" |=| (of_string id))
let cursor_error msg = condition "cursor-error" msg empty
let not_supported msg = condition "not-supported" msg empty
let initialization_error = condition "initialization-error" "Couldn't initialize" empty
end
(* --------------------------------------------------------------------------
Backend selector
-------------------------------------------------------------------------- *)
module type Backend = sig
(** RNT storage driver: "sqlite" or "memory". *)
val driver : string
(** Passed to the driver as its init argument.
For "sqlite": file path or ":memory:".
For "memory": ignored. *)
val init_arg : string
end
(* --------------------------------------------------------------------------
Functor — one instance per backend
-------------------------------------------------------------------------- *)
module Make (B : Backend) = struct
let ( let* ) = Result.bind
(* --------------------------------------------------------------------------
Authentication
-------------------------------------------------------------------------- *)
let authenticate (method_ : auth_method) : (claims, Condition.t) result =
let method_str = match method_ with
| Certificate -> "certificate"
| PlainText -> "plain_text"
in
let (rc, claims_opt) =
Nt_ffi.with_out_string (fun pp -> Nt_ffi.rnt_firewall method_str pp)
in
match (rc, claims_opt) with
| (0, Some s) -> Ok s
| (_, Some s) -> Error (Error.auth_failed s)
| _ -> Error (Error.auth_failed "firewall rejected connection")
(* --------------------------------------------------------------------------
Path helpers (shared by branch and relation lifecycle)
-------------------------------------------------------------------------- *)
let branch_path (branch_name : string) : string =
"/system/branches/" ^ branch_name
let multigroup_path ~(branch_name : string) ~(mg_name : string) : string =
branch_path branch_name ^ "/multigroups/" ^ mg_name
let relation_path ~(branch_name : string) ~(mg_name : string)
~(rel_name : string) : string =
multigroup_path ~branch_name ~mg_name ^ "/relations/" ^ rel_name
(* --------------------------------------------------------------------------
Branch lifecycle
--------------------------------------------------------------------------
RNT now owns branch state: each branch is a named hash-pointer to an
immutable snapshot. Sakura's [multigroup] is the OCaml-side mirror of
that snapshot's content (relations, schemas, constraints, lineage).
There is no payload to serialise — every relation/tuple mutation
advances the branch to a new snapshot internally. *)
let ensure_branch (path : string) : unit =
(* Idempotent: rnt_register_branch with "" creates an unborn branch when
missing, no-ops when present. *)
ignore (Nt_ffi.rnt_register_branch path "")
let branch_target_of_handle (bh : branch_handle) : (string, Condition.t) result =
let (rc, target_opt) =
Nt_ffi.with_out_string
(fun pp -> Nt_ffi.rnt_branch_target (Nt_ffi.nint_to_ptr bh) pp)
in
match (rc, target_opt) with
| (0, Some s) -> Ok s
| (0, None) -> Ok ""
| _ -> Error (Error.handle_error rc "branch_target read failed")
let list_relations ~(branch_name : string) ~(mg_name : string) :
((string * string) list, Condition.t) result =
let path = multigroup_path ~branch_name ~mg_name in
let (rc, out_opt) =
Nt_ffi.with_out_string (fun pp -> Nt_ffi.rnt_list_relations path pp)
in
match (rc, out_opt) with
| (0, Some s) ->
let pairs =
String.split_on_char '\n' s
|> List.filter_map (fun line ->
match String.split_on_char '\t' line with
| [n; r] when n <> "" -> Some (n, r)
| _ -> None)
in
Ok pairs
| (0, None) -> Ok []
| _ -> Error (Error.handle_error rc
("list_relations failed: " ^ branch_name
^ "/" ^ mg_name))
let list_branch_multigroups (branch_name : string) :
((string * string) list, Condition.t) result =
let path = branch_path branch_name in
let (rc, out_opt) =
Nt_ffi.with_out_string
(fun pp -> Nt_ffi.rnt_list_branch_multigroups path pp)
in
match (rc, out_opt) with
| (0, Some s) ->
let pairs =
String.split_on_char '\n' s
|> List.filter_map (fun line ->
match String.split_on_char '\t' line with
| [n; h] when n <> "" -> Some (n, h)
| _ -> None)
in
Ok pairs
| (0, None) -> Ok []
| _ -> Error (Error.handle_error rc
("list_branch_multigroups failed: " ^ branch_name))
let session_open () : (string, Condition.t) result =
let (rc, id_opt) =
Nt_ffi.with_out_string
(fun pp -> Nt_ffi.rnt_session_open (from_voidp void null) pp)
in
match (rc, id_opt) with
| (0, Some id) -> Ok id
| _ -> Error (Error.handle_error rc "session_open failed")
let session_close (sid : string) : (unit, Condition.t) result =
let rc = Nt_ffi.rnt_session_close sid in
if rc = 0 then Ok () else Error (Error.handle_error rc "session_close failed")
let session_set_branch (sid : string) (branch_name : string)
(target_hash : string) : (unit, Condition.t) result =
let rc = Nt_ffi.rnt_session_set_branch sid branch_name target_hash in
if rc = 0 then Ok () else Error (Error.handle_error rc "session_set_branch failed")
let open_branch (_claims : claims) (name : string) :
(branch_handle * Management.Multigroup.multigroup, Condition.t) result =
let path = branch_path name in
ensure_branch path;
let raw = Nt_ffi.rnt_open_handle path (from_voidp void null) in
match Nt_ffi.ptr_to_opt raw with
| None -> Error (Error.object_not_found "branch" name)
| Some bh ->
let* _tip = branch_target_of_handle bh in
let* rel_entries = list_relations ~branch_name:name ~mg_name:name in
(* Populate mg with stored relations from RNT snapshot. Schema is not
persisted in the snapshot codec so relations start schema-less; the
session rebuilds schema state via DDL or catalog queries. *)
let mg =
List.fold_left
(fun mg (rel_name, root) ->
let rel =
(new Relation.stored
~name:rel_name
~schema:[]
~constraints:None
~cardinality:Conventions.Cardinality.AlephZero
~lineage:(Some (Relation.Lineage.Base rel_name))
~provenance:(Some (Relation.Provenance.base
~relation:rel_name ~attributes:[]))
~membership_criteria:(fun _ -> true))
#set_tree_pointer (Some root)
in
mg#add_relation (rel :> Relation.relation))
(new Management.Multigroup.multigroup ~name)
rel_entries
in
Ok (bh, mg)
let close_branch (bh : branch_handle) : (unit, Condition.t) result =
let rc = Nt_ffi.rnt_close_handle (Nt_ffi.nint_to_ptr bh) in
if rc = 0 then Ok () else Error (Error.handle_error rc "close_branch failed")
let list_snapshot_relations (snapshot_hash : string) :
((string * string) list, Condition.t) result =
let (rc, out_opt) =
Nt_ffi.with_out_string
(fun pp -> Nt_ffi.rnt_list_snapshot_relations snapshot_hash pp)
in
match (rc, out_opt) with
| (0, Some s) ->
let pairs =
String.split_on_char '\n' s
|> List.filter_map (fun line ->
match String.split_on_char '\t' line with
| [n; r] when n <> "" -> Some (n, r)
| _ -> None)
in
Ok pairs
| (0, None) -> Ok []
| _ -> Error (Error.handle_error rc ("list_snapshot_relations failed: " ^ snapshot_hash))
(* Opens a read-only view pinned to a specific snapshot hash.
Returns (bh, mg) where bh is a handle on the snapshot object and mg
reflects exactly the relation set at that hash. The caller is
responsible for refusing mutations (see Branch.Detached). *)
let open_snapshot (_claims : claims) (branch_name : string)
(snapshot_hash : string) :
(branch_handle * Management.Multigroup.multigroup, Condition.t) result =
let snap_path = "/system/snapshots/" ^ snapshot_hash in
let raw = Nt_ffi.rnt_open_handle snap_path (from_voidp void null) in
match Nt_ffi.ptr_to_opt raw with
| None -> Error (Error.object_not_found "snapshot" snapshot_hash)
| Some bh ->
let* rel_entries = list_snapshot_relations snapshot_hash in
let mg =
List.fold_left
(fun mg (rel_name, root) ->
let rel =
(new Relation.stored
~name:rel_name
~schema:[]
~constraints:None
~cardinality:Conventions.Cardinality.AlephZero
~lineage:(Some (Relation.Lineage.Base rel_name))
~provenance:(Some (Relation.Provenance.base
~relation:rel_name ~attributes:[]))
~membership_criteria:(fun _ -> true))
#set_tree_pointer (Some root)
in
mg#add_relation (rel :> Relation.relation))
(new Management.Multigroup.multigroup ~name:branch_name)
rel_entries
in
Ok (bh, mg)
(* --------------------------------------------------------------------------
Relation handles
-------------------------------------------------------------------------- *)
let open_relation ~(branch_name : string) ~(mg_name : string)
~(rel_name : string) : (relation_handle, Condition.t) result =
let path = relation_path ~branch_name ~mg_name ~rel_name in
ignore (Nt_ffi.rnt_register_relation path);
let raw = Nt_ffi.rnt_open_handle path (from_voidp void null) in
match Nt_ffi.ptr_to_opt raw with
| None -> Error (Error.object_not_found "relation" rel_name)
| Some rh -> Ok rh
let close_relation (rh : relation_handle) : (unit, Condition.t) result =
let rc = Nt_ffi.rnt_close_handle (Nt_ffi.nint_to_ptr rh) in
if rc = 0 then Ok () else Error (Error.handle_error rc "close_relation failed")
(* --------------------------------------------------------------------------
Query execution via the Tarski VM plan builder
-------------------------------------------------------------------------- *)
(* Recursively builds a C side plan tree via rnt_plan_assemble.
On partial failure the partially built plan is freed before returning
Error (assemble itself frees child plans it took ownership of when it
fails, hopefully). *)
let make_action (op : int) : Nt_ffi.plan_action structure =
let act = Ctypes.make Nt_ffi.plan_action in
setf act Nt_ffi.action_operation op;
act
let rec build_plan (plan : plan_node) : (nativeint, Condition.t) result =
match plan with
| Scan { path; _ } ->
let act = make_action Nt_ffi.fol_operation_scan in
let cpath = Nt_ffi.cstring path in
let scan = getf act Nt_ffi.action_scan in
setf scan Nt_ffi.scan_relation_path (Ctypes.CArray.start cpath);
let raw = Nt_ffi.rnt_plan_assemble act in
(* keep the C string alive across the call above *)
ignore (Sys.opaque_identity cpath);
(match Nt_ffi.ptr_to_opt raw with
| None -> Error (Error.cursor_error ("plan_scan failed for path: " ^ path))
| Some p -> Ok p)
| Join { left; right; _ } ->
let* lp = build_plan left in
(match build_plan right with
| Error e ->
Nt_ffi.rnt_plan_free (Nt_ffi.nint_to_ptr lp);
Error e
| Ok rp ->
let act = make_action Nt_ffi.fol_operation_join in
let join = getf act Nt_ffi.action_join in
setf join Nt_ffi.join_left (Nt_ffi.nint_to_ptr lp);
setf join Nt_ffi.join_right (Nt_ffi.nint_to_ptr rp);
(* assemble takes ownership of both children *)
(match Nt_ffi.ptr_to_opt (Nt_ffi.rnt_plan_assemble act) with
| None -> Error (Error.cursor_error "plan_join failed")
| Some p -> Ok p))
| Take { limit; source } ->
let* sp = build_plan source in
let act = make_action Nt_ffi.fol_operation_take in
let take = getf act Nt_ffi.action_take in
setf take Nt_ffi.take_source (Nt_ffi.nint_to_ptr sp);
setf take Nt_ffi.take_limit (Unsigned.Size_t.of_int limit);
(* assemble takes ownership of source *)
(match Nt_ffi.ptr_to_opt (Nt_ffi.rnt_plan_assemble act) with
| None -> Error (Error.cursor_error "plan_take failed")
| Some p -> Ok p)
| Project { attrs; source } ->
let* sp = build_plan source in
let act = make_action Nt_ffi.fol_operation_project in
let proj = getf act Nt_ffi.action_project in
setf proj Nt_ffi.project_source (Nt_ffi.nint_to_ptr sp);
(* NULL-terminated const char**; backing arrays stay reachable until
assemble returns *)
let cattrs = List.map Nt_ffi.cstring attrs in
let arr =
Ctypes.CArray.of_list (Ctypes.ptr Ctypes.char)
(List.map Ctypes.CArray.start cattrs
@ [ Nt_ffi.null_char_ptr ])
in
setf proj Nt_ffi.project_attrs (Ctypes.CArray.start arr);
let raw = Nt_ffi.rnt_plan_assemble act in
ignore (Sys.opaque_identity (cattrs, arr));
(match Nt_ffi.ptr_to_opt raw with
| None -> Error (Error.cursor_error "plan_project failed")
| Some p -> Ok p)
let execute_query (plan : plan_node) ~(rel_name : string) :
(tuple_stream, Condition.t) result =
let* plan_ptr = build_plan plan in
let raw = Nt_ffi.rnt_vm_execute_plan (Nt_ffi.nint_to_ptr plan_ptr) in
match Nt_ffi.ptr_to_opt raw with
| None -> Error (Error.cursor_error "vm_execute_plan failed")
| Some ts_cursor -> Ok { ts_cursor; ts_relation = rel_name; ts_kind = Vm }
let parse_kv_tuple (relation : string) (kv : string) : Tuple.materialized =
let attributes =
String.split_on_char '\n' kv
|> List.filter_map (fun line ->
match String.split_on_char '=' line with
| k :: rest when k <> "" ->
let v = String.concat "=" rest in
Some (k, Attribute.{ value = Obj.repr v })
| _ -> None)
|> List.fold_left
(fun m (k, v) -> Tuple.AttributeMap.add k v m)
Tuple.AttributeMap.empty
in
{ Tuple.relation; attributes }
let stream_next (stream : tuple_stream) :
(Tuple.materialized option, Condition.t) result =
let next_fn = match stream.ts_kind with
| Simple -> Nt_ffi.rnt_cursor_next
| Vm -> Nt_ffi.rnt_vm_cursor_next
in
let (rc, kv_opt) =
Nt_ffi.with_out_string
(fun pp -> next_fn (Nt_ffi.nint_to_ptr stream.ts_cursor) pp)
in
match rc with
| 0 -> Ok None
| 1 -> (match kv_opt with
| None -> Ok None
| Some kv -> Ok (Some (parse_kv_tuple stream.ts_relation kv)))
| _ -> Error (Error.cursor_error "cursor_next error")
let stream_close (stream : tuple_stream) : (unit, Condition.t) result =
let close_fn = match stream.ts_kind with
| Simple -> Nt_ffi.rnt_cursor_close
| Vm -> Nt_ffi.rnt_vm_cursor_close
in
let rc = close_fn (Nt_ffi.nint_to_ptr stream.ts_cursor) in
if rc = 0 then Ok () else Error (Error.cursor_error "cursor_close failed")
(* --------------------------------------------------------------------------
Tuple storage helpers
-------------------------------------------------------------------------- *)
let materialized_to_kv (tuple : Tuple.materialized) : (string * string) list =
Tuple.AttributeMap.fold
(fun k v acc -> (k, Obj.obj v.Attribute.value) :: acc)
tuple.Tuple.attributes []
(* --------------------------------------------------------------------------
Ephemeral relations
RNT invokes a registered generator through a C function pointer for as
long as the registry entry is aive. ctypes does not root the closure, so
it is retained here keyed by registered path; reregistration replaces
the previous retention. (A dropped view's closure is released on the
next replacement, leaking one closure per named view until then is an
accepted cost of this slice)
-------------------------------------------------------------------------- *)
let retained_generators
: (string,
unit Ctypes.ptr -> string -> Unsigned.size_t -> Unsigned.size_t ->
unit Ctypes.ptr -> int)
Hashtbl.t =
Hashtbl.create 16
let register_ephemeral ~(session : string) ~(named : bool) ~(name : string)
~(generator : offset:int -> limit:int -> Tuple.materialized list)
~(cardinality : Conventions.Cardinality.t) ~(identity : string)
~(schema : Schema.t) ~(deps : string list) : (string, Condition.t) result =
(* The callback re-enters the RNT API (the generator typically executes a
VM plan over the base relations); the runtime is single threaded, so
this is safe. Exceptions must not cross the FFI boundary! *)
let cb _ctx _args offset limit sink =
try
let tuples =
generator
~offset:(Unsigned.Size_t.to_int offset)
~limit:(Unsigned.Size_t.to_int limit)
in
List.iter
(fun t ->
let kv =
materialized_to_kv t
|> List.map (fun (k, v) -> k ^ "=" ^ v)
|> String.concat "\n"
in
ignore (Nt_ffi.rnt_sink_emit sink (kv ^ "\n")))
tuples;
0
with _ -> -1
in
let schema_kv =
schema |> List.map (fun (n, d) -> n ^ "=" ^ d) |> String.concat "\n"
in
let card =
match cardinality with
| Conventions.Cardinality.Finite _ -> 0
| Conventions.Cardinality.ConstrainedFinite -> 1
| Conventions.Cardinality.AlephZero -> 2
| Conventions.Cardinality.Continuum -> 3
in
let (rc, path_opt) =
Nt_ffi.with_out_string (fun pp ->
Nt_ffi.rnt_register_ephemeral_relation session
(if named then 1 else 0) name cb (from_voidp void null) card
identity schema_kv (String.concat "\n" deps) pp)
in
match (rc, path_opt) with
| (0, Some path) ->
Hashtbl.replace retained_generators path cb;
Ok path
| (rc, _) -> Error (Error.handle_error rc ("register_ephemeral failed: " ^ name))
let drop_ephemeral ~(session : string) ~(name : string) :
(unit, Condition.t) result =
(* TODO: we leak the retained closure here. We can rebuild the path from
session + name (same formula as Sublanguage_context.ephemeral_path) and
Hashtbl.remove it, instead of holding onto one closure per dropped view
until a later re-registration overwrites it. *)
let rc = Nt_ffi.rnt_drop_ephemeral_relation session name in
if rc = 0 then Ok ()
else Error (Error.handle_error rc ("drop_ephemeral failed: " ^ name))
(* --------------------------------------------------------------------------
Multigroup / branch mutations
Each of these advances the underlying RNT branch to a new snapshot as a
side effect of the [rnt_register_relation] / [rnt_link_tuple] / ... call.
The OCaml multigroup is the pure-value mirror: the caller threads the
returned [mg] through subsequent calls. There is no separate commit step.
-------------------------------------------------------------------------- *)
let create_multigroup (claims : claims) (name : string) :
(branch_handle * Management.Multigroup.multigroup, Condition.t) result =
open_branch claims name
let create_relation (bh : branch_handle) (mg : Management.Multigroup.multigroup)
~(branch_name : string) ~(name : string) ~(schema : Schema.t) :
(branch_handle * Management.Multigroup.multigroup, Condition.t) result =
let mg_name = mg#name in
let path = relation_path ~branch_name ~mg_name ~rel_name:name in
let rc = Nt_ffi.rnt_register_relation path in
if rc <> 0 then Error (Error.handle_error rc ("register_relation failed: " ^ name))
else
let rel_root =
let (rc, opt) =
Nt_ffi.with_out_string (fun pp -> Nt_ffi.rnt_relation_root path pp)
in
if rc = 0 then Option.value opt ~default:"" else ""
in
let rel = (new Relation.stored
~name
~schema
~constraints:None
~cardinality:Conventions.Cardinality.AlephZero
~lineage:(Some (Relation.Lineage.Base name))
~provenance:(Some (Relation.Provenance.base ~relation:name
~attributes:(List.map fst schema)))
~membership_criteria:(fun _ -> true))#set_tree_pointer (Some rel_root)
in
let* _tip = branch_target_of_handle bh in
Ok (bh, mg#add_relation (rel :> Relation.relation))
let retract_relation (bh : branch_handle) (mg : Management.Multigroup.multigroup)
~(name : string) :
(branch_handle * Management.Multigroup.multigroup, Condition.t) result =
(* TODO: there is no rnt_unregister_relation today; the OCaml mg drops the
relation but RNT continues to carry it in the current snapshot. This
drifts the two views — fine while the in-memory mg is the source of
truth for the connection, but worth revisiting once a removal API
exists on the RNT side. *)
Ok (bh, mg#remove_relation name)
let clear_relation (bh : branch_handle) (mg : Management.Multigroup.multigroup)
~(branch_name : string) ~(mg_name : string)
(rel : Relation.relation) :
(branch_handle * Management.Multigroup.multigroup, Condition.t) result =
let path = relation_path ~branch_name ~mg_name ~rel_name:rel#name in
let rc = Nt_ffi.rnt_clear_relation path in
if rc <> 0 then Error (Error.handle_error rc ("clear_relation failed: " ^ rel#name))
else
let* _tip = branch_target_of_handle bh in
Ok (bh, mg)
let register_domain (_bh : branch_handle) (mg : Management.Multigroup.multigroup)
(domain : Relation.domain) :
(branch_handle * Management.Multigroup.multigroup, Condition.t) result =
Ok (_bh, mg#add_relation (domain :> Relation.relation))
let create_immutable_relation (bh : branch_handle)
(mg : Management.Multigroup.multigroup) ~name ~schema ~generator
~membership_criteria ~cardinality ~(producer : Relation.producer option) :
(branch_handle * Management.Multigroup.multigroup * Relation.relation, Condition.t) result =
let rel : Relation.relation =
match producer with
| Some p ->
(new Relation.pseudo ~name ~schema ~constraints:None ~cardinality
~membership_criteria ~lineage:None
~provenance:Relation.Provenance.empty ~producer:p
:> Relation.relation)
| None ->
(new Relation.ephemeral ~name ~schema ~constraints:None ~cardinality
~membership_criteria ~lineage:None ~provenance:None ~generator
:> Relation.relation)
in
Ok (bh, mg#add_relation rel, rel)
let register_constraint (bh : branch_handle)
(mg : Management.Multigroup.multigroup) ~constraint_name ~relation_name
~(body : Constraint.t) :
(branch_handle * Management.Multigroup.multigroup, Condition.t) result =
match mg#get_relation relation_name with
| None -> Error (Error.object_not_found "relation" relation_name)
| Some rel ->
let existing =
match rel#constraints with Some cs -> cs | None -> []
in
let new_constraints = (constraint_name, body) :: existing in
let updated_rel =
(Obj.magic rel
: < set_constraints :
Relation.RelationConstraint.t ->
< constraints : Relation.RelationConstraint.t option
; .. > >)
#set_constraints new_constraints
in
Ok (bh, mg#update_relation (updated_rel :> Relation.relation))
(* --------------------------------------------------------------------------
Tuple mutations
-------------------------------------------------------------------------- *)
let insert_tuple ~(branch_name : string) ~(mg_name : string)
~(rel_name : string) (attrs : (string * string) list) :
(string, Condition.t) result =
let path = relation_path ~branch_name ~mg_name ~rel_name in
let kv_str = List.map (fun (k, v) -> k ^ "=" ^ v ^ "\n") attrs
|> String.concat "" in
let (rc, hash_opt) =
Nt_ffi.with_out_string (fun pp -> Nt_ffi.rnt_link_tuple path kv_str pp)
in
match (rc, hash_opt) with
| (0, Some h) -> Ok h
| _ -> Error (Error.handle_error rc "insert_tuple failed")
let create_tuple ~branch_name ~mg_name ~rel_name (tuple : Tuple.materialized) :
(string, Condition.t) result =
insert_tuple ~branch_name ~mg_name ~rel_name (materialized_to_kv tuple)
let create_tuples ~branch_name ~mg_name ~rel_name
(tuples : Tuple.materialized list) : (string, Condition.t) result =
List.fold_left
(fun acc t ->
let* _ = acc in
let* h = create_tuple ~branch_name ~mg_name ~rel_name t in
Ok h)
(Ok "") tuples
let retract_tuple ~branch_name ~mg_name ~rel_name (hash : string) :
(unit, Condition.t) result =
let path = relation_path ~branch_name ~mg_name ~rel_name in
let rc = Nt_ffi.rnt_unlink_tuple path hash in
if rc = 0 then Ok () else Error (Error.handle_error rc "retract_tuple failed")
let relation_root ~(branch_name : string) ~(mg_name : string)
~(rel_name : string) : (string, Condition.t) result =
let path = relation_path ~branch_name ~mg_name ~rel_name in
let (rc, root_opt) =
Nt_ffi.with_out_string (fun pp -> Nt_ffi.rnt_relation_root path pp)
in
match (rc, root_opt) with
| (0, Some r) -> Ok r
| (0, None) -> Ok ""
| _ -> Error (Error.handle_error rc "relation_root query failed")
(* --------------------------------------------------------------------------
Prelude / catalog bootstrap
-------------------------------------------------------------------------- *)
(* Seed the catalog prelude into a brand-new (empty) multigroup.
Creates all catalog relations as stored relations and inserts the
self-describing tuples into public:relation and public:attribute. Each
mutation auto-advances the branch internally; there is no separate
commit step. *)
let seed_prelude ~(branch_name : string) (branch_handle : branch_handle)
(multigroup : Management.Multigroup.multigroup) :
(branch_handle * Management.Multigroup.multigroup, Condition.t) result =
let mg_name = multigroup#name in
let* (branch_handle, multigroup) =
List.fold_left
(fun acc (name, schema) ->
let* (branch_handle, multigroup) = acc in
create_relation branch_handle multigroup
~branch_name ~name ~schema)
(Ok (branch_handle, multigroup))
Prelude.Catalog.catalog_definitions
in
let rel_tuples =
List.map
(fun (name, _) -> Prelude.Catalog.build_relation_tuple name)
Prelude.Catalog.catalog_definitions
in
let* _ =
create_tuples ~branch_name ~mg_name
~rel_name:Prelude.Catalog.relation_rel_name rel_tuples
in
let attr_tuples =
List.concat_map
(fun (rel_name, schema) ->
Prelude.Catalog.build_attribute_tuples ~relation_name:rel_name schema)
Prelude.Catalog.catalog_definitions
in
let* _ =
create_tuples ~branch_name ~mg_name
~rel_name:Prelude.Catalog.attribute_rel_name attr_tuples
in
Ok (branch_handle, multigroup)
(* Runtime initialization
Seeds the catalog of the master branch when it doesn't exist yet; otherwise
trusts the registry's existing snapshot. The seeded multigroup is
discarded — each connection's [open_branch] starts from an empty mg
and the caller threads catalog state from there. Persistence of the
OCaml-side metadata across process restarts is a separate concern. *)
let initialize () : (unit, Condition.t) result =
let runtime_count = Nt_ffi.rnt_init B.driver B.init_arg in
let* () = if runtime_count <> 0 then Error Error.initialization_error else Ok () in
let path = "/system/branches/master" in
ensure_branch path;
let raw = Nt_ffi.rnt_open_handle path (from_voidp void null) in
match Nt_ffi.ptr_to_opt raw with
| None -> Error (Error.object_not_found "branch" "master")
| Some bh ->
let* target = branch_target_of_handle bh in
let result =
if target = "" then
let mg =
new Management.Multigroup.multigroup
~name:Prelude.Catalog.prelude_mg
in
seed_prelude ~branch_name:"master" bh mg
|> Result.map ignore
else Ok ()
in
ignore (Nt_ffi.rnt_close_handle (Nt_ffi.nint_to_ptr bh));
result
(* --------------------------------------------------------------------------
Multigroup queries
-------------------------------------------------------------------------- *)
let get_relation (mg : Management.Multigroup.multigroup) (name : string) :
Relation.ephemeral option =
match mg#get_relation name with
| None -> None
| Some rel ->
let gen : Generator.t =
match rel#kind with
| `Ephemeral | `Domain ->
((Obj.magic rel) : < generator : Generator.t >)#generator
| `Pseudo ->
((Obj.magic rel)
: < as_generator :
(string * Conventions.AbstractValue.t) list -> Generator.t >)
#as_generator []
| `Stored ->
fun _pos ->
Generator.Error
("stored relation must be accessed via Nt.execute_query: "
^ rel#name)
in
Some
(new Relation.ephemeral ~name:rel#name ~schema:rel#schema
~constraints:rel#constraints ~cardinality:rel#cardinality
~membership_criteria:rel#membership_criteria
~lineage:(Some rel#lineage) ~provenance:(Some rel#provenance)
~generator:gen)
end
module type S = sig
val initialize : unit -> (unit, Condition.t) result
val authenticate : auth_method -> (claims, Condition.t) result
val session_open : unit -> (string, Condition.t) result
val session_close : string -> (unit, Condition.t) result
val session_set_branch : string -> string -> string -> (unit, Condition.t) result
val open_branch : claims -> string -> (branch_handle * Management.Multigroup.multigroup, Condition.t) result
val open_snapshot : claims -> string -> string -> (branch_handle * Management.Multigroup.multigroup, Condition.t) result
val close_branch : branch_handle -> (unit, Condition.t) result
val branch_target_of_handle : branch_handle -> (string, Condition.t) result
val list_relations : branch_name:string -> mg_name:string -> ((string * string) list, Condition.t) result
val list_branch_multigroups : string -> ((string * string) list, Condition.t) result
val list_snapshot_relations : string -> ((string * string) list, Condition.t) result
val open_relation : branch_name:string -> mg_name:string -> rel_name:string -> (relation_handle, Condition.t) result
val close_relation : relation_handle -> (unit, Condition.t) result
val register_ephemeral :
session:string -> named:bool -> name:string ->
generator:(offset:int -> limit:int -> Tuple.materialized list) ->
cardinality:Conventions.Cardinality.t -> identity:string ->
schema:Schema.t -> deps:string list -> (string, Condition.t) result
val drop_ephemeral : session:string -> name:string -> (unit, Condition.t) result
val execute_query : plan_node -> rel_name:string -> (tuple_stream, Condition.t) result
val stream_next : tuple_stream -> (Tuple.materialized option, Condition.t) result
val stream_close : tuple_stream -> (unit, Condition.t) result
val get_relation : Management.Multigroup.multigroup -> string -> Relation.ephemeral option
val create_multigroup : claims -> string -> (branch_handle * Management.Multigroup.multigroup, Condition.t) result
val create_relation : branch_handle -> Management.Multigroup.multigroup -> branch_name:string -> name:string -> schema:Schema.t -> (branch_handle * Management.Multigroup.multigroup, Condition.t) result
val retract_relation : branch_handle -> Management.Multigroup.multigroup -> name:string -> (branch_handle * Management.Multigroup.multigroup, Condition.t) result
val clear_relation : branch_handle -> Management.Multigroup.multigroup -> branch_name:string -> mg_name:string -> Relation.relation -> (branch_handle * Management.Multigroup.multigroup, Condition.t) result
val register_domain : branch_handle -> Management.Multigroup.multigroup -> Relation.domain -> (branch_handle * Management.Multigroup.multigroup, Condition.t) result
val create_immutable_relation : branch_handle -> Management.Multigroup.multigroup -> name:string -> schema:Schema.t -> generator:Generator.t -> membership_criteria:(Tuple.t -> bool) -> cardinality:Conventions.Cardinality.t -> producer:Relation.producer option -> (branch_handle * Management.Multigroup.multigroup * Relation.relation, Condition.t) result
val register_constraint : branch_handle -> Management.Multigroup.multigroup -> constraint_name:string -> relation_name:string -> body:Constraint.t -> (branch_handle * Management.Multigroup.multigroup, Condition.t) result
val create_tuple : branch_name:string -> mg_name:string -> rel_name:string -> Tuple.materialized -> (string, Condition.t) result
val create_tuples : branch_name:string -> mg_name:string -> rel_name:string -> Tuple.materialized list -> (string, Condition.t) result
val retract_tuple : branch_name:string -> mg_name:string -> rel_name:string -> string -> (unit, Condition.t) result
val insert_tuple : branch_name:string -> mg_name:string -> rel_name:string -> (string * string) list -> (string, Condition.t) result
val relation_root : branch_name:string -> mg_name:string -> rel_name:string -> (string, Condition.t) result
end
module Sqlite = Make (struct
let driver = "sqlite"
let init_arg = ":memory:"
end)
module Memory = Make (struct
let driver = "memory"
let init_arg = ""
end)
|