aboutsummaryrefslogblamecommitdiffstats
path: root/test/query_pipeline_test.erl
blob: a76c2ff202dec9bbf04d13c54487804de30f9b46 (plain) (tree)












































































































































                                                                                                                   
                                                    

                               

                                                                                       




































                                                                                                
-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)
    ].