diff options
Diffstat (limited to 'test/function_relations_test.erl')
| -rw-r--r-- | test/function_relations_test.erl | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/test/function_relations_test.erl b/test/function_relations_test.erl new file mode 100644 index 0000000..78c9ec3 --- /dev/null +++ b/test/function_relations_test.erl @@ -0,0 +1,339 @@ +-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), + + % First set up the basic number types (integers, naturals, etc) + {DB1, _Integers} = operations:create_infinite_relation(DB, #{ + name => integers, + schema => #{}, % empty schema - this is a base type + cardinality => aleph_zero, + generator => {primitive, integers} + }), + + {DB2, _Naturals} = operations:create_infinite_relation(DB1, #{ + name => naturals, + schema => #{}, + cardinality => aleph_zero, + generator => {primitive, naturals} + }), + + % Now create the function relations - note how their schemas reference the domain relations above + {DB3, _Plus} = operations:create_infinite_relation(DB2, #{ + name => plus, + schema => #{a => integers, b => integers, sum => integers}, + cardinality => aleph_zero, + generator => {primitive, plus} + }), + + {DB4, _Times} = operations:create_infinite_relation(DB3, #{ + name => times, + schema => #{a => integers, b => integers, product => integers}, + cardinality => aleph_zero, + generator => {primitive, times} + }), + + {DB5, _Minus} = operations:create_infinite_relation(DB4, #{ + name => minus, + schema => #{a => integers, b => integers, difference => integers}, + cardinality => aleph_zero, + generator => {primitive, minus} + }), + + {DB6, _Divide} = operations:create_infinite_relation(DB5, #{ + name => divide, + schema => #{a => integers, b => integers, quotient => integers, remainder => naturals}, + cardinality => aleph_zero, + generator => {primitive, divide} + }), + + DB6. + +cleanup(_DB) -> + ok. + +%%% 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), + + % 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}, Results)), + ?_assert(lists:member(#{a => 2, b => 3, sum => 5}, Results)), + ?_assert(lists:member(#{a => 3, b => 4, sum => 7}, Results)), + ?_assert(lists:all(fun(#{a := A, b := B, sum := S}) -> + S =:= A + B + end, Results)) + ]. + +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), + + % 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, Results)), + ?_assert(lists:member(#{a => 1, b => 4, sum => 5}, Results)), + ?_assert(lists:member(#{a => 3, b => 4, sum => 7}, Results)) + ]. + +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), + + [ + ?_assert(length(Results) > 0), + ?_assert(lists:all(fun(#{a := A, b := B, sum := S}) -> + S =:= A + B andalso A >= 0 + end, Results)), + ?_assert(lists:member(#{a => 3, b => 2, sum => 5}, Results)), + ?_assert(lists:member(#{a => 2, b => 4, sum => 6}, Results)) + ]. + +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), + + [ + ?_assertEqual(6, length(Results)), + ?_assert(lists:member(#{a => 0, b => 5, sum => 5}, Results)), + ?_assert(lists:member(#{a => 2, b => 3, sum => 5}, Results)), + ?_assert(lists:member(#{a => 5, b => 0, sum => 5}, Results)), + ?_assert(lists:all(fun(#{a := A, b := B, sum := S}) -> + S =:= A + B andalso S =:= 5 + end, Results)) + ]. + +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), + + % 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, Results)), + ?_assert(lists:member(#{a => 0, b => 2, sum => 2}, Results)), + ?_assert(lists:member(#{a => 3, b => 0, sum => 3}, Results)) + ]. + +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), + + % 3×3 = 9 tuples + [ + ?_assertEqual(9, length(Results)), + ?_assert(lists:member(#{a => 2, b => 3, product => 6}, Results)), + ?_assert(lists:member(#{a => 3, b => 4, product => 12}, Results)), + ?_assert(lists:member(#{a => 4, b => 5, product => 20}, Results)), + ?_assert(lists:all(fun(#{a := A, b := B, product := P}) -> + P =:= A * B + end, Results)) + ]. + +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), + + % 3 × 2 = 6 tuples + [ + ?_assertEqual(6, length(Results)), + ?_assert(lists:member(#{a => 5, b => 2, difference => 3}, Results)), + ?_assert(lists:member(#{a => 7, b => 3, difference => 4}, Results)), + ?_assert(lists:all(fun(#{a := A, b := B, difference := D}) -> + D =:= A - B + end, Results)) + ]. + +%%% 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), + + % 3 × 2 = 6 tuples + [ + ?_assertEqual(6, length(Results)), + ?_assert(lists:member(#{a => 10, b => 3, quotient => 3, remainder => 1}, Results)), + ?_assert(lists:member(#{a => 12, b => 4, quotient => 3, remainder => 0}, Results)), + ?_assert(lists:all(fun(#{a := A, b := B, quotient := Q, remainder := R}) -> + A =:= B * Q + R andalso R >= 0 andalso R < abs(B) + end, Results)) + ]. + +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)) + ]. |
