aboutsummaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--test/function_relations_test.erl339
-rw-r--r--test/infinite_relations_test.erl147
-rw-r--r--test/query_pipeline_test.erl190
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)
+ ].