diff options
| author | Marcos Magueta <maguetamarcos@gmail.com> | 2025-12-14 00:27:18 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-14 00:27:18 -0300 |
| commit | ff5691ffb1a969e50e0bd5e4638be8e2bb342c4e (patch) | |
| tree | 565346b82901f2ee26cb49b04e1e99ba1fb0e896 /test | |
| parent | 0862cd7c3fe4fbb5b56a0a87c3789c8361bc022c (diff) | |
| download | Sakura-ff5691ffb1a969e50e0bd5e4638be8e2bb342c4e.tar.gz Sakura-ff5691ffb1a969e50e0bd5e4638be8e2bb342c4e.zip | |
Ephemeral relations, Refactor & Cleanup (#34)
Diffstat (limited to '')
| -rw-r--r-- | test/function_relations_test.erl | 347 | ||||
| -rw-r--r-- | test/infinite_relations_test.erl | 128 | ||||
| -rw-r--r-- | test/join_test.erl | 273 | ||||
| -rw-r--r-- | test/operations_test.erl | 366 | ||||
| -rw-r--r-- | test/provenance_test.erl | 420 | ||||
| -rw-r--r-- | test/query_pipeline_test.erl | 183 |
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) - ]. |
