aboutsummaryrefslogblamecommitdiffstats
path: root/lib/nt.ml
blob: 1e7077ff7ec25a9ad52310a5ea763f733626f557 (plain) (tree)
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























































































































































































































































































































































































































































































































































































































































































































                                                                                                                                                                                                                                                                                                                                                             
(* 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 }

type error =
  | AuthFailed     of string
  | HandleError    of string
  | CursorError    of string
  | NotSupported   of string
  | InitializationError

let string_of_error = function
  | AuthFailed  s -> "AuthFailed: "   ^ s
  | HandleError s -> "HandleError: "  ^ s
  | CursorError s -> "CursorError: "  ^ s
  | NotSupported s -> "NotSupported: " ^ s
  | InitializationError -> "InitializationError"

(* --------------------------------------------------------------------------
   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, error) 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 (AuthFailed s)
    | _           -> Error (AuthFailed "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, error) 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 (HandleError "branch_target read failed")

  let list_relations ~(branch_name : string) ~(mg_name : string) :
      ((string * string) list, error) 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 (HandleError
                              ("list_relations failed: " ^ branch_name
                               ^ "/" ^ mg_name))

  let list_branch_multigroups (branch_name : string) :
      ((string * string) list, error) 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 (HandleError
                              ("list_branch_multigroups failed: " ^ branch_name))

  let session_open () : (string, error) 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 (HandleError "session_open failed")

  let session_close (sid : string) : (unit, error) result =
    let rc = Nt_ffi.rnt_session_close sid in
    if rc = 0 then Ok () else Error (HandleError "session_close failed")

  let session_set_branch (sid : string) (branch_name : string)
      (target_hash : string) : (unit, error) result =
    let rc = Nt_ffi.rnt_session_set_branch sid branch_name target_hash in
    if rc = 0 then Ok () else Error (HandleError "session_set_branch failed")

  let open_branch (_claims : claims) (name : string) :
      (branch_handle * Management.Multigroup.multigroup, error) 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 (HandleError ("branch not found: " ^ 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, error) result =
    let rc = Nt_ffi.rnt_close_handle (Nt_ffi.nint_to_ptr bh) in
    if rc = 0 then Ok () else Error (HandleError "close_branch failed")

  let list_snapshot_relations (snapshot_hash : string) :
      ((string * string) list, error) 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 (HandleError ("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, error) 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 (HandleError ("snapshot not found: " ^ 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, error) 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 (HandleError ("relation not found: " ^ rel_name))
    | Some rh -> Ok rh

  let close_relation (rh : relation_handle) : (unit, error) result =
    let rc = Nt_ffi.rnt_close_handle (Nt_ffi.nint_to_ptr rh) in
    if rc = 0 then Ok () else Error (HandleError "close_relation failed")

  (* --------------------------------------------------------------------------
     Query execution via the Tarski VM plan builder
     -------------------------------------------------------------------------- *)

  (* Recursively builds a C-side PlanWrapper from an OCaml plan_node tree.
     On partial failure the partially-built plan is freed before returning Error. *)
  let rec build_plan (plan : plan_node) : (nativeint, error) result =
    match plan with
    | Scan { path; _ } ->
        let raw = Nt_ffi.rnt_plan_scan path in
        (match Nt_ffi.ptr_to_opt raw with
         | None   -> Error (CursorError ("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 raw = Nt_ffi.rnt_plan_join
                         (Nt_ffi.nint_to_ptr lp) (Nt_ffi.nint_to_ptr rp) in
             (* join takes ownership of both children — no manual free needed *)
             (match Nt_ffi.ptr_to_opt raw with
              | None   -> Error (CursorError "plan_join failed")
              | Some p -> Ok p))
    | Take { limit; source } ->
        let* sp = build_plan source in
        let raw = Nt_ffi.rnt_plan_take
                    (Nt_ffi.nint_to_ptr sp)
                    (Unsigned.Size_t.of_int limit) in
        (* take takes ownership of source — no manual free needed *)
        (match Nt_ffi.ptr_to_opt raw with
         | None   -> Error (CursorError "plan_take failed")
         | Some p -> Ok p)

  let execute_query (plan : plan_node) ~(rel_name : string) :
      (tuple_stream, error) 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 (CursorError "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, error) 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 (CursorError "cursor_next error")

  let stream_close (stream : tuple_stream) : (unit, error) 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 (CursorError "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 []

  (* --------------------------------------------------------------------------
     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, error) result =
    open_branch claims name

  let create_relation (bh : branch_handle) (mg : Management.Multigroup.multigroup)
      ~(branch_name : string) ~(mg_name : string) ~(name : string)
      ~(schema : Schema.t) :
      (branch_handle * Management.Multigroup.multigroup, error) result =
    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 (HandleError ("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, error) 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, error) 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 (HandleError ("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, error) 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, error) 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, error) result =
    match mg#get_relation relation_name with
    | None -> Error (HandleError ("relation not found: " ^ 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, error) 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 (HandleError "insert_tuple failed")

  let create_tuple ~branch_name ~mg_name ~rel_name (tuple : Tuple.materialized) :
      (string, error) 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, error) 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, error) 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 (HandleError "retract_tuple failed")

  let relation_root ~(branch_name : string) ~(mg_name : string)
      ~(rel_name : string) : (string, error) 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 (HandleError "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, error) 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 ~mg_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, error) result =
    let runtime_count = Nt_ffi.rnt_init B.driver B.init_arg in
    let* () = if runtime_count <> 0 then Error InitializationError 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 (HandleError "init: could not open master branch")
    | 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
            (match seed_prelude ~branch_name:"master" bh mg with
             | Ok _    -> Ok ()
             | Error e -> Error e)
          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, error) result
  val authenticate : auth_method -> (claims, error) result

  val session_open      : unit -> (string, error) result
  val session_close     : string -> (unit, error) result
  val session_set_branch : string -> string -> string -> (unit, error) result

  val open_branch             : claims -> string -> (branch_handle * Management.Multigroup.multigroup, error) result
  val open_snapshot           : claims -> string -> string -> (branch_handle * Management.Multigroup.multigroup, error) result
  val close_branch            : branch_handle -> (unit, error) result
  val branch_target_of_handle : branch_handle -> (string, error) result

  val list_relations          : branch_name:string -> mg_name:string -> ((string * string) list, error) result
  val list_branch_multigroups : string -> ((string * string) list, error) result
  val list_snapshot_relations : string -> ((string * string) list, error) result

  val open_relation  : branch_name:string -> mg_name:string -> rel_name:string -> (relation_handle, error) result
  val close_relation : relation_handle -> (unit, error) result

  val execute_query : plan_node -> rel_name:string -> (tuple_stream, error) result
  val stream_next   : tuple_stream -> (Tuple.materialized option, error) result
  val stream_close  : tuple_stream -> (unit, error) result

  val get_relation : Management.Multigroup.multigroup -> string -> Relation.ephemeral option

  val create_multigroup : claims -> string -> (branch_handle * Management.Multigroup.multigroup, error) result
  val create_relation   : branch_handle -> Management.Multigroup.multigroup -> branch_name:string -> mg_name:string -> name:string -> schema:Schema.t -> (branch_handle * Management.Multigroup.multigroup, error) result
  val retract_relation  : branch_handle -> Management.Multigroup.multigroup -> name:string -> (branch_handle * Management.Multigroup.multigroup, error) result
  val clear_relation    : branch_handle -> Management.Multigroup.multigroup -> branch_name:string -> mg_name:string -> Relation.relation -> (branch_handle * Management.Multigroup.multigroup, error) result
  val register_domain   : branch_handle -> Management.Multigroup.multigroup -> Relation.domain -> (branch_handle * Management.Multigroup.multigroup, error) 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, error) result
  val register_constraint : branch_handle -> Management.Multigroup.multigroup -> constraint_name:string -> relation_name:string -> body:Constraint.t -> (branch_handle * Management.Multigroup.multigroup, error) result

  val create_tuple  : branch_name:string -> mg_name:string -> rel_name:string -> Tuple.materialized -> (string, error) result
  val create_tuples : branch_name:string -> mg_name:string -> rel_name:string -> Tuple.materialized list -> (string, error) result
  val retract_tuple : branch_name:string -> mg_name:string -> rel_name:string -> string -> (unit, error) result

  val insert_tuple    : branch_name:string -> mg_name:string -> rel_name:string -> (string * string) list -> (string, error) result
  val relation_root   : branch_name:string -> mg_name:string -> rel_name:string -> (string, error) 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)