Skip to content

Commit d922401

Browse files
robacourtclaude
andcommitted
Add DNF/active_conditions unit tests ported from TanStack/db#1270
Adds 4 new test cases covering DNF scenarios: multi-disjunct partial/full deactivation, active_conditions overwrite on re-send, simple shapes without active_conditions, and mixed rows with/without active_conditions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 27408fb commit d922401

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

packages/elixir-client/test/electric/client/tag_tracker_test.exs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,139 @@ defmodule Electric.Client.TagTrackerTest do
413413
assert dp == [[0], [1]]
414414
end
415415

416+
test "multi-disjunct: row stays when one disjunct lost, deleted when all lost" do
417+
# Tags: ["hash_a/hash_b/", "//hash_c"]
418+
# Disjunct 0 covers positions [0, 1], disjunct 1 covers position [2]
419+
msg =
420+
make_change_msg("key1", :insert,
421+
tags: ["hash_a/hash_b/", "//hash_c"],
422+
active_conditions: [true, true, true],
423+
value: %{"id" => "1", "name" => "User 1"}
424+
)
425+
426+
{ttk, kd, dp} = TagTracker.update_tag_index(%{}, %{}, nil, msg)
427+
assert dp == [[0, 1], [2]]
428+
429+
# Move-out at position 0 → disjunct 0 fails (needs [0,1]), disjunct 1 (pos 2) still satisfied
430+
patterns = [%{pos: 0, value: "hash_a"}]
431+
timestamp = DateTime.utc_now()
432+
433+
{deletes, ttk, kd} =
434+
TagTracker.generate_synthetic_deletes(ttk, kd, dp, patterns, timestamp)
435+
436+
assert deletes == []
437+
assert kd["key1"].active_conditions == [false, true, true]
438+
439+
# Move-out at position 2 → disjunct 1 also fails, no disjunct satisfied
440+
patterns = [%{pos: 2, value: "hash_c"}]
441+
442+
{deletes, _ttk, _kd} =
443+
TagTracker.generate_synthetic_deletes(ttk, kd, dp, patterns, timestamp)
444+
445+
assert length(deletes) == 1
446+
assert hd(deletes).key == "key1"
447+
end
448+
449+
test "overwrite active_conditions when row is re-sent (move-in overwrite)" do
450+
# Insert row with active_conditions [true, false]
451+
msg1 =
452+
make_change_msg("key1", :insert,
453+
tags: ["hash_a/hash_b"],
454+
active_conditions: [true, false],
455+
value: %{"id" => "1", "name" => "User 1"}
456+
)
457+
458+
{ttk, kd, dp} = TagTracker.update_tag_index(%{}, %{}, nil, msg1)
459+
assert kd["key1"].active_conditions == [true, false]
460+
461+
# Server re-sends the same row with updated active_conditions
462+
msg2 =
463+
make_change_msg("key1", :update,
464+
tags: ["hash_a/hash_b"],
465+
active_conditions: [true, true],
466+
value: %{"id" => "1", "name" => "User 1 updated"}
467+
)
468+
469+
{ttk, kd, dp} = TagTracker.update_tag_index(ttk, kd, dp, msg2)
470+
assert kd["key1"].active_conditions == [true, true]
471+
472+
# Verify the overwritten active_conditions work correctly:
473+
# With single disjunct [0,1], move-out at pos 0 should make row invisible
474+
patterns = [%{pos: 0, value: "hash_a"}]
475+
timestamp = DateTime.utc_now()
476+
477+
{deletes, _ttk, _kd} =
478+
TagTracker.generate_synthetic_deletes(ttk, kd, dp, patterns, timestamp)
479+
480+
assert length(deletes) == 1
481+
assert hd(deletes).key == "key1"
482+
end
483+
484+
test "delete on empty tag set for simple shapes (no active_conditions)" do
485+
# Insert row with a single-position tag but NO active_conditions
486+
msg =
487+
make_change_msg("key1", :insert,
488+
tags: ["hash1"],
489+
value: %{"id" => "1", "name" => "User 1"}
490+
)
491+
492+
{ttk, kd, dp} = TagTracker.update_tag_index(%{}, %{}, nil, msg)
493+
assert kd["key1"].active_conditions == nil
494+
495+
# Move-out at position 0 — no active_conditions: tag removed, tag set empty → delete
496+
patterns = [%{pos: 0, value: "hash1"}]
497+
timestamp = DateTime.utc_now()
498+
499+
{deletes, new_ttk, new_kd} =
500+
TagTracker.generate_synthetic_deletes(ttk, kd, dp, patterns, timestamp)
501+
502+
assert length(deletes) == 1
503+
assert hd(deletes).key == "key1"
504+
assert new_kd == %{}
505+
assert new_ttk == %{}
506+
end
507+
508+
test "mixed rows: some with active_conditions, some without" do
509+
# Row 1: DNF shape (with active_conditions)
510+
msg1 =
511+
make_change_msg("key1", :insert,
512+
tags: ["hash_a/", "/hash_b"],
513+
active_conditions: [true, true],
514+
value: %{"id" => "1", "name" => "DNF User"}
515+
)
516+
517+
# Row 2: simple shape (single-position tag, no active_conditions)
518+
msg2 =
519+
make_change_msg("key2", :insert,
520+
tags: ["hash_a"],
521+
value: %{"id" => "2", "name" => "Simple User"}
522+
)
523+
524+
{ttk, kd, dp} = TagTracker.update_tag_index(%{}, %{}, nil, msg1)
525+
{ttk, kd, dp} = TagTracker.update_tag_index(ttk, kd, dp, msg2)
526+
527+
assert Map.has_key?(kd, "key1")
528+
assert Map.has_key?(kd, "key2")
529+
530+
# Move-out at position 0 with value hash_a
531+
# DNF row: disjunct 0 ([0]) fails, but disjunct 1 ([1]) still satisfied → stays
532+
# Simple row: tag "hash_a" at pos 0 removed, tag set empty → deleted
533+
patterns = [%{pos: 0, value: "hash_a"}]
534+
timestamp = DateTime.utc_now()
535+
536+
{deletes, _ttk, new_kd} =
537+
TagTracker.generate_synthetic_deletes(ttk, kd, dp, patterns, timestamp)
538+
539+
# DNF row stays, simple row deleted
540+
deleted_keys = Enum.map(deletes, & &1.key) |> MapSet.new()
541+
assert MapSet.member?(deleted_keys, "key2")
542+
refute MapSet.member?(deleted_keys, "key1")
543+
544+
assert Map.has_key?(new_kd, "key1")
545+
assert new_kd["key1"].active_conditions == [false, true]
546+
refute Map.has_key?(new_kd, "key2")
547+
end
548+
416549
test "disjunct_positions derived once and reused across keys" do
417550
msg1 =
418551
make_change_msg("key1", :insert,

0 commit comments

Comments
 (0)