diff options
| author | Marcos Magueta <maguetamarcos@gmail.com> | 2025-11-22 22:32:04 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-22 22:32:04 -0300 |
| commit | 09be36e5f5e2c5714b9542df8ccc363ab86ac6da (patch) | |
| tree | 06105bcbcc8cacc70e977e2370af270ae713226e /test | |
| parent | a56ea3089524b440b12b660d36c07f46b4fd9eb3 (diff) | |
| download | Sakura-09be36e5f5e2c5714b9542df8ccc363ab86ac6da.tar.gz Sakura-09be36e5f5e2c5714b9542df8ccc363ab86ac6da.zip | |
Infinite relations (#10)
Diffstat (limited to '')
| -rw-r--r-- | test/function_relations_test.erl | 339 | ||||
| -rw-r--r-- | test/infinite_relations_test.erl | 147 | ||||
| -rw-r--r-- | test/query_pipeline_test.erl | 190 |
3 files changed, 676 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)) + ]. diff --git a/test/infinite_relations_test.erl b/test/infinite_relations_test.erl new file mode 100644 index 0000000..cafd325 --- /dev/null +++ b/test/infinite_relations_test.erl @@ -0,0 +1,147 @@ +-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. + +%%% Tests + +test_create_naturals(DB) -> + %% Create infinite relation of natural numbers + %% Domain relations have empty schemas (base case) + {DB1, Naturals} = operations:create_infinite_relation(DB, #{ + name => naturals, + schema => #{}, + cardinality => aleph_zero, + generator => {primitive, naturals}, + constraints => #{value => {gte, 0}} + }), + + [ + ?_assert(is_record(Naturals, relation)), + ?_assertEqual(naturals, Naturals#relation.name), + ?_assertEqual(aleph_zero, Naturals#relation.cardinality), + ?_assertEqual({primitive, naturals}, Naturals#relation.generator) + ]. + +test_naturals_with_constraints(DB) -> + %% Create naturals relation + {DB1, _Naturals} = operations:create_infinite_relation(DB, #{ + name => naturals, + schema => #{}, + cardinality => aleph_zero, + generator => {primitive, naturals}, + constraints => #{value => {gte, 0}} + }), + + %% Query with range constraint [0, 9] + Iterator = operations:get_tuples_iterator(DB1, naturals, #{value => {range, 0, 9}}), + Tuples = operations:collect_all(Iterator), + + %% Should get exactly 10 tuples: {0, 1, 2, ..., 9} + [ + ?_assertEqual(10, length(Tuples)), + ?_assert(lists:member(#{value => 0}, Tuples)), + ?_assert(lists:member(#{value => 5}, Tuples)), + ?_assert(lists:member(#{value => 9}, Tuples)) + ]. + +test_integers_generator(DB) -> + %% Create integers relation + {DB1, Integers} = operations:create_infinite_relation(DB, #{ + name => integers, + schema => #{}, + cardinality => aleph_zero, + generator => {primitive, integers} + }), + + %% Query with range constraint [-5, 5] + Iterator = operations:get_tuples_iterator(DB1, integers, #{value => {range, -5, 5}}), + Tuples = operations:collect_all(Iterator), + + %% Should get 11 tuples: {-5, -4, ..., 0, ..., 4, 5} + [ + ?_assertEqual(aleph_zero, Integers#relation.cardinality), + ?_assertEqual(11, length(Tuples)), + ?_assert(lists:member(#{value => 0}, Tuples)), + ?_assert(lists:member(#{value => -5}, Tuples)), + ?_assert(lists:member(#{value => 5}, Tuples)) + ]. + +test_rationals_generator(DB) -> + %% Create rationals relation + {DB1, Rationals} = operations:create_infinite_relation(DB, #{ + name => rationals, + schema => #{}, + cardinality => aleph_zero, + generator => {primitive, rationals} + }), + + %% Take first 10 rationals using constraints + Iterator = operations:get_tuples_iterator(DB1, rationals, #{}), + + %% 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) -> + %% Create naturals + {DB1, _} = operations:create_infinite_relation(DB, #{ + name => naturals, + schema => #{}, + cardinality => aleph_zero, + generator => {primitive, naturals}, + constraints => #{value => {gte, 0}} + }), + + %% Take 100 naturals + {_DB2, Naturals100} = operations:take(DB1, naturals, 100), + + %% Verify result is finite + [ + ?_assertEqual({finite, 100}, Naturals100#relation.cardinality), + ?_assertEqual({take, naturals, 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/query_pipeline_test.erl b/test/query_pipeline_test.erl new file mode 100644 index 0000000..389ff7c --- /dev/null +++ b/test/query_pipeline_test.erl @@ -0,0 +1,190 @@ +-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) -> + %% Create infinite naturals + {DB1, _} = operations:create_infinite_relation(DB, #{ + name => naturals, + schema => #{}, + cardinality => aleph_zero, + generator => {primitive, naturals}, + constraints => #{value => {gte, 0}} + }), + + %% Take 100 and materialize + Pipeline = operations:get_tuples_iterator(DB1, naturals, #{value => {range, 0, 99}}), + {_DB2, Naturals100} = operations:materialize(DB1, 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) + ]. |
