aboutsummaryrefslogtreecommitdiffstats
path: root/src/query_planner.erl
diff options
context:
space:
mode:
authorMarcos Magueta <maguetamarcos@gmail.com>2026-03-05 23:42:15 -0300
committerMarcos Magueta <maguetamarcos@gmail.com>2026-03-05 23:42:15 -0300
commit8c640a262969ef44cd8bc0e0abe95e2a54f4c3f5 (patch)
tree84f9ddd9e41236fb7a6f540aab07e65db9bc93cb /src/query_planner.erl
parent295c6a5a1f2b0db9d756ffe7bf413553a0c02737 (diff)
downloadSakura-8c640a262969ef44cd8bc0e0abe95e2a54f4c3f5.tar.gz
Sakura-8c640a262969ef44cd8bc0e0abe95e2a54f4c3f5.zip
ocaml re-migration
Diffstat (limited to 'src/query_planner.erl')
-rw-r--r--src/query_planner.erl601
1 files changed, 0 insertions, 601 deletions
diff --git a/src/query_planner.erl b/src/query_planner.erl
deleted file mode 100644
index d995f88..0000000
--- a/src/query_planner.erl
+++ /dev/null
@@ -1,601 +0,0 @@
-%%% @doc Query Planner and Prepared Statement Execution
-%%%
-%%% Provides a Lisp-inspired tuple-based DSL for constructing and executing
-%%% relational algebra queries. Queries are represented as nested Erlang tuples
-%%% that can be validated, optimized, and compiled into iterator pipelines.
-%%%
-%%% == Query DSL Syntax ==
-%%%
-%%% The DSL uses nested tuples to represent relational operations:
-%%% <pre>
-%%% % Base relation (just use the relation name)
-%%% employees
-%%%
-%%% % Selection (filter)
-%%% {select, employees, fun(T) -&gt; maps:get(age, T) &gt; 30 end}
-%%%
-%%% % Projection
-%%% {project, employees, [name, age]}
-%%%
-%%% % Equijoin (natural join on common attribute)
-%%% {join, employees, departments, dept_id}
-%%%
-%%% % Theta join (join with arbitrary predicate)
-%%% {theta_join,
-%%% employees,
-%%% departments,
-%%% fun(L, R) -&gt; maps:get(dept_id, L) =:= maps:get(id, R) end}
-%%%
-%%% % Sort
-%%% {sort, employees, fun(A, B) -&gt; maps:get(age, A) =&lt; maps:get(age, B) end}
-%%%
-%%% % Take (limit)
-%%% {take, employees, 10}
-%%%
-%%% % Rename attribute
-%%% {rename, old_name, new_name, employees}
-%%%
-%%% % Composite query
-%%% {project,
-%%% {select,
-%%% {join, employees, departments, dept_id},
-%%% fun(T) -&gt; maps:get(age, T) &gt; 30 end},
-%%% [name, dept_name]}
-%%% </pre>
-%%%
-%%% == Usage ==
-%%% <pre>
-%%% % Prepare a query plan
-%%% Plan = query_planner:prepare({select, employees,
-%%% fun(T) -&gt; maps:get(age, T) &gt; 30 end}).
-%%%
-%%% % Execute and get iterator
-%%% Iter = query_planner:execute(DB, Plan).
-%%%
-%%% % Collect all results
-%%% Results = query_planner:collect(DB, Plan).
-%%% </pre>
-%%%
-%%% @author Nekoma Team
-%%% @copyright 2025
-
--module(query_planner).
-
--export([prepare/1,
- execute/2,
- collect/2,
- explain/1]).
-
--include("operations.hrl").
-
-%%% Types
-
--type query_plan() ::
- atom() % Relation name
- | {select, query_plan(), fun((map()) -> boolean())} % Filter
- | {project, query_plan(), [atom()]} % Projection
- | {join, query_plan(), query_plan(), atom()} % Equijoin
- | {theta_join, query_plan(), query_plan(),
- fun((map(), map()) -> boolean())} % Theta join
- | {sort, query_plan(), fun((map(), map()) -> boolean())} % Sort
- | {take, query_plan(), pos_integer()} % Limit
- | {rename, atom(), atom(), query_plan()} % Rename attribute
- | {materialize, query_plan()} % Materialize all
- | {materialize, query_plan(), pos_integer()}. % Materialize N tuples
-
--export_type([query_plan/0]).
-
-%%% Public API
-
-%% @doc Prepare and validate a query plan.
-%%
-%% Takes a query plan tuple and validates its structure. Returns the
-%% plan unchanged if valid, or throws an error if invalid.
-%%
-%% @param Plan The query plan to prepare
-%% @returns The validated query plan
-%% @throws {invalid_plan, Reason}
--spec prepare(query_plan()) -> query_plan().
-prepare(Plan) ->
- case validate_plan(Plan) of
- ok -> Plan;
- {error, Reason} -> error({invalid_plan, Reason})
- end.
-
-%% @doc Execute a prepared query plan against a database.
-%%
-%% Returns an ephemeral relation for top-level operations that can spawn
-%% multiple independent iterators. The relation's generator creates fresh
-%% iterator pipelines on demand, allowing reproducible results.
-%%
-%% @param DB The database state
-%% @param Plan The prepared query plan
-%% @returns Ephemeral relation record
--spec execute(term(), query_plan()) -> #relation{}.
-execute(DB, Plan) ->
- create_ephemeral_relation(DB, Plan).
-
-%% @doc Execute a query plan and collect all results.
-%%
-%% Convenience function that executes the plan and collects all
-%% tuples into a list. Uses the ephemeral relation to create an
-%% iterator and automatically closes it.
-%%
-%% If the iterator times out (e.g., when sorting an infinite relation),
-%% returns partial results with a warning.
-%%
-%% @param DB The database state
-%% @param Plan The prepared query plan
-%% @returns List of result tuples, or partial results on timeout
--spec collect(term(), query_plan()) -> [map()].
-collect(DB, Plan) ->
- EphemeralRelation = execute(DB, Plan),
- Iter = (EphemeralRelation#relation.generator)(),
- Results = operations:collect_all(Iter),
- operations:close_iterator(Iter),
- case Results of
- {error, timeout, PartialResults} ->
- io:format("~nWarning: Query timed out. Returning partial results (~b tuples).~n",
- [length(PartialResults)]),
- io:format("Tip: For infinite relations, use 'take' before operations like 'sort'.~n~n"),
- PartialResults;
- {error, Reason, PartialResults} ->
- io:format("~nWarning: Query failed (~p). Returning partial results (~b tuples).~n~n",
- [Reason, length(PartialResults)]),
- PartialResults;
- _ ->
- Results
- end.
-
-%% @doc Explain a query plan in human-readable form.
-%%
-%% Converts the query plan to a readable string representation
-%% showing the operation tree.
-%%
-%% @param Plan The query plan to explain
-%% @returns String representation of the plan
--spec explain(query_plan()) -> string().
-explain(Plan) ->
- explain_plan(Plan, 0).
-
-%%% Internal Functions
-
-%% @doc Create an ephemeral relation for the top-level query plan.
-%%
-%% Uses pure relational operators from relational_operators module to construct
-%% the query result. Returns an ephemeral relation with a generator closure that
-%% can spawn fresh iterators, allowing reproducible query results.
-%%
-%% @param DB The database state (for looking up base relations only)
-%% @param Plan The query plan
-%% @returns Ephemeral relation record
--spec create_ephemeral_relation(term(), query_plan()) -> #relation{}.
-create_ephemeral_relation(DB, Plan) ->
- % Compile plan to ephemeral relation using pure operators
- compile_to_relation(DB, Plan).
-
-%% @doc Compute the schema for a query plan.
-%%
-%% Recursively analyzes the query plan structure to determine the output schema.
-%% The schema maps attribute names to their domain/type information.
-%%
-%% @param DB The database state
-%% @param Plan The query plan
-%% @returns Schema map
--spec compute_schema(term(), query_plan()) -> map().
-compute_schema(DB, RelationName) when is_atom(RelationName) ->
- case maps:get(RelationName, DB#database_state.relations, undefined) of
- undefined ->
- % Relation not found, return empty schema
- #{};
- RelationHash ->
- [Relation] = mnesia:dirty_read(relation, RelationHash),
- Relation#relation.schema
- end;
-
-compute_schema(DB, {select, SubPlan, _Predicate}) ->
- % Select doesn't change schema, just filters tuples
- compute_schema(DB, SubPlan);
-
-compute_schema(DB, {project, SubPlan, Attributes}) ->
- % Project filters the schema to only include selected attributes
- SubSchema = compute_schema(DB, SubPlan),
- maps:with(Attributes, SubSchema);
-
-compute_schema(DB, {join, LeftPlan, RightPlan, _Attribute}) ->
- % Join combines schemas from both sides, handling attribute conflicts
- LeftSchema = compute_schema(DB, LeftPlan),
- RightSchema = compute_schema(DB, RightPlan),
- merge_join_schemas(LeftSchema, RightSchema);
-
-compute_schema(DB, {theta_join, LeftPlan, RightPlan, _Predicate}) ->
- % Theta join also combines schemas from both sides, handling attribute conflicts
- LeftSchema = compute_schema(DB, LeftPlan),
- RightSchema = compute_schema(DB, RightPlan),
- merge_join_schemas(LeftSchema, RightSchema);
-
-compute_schema(DB, {sort, SubPlan, _Comparator}) ->
- % Sort doesn't change schema
- compute_schema(DB, SubPlan);
-
-compute_schema(DB, {take, SubPlan, _N}) ->
- % Take doesn't change schema, just limits tuples
- compute_schema(DB, SubPlan);
-
-compute_schema(DB, {rename, OldAttr, NewAttr, SubPlan}) ->
- % Rename changes one attribute name in the schema
- SubSchema = compute_schema(DB, SubPlan),
- case maps:is_key(OldAttr, SubSchema) of
- true ->
- AttrType = maps:get(OldAttr, SubSchema),
- SubSchema1 = maps:remove(OldAttr, SubSchema),
- maps:put(NewAttr, AttrType, SubSchema1);
- false ->
- % Attribute doesn't exist, return unchanged schema
- SubSchema
- end;
-
-compute_schema(DB, {materialize, SubPlan}) ->
- % Materialize doesn't change schema
- compute_schema(DB, SubPlan);
-
-compute_schema(DB, {materialize, SubPlan, _N}) ->
- % Materialize with limit doesn't change schema
- compute_schema(DB, SubPlan);
-
-compute_schema(_DB, _UnknownPlan) ->
- % Unknown plan type, return empty schema
- #{}.
-
-%% @doc Merge schemas for join operations, handling attribute conflicts elegantly.
-%%
-%% Instead of prefixing conflicting attribute names, this approach maintains
-%% both provenances in the schema value. For attributes that exist in both
-%% relations, the schema value becomes a list of the original values.
-%% This is especially important for natural joins where the join attribute
-%% logically represents the same entity from both sides.
-%%
-%% @param LeftSchema Schema from left relation
-%% @param RightSchema Schema from right relation
-%% @returns Merged schema map with provenance lists for conflicts
--spec merge_join_schemas(map(), map()) -> map().
-merge_join_schemas(LeftSchema, RightSchema) ->
- maps:fold(
- fun(Key, RightValue, Acc) ->
- case maps:get(Key, Acc, undefined) of
- undefined ->
- % No conflict, add the right attribute
- maps:put(Key, RightValue, Acc);
- LeftValue when LeftValue =:= RightValue ->
- % Same type/domain, keep single value (common in natural joins)
- maps:put(Key, LeftValue, Acc);
- LeftValue when is_list(LeftValue) ->
- % Left value is already a list, append if not already present
- case lists:member(RightValue, LeftValue) of
- true -> maps:put(Key, LeftValue, Acc);
- false -> maps:put(Key, LeftValue ++ [RightValue], Acc)
- end;
- LeftValue ->
- % Conflict with different types - create provenance list
- maps:put(Key, [LeftValue, RightValue], Acc)
- end
- end,
- LeftSchema,
- RightSchema
- ).
-
-%% @doc Compute provenance for a query plan.
-%%
-%% Recursively analyzes the query plan structure to determine the provenance,
-%% following the provenance types defined in operations.hrl.
-%%
-%% @param Plan The query plan
-%% @returns Provenance term
--spec compute_provenance(query_plan()) -> term().
-compute_provenance(RelationName) when is_atom(RelationName) ->
- {base, RelationName};
-
-compute_provenance({select, SubPlan, _Predicate}) ->
- % For select, we'd need to represent the predicate constraints
- % For now, simplified without constraint details
- SubProvenance = compute_provenance(SubPlan),
- {select, SubProvenance, #{}};
-
-compute_provenance({project, SubPlan, Attributes}) ->
- SubProvenance = compute_provenance(SubPlan),
- {project, SubProvenance, Attributes};
-
-compute_provenance({join, LeftPlan, RightPlan, _Attribute}) ->
- LeftProvenance = compute_provenance(LeftPlan),
- RightProvenance = compute_provenance(RightPlan),
- {join, LeftProvenance, RightProvenance};
-
-compute_provenance({theta_join, LeftPlan, RightPlan, _Predicate}) ->
- LeftProvenance = compute_provenance(LeftPlan),
- RightProvenance = compute_provenance(RightPlan),
- {join, LeftProvenance, RightProvenance};
-
-compute_provenance({sort, SubPlan, _Comparator}) ->
- % Sort doesn't change provenance
- compute_provenance(SubPlan);
-
-compute_provenance({take, SubPlan, N}) ->
- SubProvenance = compute_provenance(SubPlan),
- {take, SubProvenance, N};
-
-compute_provenance({rename, _OldAttr, _NewAttr, SubPlan}) ->
- % Rename preserves underlying provenance
- compute_provenance(SubPlan);
-
-compute_provenance({materialize, SubPlan}) ->
- % Materialize doesn't change provenance
- compute_provenance(SubPlan);
-
-compute_provenance({materialize, SubPlan, _N}) ->
- % Materialize with limit doesn't change provenance
- compute_provenance(SubPlan);
-
-compute_provenance(_UnknownPlan) ->
- % Unknown plan type
- undefined.
-
-%% Validate query plan structure
--spec validate_plan(query_plan()) -> ok | {error, term()}.
-validate_plan(Name) when is_atom(Name) ->
- ok;
-validate_plan({select, SubPlan, Pred}) when is_function(Pred, 1) ->
- validate_plan(SubPlan);
-validate_plan({project, SubPlan, Attrs}) when is_list(Attrs) ->
- case lists:all(fun is_atom/1, Attrs) of
- true -> validate_plan(SubPlan);
- false -> {error, {invalid_attributes, Attrs}}
- end;
-validate_plan({join, Left, Right, Attr}) when is_atom(Attr) ->
- case {validate_plan(Left), validate_plan(Right)} of
- {ok, ok} -> ok;
- {{error, _} = Error, _} -> Error;
- {_, {error, _} = Error} -> Error
- end;
-validate_plan({theta_join, Left, Right, Pred}) when is_function(Pred, 2) ->
- case {validate_plan(Left), validate_plan(Right)} of
- {ok, ok} -> ok;
- {{error, _} = Error, _} -> Error;
- {_, {error, _} = Error} -> Error
- end;
-validate_plan({sort, SubPlan, Comp}) when is_function(Comp, 2) ->
- validate_plan(SubPlan);
-validate_plan({take, SubPlan, N}) when is_integer(N), N > 0 ->
- validate_plan(SubPlan);
-validate_plan({rename, OldAttr, NewAttr, SubPlan}) when is_atom(OldAttr), is_atom(NewAttr) ->
- validate_plan(SubPlan);
-validate_plan({materialize, SubPlan}) ->
- validate_plan(SubPlan);
-validate_plan({materialize, SubPlan, N}) when is_integer(N), N > 0 ->
- validate_plan(SubPlan);
-validate_plan(Plan) ->
- {error, {unknown_operator, Plan}}.
-
-%% Compile query plan to iterator pipeline
--spec compile_to_iterator(term(), query_plan()) -> pid().
-compile_to_iterator(DB, Name) when is_atom(Name) ->
- operations:get_tuples_iterator(DB, Name);
-
-compile_to_iterator(DB, {select, SubPlan, Predicate}) ->
- SubIter = compile_to_iterator(DB, SubPlan),
- operations:select_iterator(SubIter, Predicate);
-
-compile_to_iterator(DB, {project, SubPlan, Attributes}) ->
- SubIter = compile_to_iterator(DB, SubPlan),
- operations:project_iterator(SubIter, Attributes);
-
-compile_to_iterator(DB, {join, LeftPlan, RightPlan, Attribute}) ->
- LeftIter = compile_to_iterator(DB, LeftPlan),
- RightIter = compile_to_iterator(DB, RightPlan),
- operations:equijoin_iterator(LeftIter, RightIter, Attribute);
-
-compile_to_iterator(DB, {theta_join, LeftPlan, RightPlan, Predicate}) ->
- LeftIter = compile_to_iterator(DB, LeftPlan),
- RightIter = compile_to_iterator(DB, RightPlan),
- operations:theta_join_iterator(LeftIter, RightIter, Predicate);
-
-compile_to_iterator(DB, {sort, SubPlan, Comparator}) ->
- SubIter = compile_to_iterator(DB, SubPlan),
- operations:sort_iterator(SubIter, Comparator);
-
-compile_to_iterator(DB, {take, SubPlan, N}) ->
- %% For iterator-based compilation, use take_iterator
- SubIter = compile_to_iterator(DB, SubPlan),
- operations:take_iterator(SubIter, N);
-
-compile_to_iterator(DB, {rename, OldAttr, NewAttr, SubPlan}) ->
- SubIter = compile_to_iterator(DB, SubPlan),
- %% Inline simple rename - no need for separate function
- spawn(fun() ->
- rename_loop_inline(SubIter, OldAttr, NewAttr)
- end);
-
-compile_to_iterator(DB, {materialize, SubPlan}) ->
- % Materialize returns all tuples as a "static" iterator
- SubIter = compile_to_iterator(DB, SubPlan),
- Results = operations:collect_all(SubIter),
- operations:close_iterator(SubIter),
- static_iterator(Results);
-
-compile_to_iterator(DB, {materialize, SubPlan, Limit}) ->
- % Materialize with limit
- SubIter = compile_to_iterator(DB, SubPlan),
- LimitedIter = operations:take_iterator(SubIter, Limit),
- Results = operations:collect_all(LimitedIter),
- operations:close_iterator(LimitedIter),
- static_iterator(Results).
-
-%% Static iterator - serves pre-materialized tuples
--spec static_iterator([map()]) -> pid().
-static_iterator(Tuples) ->
- spawn(fun() -> static_loop(Tuples) end).
-
-static_loop(Tuples) ->
- receive
- {next, Caller} ->
- case Tuples of
- [] ->
- Caller ! done,
- static_loop([]);
- [Tuple | Rest] ->
- Caller ! {tuple, Tuple},
- static_loop(Rest)
- end;
- {close, Caller} ->
- Caller ! ok
- end.
-
-%% @private
-%% Inline helper for compile_to_iterator rename case
-rename_loop_inline(SourceIter, OldAttr, NewAttr) ->
- receive
- {next, Caller} ->
- case operations:next_tuple(SourceIter) of
- done ->
- Caller ! done;
- {ok, Tuple} ->
- RenamedTuple = case maps:is_key(OldAttr, Tuple) of
- true ->
- Value = maps:get(OldAttr, Tuple),
- Tuple1 = maps:remove(OldAttr, Tuple),
- maps:put(NewAttr, Value, Tuple1);
- false ->
- Tuple
- end,
- Caller ! {tuple, RenamedTuple},
- rename_loop_inline(SourceIter, OldAttr, NewAttr)
- end;
- {close, Caller} ->
- operations:close_iterator(SourceIter),
- Caller ! ok
- end.
-
-%% Pretty-print query plan
--spec explain_plan(query_plan(), non_neg_integer()) -> string().
-explain_plan(Name, Indent) when is_atom(Name) ->
- indent(Indent) ++ "Relation: " ++ atom_to_list(Name) ++ "\n";
-
-explain_plan({select, SubPlan, _Pred}, Indent) ->
- indent(Indent) ++ "Select (filter)\n" ++
- explain_plan(SubPlan, Indent + 2);
-
-explain_plan({project, SubPlan, Attrs}, Indent) ->
- AttrStr = string:join([atom_to_list(A) || A <- Attrs], ", "),
- indent(Indent) ++ "Project [" ++ AttrStr ++ "]\n" ++
- explain_plan(SubPlan, Indent + 2);
-
-explain_plan({join, LeftPlan, RightPlan, Attr}, Indent) ->
- indent(Indent) ++ "Join on " ++ atom_to_list(Attr) ++ "\n" ++
- explain_plan(LeftPlan, Indent + 2) ++
- explain_plan(RightPlan, Indent + 2);
-
-explain_plan({theta_join, LeftPlan, RightPlan, _Pred}, Indent) ->
- indent(Indent) ++ "Theta Join\n" ++
- explain_plan(LeftPlan, Indent + 2) ++
- explain_plan(RightPlan, Indent + 2);
-
-explain_plan({sort, SubPlan, _Comp}, Indent) ->
- indent(Indent) ++ "Sort\n" ++
- explain_plan(SubPlan, Indent + 2);
-
-explain_plan({take, SubPlan, N}, Indent) ->
- indent(Indent) ++ "Take " ++ integer_to_list(N) ++ "\n" ++
- explain_plan(SubPlan, Indent + 2);
-
-explain_plan({rename, OldAttr, NewAttr, SubPlan}, Indent) ->
- indent(Indent) ++ "Rename " ++ atom_to_list(OldAttr) ++ " -> " ++ atom_to_list(NewAttr) ++ "\n" ++
- explain_plan(SubPlan, Indent + 2);
-
-explain_plan({materialize, SubPlan}, Indent) ->
- indent(Indent) ++ "Materialize (all)\n" ++
- explain_plan(SubPlan, Indent + 2);
-
-explain_plan({materialize, SubPlan, N}, Indent) ->
- indent(Indent) ++ "Materialize (" ++ integer_to_list(N) ++ " tuples)\n" ++
- explain_plan(SubPlan, Indent + 2).
-
-%% Helper: indent string
--spec indent(non_neg_integer()) -> string().
-indent(N) -> lists:duplicate(N, $\s).
-
-%% Helper: get relation from database
--spec get_relation_from_db(term(), atom()) -> #relation{}.
-get_relation_from_db(DB, RelationName) ->
- case maps:get(RelationName, DB#database_state.relations, undefined) of
- undefined ->
- erlang:error({relation_not_found, RelationName});
- RelationHash ->
- [Relation] = mnesia:dirty_read(relation, RelationHash),
- Relation
- end.
-
-%% @doc Compile query plan to ephemeral relation using pure relational operators.
-%%
-%% This is the new approach: build a relation tree using pure operators,
-%% then return the final ephemeral relation. No iterators are created until
-%% the generator is called.
--spec compile_to_relation(term(), query_plan()) -> #relation{}.
-compile_to_relation(DB, RelationName) when is_atom(RelationName) ->
- BaseRelation = get_relation_from_db(DB, RelationName),
- GeneratorFun = fun() ->
- operations:get_tuples_iterator(DB, RelationName)
- end,
- BaseRelation#relation{generator = GeneratorFun};
-
-compile_to_relation(DB, {select, SubPlan, Predicate}) ->
- SubRelation = compile_to_relation(DB, SubPlan),
- relational_operators:select(SubRelation, Predicate);
-
-compile_to_relation(DB, {project, SubPlan, Attributes}) ->
- SubRelation = compile_to_relation(DB, SubPlan),
- relational_operators:project(SubRelation, Attributes);
-
-compile_to_relation(DB, {join, LeftPlan, RightPlan, Attribute}) ->
- LeftRelation = compile_to_relation(DB, LeftPlan),
- RightRelation = compile_to_relation(DB, RightPlan),
- relational_operators:join(LeftRelation, RightRelation, Attribute);
-
-compile_to_relation(DB, {theta_join, LeftPlan, RightPlan, Predicate}) ->
- LeftRelation = compile_to_relation(DB, LeftPlan),
- RightRelation = compile_to_relation(DB, RightPlan),
- relational_operators:theta_join(LeftRelation, RightRelation, Predicate);
-
-compile_to_relation(DB, {sort, SubPlan, Comparator}) ->
- SubRelation = compile_to_relation(DB, SubPlan),
- relational_operators:sort(SubRelation, Comparator);
-
-compile_to_relation(DB, {take, SubPlan, N}) ->
- SubRelation = compile_to_relation(DB, SubPlan),
- relational_operators:take(SubRelation, N);
-
-compile_to_relation(DB, {rename, OldAttr, NewAttr, SubPlan}) ->
- SubRelation = compile_to_relation(DB, SubPlan),
- relational_operators:rename(SubRelation, #{OldAttr => NewAttr});
-
-compile_to_relation(DB, {materialize, SubPlan}) ->
- GeneratorFun = fun() ->
- compile_to_iterator(DB, SubPlan)
- end,
- EphemeralName = list_to_atom("materialized_" ++ integer_to_list(erlang:unique_integer([positive]))),
- #relation{
- hash = crypto:hash(sha256, term_to_binary({materialize, SubPlan})),
- name = EphemeralName,
- tree = undefined,
- schema = compute_schema(DB, SubPlan),
- constraints = constraint:empty_constraints(),
- cardinality = unknown,
- generator = GeneratorFun,
- membership_criteria = #{},
- provenance = compute_provenance(SubPlan)
- };
-
-compile_to_relation(DB, {materialize, SubPlan, N}) ->
- % Materialize with limit
- SubRelation = compile_to_relation(DB, SubPlan),
- TakeRelation = relational_operators:take(SubRelation, N),
- % Then wrap in a materialized form (for now just return take)
- TakeRelation.