Skip to content

Commit 6c302cd

Browse files
committed
fix: tx-memo edge cases — tx entities, nesting/exceptions, bounded attr keys
- Memo freshness now requires e <= max_allocatable_entity_id: datoms on transaction entities (e >= tx0) can already carry index datoms from earlier transactions, so routing them through the empty memo bucket dropped the retraction a cardinality-one update needs. - apply_tx saves/restores the shared memo record (Fun.protect): a nested transaction via Call, or an exception raised mid-tx, no longer leaks this transaction's state into the outer one — the outer transaction also keeps its memo instead of losing it to the inner reset. - attr_key_tbl is capped (65536 entries): keyword values flow through compare_attr too, so an unbounded vocabulary would grow the table without bound; past the cap keys are recomputed per call. - Regression test: cardinality-one update on a past tx entity.
1 parent f21006a commit 6c302cd

5 files changed

Lines changed: 64 additions & 11 deletions

File tree

‎impl/datascript.ml‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ let tx_memo_apply tx_data =
6363
if transact_tx_memo.active then
6464
List.iter
6565
(fun d ->
66-
if d.e > transact_tx_memo.base_max_eid then
66+
if d.e > transact_tx_memo.base_max_eid
67+
&& d.e <= Db_impl.max_allocatable_entity_id
68+
then
6769
let key = d.e, d.a in
6870
let bucket =
6971
Option.value (Hashtbl.find_opt transact_tx_memo.datoms key)
@@ -516,8 +518,13 @@ let add_active_datom_with_report_db ?(allow_tuple = false) ?(validate_value = tr
516518
(* tx-fresh entities (e allocated inside this transaction) cannot exist
517519
in the pre-tx index, so their same-fact and entity/attr lookups are
518520
answered from the tx memo instead of descending the index trees. *)
521+
(* Transaction entities live above max_allocatable_entity_id and can
522+
already carry index datoms from earlier transactions, so they are
523+
never memo-fresh. *)
519524
let tx_fresh_bucket =
520-
if transact_tx_memo.active && d.e > transact_tx_memo.base_max_eid then
525+
if transact_tx_memo.active && d.e > transact_tx_memo.base_max_eid
526+
&& d.e <= Db_impl.max_allocatable_entity_id
527+
then
521528
Some
522529
(Option.value
523530
(Hashtbl.find_opt transact_tx_memo.datoms (d.e, d.a))

‎impl/transact.ml‎

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,13 @@ let remap_tempid_entity old_e new_e tempids =
397397

398398

399399
(* Per-transaction memo of datoms added to entities allocated inside this
400-
transaction (e > base_max_eid): they cannot exist in the pre-tx index, so
401-
same-fact and entity/attr lookups for them can be answered from this table
402-
instead of descending the index trees. Bulk seed transactions (imports)
403-
consist almost entirely of such datoms. *)
400+
transaction (base_max_eid < e <= max_allocatable_entity_id): they cannot
401+
exist in the pre-tx index, so same-fact and entity/attr lookups for them
402+
can be answered from this table instead of descending the index trees.
403+
Bulk seed transactions (imports) consist almost entirely of such datoms.
404+
Transaction entities (e >= tx0) are excluded: they are ids, not freshly
405+
allocated entities, and may already carry index datoms from earlier
406+
transactions. *)
404407
type tx_memo =
405408
{ mutable active : bool
406409
; mutable base_max_eid : entity_id
@@ -447,6 +450,24 @@ type apply_context =
447450
let apply_tx context tx_ops db =
448451
if context.is_filtered db then invalid_arg "filtered db is read-only";
449452
let tx_memo = context.tx_memo in
453+
(* The memo record is shared by every transaction, so a nested transaction
454+
(a Call fn that transacts) or an exception raised mid-tx must not leak
455+
this transaction's state into the outer one: save any enclosing state
456+
and restore it when this transaction ends or aborts. *)
457+
let saved_memo =
458+
if tx_memo.active then
459+
Some (tx_memo.base_max_eid, Hashtbl.copy tx_memo.datoms)
460+
else None
461+
in
462+
let restore_memo () =
463+
match saved_memo with
464+
| Some (base_max_eid, datoms) ->
465+
Hashtbl.reset tx_memo.datoms;
466+
Hashtbl.iter (Hashtbl.replace tx_memo.datoms) datoms;
467+
tx_memo.base_max_eid <- base_max_eid
468+
| None -> tx_memo.active <- false
469+
in
470+
Fun.protect ~finally:restore_memo @@ fun () ->
450471
tx_memo.base_max_eid <- db.max_eid;
451472
Hashtbl.reset tx_memo.datoms;
452473
tx_memo.active <- true;
@@ -1890,5 +1911,4 @@ let apply_tx context tx_ops db =
18901911
, tx_data
18911912
)
18921913
in
1893-
tx_memo.active <- false;
18941914
result

‎impl/transact.mli‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ val remap_resolved_tx_value : context -> entity_id -> entity_id -> tx_value -> t
4949
val remap_tempid_entity : entity_id -> entity_id -> tempid_map -> tempid_map
5050

5151
(* Per-transaction memo of datoms added to entities allocated inside this
52-
transaction (e > base_max_eid): they cannot exist in the pre-tx index, so
53-
same-fact and entity/attr lookups for them can be answered from this table
54-
instead of descending the index trees. *)
52+
transaction (base_max_eid < e <= max_allocatable_entity_id): they cannot
53+
exist in the pre-tx index, so same-fact and entity/attr lookups for them
54+
can be answered from this table instead of descending the index trees.
55+
Transaction entities (e >= tx0) are excluded: they may already carry
56+
index datoms from earlier transactions. *)
5557
type tx_memo =
5658
{ mutable active : bool
5759
; mutable base_max_eid : entity_id

‎impl/util.ml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ let split_keyword keyword =
124124
a missing ns sorts first — exactly the pairwise order. Keys are
125125
memoized by attr content since index comparators see the same small
126126
set of attrs over and over. *)
127+
(* Bounded: keyword values also flow through compare_attr, so an unlimited
128+
vocabulary would grow this table without bound; past the cap, keys are
129+
recomputed per call. *)
130+
let attr_key_tbl_cap = 65536
127131
let attr_key_tbl : (attr, attr) Hashtbl.t = Hashtbl.create 256
128132

129133
let attr_key attr =
@@ -137,7 +141,8 @@ let attr_key attr =
137141
String.init (String.length attr) (fun j ->
138142
if j = i then '\x00' else attr.[j])
139143
in
140-
Hashtbl.replace attr_key_tbl attr key;
144+
if Hashtbl.length attr_key_tbl < attr_key_tbl_cap then
145+
Hashtbl.replace attr_key_tbl attr key;
141146
key
142147

143148
let compare_attr left right =

‎test/test_datascript.ml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2314,6 +2314,24 @@ let test_transact__test_resolve_current_tx () =
23142314
[ "db/current-tx", tx0 + 1; "entity", 1 ]
23152315
report.tempids
23162316

2317+
let test_transact__test_tx_entity_cardinality () =
2318+
(* Datoms on a transaction entity created by an earlier transaction
2319+
already live in the index: a cardinality-one update on it in a later
2320+
transaction must retract the previous value — tx ids are above
2321+
max_allocatable_entity_id, not transaction-fresh entities. *)
2322+
let schema = [ "note", indexed ] in
2323+
let report1 =
2324+
transact (empty_db ~schema ()) [ Add (CurrentTx, "note", String "v1") ]
2325+
in
2326+
let tx_eid = Option.get (resolve_tempid report1.tempids "db/current-tx") in
2327+
let report2 =
2328+
transact report1.db_after [ Add (Entity_id tx_eid, "note", String "v2") ]
2329+
in
2330+
assert_equal_triples
2331+
"cardinality-one update on a past tx entity retracts the old value"
2332+
[ tx_eid, "note", String "v2" ]
2333+
(datoms report2.db_after Eavt ~e:tx_eid ~a:"note" ())
2334+
23172335
let test_current_tx_string_aliases_resolve_in_transactions () =
23182336
let report =
23192337
transact
@@ -17560,6 +17578,7 @@ let () =
1756017578
test_transact_report_exposes_tempids ();
1756117579
test_resolve_tempid_reads_tx_report_tempids ();
1756217580
test_transact__test_resolve_current_tx ();
17581+
test_transact__test_tx_entity_cardinality ();
1756317582
test_current_tx_string_aliases_resolve_in_transactions ();
1756417583
test_current_tx_string_aliases_can_be_value_only ();
1756517584
test_current_tx_colon_string_alias_resolves_in_transactions ();

0 commit comments

Comments
 (0)