aboutsummaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
authorMarcos Magueta <maguetamarcos@gmail.com>2025-12-14 00:27:18 -0300
committerGitHub <noreply@github.com>2025-12-14 00:27:18 -0300
commitff5691ffb1a969e50e0bd5e4638be8e2bb342c4e (patch)
tree565346b82901f2ee26cb49b04e1e99ba1fb0e896 /test
parent0862cd7c3fe4fbb5b56a0a87c3789c8361bc022c (diff)
downloadSakura-ff5691ffb1a969e50e0bd5e4638be8e2bb342c4e.tar.gz
Sakura-ff5691ffb1a969e50e0bd5e4638be8e2bb342c4e.zip
Ephemeral relations, Refactor & Cleanup (#34)
Diffstat (limited to '')
-rw-r--r--test/function_relations_test.erl347
-rw-r--r--test/infinite_relations_test.erl128
-rw-r--r--test/join_test.erl273
-rw-r--r--test/operations_test.erl366
-rw-r--r--test/provenance_test.erl420
-rw-r--r--test/query_pipeline_test.erl183
6 files changed, 0 insertions, 1717 deletions
diff --git a/test/function_relations_test.erl b/test/function_relations_test.erl
deleted file mode 100644
index a6ab2d2..0000000
--- a/test/function_relations_test.erl
+++ /dev/null
@@ -1,347 +0,0 @@
--module(function_relations_test).
--include_lib("eunit/include/eunit.hrl").
--include("operations.hrl").
-
-%%% Function Relations Test Suite
-%%%
-%%% Testing infinite relations that represent arithmetic operations.
-%%% These work like functions but backwards too (Prolog-style)
-%%%
-%%% - Plus: {(a, b, sum) | sum = a + b }
-%%% - Times: {(a, b, product) | product = a * b}
-%%% - Minus: {(a, b, difference) | difference = a - b}
-%%% - Divide: {(a, b, quotient, remainder) | a = b*quotient + remainder}
-
-function_relations_test_() ->
- {foreach,
- fun setup/0,
- fun cleanup/1,
- [
- fun test_plus_both_operands_constrained/1,
- fun test_plus_a_and_sum_constrained/1,
- fun test_plus_b_and_sum_constrained/1,
- fun test_plus_only_sum_constrained/1,
- fun test_plus_sum_range_constrained/1,
- fun test_plus_insufficient_constraints/1,
- fun test_times_both_operands_constrained/1,
- fun test_times_insufficient_constraints/1,
- fun test_minus_both_operands_constrained/1,
- fun test_divide_both_operands_constrained/1,
- fun test_divide_avoids_zero/1,
- fun test_plus_join_with_finite/1,
- fun test_materialized_plus_relation/1
- ]}.
-
-%%% Setup and Cleanup
-
-setup() ->
- operations:setup(),
- DB = operations:create_database(test_db),
-
- % Integers and naturals are now built-in to every database
- % Just create the function relations using the new format
-
- % Create function relations - note how their schemas reference the domain relations
- {DB1, _Plus} = operations:create_immutable_relation(DB, #immutable_relation_spec{
- name = plus,
- schema = #{a => integer, b => integer, sum => integer},
- generator = {generators, plus},
- membership_criteria = #{},
- cardinality = aleph_zero
- }),
-
- {DB2, _Times} = operations:create_immutable_relation(DB1, #immutable_relation_spec{
- name = times,
- schema = #{a => integer, b => integer, product => integer},
- generator = {generators, times},
- membership_criteria = #{},
- cardinality = aleph_zero
- }),
-
- {DB3, _Minus} = operations:create_immutable_relation(DB2, #immutable_relation_spec{
- name = minus,
- schema = #{a => integer, b => integer, difference => integer},
- generator = {generators, minus},
- membership_criteria = #{},
- cardinality = aleph_zero
- }),
-
- {DB4, _Divide} = operations:create_immutable_relation(DB3, #immutable_relation_spec{
- name = divide,
- schema = #{a => integer, b => integer, quotient => integer, remainder => natural},
- generator = {generators, divide},
- membership_criteria = #{},
- cardinality = aleph_zero
- }),
-
- DB4.
-
-cleanup(_DB) ->
- ok.
-
-%%% Helper Functions
-
-%% Strip metadata from tuples for comparison
-strip_meta(Tuple) when is_map(Tuple) ->
- maps:remove(meta, Tuple);
-strip_meta(Tuples) when is_list(Tuples) ->
- [strip_meta(T) || T <- Tuples].
-
-%%% Plus Relation Tests
-
-test_plus_both_operands_constrained(DB) ->
- % Both operands bounded: a ∈ [1,3], b ∈ [2,4]
- % TODO: eventually this should be a proper join like Integer[A], Integer[B], Plus[A,B,Sum]
- % but for now just pass constraints directly
- Iterator = operations:get_tuples_iterator(DB, plus, #{
- a => {range, 1, 3},
- b => {range, 2, 4}
- }),
- Results = operations:collect_all(Iterator),
- DataOnly = strip_meta(Results),
-
- % should get 9 tuples total (3 × 3 = 9)
- % like (1,2,3), (1,3,4), (1,4,5), (2,2,4), ..., (3,4,7)
- [
- ?_assertEqual(9, length(Results)),
- ?_assert(lists:member(#{a => 1, b => 2, sum => 3}, DataOnly)),
- ?_assert(lists:member(#{a => 2, b => 3, sum => 5}, DataOnly)),
- ?_assert(lists:member(#{a => 3, b => 4, sum => 7}, DataOnly)),
- ?_assert(lists:all(fun(#{a := A, b := B, sum := S}) ->
- S =:= A + B
- end, DataOnly))
- ].
-
-test_plus_a_and_sum_constrained(DB) ->
- % Test reversibility - given A and Sum, compute B
- % a ∈ [1,3], sum ∈ [5,7], so b = sum - a
- % This is like asking: Plus[A, ?, Sum]
- Iterator = operations:get_tuples_iterator(DB, plus, #{
- a => {range, 1, 3},
- sum => {range, 5, 7}
- }),
- Results = operations:collect_all(Iterator),
- DataOnly = strip_meta(Results),
-
- % Expected tuples: (1,4,5), (1,5,6), (1,6,7), (2,3,5), (2,4,6), (2,5,7), (3,2,5), (3,3,6), (3,4,7)
- % (filtered to only keep where b >= 0)
- [
- ?_assert(length(Results) > 0),
- ?_assert(lists:all(fun(#{a := A, b := B, sum := S}) ->
- S =:= A + B andalso B >= 0
- end, DataOnly)),
- ?_assert(lists:member(#{a => 1, b => 4, sum => 5}, DataOnly)),
- ?_assert(lists:member(#{a => 3, b => 4, sum => 7}, DataOnly))
- ].
-
-test_plus_b_and_sum_constrained(DB) ->
- % Another reversibility test - now given B and Sum, find A
- % b ∈ [2,4], sum ∈ [5,6] → a = sum - b
- % Like: Plus[?, B, Sum]
- Iterator = operations:get_tuples_iterator(DB, plus, #{
- b => {range, 2, 4},
- sum => {range, 5, 6}
- }),
- Results = operations:collect_all(Iterator),
- DataOnly = strip_meta(Results),
-
- [
- ?_assert(length(Results) > 0),
- ?_assert(lists:all(fun(#{a := A, b := B, sum := S}) ->
- S =:= A + B andalso A >= 0
- end, DataOnly)),
- ?_assert(lists:member(#{a => 3, b => 2, sum => 5}, DataOnly)),
- ?_assert(lists:member(#{a => 2, b => 4, sum => 6}, DataOnly))
- ].
-
-test_plus_only_sum_constrained(DB) ->
- % Only constrain the sum - should give us all ways to add up to 5
- % Plus[?, ?, 5] generates: (0,5), (1,4), (2,3), (3,2), (4,1), (5,0)
- Iterator = operations:get_tuples_iterator(DB, plus, #{
- sum => {eq, 5}
- }),
- Results = operations:collect_all(Iterator),
- DataOnly = strip_meta(Results),
-
- [
- ?_assertEqual(6, length(Results)),
- ?_assert(lists:member(#{a => 0, b => 5, sum => 5}, DataOnly)),
- ?_assert(lists:member(#{a => 2, b => 3, sum => 5}, DataOnly)),
- ?_assert(lists:member(#{a => 5, b => 0, sum => 5}, DataOnly)),
- ?_assert(lists:all(fun(#{a := A, b := B, sum := S}) ->
- S =:= A + B andalso S =:= 5
- end, DataOnly))
- ].
-
-test_plus_sum_range_constrained(DB) ->
- % sum in a range [2,3] instead of single value
- Iterator = operations:get_tuples_iterator(DB, plus, #{
- sum => {range, 2, 3}
- }),
- Results = operations:collect_all(Iterator),
- DataOnly = strip_meta(Results),
-
- % For sum=2: (0,2), (1,1), (2,0) → 3 pairs
- % For sum=3: (0,3), (1,2), (2,1), (3,0) → 4 pairs
- % Total = 7
- [
- ?_assertEqual(7, length(Results)),
- ?_assert(lists:all(fun(#{sum := S}) -> S >= 2 andalso S =< 3 end, DataOnly)),
- ?_assert(lists:member(#{a => 0, b => 2, sum => 2}, DataOnly)),
- ?_assert(lists:member(#{a => 3, b => 0, sum => 3}, DataOnly))
- ].
-
-test_plus_insufficient_constraints(DB) ->
- % Try querying with zero constraints - should fail since we'd get infinite results
- Iterator = operations:get_tuples_iterator(DB, plus, #{}),
- Result = operations:next_tuple(Iterator),
-
- [
- ?_assertMatch({error, {unbounded_plus, _}}, Result)
- ].
-
-%%% Times (multiplication) Tests
-
-test_times_both_operands_constrained(DB) ->
- % Basic multiplication with both inputs bounded
- Iterator = operations:get_tuples_iterator(DB, times, #{
- a => {range, 2, 4},
- b => {range, 3, 5}
- }),
- Results = operations:collect_all(Iterator),
- DataOnly = strip_meta(Results),
-
- % 3×3 = 9 tuples
- [
- ?_assertEqual(9, length(Results)),
- ?_assert(lists:member(#{a => 2, b => 3, product => 6}, DataOnly)),
- ?_assert(lists:member(#{a => 3, b => 4, product => 12}, DataOnly)),
- ?_assert(lists:member(#{a => 4, b => 5, product => 20}, DataOnly)),
- ?_assert(lists:all(fun(#{a := A, b := B, product := P}) ->
- P =:= A * B
- end, DataOnly))
- ].
-
-test_times_insufficient_constraints(DB) ->
- % Times doesn't support full reversibility yet (would need factorization)
- % So constraining just one operand should error
- Iterator = operations:get_tuples_iterator(DB, times, #{a => {range, 1, 5}}),
- Result = operations:next_tuple(Iterator),
-
- [
- ?_assertMatch({error, {unbounded_times, _}}, Result)
- ].
-
-%%% Minus (subtraction) Tests
-
-test_minus_both_operands_constrained(DB) ->
- % subtraction with both operands bounded
- Iterator = operations:get_tuples_iterator(DB, minus, #{
- a => {range, 5, 7},
- b => {range, 2, 3}
- }),
- Results = operations:collect_all(Iterator),
- DataOnly = strip_meta(Results),
-
- % 3 × 2 = 6 tuples
- [
- ?_assertEqual(6, length(Results)),
- ?_assert(lists:member(#{a => 5, b => 2, difference => 3}, DataOnly)),
- ?_assert(lists:member(#{a => 7, b => 3, difference => 4}, DataOnly)),
- ?_assert(lists:all(fun(#{a := A, b := B, difference := D}) ->
- D =:= A - B
- end, DataOnly))
- ].
-
-%%% Divide (integer division) Tests
-
-test_divide_both_operands_constrained(DB) ->
- % Integer division with quotient and remainder
- Iterator = operations:get_tuples_iterator(DB, divide, #{
- a => {range, 10, 12},
- b => {range, 3, 4}
- }),
- Results = operations:collect_all(Iterator),
- DataOnly = strip_meta(Results),
-
- % 3 × 2 = 6 tuples
- [
- ?_assertEqual(6, length(Results)),
- ?_assert(lists:member(#{a => 10, b => 3, quotient => 3, remainder => 1}, DataOnly)),
- ?_assert(lists:member(#{a => 12, b => 4, quotient => 3, remainder => 0}, DataOnly)),
- ?_assert(lists:all(fun(#{a := A, b := B, quotient := Q, remainder := R}) ->
- A =:= B * Q + R andalso R >= 0 andalso R < abs(B)
- end, DataOnly))
- ].
-
-test_divide_avoids_zero(DB) ->
- % Make sure we don't try to divide by zero
- % even if the range includes 0
- Iterator = operations:get_tuples_iterator(DB, divide, #{
- a => {range, 5, 7},
- b => {range, 0, 2}
- }),
- Results = operations:collect_all(Iterator),
-
- % 3 × 2 = 6 (skips b=0, only uses b=1,2)
- [
- ?_assertEqual(6, length(Results)),
- ?_assert(lists:all(fun(#{b := B}) -> B =/= 0 end, Results))
- ].
-
-%%% Integration Tests
-
-test_plus_join_with_finite(DB) ->
- % Simulate joining an infinite relation with a finite one
- {DB1, _Employees} = operations:create_relation(DB, employees, #{
- name => string,
- age => integer
- }),
-
- {DB2, _} = operations:create_tuple(DB1, employees, #{name => "Alice", age => 30}),
- {DB3, _} = operations:create_tuple(DB2, employees, #{name => "Bob", age => 35}),
-
- EmployeeIter = operations:get_tuples_iterator(DB3, employees, #{}),
- Employees = operations:collect_all(EmployeeIter),
-
- % For each employee's age, find all the ways to add up to it
- % (simulating: Employee JOIN Plus ON Plus.sum = Employee.age)
- Results = lists:flatmap(fun(#{age := Age}) ->
- PlusIter = operations:get_tuples_iterator(DB3, plus, #{
- a => {range, 0, 15},
- b => {range, 0, 15},
- sum => {eq, Age}
- }),
- Tuples = operations:collect_all(PlusIter),
- [#{age => Age, plus_tuple => T} || T <- Tuples]
- end, Employees),
-
- % verify we got some results
- [
- ?_assert(length(Results) > 0),
- ?_assert(lists:any(fun(#{age := Age, plus_tuple := #{sum := S}}) ->
- Age =:= S
- end, Results))
- ].
-
-test_materialized_plus_relation(DB) ->
- % Take an infinite relation, constrain it, and materialize into a finite one
- PlusIter = operations:get_tuples_iterator(DB, plus, #{
- a => {range, 0, 2},
- b => {range, 0, 2}
- }),
-
- % Turn it into an actual stored relation
- {DB1, SmallPlus} = operations:materialize(DB, PlusIter, small_plus),
-
- % check that it worked
- ResultIter = operations:get_tuples_iterator(DB1, small_plus, #{}),
- Results = operations:collect_all(ResultIter),
-
- [
- ?_assertEqual({finite, 9}, SmallPlus#relation.cardinality),
- ?_assertEqual(9, length(Results)),
- ?_assert(lists:all(fun(#{a := A, b := B, sum := S}) ->
- S =:= A + B andalso A >= 0 andalso A =< 2 andalso B >= 0 andalso B =< 2
- end, Results))
- ].
diff --git a/test/infinite_relations_test.erl b/test/infinite_relations_test.erl
deleted file mode 100644
index 5f87acb..0000000
--- a/test/infinite_relations_test.erl
+++ /dev/null
@@ -1,128 +0,0 @@
--module(infinite_relations_test).
--include_lib("eunit/include/eunit.hrl").
--include("operations.hrl").
-
-%%% Test Suite for Infinite Relations
-
-infinite_relations_test_() ->
- {foreach,
- fun setup/0,
- fun cleanup/1,
- [
- fun test_create_naturals/1,
- fun test_naturals_with_constraints/1,
- fun test_integers_generator/1,
- fun test_rationals_generator/1,
- fun test_take_operator/1,
- fun test_finite_relation_cardinality/1
- ]}.
-
-%%% Setup and Cleanup
-
-setup() ->
- operations:setup(),
- operations:create_database(test_db).
-
-cleanup(_DB) ->
- ok.
-
-%%% Helper Functions
-
-%% Strip metadata from tuples for comparison
-strip_meta(Tuple) when is_map(Tuple) ->
- maps:remove(meta, Tuple);
-strip_meta(Tuples) when is_list(Tuples) ->
- [strip_meta(T) || T <- Tuples].
-
-%%% Tests
-
-test_create_naturals(DB) ->
- %% Naturals are now built-in to every database
- %% Just retrieve the relation to verify it exists
- {ok, NaturalsHash} = operations:get_relation_hash(DB, natural),
- [NaturalsRel] = mnesia:dirty_read(relation, NaturalsHash),
-
- [
- ?_assert(is_record(NaturalsRel, relation)),
- ?_assertEqual(natural, NaturalsRel#relation.name),
- ?_assertEqual(aleph_zero, NaturalsRel#relation.cardinality),
- ?_assertEqual({generators, natural}, NaturalsRel#relation.generator)
- ].
-
-test_naturals_with_constraints(DB) ->
- %% Naturals are built-in, just query them
- %% Query with range constraint [0, 9]
- Iterator = operations:get_tuples_iterator(DB, natural, #{value => {range, 0, 9}}),
- Tuples = operations:collect_all(Iterator),
- DataOnly = strip_meta(Tuples),
-
- %% Should get exactly 10 tuples: {0, 1, 2, ..., 9}
- [
- ?_assertEqual(10, length(Tuples)),
- ?_assert(lists:member(#{value => 0}, DataOnly)),
- ?_assert(lists:member(#{value => 5}, DataOnly)),
- ?_assert(lists:member(#{value => 9}, DataOnly))
- ].
-
-test_integers_generator(DB) ->
- %% Integers are built-in
- {ok, IntegersHash} = operations:get_relation_hash(DB, integer),
- [Integers] = mnesia:dirty_read(relation, IntegersHash),
-
- %% Query with range constraint [-5, 5]
- Iterator = operations:get_tuples_iterator(DB, integer, #{value => {range, -5, 5}}),
- Tuples = operations:collect_all(Iterator),
- DataOnly = strip_meta(Tuples),
-
- %% Should get 11 tuples: {-5, -4, ..., 0, ..., 4, 5}
- [
- ?_assertEqual(aleph_zero, Integers#relation.cardinality),
- ?_assertEqual(11, length(Tuples)),
- ?_assert(lists:member(#{value => 0}, DataOnly)),
- ?_assert(lists:member(#{value => -5}, DataOnly)),
- ?_assert(lists:member(#{value => 5}, DataOnly))
- ].
-
-test_rationals_generator(DB) ->
- %% Rationals are built-in
- {ok, RationalsHash} = operations:get_relation_hash(DB, rational),
- [Rationals] = mnesia:dirty_read(relation, RationalsHash),
-
- %% Take first 10 rationals using constraints
- Iterator = operations:get_tuples_iterator(DB, rational, #{}),
-
- %% Just get a few tuples to verify generator works
- {ok, First} = operations:next_tuple(Iterator),
- {ok, Second} = operations:next_tuple(Iterator),
- operations:close_iterator(Iterator),
-
- [
- ?_assertEqual(aleph_zero, Rationals#relation.cardinality),
- ?_assert(is_map(First)),
- ?_assert(maps:is_key(numerator, First)),
- ?_assert(maps:is_key(denominator, First)),
- ?_assert(is_map(Second))
- ].
-
-test_take_operator(DB) ->
- %% Naturals are built-in, just use them
- %% Take 100 naturals
- {_DB2, Naturals100} = operations:take(DB, natural, 100),
-
- %% Verify result is finite
- [
- ?_assertEqual({finite, 100}, Naturals100#relation.cardinality),
- ?_assertEqual({take, natural, 100}, Naturals100#relation.generator)
- ].
-
-test_finite_relation_cardinality(DB) ->
- %% Create finite relation
- {DB1, _} = operations:create_relation(DB, users, #{name => string, age => integer}),
-
- %% Add tuple
- {_DB2, UpdatedRel} = operations:create_tuple(DB1, users, #{name => "Alice", age => 30}),
-
- %% Cardinality should be 1
- [
- ?_assertEqual({finite, 1}, UpdatedRel#relation.cardinality)
- ].
diff --git a/test/join_test.erl b/test/join_test.erl
deleted file mode 100644
index de62127..0000000
--- a/test/join_test.erl
+++ /dev/null
@@ -1,273 +0,0 @@
--module(join_test).
--include_lib("eunit/include/eunit.hrl").
--include("operations.hrl").
-
-%%% Join Operators Test Suite
-%%%
-%%% Tests for relational join operations:
-%%% - Equijoin (natural join on attribute equality)
-%%% - Theta-join (join with arbitrary predicate)
-
-join_test_() ->
- {foreach,
- fun setup/0,
- fun cleanup/1,
- [
- fun test_equijoin_simple/1,
- fun test_equijoin_multiple_matches/1,
- fun test_equijoin_no_matches/1,
- fun test_equijoin_with_conflicts/1,
- fun test_theta_join_greater_than/1,
- fun test_theta_join_complex_predicate/1,
- fun test_join_with_infinite_relation/1,
- fun test_join_materialize_pipeline/1
- ]}.
-
-%%% Setup and Cleanup
-
-setup() ->
- operations:setup(),
- DB = operations:create_database(test_db),
-
- % Create employees relation
- {DB1, _} = operations:create_relation(DB, employees, #{
- id => integer,
- name => string,
- dept_id => integer,
- age => integer
- }),
-
- {DB2, _} = operations:create_tuple(DB1, employees, #{id => 1, name => "Alice", dept_id => 10, age => 30}),
- {DB3, _} = operations:create_tuple(DB2, employees, #{id => 2, name => "Bob", dept_id => 20, age => 35}),
- {DB4, _} = operations:create_tuple(DB3, employees, #{id => 3, name => "Carol", dept_id => 10, age => 28}),
- {DB5, _} = operations:create_tuple(DB4, employees, #{id => 4, name => "Dave", dept_id => 30, age => 42}),
-
- % Create departments relation
- {DB6, _} = operations:create_relation(DB5, departments, #{
- dept_id => integer,
- dept_name => string,
- budget => integer
- }),
-
- {DB7, _} = operations:create_tuple(DB6, departments, #{dept_id => 10, dept_name => "Engineering", budget => 100000}),
- {DB8, _} = operations:create_tuple(DB7, departments, #{dept_id => 20, dept_name => "Sales", budget => 80000}),
- {DB9, _} = operations:create_tuple(DB8, departments, #{dept_id => 30, dept_name => "Marketing", budget => 90000}),
-
- DB9.
-
-cleanup(_DB) ->
- ok.
-
-%%% Equijoin Tests
-
-test_equijoin_simple(DB) ->
- % Join employees with departments on dept_id
- EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
- DeptIter = operations:get_tuples_iterator(DB, departments, #{}),
-
- JoinIter = operations:equijoin_iterator(EmpIter, DeptIter, dept_id),
- Results = operations:collect_all(JoinIter),
-
- % Should get 4 joined tuples (one for each employee)
- [
- ?_assertEqual(4, length(Results)),
- % Check Alice joined with Engineering
- ?_assert(lists:any(fun(T) ->
- maps:get(name, T, undefined) =:= "Alice" andalso
- maps:get(dept_name, T, undefined) =:= "Engineering"
- end, Results)),
- % Check Bob joined with Sales
- ?_assert(lists:any(fun(T) ->
- maps:get(name, T, undefined) =:= "Bob" andalso
- maps:get(dept_name, T, undefined) =:= "Sales"
- end, Results)),
- % All results should have both employee and department attributes
- ?_assert(lists:all(fun(T) ->
- maps:is_key(name, T) andalso
- maps:is_key(dept_name, T) andalso
- maps:is_key(dept_id, T)
- end, Results))
- ].
-
-test_equijoin_multiple_matches(DB) ->
- % Multiple employees in same department should produce multiple results
- EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
- DeptIter = operations:get_tuples_iterator(DB, departments, #{}),
-
- JoinIter = operations:equijoin_iterator(EmpIter, DeptIter, dept_id),
- Results = operations:collect_all(JoinIter),
-
- % Engineering dept (10) has 2 employees (Alice and Carol)
- EngineeringResults = lists:filter(fun(T) ->
- maps:get(dept_name, T, undefined) =:= "Engineering"
- end, Results),
-
- [
- ?_assertEqual(2, length(EngineeringResults)),
- ?_assert(lists:any(fun(T) -> maps:get(name, T) =:= "Alice" end, EngineeringResults)),
- ?_assert(lists:any(fun(T) -> maps:get(name, T) =:= "Carol" end, EngineeringResults))
- ].
-
-test_equijoin_no_matches(DB) ->
- % Create a department with no employees
- {DB1, _} = operations:create_tuple(DB, departments, #{dept_id => 40, dept_name => "HR", budget => 70000}),
-
- % Filter to only dept_id=40 which has no employees
- DeptIter = operations:get_tuples_iterator(DB1, departments, #{}),
- FilteredDept = operations:select_iterator(DeptIter, fun(D) -> maps:get(dept_id, D) =:= 40 end),
-
- EmpIter = operations:get_tuples_iterator(DB1, employees, #{}),
-
- JoinIter = operations:equijoin_iterator(EmpIter, FilteredDept, dept_id),
- Results = operations:collect_all(JoinIter),
-
- % No employees in HR, so no results
- [
- ?_assertEqual(0, length(Results))
- ].
-
-test_equijoin_with_conflicts(DB) ->
- % Create relations with conflicting attribute names
- {DB1, _} = operations:create_relation(DB, table_a, #{
- id => integer,
- value => integer
- }),
- {DB2, _} = operations:create_tuple(DB1, table_a, #{id => 1, value => 100}),
-
- {DB3, _} = operations:create_relation(DB2, table_b, #{
- id => integer,
- value => integer % Same name as table_a.value
- }),
- {DB4, _} = operations:create_tuple(DB3, table_b, #{id => 1, value => 200}),
-
- AIter = operations:get_tuples_iterator(DB4, table_a, #{}),
- BIter = operations:get_tuples_iterator(DB4, table_b, #{}),
-
- JoinIter = operations:equijoin_iterator(AIter, BIter, id),
- Results = operations:collect_all(JoinIter),
-
- % Should have one result with both values
- % Right side 'value' should be prefixed as 'right_value'
- [
- ?_assertEqual(1, length(Results)),
- ?_assertMatch([#{id := 1, value := 100, right_value := 200}], Results)
- ].
-
-%%% Theta-Join Tests
-
-test_theta_join_greater_than(DB) ->
- % Join where employee.age > department.budget/1000
- % (contrived example for testing)
-
- EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
- DeptIter = operations:get_tuples_iterator(DB, departments, #{}),
-
- Predicate = fun(Emp, Dept) ->
- Age = maps:get(age, Emp),
- Budget = maps:get(budget, Dept),
- Age > (Budget div 2000) % Age > Budget/2000
- end,
-
- JoinIter = operations:theta_join_iterator(EmpIter, DeptIter, Predicate),
- Results = operations:collect_all(JoinIter),
-
- % Should get some results based on predicate
- [
- ?_assert(length(Results) > 0),
- % Verify predicate holds for all results
- ?_assert(lists:all(fun(T) ->
- Age = maps:get(age, T),
- Budget = maps:get(budget, T),
- Age > (Budget div 2000)
- end, Results))
- ].
-
-test_theta_join_complex_predicate(DB) ->
- % Join with multiple conditions
- EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
- DeptIter = operations:get_tuples_iterator(DB, departments, #{}),
-
- % Join where employee is in department AND age > 30
- Predicate = fun(Emp, Dept) ->
- maps:get(dept_id, Emp) =:= maps:get(dept_id, Dept) andalso
- maps:get(age, Emp) > 30
- end,
-
- JoinIter = operations:theta_join_iterator(EmpIter, DeptIter, Predicate),
- Results = operations:collect_all(JoinIter),
-
- % Should get Bob (35) and Dave (42), not Alice (30) or Carol (28)
- [
- ?_assertEqual(2, length(Results)),
- ?_assert(lists:all(fun(T) -> maps:get(age, T) > 30 end, Results)),
- ?_assert(lists:any(fun(T) -> maps:get(name, T) =:= "Bob" end, Results)),
- ?_assert(lists:any(fun(T) -> maps:get(name, T) =:= "Dave" end, Results))
- ].
-
-%%% Integration Tests
-
-test_join_with_infinite_relation(DB) ->
- % Integers are built-in, no need to create them
-
- % Get small set of integers
- IntIter = operations:get_tuples_iterator(DB, integer, #{value => {range, 1, 5}}),
-
- % Get employees
- EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
-
- % Join where employee.id matches integer.value
- Predicate = fun(Emp, Int) ->
- maps:get(id, Emp) =:= maps:get(value, Int)
- end,
-
- JoinIter = operations:theta_join_iterator(EmpIter, IntIter, Predicate),
- Results = operations:collect_all(JoinIter),
-
- % Should get employees with id in [1,5] (all 4 employees)
- [
- ?_assertEqual(4, length(Results)),
- ?_assert(lists:all(fun(T) ->
- Id = maps:get(id, T),
- Value = maps:get(value, T),
- Id =:= Value andalso Id >= 1 andalso Id =< 5
- end, Results))
- ].
-
-test_join_materialize_pipeline(DB) ->
- % Complex pipeline: join, filter, project, materialize
-
- % 1. Join employees with departments
- EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
- DeptIter = operations:get_tuples_iterator(DB, departments, #{}),
- JoinIter = operations:equijoin_iterator(EmpIter, DeptIter, dept_id),
-
- % 2. Filter to high-budget departments
- FilteredIter = operations:select_iterator(JoinIter, fun(T) ->
- maps:get(budget, T) >= 90000
- end),
-
- % 3. Project to [name, dept_name, budget]
- ProjectedIter = operations:project_iterator(FilteredIter, [name, dept_name, budget]),
-
- % 4. Materialize
- {DB1, HighBudgetEmps} = operations:materialize(DB, ProjectedIter, high_budget_employees),
-
- % Verify materialized relation
- ResultIter = operations:get_tuples_iterator(DB1, high_budget_employees, #{}),
- Results = operations:collect_all(ResultIter),
-
- % Engineering (100k) has Alice and Carol
- % Marketing (90k) has Dave
- % Sales (80k) is excluded
- [
- ?_assertEqual({finite, 3}, HighBudgetEmps#relation.cardinality),
- ?_assertEqual(3, length(Results)),
- ?_assert(lists:all(fun(T) ->
- maps:is_key(name, T) andalso
- maps:is_key(dept_name, T) andalso
- maps:is_key(budget, T) andalso
- not maps:is_key(id, T) andalso % Projected out
- not maps:is_key(age, T) % Projected out
- end, Results)),
- ?_assert(lists:all(fun(T) -> maps:get(budget, T) >= 90000 end, Results))
- ].
diff --git a/test/operations_test.erl b/test/operations_test.erl
deleted file mode 100644
index 31bde43..0000000
--- a/test/operations_test.erl
+++ /dev/null
@@ -1,366 +0,0 @@
--module(operations_test).
--include("operations.hrl").
--include_lib("eunit/include/eunit.hrl").
-
-%% Test fixtures and setup
-setup_test_env() ->
- operations:setup(),
- ok.
-
-cleanup_test_env() ->
- mnesia:stop(),
- ok.
-
-%% Fixture: Create a sample database with a users relation
-sample_database() ->
- DB = operations:create_database(test_db),
- {DB1, _Relation} = operations:create_relation(DB, users, #{
- name => string,
- age => integer
- }),
- DB1.
-
-%% Fixture: Add sample tuples to a database
-sample_database_with_tuples() ->
- DB = sample_database(),
- {DB1, _} = operations:create_tuple(DB, users, #{name => "Alice", age => 30}),
- {DB2, _} = operations:create_tuple(DB1, users, #{name => "Bob", age => 25}),
- DB2.
-
-%%====================================================================
-%% Database Creation Tests
-%%====================================================================
-
-database_creation_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- DB = operations:create_database(my_db),
- ?assertEqual(my_db, DB#database_state.name),
- % Database now comes with built-in infinite relations
- Relations = operations:get_relations(DB),
- ?assert(lists:member(natural, Relations)),
- ?assert(lists:member(integer, Relations)),
- ?assert(lists:member(rational, Relations)),
- ?assertNotEqual(undefined, DB#database_state.timestamp)
- end)]}.
-
-%%====================================================================
-%% Relation Creation Tests
-%%====================================================================
-
-relation_creation_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- DB = operations:create_database(test_db),
- Schema = #{name => string, email => string},
- {DB1, Relation} = operations:create_relation(DB, users, Schema),
-
- % Verify relation was created
- ?assertEqual(users, Relation#relation.name),
- ?assertEqual(Schema, Relation#relation.schema),
-
- % Verify database was updated (includes built-in relations)
- Relations = operations:get_relations(DB1),
- ?assert(lists:member(users, Relations)),
- ?assert(lists:member(natural, Relations)),
- ?assert(lists:member(integer, Relations)),
- ?assert(lists:member(rational, Relations)),
- {ok, RelationHash} = operations:get_relation_hash(DB1, users),
- ?assertEqual(Relation#relation.hash, RelationHash)
- end)]}.
-
-%%====================================================================
-%% Tuple Operations Tests
-%%====================================================================
-
-tuple_creation_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- DB = sample_database(),
- InitialHash = DB#database_state.hash,
-
- % Add a tuple
- {DB1, _} = operations:create_tuple(DB, users, #{name => "Alice", age => 30}),
-
- % Verify database hash changed
- ?assertNotEqual(InitialHash, DB1#database_state.hash),
-
- % Verify tuple can be retrieved
- Iterator = operations:get_tuples_iterator(DB1, users),
- Tuples = operations:collect_all(Iterator),
- ?assertEqual(1, length(Tuples)),
- [Tuple] = Tuples,
- ?assertEqual("Alice", maps:get(name, Tuple)),
- ?assertEqual(30, maps:get(age, Tuple))
- end)]}.
-
-tuple_multiple_insertions_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- DB = sample_database(),
-
- % Add multiple tuples
- {DB1, _} = operations:create_tuple(DB, users, #{name => "Alice", age => 30}),
- {DB2, _} = operations:create_tuple(DB1, users, #{name => "Bob", age => 25}),
- {DB3, _} = operations:create_tuple(DB2, users, #{name => "Charlie", age => 35}),
-
- % Verify all tuples can be retrieved
- Iterator = operations:get_tuples_iterator(DB3, users),
- Tuples = operations:collect_all(Iterator),
- ?assertEqual(3, length(Tuples)),
-
- % Verify hashes changed with each insertion
- ?assertNotEqual(DB#database_state.hash, DB1#database_state.hash),
- ?assertNotEqual(DB1#database_state.hash, DB2#database_state.hash),
- ?assertNotEqual(DB2#database_state.hash, DB3#database_state.hash)
- end)]}.
-
-%%====================================================================
-%% Hash Comparison Tests (Time-based)
-%%====================================================================
-
-database_state_hash_comparison_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- DB = sample_database(),
- Hash0 = DB#database_state.hash,
-
- % State 1: Add first tuple
- {DB1, _} = operations:create_tuple(DB, users, #{name => "Alice", age => 30}),
- Hash1 = DB1#database_state.hash,
-
- % State 2: Add second tuple
- {DB2, _} = operations:create_tuple(DB1, users, #{name => "Bob", age => 25}),
- Hash2 = DB2#database_state.hash,
-
- % State 3: Add third tuple
- {DB3, _} = operations:create_tuple(DB2, users, #{name => "Charlie", age => 35}),
- Hash3 = DB3#database_state.hash,
-
- % Verify all hashes are unique
- ?assertNotEqual(Hash0, Hash1),
- ?assertNotEqual(Hash1, Hash2),
- ?assertNotEqual(Hash2, Hash3),
- ?assertNotEqual(Hash0, Hash2),
- ?assertNotEqual(Hash0, Hash3),
- ?assertNotEqual(Hash1, Hash3)
- end)]}.
-
-relation_hash_comparison_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- DB = sample_database(),
- {ok, RelationHash0} = operations:get_relation_hash(DB, users),
-
- % Add tuple and get new relation hash
- {DB1, Relation1} = operations:create_tuple(DB, users, #{name => "Alice", age => 30}),
- RelationHash1 = Relation1#relation.hash,
-
- % Add another tuple
- {DB2, Relation2} = operations:create_tuple(DB1, users, #{name => "Bob", age => 25}),
- RelationHash2 = Relation2#relation.hash,
-
- % Verify relation hashes changed over time
- ?assertNotEqual(RelationHash0, RelationHash1),
- ?assertNotEqual(RelationHash1, RelationHash2),
- ?assertNotEqual(RelationHash0, RelationHash2),
-
- % Verify we can still get relation hash from database
- {ok, CurrentHash} = operations:get_relation_hash(DB2, users),
- ?assertEqual(RelationHash2, CurrentHash)
- end)]}.
-
-%%====================================================================
-%% Tuple Retraction Tests
-%%====================================================================
-
-retract_tuple_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- DB = sample_database_with_tuples(),
- {ok, RelationHash} = operations:get_relation_hash(DB, users),
-
- % Get tuple hashes
- TupleHashes = operations:hashes_from_tuple(RelationHash),
- ?assertEqual(2, length(TupleHashes)),
-
- % Retract one tuple
- [TupleHash | _] = TupleHashes,
- {atomic, {DB1, _}} = operations:retract_tuple(DB, RelationHash, TupleHash),
-
- % Verify tuple was removed
- {ok, NewRelationHash} = operations:get_relation_hash(DB1, users),
- NewTupleHashes = operations:hashes_from_tuple(NewRelationHash),
- ?assertEqual(1, length(NewTupleHashes)),
-
- % Verify database hash changed
- ?assertNotEqual(DB#database_state.hash, DB1#database_state.hash)
- end)]}.
-
-%%====================================================================
-%% Relation Operations Tests
-%%====================================================================
-
-clear_relation_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- DB = sample_database_with_tuples(),
- {ok, RelationHash} = operations:get_relation_hash(DB, users),
-
- % Verify tuples exist
- TupleHashes = operations:hashes_from_tuple(RelationHash),
- ?assertEqual(2, length(TupleHashes)),
-
- % Clear relation
- {atomic, {DB1, ClearedRelation}} = operations:clear_relation(DB, RelationHash),
-
- % Verify relation is empty
- ?assertEqual(undefined, ClearedRelation#relation.tree),
- NewTupleHashes = operations:hashes_from_tuple(ClearedRelation#relation.hash),
- ?assertEqual([], NewTupleHashes),
-
- % Verify database hash changed
- ?assertNotEqual(DB#database_state.hash, DB1#database_state.hash)
- end)]}.
-
-retract_relation_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- DB = sample_database_with_tuples(),
-
- % Verify relation exists (along with built-in relations)
- Relations = operations:get_relations(DB),
- ?assert(lists:member(users, Relations)),
-
- % Retract relation
- {atomic, DB1} = operations:retract_relation(DB, users),
-
- % Verify users relation was removed (built-in relations remain)
- Relations1 = operations:get_relations(DB1),
- ?assertNot(lists:member(users, Relations1)),
- ?assert(lists:member(natural, Relations1)),
- ?assert(lists:member(integer, Relations1)),
- ?assert(lists:member(rational, Relations1)),
- ?assertEqual({error, not_found}, operations:get_relation_hash(DB1, users)),
-
- % Verify database hash changed
- ?assertNotEqual(DB#database_state.hash, DB1#database_state.hash)
- end)]}.
-
-%%====================================================================
-%% Iterator Tests
-%%====================================================================
-
-iterator_basic_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- DB = sample_database_with_tuples(),
-
- % Get iterator and retrieve tuples one by one
- Iterator = operations:get_tuples_iterator(DB, users),
-
- % Get first tuple
- {ok, Tuple1} = operations:next_tuple(Iterator),
- ?assert(is_map(Tuple1)),
- ?assert(maps:is_key(name, Tuple1)),
- ?assert(maps:is_key(age, Tuple1)),
-
- % Get second tuple
- {ok, Tuple2} = operations:next_tuple(Iterator),
- ?assert(is_map(Tuple2)),
-
- % No more tuples
- ?assertEqual(done, operations:next_tuple(Iterator)),
-
- operations:close_iterator(Iterator)
- end)]}.
-
-iterator_collect_all_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- DB = sample_database_with_tuples(),
-
- Iterator = operations:get_tuples_iterator(DB, users),
- Tuples = operations:collect_all(Iterator),
-
- ?assertEqual(2, length(Tuples)),
-
- % Verify both tuples are present
- Names = [maps:get(name, T) || T <- Tuples],
- ?assert(lists:member("Alice", Names)),
- ?assert(lists:member("Bob", Names))
- end)]}.
-
-iterator_empty_relation_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- DB = sample_database(),
-
- Iterator = operations:get_tuples_iterator(DB, users),
- Tuples = operations:collect_all(Iterator),
-
- ?assertEqual([], Tuples)
- end)]}.
-
-%%====================================================================
-%% Hash Consistency Tests
-%%====================================================================
-
-hash_determinism_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- % Same value should produce same hash
- Hash1 = operations:hash(<<"test">>),
- Hash2 = operations:hash(<<"test">>),
- ?assertEqual(Hash1, Hash2),
-
- % Different values should produce different hashes
- Hash3 = operations:hash(<<"different">>),
- ?assertNotEqual(Hash1, Hash3)
- end)]}.
-
-database_state_consistency_test_() ->
- {setup,
- fun setup_test_env/0,
- fun(_) -> cleanup_test_env() end,
- [?_test(begin
- % Create two identical databases with same operations
- DB1 = operations:create_database(db1),
- {DB1_1, _} = operations:create_relation(DB1, users, #{name => string}),
- {DB1_2, _} = operations:create_tuple(DB1_1, users, #{name => "Alice"}),
-
- DB2 = operations:create_database(db2),
- {DB2_1, _} = operations:create_relation(DB2, users, #{name => string}),
- {DB2_2, _} = operations:create_tuple(DB2_1, users, #{name => "Alice"}),
-
- % Relations should have the same hash (same schema and tuples)
- {ok, RelHash1} = operations:get_relation_hash(DB1_2, users),
- {ok, RelHash2} = operations:get_relation_hash(DB2_2, users),
- ?assertEqual(RelHash1, RelHash2)
- end)]}.
diff --git a/test/provenance_test.erl b/test/provenance_test.erl
deleted file mode 100644
index c98e372..0000000
--- a/test/provenance_test.erl
+++ /dev/null
@@ -1,420 +0,0 @@
--module(provenance_test).
--include_lib("eunit/include/eunit.hrl").
--include("operations.hrl").
-
-%%% Provenance & Lineage Tracking Test Suite
-%%%
-%%% Tests for tuple metadata tracking in relational operations.
-%%% Metadata is ALWAYS tracked and included in every tuple via the nested 'meta' field:
-%%% - Provenance: Tracks the immediate source of each attribute
-%%% - Lineage: Tracks the complete operational history of tuple creation
-%%%
-%%% NOTE ON TUPLE ORDERING:
-%%% These tests observe tuples in a specific order (e.g., Alice before Bob) because
-%%% collect_all/1 currently returns results in Merkle tree traversal order for finite
-%%% relations. This ordering is an IMPLEMENTATION DETAIL and is NOT guaranteed.
-%%% Relations are unordered sets - the current order is based on how tuples were
-%%% inserted and hashed into the Merkle tree. Future implementations may return tuples
-%%% in different orders. Tests use lists:all/2 to verify properties hold for ALL tuples
-%%% rather than relying on specific ordering.
-
-provenance_test_() ->
- {foreach,
- fun setup/0,
- fun cleanup/1,
- [
- fun test_base_provenance/1,
- fun test_join_provenance/1,
- fun test_join_with_conflicts_provenance/1,
- fun test_equijoin_preserves_both_sources/1,
- fun test_base_lineage/1,
- fun test_join_lineage/1,
- fun test_select_lineage/1,
- fun test_project_lineage/1,
- fun test_sort_lineage/1,
- fun test_take_lineage/1,
- fun test_multi_operation_pipeline_lineage/1,
- fun test_theta_join_lineage/1,
- fun test_complex_nested_lineage/1
- ]}.
-
-%%% Setup and Cleanup
-
-setup() ->
- operations:setup(),
- DB = operations:create_database(test_db),
-
- % Create employees relation
- {DB1, _} = operations:create_relation(DB, employees, #{
- id => integer,
- name => string,
- dept_id => integer
- }),
-
- {DB2, _} = operations:create_tuple(DB1, employees, #{id => 1, name => "Alice", dept_id => 10}),
- {DB3, _} = operations:create_tuple(DB2, employees, #{id => 2, name => "Bob", dept_id => 20}),
-
- % Create departments relation
- {DB4, _} = operations:create_relation(DB3, departments, #{
- dept_id => integer,
- dept_name => string,
- budget => integer
- }),
-
- {DB5, _} = operations:create_tuple(DB4, departments, #{dept_id => 10, dept_name => "Engineering", budget => 100000}),
- {DB6, _} = operations:create_tuple(DB5, departments, #{dept_id => 20, dept_name => "Sales", budget => 80000}),
-
- DB6.
-
-cleanup(_DB) ->
- ok.
-
-%%% Base Provenance Tests
-
-test_base_provenance(DB) ->
- % Query tuples (metadata always included)
- Iter = operations:get_tuples_iterator(DB, employees, #{}),
- Results = operations:collect_all(Iter),
-
- % All tuples should have correct provenance structure
- [
- ?_assertEqual(2, length(Results)),
- ?_assert(lists:all(fun(T) ->
- maps:is_key(meta, T) andalso
- begin
- Meta = maps:get(meta, T),
- maps:is_key(provenance, Meta)
- end
- end, Results)),
- % Check ALL tuples have correct provenance mapping
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Prov = maps:get(provenance, Meta),
- % All attributes should map to {employees, attr_name}
- maps:get(id, Prov) =:= {employees, id} andalso
- maps:get(name, Prov) =:= {employees, name} andalso
- maps:get(dept_id, Prov) =:= {employees, dept_id}
- end, Results))
- ].
-
-%%% Join Provenance Tests
-
-test_join_provenance(DB) ->
- % Create iterators (metadata always included)
- EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
- DeptIter = operations:get_tuples_iterator(DB, departments, #{}),
-
- % Join them
- JoinIter = operations:equijoin_iterator(EmpIter, DeptIter, dept_id),
- Results = operations:collect_all(JoinIter),
-
- % All results should have metadata with provenance
- [
- ?_assertEqual(2, length(Results)),
- ?_assert(lists:all(fun(T) -> maps:is_key(meta, T) end, Results)),
- % Check that ALL tuples have correct provenance structure
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Prov = maps:get(provenance, Meta),
- % Employee attributes should come from employees
- maps:get(id, Prov) =:= {employees, id} andalso
- maps:get(name, Prov) =:= {employees, name} andalso
- % Department attributes should come from departments
- maps:get(dept_name, Prov) =:= {departments, dept_name} andalso
- maps:get(budget, Prov) =:= {departments, budget}
- end, Results))
- ].
-
-test_join_with_conflicts_provenance(DB) ->
- % Create relations with conflicting attribute names
- {DB1, _} = operations:create_relation(DB, table_a, #{
- id => integer,
- value => integer
- }),
- {DB2, _} = operations:create_tuple(DB1, table_a, #{id => 1, value => 100}),
-
- {DB3, _} = operations:create_relation(DB2, table_b, #{
- id => integer,
- value => integer % Conflict!
- }),
- {DB4, _} = operations:create_tuple(DB3, table_b, #{id => 1, value => 200}),
-
- % Create iterators with provenance
- AIter = operations:get_tuples_iterator(DB4, table_a, #{}),
- BIter = operations:get_tuples_iterator(DB4, table_b, #{}),
-
- JoinIter = operations:equijoin_iterator(AIter, BIter, id),
- Results = operations:collect_all(JoinIter),
-
- % Should be exactly one result tuple
- ?_assertEqual(1, length(Results)),
-
- % Check provenance handles conflicts correctly
- [Result] = Results,
- Meta = maps:get(meta, Result),
- Prov = maps:get(provenance, Meta),
-
- [
- % Left 'value' provenance
- ?_assertEqual({table_a, value}, maps:get(value, Prov)),
- % Right 'value' provenance (prefixed)
- ?_assertEqual({table_b, value}, maps:get(right_value, Prov)),
- % Join attribute from left side
- ?_assertEqual({table_a, id}, maps:get(id, Prov)),
- % Join attribute from right side (prefixed)
- ?_assertEqual({table_b, id}, maps:get(right_id, Prov))
- ].
-
-test_equijoin_preserves_both_sources(DB) ->
- % Even though equijoin only keeps one dept_id value,
- % provenance should track both sources
- EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
- DeptIter = operations:get_tuples_iterator(DB, departments, #{}),
-
- JoinIter = operations:equijoin_iterator(EmpIter, DeptIter, dept_id),
- Results = operations:collect_all(JoinIter),
-
- % Check that ALL tuples have both dept_id sources in provenance
- [
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Prov = maps:get(provenance, Meta),
- % Left side dept_id
- maps:is_key(dept_id, Prov) andalso
- % Right side dept_id (prefixed)
- maps:is_key(right_dept_id, Prov)
- end, Results)),
- % Verify the sources are correct for ALL tuples
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Prov = maps:get(provenance, Meta),
- maps:get(dept_id, Prov) =:= {employees, dept_id} andalso
- maps:get(right_dept_id, Prov) =:= {departments, dept_id}
- end, Results))
- ].
-
-%%% Lineage Tests
-
-test_base_lineage(DB) ->
- % Query tuples (metadata always included)
- Iter = operations:get_tuples_iterator(DB, employees, #{}),
- Results = operations:collect_all(Iter),
-
- % All tuples should have lineage showing base relation
- [
- ?_assertEqual(2, length(Results)),
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Lineage = maps:get(lineage, Meta),
- Lineage =:= {base, employees}
- end, Results))
- ].
-
-test_join_lineage(DB) ->
- % Create iterators (metadata always included)
- EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
- DeptIter = operations:get_tuples_iterator(DB, departments, #{}),
-
- % Join them
- JoinIter = operations:equijoin_iterator(EmpIter, DeptIter, dept_id),
- Results = operations:collect_all(JoinIter),
-
- % All results should have lineage showing join operation
- [
- ?_assertEqual(2, length(Results)),
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Lineage = maps:get(lineage, Meta),
- % Should be a join of two base relations
- case Lineage of
- {join, dept_id, {base, employees}, {base, departments}} -> true;
- _ -> false
- end
- end, Results))
- ].
-
-%%% Comprehensive Lineage Tests
-
-test_select_lineage(DB) ->
- % Test lineage through select (filter) operation
- Iter = operations:get_tuples_iterator(DB, employees, #{}),
-
- Predicate = fun(T) -> maps:get(id, T, 0) =:= 1 end,
- FilteredIter = operations:select_iterator(Iter, Predicate),
- Results = operations:collect_all(FilteredIter),
-
- % Should have lineage: select wrapping base
- [
- ?_assertEqual(1, length(Results)),
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Lineage = maps:get(lineage, Meta),
- case Lineage of
- {select, Pred, {base, employees}} when is_function(Pred) -> true;
- _ -> false
- end
- end, Results))
- ].
-
-test_project_lineage(DB) ->
- % Test lineage through project operation
- Iter = operations:get_tuples_iterator(DB, employees, #{}),
- ProjectedIter = operations:project_iterator(Iter, [name, dept_id]),
- Results = operations:collect_all(ProjectedIter),
-
- % Should have lineage: project wrapping base
- [
- ?_assertEqual(2, length(Results)),
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Lineage = maps:get(lineage, Meta),
- case Lineage of
- {project, [name, dept_id], {base, employees}} -> true;
- _ -> false
- end
- end, Results))
- ].
-
-test_sort_lineage(DB) ->
- % Test lineage through sort operation
- Iter = operations:get_tuples_iterator(DB, employees, #{}),
-
- CompareFun = fun(A, B) -> maps:get(id, A, 0) =< maps:get(id, B, 0) end,
- SortedIter = operations:sort_iterator(Iter, CompareFun),
- Results = operations:collect_all(SortedIter),
-
- % Should have lineage: sort wrapping base
- [
- ?_assertEqual(2, length(Results)),
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Lineage = maps:get(lineage, Meta),
- case Lineage of
- {sort, Cmp, {base, employees}} when is_function(Cmp) -> true;
- _ -> false
- end
- end, Results))
- ].
-
-test_take_lineage(DB) ->
- % Test lineage through take (limit) operation
- Iter = operations:get_tuples_iterator(DB, employees, #{}),
- TakeIter = operations:take_iterator(Iter, 1),
- Results = operations:collect_all(TakeIter),
-
- % Should have lineage: take wrapping base
- [
- ?_assertEqual(1, length(Results)),
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Lineage = maps:get(lineage, Meta),
- case Lineage of
- {take, 1, {base, employees}} -> true;
- _ -> false
- end
- end, Results))
- ].
-
-test_multi_operation_pipeline_lineage(DB) ->
- % Test complex pipeline: employees → select(id=1) → join(departments) → project([name, dept_name])
-
- % 1. Start with employees base relation
- EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
-
- % 2. Filter to Alice (id=1)
- Predicate = fun(T) -> maps:get(id, T, 0) =:= 1 end,
- FilteredEmp = operations:select_iterator(EmpIter, Predicate),
-
- % 3. Join with departments
- DeptIter = operations:get_tuples_iterator(DB, departments, #{}),
- JoinedIter = operations:equijoin_iterator(FilteredEmp, DeptIter, dept_id),
-
- % 4. Project to [name, dept_name]
- ProjectedIter = operations:project_iterator(JoinedIter, [name, dept_name]),
-
- Results = operations:collect_all(ProjectedIter),
-
- % Expected lineage:
- % {project, [name, dept_name],
- % {join, dept_id,
- % {select, Pred, {base, employees}},
- % {base, departments}}}
-
- [
- ?_assertEqual(1, length(Results)), % Only Alice
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Lineage = maps:get(lineage, Meta),
- case Lineage of
- {project, [name, dept_name],
- {join, dept_id,
- {select, Pred, {base, employees}},
- {base, departments}}} when is_function(Pred) -> true;
- _ -> false
- end
- end, Results))
- ].
-
-test_theta_join_lineage(DB) ->
- % Test lineage for theta-join operation
- EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
- DeptIter = operations:get_tuples_iterator(DB, departments, #{}),
-
- Predicate = fun(Emp, Dept) ->
- maps:get(dept_id, Emp) =:= maps:get(dept_id, Dept)
- end,
-
- JoinIter = operations:theta_join_iterator(EmpIter, DeptIter, Predicate),
- Results = operations:collect_all(JoinIter),
-
- % Should have theta_join lineage
- [
- ?_assertEqual(2, length(Results)),
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Lineage = maps:get(lineage, Meta),
- case Lineage of
- {theta_join, Pred, {base, employees}, {base, departments}}
- when is_function(Pred) -> true;
- _ -> false
- end
- end, Results))
- ].
-
-test_complex_nested_lineage(DB) ->
- % Test deeply nested lineage:
- % employees → select(id=1) → project([name, dept_id]) → join(departments → select(budget>50000)) → take(1)
-
- % Left side: employees → select → project
- EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
- FilteredEmp = operations:select_iterator(EmpIter, fun(T) -> maps:get(id, T, 0) =:= 1 end),
- ProjectedEmp = operations:project_iterator(FilteredEmp, [name, dept_id]),
-
- % Right side: departments → select
- DeptIter = operations:get_tuples_iterator(DB, departments, #{}),
- FilteredDept = operations:select_iterator(DeptIter, fun(D) -> maps:get(budget, D, 0) > 50000 end),
-
- % Join both sides
- JoinedIter = operations:equijoin_iterator(ProjectedEmp, FilteredDept, dept_id),
-
- % Take first result
- TakeIter = operations:take_iterator(JoinedIter, 1),
-
- Results = operations:collect_all(TakeIter),
-
- % Expected lineage should be deeply nested
- [
- ?_assertEqual(1, length(Results)),
- ?_assert(lists:all(fun(T) ->
- Meta = maps:get(meta, T),
- Lineage = maps:get(lineage, Meta),
- case Lineage of
- {take, 1,
- {join, dept_id,
- {project, [name, dept_id],
- {select, _, {base, employees}}},
- {select, _, {base, departments}}}} -> true;
- _ -> false
- end
- end, Results))
- ].
diff --git a/test/query_pipeline_test.erl b/test/query_pipeline_test.erl
deleted file mode 100644
index a76c2ff..0000000
--- a/test/query_pipeline_test.erl
+++ /dev/null
@@ -1,183 +0,0 @@
--module(query_pipeline_test).
--include_lib("eunit/include/eunit.hrl").
--include("operations.hrl").
-
-%%% Test Suite for Query Pipeline (Volcano Iterator Model)
-
-query_pipeline_test_() ->
- {foreach,
- fun setup/0,
- fun cleanup/1,
- [
- fun test_select_iterator/1,
- fun test_project_iterator/1,
- fun test_take_iterator/1,
- fun test_sort_iterator/1,
- fun test_composed_pipeline/1,
- fun test_materialize_finite/1,
- fun test_materialize_infinite/1,
- fun test_complete_query_pipeline/1
- ]}.
-
-%%% Setup and Cleanup
-
-setup() ->
- operations:setup(),
- DB = operations:create_database(test_db),
-
- %% Create employees relation with test data
- {DB1, _} = operations:create_relation(DB, employees, #{
- name => string,
- age => integer,
- department => string
- }),
-
- {DB2, _} = operations:create_tuple(DB1, employees, #{name => "Alice", age => 35, department => "Engineering"}),
- {DB3, _} = operations:create_tuple(DB2, employees, #{name => "Bob", age => 28, department => "Sales"}),
- {DB4, _} = operations:create_tuple(DB3, employees, #{name => "Carol", age => 42, department => "Engineering"}),
- {DB5, _} = operations:create_tuple(DB4, employees, #{name => "Dave", age => 31, department => "Marketing"}),
- {DB6, _} = operations:create_tuple(DB5, employees, #{name => "Eve", age => 29, department => "Engineering"}),
-
- DB6.
-
-cleanup(_DB) ->
- ok.
-
-%%% Tests
-
-test_select_iterator(DB) ->
- %% Filter employees age > 30
- BaseIter = operations:get_tuples_iterator(DB, employees, #{}),
- FilteredIter = operations:select_iterator(BaseIter,
- fun(E) -> maps:get(age, E) > 30 end),
- Results = operations:collect_all(FilteredIter),
-
- %% Should get 3 employees: Alice (35), Carol (42), Dave (31)
- [
- ?_assertEqual(3, length(Results)),
- ?_assert(lists:all(fun(E) -> maps:get(age, E) > 30 end, Results))
- ].
-
-test_project_iterator(DB) ->
- %% Project to [name, age] only
- BaseIter = operations:get_tuples_iterator(DB, employees, #{}),
- ProjectedIter = operations:project_iterator(BaseIter, [name, age]),
- Results = operations:collect_all(ProjectedIter),
-
- %% Should have only name and age fields
- [
- ?_assertEqual(5, length(Results)),
- ?_assert(lists:all(fun(E) ->
- maps:is_key(name, E) andalso
- maps:is_key(age, E) andalso
- not maps:is_key(department, E)
- end, Results))
- ].
-
-test_take_iterator(DB) ->
- %% Take first 3 employees
- BaseIter = operations:get_tuples_iterator(DB, employees, #{}),
- LimitedIter = operations:take_iterator(BaseIter, 3),
- Results = operations:collect_all(LimitedIter),
-
- [
- ?_assertEqual(3, length(Results))
- ].
-
-test_sort_iterator(DB) ->
- %% Sort by age ascending
- BaseIter = operations:get_tuples_iterator(DB, employees, #{}),
- SortedIter = operations:sort_iterator(BaseIter,
- fun(A, B) -> maps:get(age, A) =< maps:get(age, B) end),
- Results = operations:collect_all(SortedIter),
-
- Ages = [maps:get(age, E) || E <- Results],
-
- [
- ?_assertEqual(5, length(Results)),
- ?_assertEqual([28, 29, 31, 35, 42], Ages)
- ].
-
-test_composed_pipeline(DB) ->
- %% Complex pipeline: filter age > 30, sort by age, take 2, project [name]
- Iter1 = operations:get_tuples_iterator(DB, employees, #{}),
- Iter2 = operations:select_iterator(Iter1, fun(E) -> maps:get(age, E) > 30 end),
- Iter3 = operations:sort_iterator(Iter2, fun(A, B) -> maps:get(age, A) =< maps:get(age, B) end),
- Iter4 = operations:take_iterator(Iter3, 2),
- Pipeline = operations:project_iterator(Iter4, [name]),
-
- Results = operations:collect_all(Pipeline),
-
- %% Should get 2 oldest employees over 30: Dave (31), Alice (35)
- Names = [maps:get(name, R) || R <- Results],
-
- [
- ?_assertEqual(2, length(Results)),
- ?_assertEqual(["Dave", "Alice"], Names),
- ?_assert(lists:all(fun(R) -> not maps:is_key(age, R) end, Results))
- ].
-
-test_materialize_finite(DB) ->
- %% Build pipeline and materialize
- Iter1 = operations:get_tuples_iterator(DB, employees, #{}),
- Iter2 = operations:select_iterator(Iter1, fun(E) ->
- maps:get(department, E) =:= "Engineering"
- end),
- Pipeline = operations:project_iterator(Iter2, [name, age]),
-
- {DB1, EngineersRel} = operations:materialize(DB, Pipeline, engineers),
-
- %% Verify new relation
- [
- ?_assert(is_record(EngineersRel, relation)),
- ?_assertEqual(engineers, EngineersRel#relation.name),
- ?_assertEqual({finite, 3}, EngineersRel#relation.cardinality),
-
- %% Can query the materialized relation
- ?_assertMatch([_,_,_],
- operations:collect_all(operations:get_tuples_iterator(DB1, engineers, #{})))
- ].
-
-test_materialize_infinite(DB) ->
- %% Naturals are built-in, no need to create them
-
- %% Take 100 and materialize
- Pipeline = operations:get_tuples_iterator(DB, natural, #{value => {range, 0, 99}}),
- {_DB2, Naturals100} = operations:materialize(DB, Pipeline, naturals_100),
-
- [
- ?_assertEqual({finite, 100}, Naturals100#relation.cardinality),
- ?_assertEqual(naturals_100, Naturals100#relation.name)
- ].
-
-test_complete_query_pipeline(DB) ->
- %% Realistic query:
- %% SELECT name, age FROM employees
- %% WHERE age > 28
- %% ORDER BY age DESC
- %% LIMIT 3
-
- Iter1 = operations:get_tuples_iterator(DB, employees, #{}),
- Iter2 = operations:select_iterator(Iter1, fun(E) -> maps:get(age, E) > 28 end),
- Iter3 = operations:sort_iterator(Iter2, fun(A, B) ->
- maps:get(age, A) >= maps:get(age, B) % DESC
- end),
- Iter4 = operations:take_iterator(Iter3, 3),
- Pipeline = operations:project_iterator(Iter4, [name, age]),
-
- {DB1, TopEmployees} = operations:materialize(DB, Pipeline, top_3_employees),
-
- %% Verify result - NOTE: Relations are unordered sets!
- %% After materialization, tuples come back in hash order, not sort order
- Results = operations:collect_all(operations:get_tuples_iterator(DB1, top_3_employees, #{})),
- Ages = lists:sort(fun(A, B) -> A >= B end, [maps:get(age, R) || R <- Results]),
-
- [
- ?_assertEqual(3, length(Results)),
- ?_assertEqual([42, 35, 31], Ages), % Verify we have the right ages (sorted DESC for check)
- ?_assert(lists:all(fun(R) ->
- Age = maps:get(age, R),
- lists:member(Age, [42, 35, 31])
- end, Results)),
- ?_assertEqual({finite, 3}, TopEmployees#relation.cardinality)
- ].