aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMarcos Magueta <maguetamarcos@gmail.com>2025-12-14 00:27:18 -0300
committerGitHub <noreply@github.com>2025-12-14 00:27:18 -0300
commitff5691ffb1a969e50e0bd5e4638be8e2bb342c4e (patch)
tree565346b82901f2ee26cb49b04e1e99ba1fb0e896 /src
parent0862cd7c3fe4fbb5b56a0a87c3789c8361bc022c (diff)
downloadSakura-ff5691ffb1a969e50e0bd5e4638be8e2bb342c4e.tar.gz
Sakura-ff5691ffb1a969e50e0bd5e4638be8e2bb342c4e.zip
Ephemeral relations, Refactor & Cleanup (#34)
Diffstat (limited to '')
-rw-r--r--src/main.erl60
-rw-r--r--src/operations.erl593
-rw-r--r--src/query_planner.erl356
-rw-r--r--src/relational_operators.erl366
-rw-r--r--src/repl.erl49
-rw-r--r--src/xml_server.erl9
-rw-r--r--src/xml_server_app.erl2
7 files changed, 894 insertions, 541 deletions
diff --git a/src/main.erl b/src/main.erl
index 5b8d6e3..d2fe0a7 100644
--- a/src/main.erl
+++ b/src/main.erl
@@ -1,7 +1,54 @@
-module(main).
-behaviour(application).
--export([start/2, stop/1]).
+-export([start/2, stop/1, setup/0]).
+
+%% @doc Initialize Mnesia database schema and tables.
+%%
+%% Creates a fresh Mnesia schema with four tables for content-addressed storage:
+%% <ul>
+%% <li>`attribute' - Hash → Value mapping for deduplication</li>
+%% <li>`tuple' - Hash → Tuple mapping with attribute references</li>
+%% <li>`relation' - Hash → Relation with merkle tree and schema</li>
+%% <li>`database_state' - Hash → Database with relation merkle tree</li>
+%% </ul>
+%%
+%% <b>Warning:</b> This function deletes any existing schema and recreates it from scratch.
+%%
+%% @returns `ok' on success
+%% @see operations:create_database/1
+setup() ->
+ mnesia:stop(),
+ mnesia:delete_schema([node()]),
+ mnesia:create_schema([node()]),
+ mnesia:start(),
+ mnesia:create_table(attribute, [
+ {attributes, [hash, value]},
+ {disc_copies, [node()]},
+ {type, set}
+ ]),
+ mnesia:create_table(tuple, [
+ {attributes, [hash, relation, attribute_map]},
+ {disc_copies, [node()]},
+ {type, set}
+ ]),
+ mnesia:create_table(relation, [
+ {attributes, [hash, name, tree, schema, constraints, cardinality, generator, membership_criteria, mutability, provenance, lineage]},
+ {disc_copies, [node()]},
+ {type, set},
+ {index, [name]} % Index on name for efficient lookup
+ ]),
+ mnesia:create_table(database_state, [
+ {attributes, [hash, name, tree, relations, timestamp]},
+ {disc_copies, [node()]},
+ {type, set}
+ ]).
+
+uuid() ->
+ ok = application:load(quickrand),
+ ok = quickrand:seed(),
+ ok = quickrand_cache:init(),
+ quickrand_cache:new().
start(_StartType, _StartArgs) ->
%% Start Mnesia (or other children) here
@@ -9,18 +56,11 @@ start(_StartType, _StartArgs) ->
%% Ensure Mnesia is started
mnesia:start(),
+ setup(),
+ _ = uuid(),
{ok, self()}.
stop(_State) ->
io:format("Stopping app...~n"),
ok.
-
-%% setup() ->
-%% mnesia:create_table(attributes, [{type, set},
-%% {attributes, [name, value, type]},
-%% {ram_copies, [node()]},
-%% {index, [name]}
-%% ]).
-
-
diff --git a/src/operations.erl b/src/operations.erl
index 4a492f4..e224800 100644
--- a/src/operations.erl
+++ b/src/operations.erl
@@ -38,25 +38,24 @@
%%% @reference E. F. Codd (1970). "A Relational Model of Data for Large Shared Data Banks"
-module(operations).
--include("operations.hrl").
--export([setup/0,
- hash/1,
- create_database/1,
+-include("../include/operations.hrl").
+-export([
+ create_database/1,
create_relation/3,
create_immutable_relation/2,
create_tuple/3,
+ create_all_tuples/3,
retract_tuple/3,
retract_tuple/4,
clear_relation/2,
clear_relation/3,
retract_relation/2,
retract_relation/3,
- get_relations/1,
- get_relation_hash/2,
- hashes_from_tuple/1,
- get_tuples_iterator/2,
- get_tuples_iterator/3,
- take/3,
+ get_relation_hash/2,
+ get_relations/1,
+ hashes_from_tuple/1,
+ get_tuples_iterator/2,
+ get_iterator_from_generator/2,
next_tuple/1,
close_iterator/1,
collect_all/1,
@@ -69,48 +68,10 @@
equijoin_iterator/3,
theta_join_iterator/3,
%% Materialization (eager)
- materialize/3]).
-
-%% @doc Initialize Mnesia database schema and tables.
-%%
-%% Creates a fresh Mnesia schema with four tables for content-addressed storage:
-%% <ul>
-%% <li>`attribute' - Hash → Value mapping for deduplication</li>
-%% <li>`tuple' - Hash → Tuple mapping with attribute references</li>
-%% <li>`relation' - Hash → Relation with merkle tree and schema</li>
-%% <li>`database_state' - Hash → Database with relation merkle tree</li>
-%% </ul>
-%%
-%% <b>Warning:</b> This function deletes any existing schema and recreates it from scratch.
-%%
-%% @returns `ok' on success
-%% @see create_database/1
-setup() ->
- mnesia:stop(),
- mnesia:delete_schema([node()]),
- mnesia:create_schema([node()]),
- mnesia:start(),
- mnesia:create_table(attribute, [
- {attributes, [hash, value]},
- {disc_copies, [node()]},
- {type, set}
- ]),
- mnesia:create_table(tuple, [
- {attributes, [hash, relation, attribute_map]},
- {disc_copies, [node()]},
- {type, set}
- ]),
- mnesia:create_table(relation, [
- {attributes, [hash, name, tree, schema, constraints, cardinality, generator, membership_criteria, mutability, provenance]},
- {disc_copies, [node()]},
- {type, set}
- ]),
- mnesia:create_table(database_state, [
- {attributes, [hash, name, tree, relations, timestamp]},
- {disc_copies, [node()]},
- {type, set}
- ]).
+ materialize/3
+]).
+%% @private
%% @doc Compute SHA-256 hash of an Erlang term.
%%
%% This is the foundational hashing function used throughout the system for
@@ -157,18 +118,10 @@ create_tuple(Database, RelationName, Tuple) when is_map(Tuple), is_record(Databa
%% Check if relation is immutable before attempting insert
CurrentRelationHash = maps:get(RelationName, Database#database_state.relations),
[RelationRecord] = mnesia:dirty_read(relation, CurrentRelationHash),
+ create_tuple_internal(Database, RelationName, Tuple, RelationRecord).
- case RelationRecord#relation.mutability of
- immutable ->
- {error, {immutable_relation, RelationName}};
- mutable ->
- create_tuple_internal(Database, RelationName, Tuple, RelationRecord);
- undefined ->
- %% Backwards compatibility: treat undefined as mutable
- create_tuple_internal(Database, RelationName, Tuple, RelationRecord)
- end.
-
-%% Internal function for tuple creation (after mutability check)
+%% @private
+%% Internal function for tuple creation
create_tuple_internal(Database, RelationName, Tuple, _RelationRecord) ->
F = fun() ->
%% Step 1: Store each attribute value and build attribute_map
@@ -212,8 +165,13 @@ create_tuple_internal(Database, RelationName, Tuple, _RelationRecord) ->
constraints = RelationRecord#relation.constraints,
cardinality = NewCardinality,
generator = RelationRecord#relation.generator,
- provenance = RelationRecord#relation.provenance
+ provenance = RelationRecord#relation.provenance,
+ lineage = RelationRecord#relation.lineage,
+ membership_criteria = RelationRecord#relation.membership_criteria
},
+ %% Delete old relation record before writing new one
+ %% This ensures only one relation record exists per name
+ mnesia:delete({relation, CurrentRelationHash}),
mnesia:write(relation, UpdatedRelation, write),
%% Step 7: Update database relations map
@@ -423,6 +381,20 @@ create_relation(Database, Name, Definition) when is_record(Database, database_st
%% Two relations might share definitions but their name gives another interpretation
%% and therefore different tuples
RelationHash = hash({Name, Definition, undefined}),
+
+ %% Returns a generator function (not a PID) that yields tuples one at a time
+ %% TODO: investigate if a generator should or should not be bound to a specific DATABASE VERSION. If so, create the fun passing the DB as an attribute
+ GeneratorFun = fun() ->
+ [CurrentRelation] = mnesia:dirty_index_read(relation, Name, #relation.name),
+ CurrentHash = CurrentRelation#relation.hash,
+ TupleHashes = case CurrentRelation#relation.tree of
+ undefined -> [];
+ _ -> hashes_from_tuple(CurrentHash)
+ end,
+ InnerGen = make_finite_generator(TupleHashes),
+ spawn(fun() -> generator_iterator_loop(InnerGen) end)
+ end,
+
NewRelation = #relation{
hash = RelationHash,
name = Name,
@@ -430,10 +402,10 @@ create_relation(Database, Name, Definition) when is_record(Database, database_st
schema = Definition,
constraints = #{}, % No constraints by default
cardinality = {finite, 0}, % Empty relation
- generator = undefined, % Finite relation has no generator
+ generator = GeneratorFun, % Function generator for finite relations
membership_criteria = #{}, % No membership criteria by default
- mutability = mutable, % Mutable by default
- provenance = {base, Name} % Base relation
+ provenance = build_base_provenance(Definition, Name), % Base relation provenance
+ lineage = {base, Name} % Base relation lineage
},
mnesia:write(relation, NewRelation, write),
@@ -494,42 +466,42 @@ create_database(Name) ->
%% Standard immutable relations (domains and their predicates)
StandardTypeRelations = [
%% Boolean domain - finite, immutable
- #immutable_relation_spec{
+ #domain{
name = boolean,
schema = #{value => boolean},
- generator = {generators, boolean},
+ generator = fun() -> spawn(fun() -> generator_iterator_loop(generators:boolean(#{})) end) end,
membership_criteria = #{value => {in, [true, false]}},
cardinality = {finite, 2}
},
%% Natural numbers - infinite, immutable
- #immutable_relation_spec{
+ #domain{
name = natural,
schema = #{value => natural},
- generator = {generators, natural},
+ generator = fun() -> spawn(fun() -> generator_iterator_loop(generators:natural(#{})) end) end,
membership_criteria = #{value => {'and', {gte, 0}, is_integer}},
cardinality = aleph_zero
},
%% Integers - infinite, immutable
- #immutable_relation_spec{
+ #domain{
name = integer,
schema = #{value => integer},
- generator = {generators, integer},
+ generator = fun() -> spawn(fun() -> generator_iterator_loop(generators:integer(#{})) end) end,
membership_criteria = #{value => is_integer},
cardinality = aleph_zero
},
%% Rationals - infinite, immutable
- #immutable_relation_spec{
+ #domain{
name = rational,
schema = #{numerator => integer, denominator => integer},
- generator = {generators, rational},
+ generator = fun() -> spawn(fun() -> generator_iterator_loop(generators:rational(#{})) end) end,
membership_criteria = #{denominator => {neq, 0}},
cardinality = aleph_zero
}
%% Reals - TODO: implement
- %% #immutable_relation_spec{
+ %% #domain{
%% name = reals,
%% schema = #{value => reals},
- %% generator = {generators, reals},
+ %% generator = fun() -> spawn(fun() -> generator_iterator_loop(generators:reals(#{})) end) end,
%% membership_criteria = #{value => is_float},
%% cardinality = continuum
%% }
@@ -561,7 +533,7 @@ create_database(Name) ->
%% == Example ==
%% <pre>
%% %% Create immutable relation of natural numbers
-%% {DB1, Naturals} = operations:create_immutable_relation(DB, #immutable_relation_spec{
+%% {DB1, Naturals} = operations:create_immutable_relation(DB, #domain{
%% name = naturals,
%% schema = #{value => naturals},
%% generator = {generators, naturals},
@@ -575,19 +547,19 @@ create_database(Name) ->
%% </pre>
%%
%% @param Database `#database_state{}' record
-%% @param Specification `#immutable_relation_spec{}' record
+%% @param Specification `#domain{}' record
%% @returns `{UpdatedDatabase, NewRelation}'
%%
%% @see create_relation/3
%% @see get_tuples_iterator/3
create_immutable_relation(Database, Specification)
when is_record(Database, database_state)
- andalso is_record(Specification, immutable_relation_spec) ->
- Name = Specification#immutable_relation_spec.name,
- Schema = Specification#immutable_relation_spec.schema,
- Generator = Specification#immutable_relation_spec.generator,
- MembershipCriteria = Specification#immutable_relation_spec.membership_criteria,
- Cardinality = Specification#immutable_relation_spec.cardinality,
+ andalso is_record(Specification, domain) ->
+ Name = Specification#domain.name,
+ Schema = Specification#domain.schema,
+ Generator = Specification#domain.generator,
+ MembershipCriteria = Specification#domain.membership_criteria,
+ Cardinality = Specification#domain.cardinality,
F = fun() ->
RelationHash = hash({Name, Schema, undefined}),
@@ -601,8 +573,8 @@ create_immutable_relation(Database, Specification)
cardinality = Cardinality,
generator = Generator,
membership_criteria = MembershipCriteria,
- mutability = immutable, % Always immutable
- provenance = {base, Name}
+ provenance = build_base_provenance(Schema, Name), % Base relation provenance
+ lineage = {base, Name} % Base relation lineage
},
mnesia:write(relation, NewRelation, write),
@@ -627,19 +599,6 @@ create_immutable_relation(Database, Specification)
{atomic, Result} = mnesia:transaction(F),
Result.
-%% @doc Get all relation names in a database.
-%%
-%% Returns a list of atoms representing all relation names currently
-%% defined in the database.
-%%
-%% @param Database `#database_state{}' record
-%% @returns List of atoms (relation names)
-%%
-%% @see get_relation_hash/2
-%% @see create_relation/3
-get_relations(Database) ->
- maps:keys(Database#database_state.relations).
-
%% @doc Get relation hash by name.
%%
%% Looks up a relation's hash in the database's relation map. This hash
@@ -657,6 +616,17 @@ get_relation_hash(Database, RelationName) ->
X -> X
end.
+%% @doc Get list of all relation names in the database.
+%%
+%% Returns a list of all relation names (atoms) currently stored in the database.
+%% This includes both user-defined relations and built-in infinite relations.
+%%
+%% @param Database `#database_state{}' record
+%% @returns List of relation names (atoms)
+-spec get_relations(#database_state{}) -> [atom()].
+get_relations(Database) ->
+ maps:keys(Database#database_state.relations).
+
%% @doc Resolve a tuple hash to actual values.
%%
%% This is an internal function that materializes a tuple by:
@@ -699,75 +669,6 @@ hashes_from_tuple(RelationHash) ->
Tree -> merklet:keys(Tree)
end.
-%% @doc Take operator (τ): Returns N arbitrary elements from a relation.
-%%
-%% The take operator is a unary relational operator that produces a finite
-%% relation containing N arbitrary elements from the input relation. Since
-%% relations are sets (unordered), the specific elements returned are
-%% determined by the enumeration order.
-%%
-%% For finite relations smaller than N, returns all elements.
-%% For infinite relations, returns first N elements by generator order.
-%%
-%% == Example ==
-%% <pre>
-%% %% Take 100 naturals (returns {0, 1, 2, ..., 99})
-%% {DB1, Naturals100} = operations:take(DB, naturals, 100).
-%%
-%% %% Can compose with other operators
-%% Iterator = operations:get_tuples_iterator(DB1, naturals100).
-%% </pre>
-%%
-%% @param Database `#database_state{}' record
-%% @param RelationName Atom naming the source relation
-%% @param N Number of elements to take
-%% @returns `{UpdatedDatabase, TakeRelation}' where TakeRelation is finite
-%%
-%% @see get_tuples_iterator/3
-take(Database, RelationName, N) when is_record(Database, database_state), is_integer(N), N > 0 ->
- case maps:get(RelationName, Database#database_state.relations, error) of
- error ->
- erlang:error({relation_not_found, RelationName});
- RelationHash ->
- [SourceRelation] = mnesia:dirty_read(relation, RelationHash),
-
- %% Compute cardinality of result
- ResultCardinality = case SourceRelation#relation.cardinality of
- {finite, M} when M < N -> {finite, M};
- {finite, _} -> {finite, N};
- _ -> {finite, N} % Taking from infinite yields finite
- end,
-
- %% Create take relation
- TakeName = list_to_atom(atom_to_list(RelationName) ++ "_take_" ++ integer_to_list(N)),
-
- TakeHash = hash({take, RelationName, N}),
-
- TakeRelation = #relation{
- hash = TakeHash,
- name = TakeName,
- tree = undefined, % Generated on-demand
- schema = SourceRelation#relation.schema,
- constraints = SourceRelation#relation.constraints,
- cardinality = ResultCardinality,
- generator = {take, RelationName, N},
- membership_criteria = #{},
- mutability = immutable, % Take relations are read-only
- provenance = {take, SourceRelation#relation.provenance, N}
- },
-
- %% Register the take relation (ephemeral - not persisted to Mnesia disk)
- %% This allows get_tuples_iterator to find it
- UpdatedDatabase = Database#database_state{
- relations = maps:put(TakeName, TakeHash, Database#database_state.relations)
- },
-
- %% Store in Mnesia (in-memory transaction)
- mnesia:dirty_write(relation, TakeRelation),
-
- {UpdatedDatabase, TakeRelation}
- end.
-
%% @doc Create an iterator for streaming tuples from a relation with constraints.
%%
%% Extended version of get_tuples_iterator/2 that accepts boundedness constraints.
@@ -818,83 +719,32 @@ take(Database, RelationName, N) when is_record(Database, database_state), is_int
%%
%% @param Database `#database_state{}' record
%% @param RelationName Atom naming the relation to iterate
-%% @param Options Map of constraints (nested or flat format)
%% @returns Pid of iterator process
%%
%% @see get_tuples_iterator/2
%% @see take/3
-get_tuples_iterator(Database, RelationName, Options) when is_record(Database, database_state) ->
+get_tuples_iterator(Database, RelationName) when is_record(Database, database_state) ->
case maps:get(RelationName, Database#database_state.relations, error) of
error ->
- erlang:error({error_tuple_iterator_init, RelationName});
+ erlang:error({error_tuple_iterator_init, relation_not_found, RelationName});
RelationHash ->
[Relation] = mnesia:dirty_read(relation, RelationHash),
-
- %% Extract constraints from options (support both nested and flat format)
- Constraints = case maps:is_key(constraints, Options) of
- true -> maps:get(constraints, Options);
- false -> Options %% Flat format: entire map is constraints
- end,
-
- %% Metadata tracking is ALWAYS enabled
case Relation#relation.cardinality of
{finite, _} when Relation#relation.tree =/= undefined ->
- %% Finite relation with materialized tuples: use tuple hashes
TupleHashes = hashes_from_tuple(RelationHash),
- spawn(fun() -> tuple_iterator_loop(TupleHashes, RelationName, true) end);
-
+ spawn(fun() -> tuple_iterator_loop(TupleHashes) end);
_ ->
- %% Relation with generator (infinite or take) or empty finite relation
- case Relation#relation.generator of
- undefined ->
- %% Empty finite relation without materialized tuples
- spawn(fun() -> tuple_iterator_loop([], RelationName, true) end);
-
- {take, SourceRelName, N} ->
- %% Take relation: get source iterator and limit to N tuples
- SourceIter = get_tuples_iterator(Database, SourceRelName, Constraints),
- spawn(fun() -> take_iter_loop(SourceIter, N, 0, N) end);
-
- {GeneratorModule, GeneratorFunction} ->
- %% Normal generator (naturals, integers, etc.)
- Generator = erlang:apply(GeneratorModule, GeneratorFunction, [Constraints]),
- spawn(fun() -> generator_iterator_loop(Generator, RelationName, true) end)
- end
+ get_iterator_from_generator(Relation#relation.name, Relation#relation.generator)
end
end.
-%% @doc Create an iterator for streaming tuples from a relation.
-%%
-%% Implements the Volcano Iterator Model: spawns an independent process that
-%% lazily streams tuples from the relation. This enables:
-%% <ul>
-%% <li>Memory-bounded queries (tuples materialized on demand)</li>
-%% <li>Composable query operators (future: select, project, join)</li>
-%% <li>Process-based concurrency</li>
-%% </ul>
-%%
-%% The iterator process responds to `{next, Caller}' messages and sends back
-%% either `{tuple, Tuple}' or `done' when exhausted.
-%%
-%% <b>Note:</b> For infinite relations, use `get_tuples_iterator/3' with constraints.
-%%
-%% == Example ==
-%% <pre>
-%% Iterator = operations:get_tuples_iterator(DB, users),
-%% {ok, Tuple} = operations:next_tuple(Iterator),
-%% operations:close_iterator(Iterator).
-%% </pre>
-%%
-%% @param Database `#database_state{}' record
-%% @param RelationName Atom naming the relation to iterate
-%% @returns Pid of iterator process
-%%
-%% @see get_tuples_iterator/3
-%% @see next_tuple/1
-%% @see close_iterator/1
-%% @see collect_all/1
-get_tuples_iterator(Database, RelationName) when is_record(Database, database_state) ->
- get_tuples_iterator(Database, RelationName, #{}).
+%% @doc Get an iterator out of a generator.
+get_iterator_from_generator(RelationName, GeneratorFun) ->
+ case GeneratorFun of
+ undefined -> spawn(fun() -> tuple_iterator_loop([]) end);
+ Generator when is_function(Generator) -> Generator();
+ _ -> erlang:error({error_tuple_iterator_init, non_function_generator, RelationName})
+ end.
%% @doc Get next tuple from iterator.
%%
@@ -946,24 +796,20 @@ close_iterator(IteratorPid) ->
%% </ul>
%%
%% @param TupleHashes List of binary tuple hashes to stream
-%% @param RelationName Name of the source relation (for provenance)
-%% @param EnableProvenance Boolean flag to enable provenance tracking
-tuple_iterator_loop([], _RelationName, _EnableProvenance) ->
+tuple_iterator_loop([]) ->
receive
{next, Caller} ->
Caller ! done;
stop ->
ok
end;
-tuple_iterator_loop([TupleHash | Rest], RelationName, EnableProvenance) ->
+tuple_iterator_loop([TupleHash | Rest]) ->
receive
{next, Caller} ->
%% Resolve tuple on demand
ResolvedTuple = resolve_tuple(TupleHash),
- %% Add provenance tracking (always enabled)
- FinalTuple = add_base_provenance(ResolvedTuple, RelationName),
- Caller ! {tuple, FinalTuple},
- tuple_iterator_loop(Rest, RelationName, EnableProvenance);
+ Caller ! {tuple, ResolvedTuple},
+ tuple_iterator_loop(Rest);
stop ->
ok
end.
@@ -1003,10 +849,6 @@ collect_all(IteratorPid, Acc) ->
{error, Reason, lists:reverse(Acc)}
end.
-%%% ============================================================================
-%%% Iterator Operators (Lazy - Volcano Model)
-%%% ============================================================================
-
%% @doc Select iterator: Filters tuples based on a predicate.
%%
%% Lazy operator that wraps a child iterator and only emits tuples that
@@ -1032,12 +874,9 @@ select_loop(ChildPid, Predicate) ->
{ok, Tuple} ->
case Predicate(Tuple) of
true ->
- %% Wrap lineage with select operation
- TupleWithLineage = wrap_lineage(Tuple, {select, Predicate}),
- Caller ! {tuple, TupleWithLineage},
+ Caller ! {tuple, Tuple},
select_loop(ChildPid, Predicate);
false ->
- %% Skip this tuple, get next
self() ! {next, Caller},
select_loop(ChildPid, Predicate)
end;
@@ -1076,30 +915,9 @@ project_loop(ChildPid, Attributes) ->
{next, Caller} ->
case next_tuple(ChildPid) of
{ok, Tuple} ->
- % Project data attributes
+ % Project data attributes only - no metadata handling
ProjectedTuple = maps:with(Attributes, Tuple),
-
- % Preserve and update metadata
- case maps:get(meta, Tuple, undefined) of
- undefined ->
- % No metadata, just return projected tuple
- Caller ! {tuple, ProjectedTuple};
- Meta ->
- % Filter provenance to only projected attributes
- OldProv = maps:get(provenance, Meta, #{}),
- NewProv = maps:with(Attributes, OldProv),
-
- % Build new metadata
- UpdatedMeta = case NewProv of
- Empty when map_size(Empty) =:= 0 -> maps:remove(provenance, Meta);
- _ -> maps:put(provenance, NewProv, Meta)
- end,
-
- % Add metadata back and wrap lineage
- TupleWithMeta = maps:put(meta, UpdatedMeta, ProjectedTuple),
- TupleWithLineage = wrap_lineage(TupleWithMeta, {project, Attributes}),
- Caller ! {tuple, TupleWithLineage}
- end,
+ Caller ! {tuple, ProjectedTuple},
project_loop(ChildPid, Attributes);
done ->
close_iterator(ChildPid),
@@ -1135,17 +953,11 @@ project_loop(ChildPid, Attributes) ->
%% @returns Iterator process (Pid)
sort_iterator(ChildIterator, CompareFun) when is_function(CompareFun, 2) ->
spawn(fun() ->
- %% Phase 1: Consume all tuples (blocking!)
+ %% Sync all state blocking the pipeline
AllTuples = collect_all(ChildIterator),
-
- %% Phase 2: Sort in memory
+ %% Sort in memory
SortedTuples = lists:sort(CompareFun, AllTuples),
-
- %% Phase 3: Wrap lineage for all tuples
- TuplesWithLineage = [wrap_lineage(T, {sort, CompareFun}) || T <- SortedTuples],
-
- %% Phase 4: Emit sorted tuples on demand
- sorted_emit_loop(TuplesWithLineage)
+ sorted_emit_loop(SortedTuples)
end).
sorted_emit_loop([]) ->
@@ -1184,9 +996,8 @@ take_iter_loop(ChildPid, Limit, Count, OriginalN) when Count < Limit ->
{next, Caller} ->
case next_tuple(ChildPid) of
{ok, Tuple} ->
- %% Wrap lineage with take operation
- TupleWithLineage = wrap_lineage(Tuple, {take, OriginalN}),
- Caller ! {tuple, TupleWithLineage},
+ %% Pass through tuple without metadata injection
+ Caller ! {tuple, Tuple},
take_iter_loop(ChildPid, Limit, Count + 1, OriginalN);
done ->
close_iterator(ChildPid),
@@ -1305,7 +1116,7 @@ materialize(Database, SourceIterator, ResultName) when is_record(Database, datab
{DB1, _Relation} = create_relation(Database, ResultName, Schema),
%% 4. Insert all tuples
- {FinalDB, FinalRelation} = insert_all_tuples(DB1, ResultName, AllTuples),
+ {FinalDB, FinalRelation} = create_all_tuples(DB1, ResultName, AllTuples),
{FinalDB, FinalRelation}
end.
@@ -1327,7 +1138,7 @@ infer_type(_) -> term.
%% @private
%% Insert multiple tuples into a relation.
-insert_all_tuples(DB, RelationName, Tuples) ->
+create_all_tuples(DB, RelationName, Tuples) ->
lists:foldl(
fun(Tuple, {DBacc, _RelAcc}) ->
create_tuple(DBacc, RelationName, Tuple)
@@ -1340,51 +1151,20 @@ insert_all_tuples(DB, RelationName, Tuples) ->
%%% Generator Support Functions
%%% ============================================================================
-%% Instantiate a generator from a generator specification (commented out).
-%% Converts a generator spec (stored in relation record) into an actual
-%% generator function that can produce tuples.
-%%
-%% @param GeneratorSpec Generator specification
-%% @param Constraints Boundedness constraints
-%% @returns Generator function
-%% instantiate_generator({primitive, naturals}, Constraints) ->
-%% generators:naturals(Constraints);
-%% instantiate_generator({primitive, integers}, Constraints) ->
-%% generators:integers(Constraints);
-%% instantiate_generator({primitive, rationals}, Constraints) ->
-%% generators:rationals(Constraints);
-%% instantiate_generator({primitive, plus}, Constraints) ->
-%% generators:plus(Constraints);
-%% instantiate_generator({primitive, times}, Constraints) ->
-%% generators:times(Constraints);
-%% instantiate_generator({primitive, minus}, Constraints) ->
-%% generators:minus(Constraints);
-%% instantiate_generator({primitive, divide}, Constraints) ->
-%% generators:divide(Constraints);
-%% instantiate_generator({custom, GeneratorFun}, Constraints) ->
-%% GeneratorFun(Constraints);
-%% instantiate_generator({take, RelationName, N}, Constraints) ->
-%% %% Take generator: wrap another generator with limit
-%% %% This is a simplified version - full implementation would look up the base relation
-%% make_take_generator(RelationName, N, Constraints);
-%% instantiate_generator(Other, _) ->
-%% erlang:error({unknown_generator, Other}).
%% @private
%% Generator iterator loop for infinite relations.
%% Similar to tuple_iterator_loop but works with generator functions instead
%% of tuple hashes.
-generator_iterator_loop(Generator, RelationName, EnableProvenance) ->
+generator_iterator_loop(Generator) ->
receive
{next, Caller} ->
case Generator(next) of
done ->
Caller ! done;
- {value, Tuple, NextGen} ->
- %% Add provenance tracking (always enabled)
- FinalTuple = add_base_provenance(Tuple, RelationName),
- Caller ! {tuple, FinalTuple},
- generator_iterator_loop(NextGen, RelationName, EnableProvenance);
+ {value, Tuple, NextGenerator} ->
+ Caller ! {tuple, Tuple},
+ generator_iterator_loop(NextGenerator);
{error, Reason} ->
Caller ! {error, Reason}
end;
@@ -1423,7 +1203,7 @@ equijoin_emit_loop(LeftIter, RightTuples, JoinAttr) ->
equijoin_emit_loop(LeftIter, RightTuples, JoinAttr);
LeftValue ->
% Find matching right tuples
- Matches = [merge_tuples(LeftTuple, RightTuple, {equijoin, JoinAttr})
+ Matches = [merge_tuples(LeftTuple, RightTuple)
|| RightTuple <- RightTuples,
maps:get(JoinAttr, RightTuple, undefined) =:= LeftValue],
case Matches of
@@ -1478,7 +1258,7 @@ theta_join_emit_loop(LeftIter, RightTuples, Pred) ->
Caller ! done;
{ok, LeftTuple} ->
% Find all right tuples that satisfy predicate
- Matches = [merge_tuples(LeftTuple, RightTuple, {theta_join, Pred})
+ Matches = [merge_tuples(LeftTuple, RightTuple)
|| RightTuple <- RightTuples,
Pred(LeftTuple, RightTuple)],
case Matches of
@@ -1512,133 +1292,64 @@ theta_join_emit_buffered(LeftIter, RightTuples, Pred, []) ->
theta_join_emit_loop(LeftIter, RightTuples, Pred).
%% @private
-%% Wrap existing lineage with a new operation.
-%% Takes a tuple with optional lineage and wraps it with a new operation node.
-%% If the tuple has no lineage, does nothing.
-%%
-%% @param Tuple Tuple potentially with meta.lineage
-%% @param OpInfo Operation info: {select, Pred} | {project, Attrs} | {sort, Cmp} | {take, N}
-wrap_lineage(Tuple, OpInfo) ->
- case maps:get(meta, Tuple, undefined) of
- undefined ->
- % No metadata, return as-is
- Tuple;
- Meta ->
- case maps:get(lineage, Meta, undefined) of
+%% @doc Simple tuple merge for joins
+%% Equally named and typed attributes are not repeated.
+%%
+%% @param LeftTuple Left side tuple
+%% @param RightTuple Right side tuple
+merge_tuples(LeftTuple, RightTuple) ->
+ maps:fold(
+ fun(RightKey, RightValue, LeftTupleAcc) ->
+ case maps:get(RightKey, LeftTupleAcc, undefined) of
+ LeftValue when LeftValue =:= RightValue ->
+ % Equal attributes, keep single value
+ LeftTupleAcc;
undefined ->
- % No lineage, return as-is
- Tuple;
- ChildLineage ->
- % Wrap existing lineage with new operation
- NewLineage = case OpInfo of
- {select, Pred} -> {select, Pred, ChildLineage};
- {project, Attrs} -> {project, Attrs, ChildLineage};
- {sort, CmpFun} -> {sort, CmpFun, ChildLineage};
- {take, N} -> {take, N, ChildLineage}
- end,
- maps:put(meta, maps:put(lineage, NewLineage, Meta), Tuple)
+ % No conflict, add right attribute
+ maps:put(RightKey, RightValue, LeftTupleAcc);
+ _ ->
+ % Different attributes
+ maps:put(RightKey, RightValue, LeftTupleAcc)
end
- end.
-
-%% @private
-%% Add base provenance and lineage to a tuple from a relation.
-%% Creates nested meta fields with both provenance and lineage.
-%% Example: add_base_provenance(#{id => 1, name => "Alice"}, employees)
-%% => #{id => 1, name => "Alice",
-%% meta => #{provenance => #{id => {employees, id}, name => {employees, name}},
-%% lineage => {base, employees}}}
-add_base_provenance(Tuple, RelationName) ->
- % Build provenance map for all attributes (excluding existing meta)
- DataAttrs = maps:remove(meta, Tuple),
- Provenance = maps:fold(
- fun(Key, _Value, Acc) ->
- maps:put(Key, {RelationName, Key}, Acc)
end,
- #{},
- DataAttrs
- ),
- % Add metadata with provenance and lineage
- maps:put(meta, #{
- provenance => Provenance,
- lineage => {base, RelationName}
- }, Tuple).
+ LeftTuple,
+ RightTuple
+ ).
%% @private
-%% Merge two tuples for join result, including metadata.
-%% If attribute names conflict, right side attributes are prefixed with "right_".
-%% Metadata (including provenance and lineage) is also merged.
-%%
-%% @param LeftTuple Left side tuple
-%% @param RightTuple Right side tuple
-%% @param JoinInfo Join information: {equijoin, Attr} | {theta_join, Predicate}
-merge_tuples(LeftTuple, RightTuple, JoinInfo) ->
- % Extract metadata from both sides (if present)
- LeftMeta = maps:get(meta, LeftTuple, #{}),
- RightMeta = maps:get(meta, RightTuple, #{}),
-
- % Extract provenance and lineage from metadata
- LeftProv = maps:get(provenance, LeftMeta, #{}),
- RightProv = maps:get(provenance, RightMeta, #{}),
- LeftLineage = maps:get(lineage, LeftMeta, undefined),
- RightLineage = maps:get(lineage, RightMeta, undefined),
-
- % Remove metadata from tuples for data merging
- LeftData = maps:remove(meta, LeftTuple),
- RightData = maps:remove(meta, RightTuple),
-
- % Merge data attributes
- MergedData = maps:fold(
- fun(Key, Value, Acc) ->
- case maps:is_key(Key, Acc) of
- true ->
- % Conflict - prefix right attribute
- RightKey = list_to_atom("right_" ++ atom_to_list(Key)),
- maps:put(RightKey, Value, Acc);
- false ->
- maps:put(Key, Value, Acc)
- end
- end,
- LeftData,
- RightData
- ),
-
- % Merge provenance (same conflict resolution)
- MergedProv = maps:fold(
- fun(Key, Source, Acc) ->
- case maps:is_key(Key, Acc) of
- true ->
- % Conflict - prefix right attribute provenance
- RightKey = list_to_atom("right_" ++ atom_to_list(Key)),
- maps:put(RightKey, Source, Acc);
- false ->
- maps:put(Key, Source, Acc)
- end
+%% Build base provenance mapping for a relation.
+%% Maps each attribute to its source relation and attribute name.
+%%
+%% @param Schema Map of attribute names to types
+%% @param RelationName Name of the source relation
+%% @returns Provenance map #{attr => {relation, attr}}
+build_base_provenance(Schema, RelationName) ->
+ maps:fold(
+ fun(AttrName, _Type, Acc) ->
+ maps:put(AttrName, {RelationName, AttrName}, Acc)
end,
- LeftProv,
- RightProv
- ),
-
- % Build merged lineage if both sides have lineage
- MergedLineage = case {LeftLineage, RightLineage, JoinInfo} of
- {undefined, undefined, _} -> undefined;
- {L, R, {equijoin, Attr}} when L =/= undefined, R =/= undefined ->
- {join, Attr, L, R};
- {L, R, {theta_join, Pred}} when L =/= undefined, R =/= undefined ->
- {theta_join, Pred, L, R};
- _ -> undefined
- end,
+ #{},
+ Schema
+ ).
- % Build merged metadata
- MergedMeta = case {maps:size(MergedProv), MergedLineage} of
- {0, undefined} -> #{};
- {0, Lineage} -> #{lineage => Lineage};
- {_, undefined} -> #{provenance => MergedProv};
- {_, Lineage} -> #{provenance => MergedProv, lineage => Lineage}
- end,
+%% The issue here is that the creation of a generator on the create_relation is wrong.
+%% I assume it was because of the fun(Constraints), which I believe should receive an
+%% optional database version, or just build from the current relation attributes as a factory.
- % Add merged metadata to result (only if non-empty)
- case maps:size(MergedMeta) of
- 0 -> MergedData;
- _ -> maps:put(meta, MergedMeta, MergedData)
+%% @private
+%% @doc Create a generator function from a list of tuple hashes.
+%%
+%% Converts a list of tuple hashes into a generator function compatible
+%% with the generator protocol used by infinite relations. The generator
+%% reads tuples from Mnesia on demand.
+%%
+%% @param TupleHashes List of tuple hashes to iterate over
+%% @returns Generator function that accepts 'next' and returns {value, Tuple, NextGen} or done
+make_finite_generator([]) ->
+ fun(_) -> done end;
+make_finite_generator([Hash | Rest]) ->
+ fun(next) ->
+ ResolvedTuple = resolve_tuple(Hash),
+ NextGen = make_finite_generator(Rest),
+ {value, ResolvedTuple, NextGen}
end.
-
diff --git a/src/query_planner.erl b/src/query_planner.erl
index 9d9d2bc..52bffda 100644
--- a/src/query_planner.erl
+++ b/src/query_planner.erl
@@ -8,43 +8,37 @@
%%%
%%% The DSL uses nested tuples to represent relational operations:
%%% <pre>
-%%% % Scan relation (base case)
-%%% {scan, employees}
-%%%
-%%% % Reference relation (synonym for scan)
-%%% {relation, employees}
+%%% % Base relation (just use the relation name)
+%%% employees
%%%
%%% % Selection (filter)
-%%% {select, {scan, employees}, fun(T) -&gt; maps:get(age, T) &gt; 30 end}
+%%% {select, employees, fun(T) -&gt; maps:get(age, T) &gt; 30 end}
%%%
%%% % Projection
-%%% {project, {scan, employees}, [name, age]}
+%%% {project, employees, [name, age]}
%%%
%%% % Equijoin (natural join on common attribute)
-%%% {join, {scan, employees}, {scan, departments}, dept_id}
+%%% {join, employees, departments, dept_id}
%%%
%%% % Theta join (join with arbitrary predicate)
%%% {theta_join,
-%%% {scan, employees},
-%%% {scan, departments},
+%%% employees,
+%%% departments,
%%% fun(L, R) -&gt; maps:get(dept_id, L) =:= maps:get(id, R) end}
%%%
%%% % Sort
-%%% {sort, {scan, employees}, fun(A, B) -&gt; maps:get(age, A) =&lt; maps:get(age, B) end}
+%%% {sort, employees, fun(A, B) -&gt; maps:get(age, A) =&lt; maps:get(age, B) end}
%%%
%%% % Take (limit)
-%%% {take, {scan, employees}, 10}
+%%% {take, employees, 10}
%%%
%%% % Rename attribute
-%%% {rename, old_name, new_name, {scan, employees}}
+%%% {rename, old_name, new_name, employees}
%%%
%%% % Composite query
%%% {project,
%%% {select,
-%%% {join,
-%%% {scan, employees},
-%%% {scan, departments},
-%%% dept_id},
+%%% {join, employees, departments, dept_id},
%%% fun(T) -&gt; maps:get(age, T) &gt; 30 end},
%%% [name, dept_name]}
%%% </pre>
@@ -52,7 +46,7 @@
%%% == Usage ==
%%% <pre>
%%% % Prepare a query plan
-%%% Plan = query_planner:prepare({select, {scan, employees},
+%%% Plan = query_planner:prepare({select, employees,
%%% fun(T) -&gt; maps:get(age, T) &gt; 30 end}).
%%%
%%% % Execute and get iterator
@@ -77,8 +71,7 @@
%%% Types
-type query_plan() ::
- {scan, atom()} % Scan relation
- | {relation, atom()} % Reference relation
+ atom() % Relation name
| {select, query_plan(), fun((map()) -> boolean())} % Filter
| {project, query_plan(), [atom()]} % Projection
| {join, query_plan(), query_plan(), atom()} % Equijoin
@@ -111,21 +104,22 @@ prepare(Plan) ->
%% @doc Execute a prepared query plan against a database.
%%
-%% Compiles the query plan into an iterator pipeline and returns
-%% the iterator handle. The caller must consume the iterator using
-%% operations:next_tuple/1 and close it with operations:close_iterator/1.
+%% 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 Iterator process PID
--spec execute(term(), query_plan()) -> pid().
+%% @returns Ephemeral relation record
+-spec execute(term(), query_plan()) -> #relation{}.
execute(DB, Plan) ->
- compile_to_iterator(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. Automatically closes the iterator.
+%% 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.
@@ -135,7 +129,8 @@ execute(DB, Plan) ->
%% @returns List of result tuples, or partial results on timeout
-spec collect(term(), query_plan()) -> [map()].
collect(DB, Plan) ->
- Iter = execute(DB, Plan),
+ EphemeralRelation = execute(DB, Plan),
+ Iter = (EphemeralRelation#relation.generator)(),
Results = operations:collect_all(Iter),
operations:close_iterator(Iter),
case Results of
@@ -165,11 +160,188 @@ explain(Plan) ->
%%% 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({scan, Name}) when is_atom(Name) ->
- ok;
-validate_plan({relation, Name}) when is_atom(Name) ->
+validate_plan(Name) when is_atom(Name) ->
ok;
validate_plan({select, SubPlan, Pred}) when is_function(Pred, 1) ->
validate_plan(SubPlan);
@@ -205,10 +377,7 @@ validate_plan(Plan) ->
%% Compile query plan to iterator pipeline
-spec compile_to_iterator(term(), query_plan()) -> pid().
-compile_to_iterator(DB, {scan, Name}) ->
- operations:get_tuples_iterator(DB, Name);
-
-compile_to_iterator(DB, {relation, Name}) ->
+compile_to_iterator(DB, Name) when is_atom(Name) ->
operations:get_tuples_iterator(DB, Name);
compile_to_iterator(DB, {select, SubPlan, Predicate}) ->
@@ -234,22 +403,16 @@ compile_to_iterator(DB, {sort, SubPlan, Comparator}) ->
operations:sort_iterator(SubIter, Comparator);
compile_to_iterator(DB, {take, SubPlan, N}) ->
- %% Use relational take operator when possible (for scans)
- case SubPlan of
- {scan, RelationName} ->
- %% Direct scan: use relational take operator
- {DB2, TakeRelation} = operations:take(DB, RelationName, N),
- %% Get iterator from the take relation (now properly registered)
- operations:get_tuples_iterator(DB2, TakeRelation#relation.name, #{});
- _ ->
- %% Complex plan: fall back to iterator-based take for composition
- SubIter = compile_to_iterator(DB, SubPlan),
- operations:take_iterator(SubIter, N)
- end;
+ %% 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),
- rename_iterator(SubIter, OldAttr, NewAttr);
+ %% 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
@@ -286,18 +449,14 @@ static_loop(Tuples) ->
Caller ! ok
end.
-%% Rename iterator - renames an attribute in each tuple
--spec rename_iterator(pid(), atom(), atom()) -> pid().
-rename_iterator(SourceIter, OldAttr, NewAttr) ->
- spawn(fun() -> rename_loop(SourceIter, OldAttr, NewAttr) end).
-
-rename_loop(SourceIter, OldAttr, NewAttr) ->
+%% @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,
- rename_loop(SourceIter, OldAttr, NewAttr);
+ Caller ! done;
{ok, Tuple} ->
RenamedTuple = case maps:is_key(OldAttr, Tuple) of
true ->
@@ -308,7 +467,7 @@ rename_loop(SourceIter, OldAttr, NewAttr) ->
Tuple
end,
Caller ! {tuple, RenamedTuple},
- rename_loop(SourceIter, OldAttr, NewAttr)
+ rename_loop_inline(SourceIter, OldAttr, NewAttr)
end;
{close, Caller} ->
operations:close_iterator(SourceIter),
@@ -317,10 +476,7 @@ rename_loop(SourceIter, OldAttr, NewAttr) ->
%% Pretty-print query plan
-spec explain_plan(query_plan(), non_neg_integer()) -> string().
-explain_plan({scan, Name}, Indent) ->
- indent(Indent) ++ "Scan: " ++ atom_to_list(Name) ++ "\n";
-
-explain_plan({relation, Name}, Indent) ->
+explain_plan(Name, Indent) when is_atom(Name) ->
indent(Indent) ++ "Relation: " ++ atom_to_list(Name) ++ "\n";
explain_plan({select, SubPlan, _Pred}, Indent) ->
@@ -365,3 +521,81 @@ explain_plan({materialize, SubPlan, N}, Indent) ->
%% 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 = #{},
+ 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.
diff --git a/src/relational_operators.erl b/src/relational_operators.erl
new file mode 100644
index 0000000..82e1a47
--- /dev/null
+++ b/src/relational_operators.erl
@@ -0,0 +1,366 @@
+%%% @doc Pure Relational Operators
+%%%
+%%% This module provides pure relational algebra operators that take relations
+%%% as input and return new ephemeral relations as output. No database state
+%%% is modified. Each operator returns a relation with a generator closure that
+%%% can spawn fresh iterators on demand, allowing reproducible query results.
+%%%
+%%% == Architecture ==
+%%%
+%%% Relational Operators (Pure Functions):
+%%% - Input: One or more #relation{} records
+%%% - Output: New ephemeral #relation{} with generator closure
+%%% - No side effects, no DB modification
+%%% - Generator can be called multiple times for same results
+%%%
+%%% Iterator Operators (Data Transport):
+%%% - Input: Iterator PIDs
+%%% - Output: Iterator PID
+%%% - Used inside generator closures
+%%% - Examples: select_iterator/2, project_iterator/2, take_iterator/2
+%%%
+%%% @author Nekoma Team
+%%% @copyright 2025
+
+-module(relational_operators).
+
+-include("../include/operations.hrl").
+
+-export([
+ take/2,
+ select/2,
+ project/2,
+ join/3,
+ theta_join/3,
+ rename/2,
+ sort/2
+]).
+
+%%% ============================================================================
+%%% Internal Helper Functions
+%%% ============================================================================
+
+%% @private
+%% Generate a random name for ephemeral relations.
+generate_ephemeral_name() ->
+ {UUID, _} = uuid:get_v1(uuid:new(self(), erlang)),
+ [Base64UUID] = io_lib:format("~s", [base64:encode(UUID)]),
+ Base64UUID.
+
+%%% ============================================================================
+%%% Relational Operators
+%%% ============================================================================
+
+%% @doc Take operator (τ): Returns ephemeral relation with first N tuples.
+%%
+%% Creates an ephemeral relation containing N arbitrary elements from the
+%% source relation. The generator closure captures the source relation and N,
+%% spawning fresh iterators on each call.
+%%
+%% == Example ==
+%% <pre>
+%% NaturalRel = get_relation(DB, natural),
+%% Some100 = relational_operators:take(NaturalRel, 100),
+%%
+%% %% Can call generator multiple times
+%% Iter1 = operations:get_iterator_from_generator(ephemeral, Some100#relation.generator),
+%% Results1 = operations:collect_all(Iter1),
+%%
+%% Iter2 = operations:get_iterator_from_generator(ephemeral, Some100#relation.generator),
+%% Results2 = operations:collect_all(Iter2),
+%% %% Results1 =:= Results2
+%% </pre>
+%%
+%% @param SourceRelation Source #relation{} record
+%% @param N Number of tuples to take
+%% @returns Ephemeral #relation{} with generator
+-spec take(#relation{}, pos_integer()) -> #relation{}.
+take(SourceRelation, N) when is_record(SourceRelation, relation), is_integer(N), N > 0 ->
+ %% Compute result cardinality
+ ResultCardinality = case SourceRelation#relation.cardinality of
+ {finite, M} when M < N -> {finite, M};
+ {finite, _} -> {finite, N};
+ _ -> {finite, N}
+ end,
+ Name = generate_ephemeral_name(),
+ SourceGen = SourceRelation#relation.generator,
+ GeneratorFun = fun() ->
+ SourceIter = SourceGen(),
+ operations:take_iterator(SourceIter, N)
+ end,
+ #relation{
+ hash = <<>>,
+ name = Name,
+ tree = undefined,
+ schema = SourceRelation#relation.schema,
+ constraints = SourceRelation#relation.constraints,
+ cardinality = ResultCardinality,
+ generator = GeneratorFun,
+ membership_criteria = #{},
+ provenance = SourceRelation#relation.provenance,
+ lineage = {take, N, SourceRelation#relation.lineage}
+ }.
+
+%% @doc Select operator (σ): Returns ephemeral relation filtered by predicate.
+%%
+%% Creates an ephemeral relation containing only tuples that satisfy the
+%% predicate. The generator closure captures the source relation and predicate.
+%%
+%% @param SourceRelation Source #relation{} record
+%% @param Predicate Function that takes a tuple and returns boolean
+%% @returns Ephemeral #relation{} with generator
+-spec select(#relation{}, fun((map()) -> boolean())) -> #relation{}.
+select(SourceRelation, Predicate) when is_record(SourceRelation, relation), is_function(Predicate, 1) ->
+ Name = generate_ephemeral_name(),
+
+ SourceGen = SourceRelation#relation.generator,
+ GeneratorFun = fun() ->
+ SourceIter = SourceGen(),
+ operations:select_iterator(SourceIter, Predicate)
+ end,
+
+ #relation{
+ hash = <<>>,
+ name = Name,
+ tree = undefined,
+ schema = SourceRelation#relation.schema,
+ constraints = SourceRelation#relation.constraints,
+ cardinality = unknown, % Cannot determine without evaluation
+ generator = GeneratorFun,
+ membership_criteria = #{},
+ provenance = SourceRelation#relation.provenance,
+ lineage = {select, Predicate, SourceRelation#relation.lineage}
+ }.
+
+%% @doc Project operator (π): Returns ephemeral relation with selected attributes.
+%%
+%% Creates an ephemeral relation containing only the specified attributes.
+%% The generator closure captures the source relation and attribute list.
+%%
+%% @param SourceRelation Source #relation{} record
+%% @param Attributes List of attribute names to keep
+%% @returns Ephemeral #relation{} with generator
+-spec project(#relation{}, [atom()]) -> #relation{}.
+project(SourceRelation, Attributes) when is_record(SourceRelation, relation), is_list(Attributes) ->
+ Name = generate_ephemeral_name(),
+
+ %% Compute projected schema
+ ProjectedSchema = maps:with(Attributes, SourceRelation#relation.schema),
+
+ SourceGen = SourceRelation#relation.generator,
+ GeneratorFun = fun() ->
+ SourceIter = SourceGen(),
+ operations:project_iterator(SourceIter, Attributes)
+ end,
+
+ #relation{
+ hash = <<>>,
+ name = Name,
+ tree = undefined,
+ schema = ProjectedSchema,
+ constraints = SourceRelation#relation.constraints,
+ cardinality = SourceRelation#relation.cardinality,
+ generator = GeneratorFun,
+ membership_criteria = #{},
+ provenance = SourceRelation#relation.provenance,
+ lineage = {project, Attributes, SourceRelation#relation.lineage}
+ }.
+
+%% @doc Join operator (⋈): Returns ephemeral relation from equijoin.
+%%
+%% Creates an ephemeral relation by joining two relations on a common attribute.
+%% The generator closure captures both source relations and the join attribute.
+%%
+%% @param LeftRelation Left #relation{} record
+%% @param RightRelation Right #relation{} record
+%% @param JoinAttribute Attribute name to join on
+%% @returns Ephemeral #relation{} with generator
+-spec join(#relation{}, #relation{}, atom()) -> #relation{}.
+join(LeftRelation, RightRelation, JoinAttribute)
+ when is_record(LeftRelation, relation),
+ is_record(RightRelation, relation),
+ is_atom(JoinAttribute) ->
+
+ Name = list_to_atom(
+ "join_" ++ atom_to_list(LeftRelation#relation.name) ++ "_" ++
+ atom_to_list(RightRelation#relation.name) ++ "_" ++
+ integer_to_list(erlang:unique_integer([positive]))
+ ),
+
+ %% Merge schemas
+ MergedSchema = maps:merge(LeftRelation#relation.schema, RightRelation#relation.schema),
+
+ LeftGen = LeftRelation#relation.generator,
+ RightGen = RightRelation#relation.generator,
+ GeneratorFun = fun() ->
+ LeftIter = LeftGen(),
+ RightIter = RightGen(),
+ operations:equijoin_iterator(LeftIter, RightIter, JoinAttribute)
+ end,
+
+ #relation{
+ hash = <<>>,
+ name = Name,
+ tree = undefined,
+ schema = MergedSchema,
+ constraints = #{},
+ cardinality = unknown,
+ generator = GeneratorFun,
+ membership_criteria = #{},
+ provenance = {join, LeftRelation#relation.provenance, RightRelation#relation.provenance},
+ lineage = {join, JoinAttribute, LeftRelation#relation.lineage, RightRelation#relation.lineage}
+ }.
+
+%% @doc Theta Join operator (⋈θ): Returns ephemeral relation from theta join.
+%%
+%% Creates an ephemeral relation by joining two relations with an arbitrary
+%% predicate. The generator closure captures both source relations and predicate.
+%%
+%% @param LeftRelation Left #relation{} record
+%% @param RightRelation Right #relation{} record
+%% @param Predicate Function that takes two tuples and returns boolean
+%% @returns Ephemeral #relation{} with generator
+-spec theta_join(#relation{}, #relation{}, fun((map(), map()) -> boolean())) -> #relation{}.
+theta_join(LeftRelation, RightRelation, Predicate)
+ when is_record(LeftRelation, relation),
+ is_record(RightRelation, relation),
+ is_function(Predicate, 2) ->
+
+ Name = list_to_atom(
+ "theta_join_" ++ atom_to_list(LeftRelation#relation.name) ++ "_" ++
+ atom_to_list(RightRelation#relation.name) ++ "_" ++
+ integer_to_list(erlang:unique_integer([positive]))
+ ),
+
+ MergedSchema = maps:merge(LeftRelation#relation.schema, RightRelation#relation.schema),
+
+ LeftGen = LeftRelation#relation.generator,
+ RightGen = RightRelation#relation.generator,
+ GeneratorFun = fun() ->
+ LeftIter = LeftGen(),
+ RightIter = RightGen(),
+ operations:theta_join_iterator(LeftIter, RightIter, Predicate)
+ end,
+
+ #relation{
+ hash = <<>>,
+ name = Name,
+ tree = undefined,
+ schema = MergedSchema,
+ constraints = #{},
+ cardinality = unknown,
+ generator = GeneratorFun,
+ membership_criteria = #{},
+ provenance = {join, LeftRelation#relation.provenance, RightRelation#relation.provenance},
+ lineage = {theta_join, Predicate, LeftRelation#relation.lineage, RightRelation#relation.lineage}
+ }.
+
+%% @doc Rename operator (ρ): Returns ephemeral relation with renamed attributes.
+%%
+%% Creates an ephemeral relation with attributes renamed according to the mapping.
+%% The generator closure captures the source relation and rename mappings.
+%%
+%% @param SourceRelation Source #relation{} record
+%% @param RenameMappings Map of old_name => new_name (atoms or strings)
+%% @returns Ephemeral #relation{} with generator
+-spec rename(#relation{}, #{atom() => atom()}) -> #relation{}.
+rename(SourceRelation, RenameMappings) when is_record(SourceRelation, relation), is_map(RenameMappings) ->
+ Name = generate_ephemeral_name(),
+
+ %% Compute renamed schema
+ RenamedSchema = maps:fold(
+ fun(OldAttr, NewAttr, SchemaAcc) ->
+ case maps:get(OldAttr, SchemaAcc, undefined) of
+ undefined -> SchemaAcc;
+ Type ->
+ SchemaAcc1 = maps:remove(OldAttr, SchemaAcc),
+ maps:put(NewAttr, Type, SchemaAcc1)
+ end
+ end,
+ SourceRelation#relation.schema,
+ RenameMappings
+ ),
+
+ SourceGen = SourceRelation#relation.generator,
+ GeneratorFun = fun() ->
+ SourceIter = SourceGen(),
+ rename_iterator(SourceIter, RenameMappings)
+ end,
+
+ #relation{
+ hash = <<>>,
+ name = Name,
+ tree = undefined,
+ schema = RenamedSchema,
+ constraints = SourceRelation#relation.constraints,
+ cardinality = SourceRelation#relation.cardinality,
+ generator = GeneratorFun,
+ membership_criteria = #{},
+ provenance = SourceRelation#relation.provenance,
+ lineage = {rename, RenameMappings, SourceRelation#relation.lineage}
+ }.
+
+%% @doc Sort operator: Returns ephemeral relation with sorted tuples.
+%%
+%% Creates an ephemeral relation with tuples sorted by the comparator.
+%% WARNING: This is a blocking operation - it materializes all tuples.
+%%
+%% @param SourceRelation Source #relation{} record
+%% @param Comparator Function that compares two tuples
+%% @returns Ephemeral #relation{} with generator
+-spec sort(#relation{}, fun((map(), map()) -> boolean())) -> #relation{}.
+sort(SourceRelation, Comparator) when is_record(SourceRelation, relation), is_function(Comparator, 2) ->
+ Name = generate_ephemeral_name(),
+
+ SourceGen = SourceRelation#relation.generator,
+ GeneratorFun = fun() ->
+ SourceIter = SourceGen(),
+ operations:sort_iterator(SourceIter, Comparator)
+ end,
+
+ #relation{
+ hash = <<>>,
+ name = Name,
+ tree = undefined,
+ schema = SourceRelation#relation.schema,
+ constraints = SourceRelation#relation.constraints,
+ cardinality = SourceRelation#relation.cardinality,
+ generator = GeneratorFun,
+ membership_criteria = #{},
+ provenance = SourceRelation#relation.provenance,
+ lineage = {sort, Comparator, SourceRelation#relation.lineage}
+ }.
+
+%%% Helper Functions
+
+%% @private
+%% @doc Rename iterator - applies rename mapping to each tuple
+rename_iterator(SourceIter, RenameMappings) ->
+ spawn(fun() -> rename_loop(SourceIter, RenameMappings) end).
+
+rename_loop(SourceIter, RenameMappings) ->
+ receive
+ {next, Caller} ->
+ case operations:next_tuple(SourceIter) of
+ done ->
+ Caller ! done;
+ {ok, Tuple} ->
+ RenamedTuple = maps:fold(
+ fun(OldAttr, NewAttr, TupleAcc) ->
+ case maps:get(OldAttr, TupleAcc, undefined) of
+ undefined -> TupleAcc;
+ Value ->
+ TupleAcc1 = maps:remove(OldAttr, TupleAcc),
+ maps:put(NewAttr, Value, TupleAcc1)
+ end
+ end,
+ Tuple,
+ RenameMappings
+ ),
+ Caller ! {tuple, RenamedTuple},
+ rename_loop(SourceIter, RenameMappings)
+ end;
+ {close, Caller} ->
+ operations:close_iterator(SourceIter),
+ Caller ! ok
+ end.
diff --git a/src/repl.erl b/src/repl.erl
index c61ebfe..599d721 100644
--- a/src/repl.erl
+++ b/src/repl.erl
@@ -21,30 +21,27 @@
%%%
%%% == Query Syntax Examples ==
%%% <pre>
-%%% % Scan relation
-%%% {scan, employees}
+%%% % Base relation (just use the name)
+%%% employees
%%%
%%% % Filter
-%%% {select, {scan, employees}, fun(T) -&gt; maps:get(age, T) &gt; 30 end}
+%%% {select, employees, fun(T) -&gt; maps:get(age, T) &gt; 30 end}
%%%
%%% % Project
-%%% {project, {scan, employees}, [name, age]}
+%%% {project, employees, [name, age]}
%%%
%%% % Join
-%%% {join, {scan, employees}, {scan, departments}, dept_id}
+%%% {join, employees, departments, dept_id}
%%%
%%% % Rename
-%%% {rename, age, years, {scan, employees}}
+%%% {rename, age, years, employees}
%%%
%%% % Complex pipeline
%%% {take,
%%% {sort,
%%% {project,
%%% {select,
-%%% {join,
-%%% {scan, employees},
-%%% {scan, departments},
-%%% dept_id},
+%%% {join, employees, departments, dept_id},
%%% fun(T) -&gt; maps:get(age, T) &gt; 30 end},
%%% [name, dept_name, age]},
%%% fun(A, B) -&gt; maps:get(age, A) =&lt; maps:get(age, B) end},
@@ -79,7 +76,7 @@
%% @returns ok
-spec start() -> ok.
start() ->
- operations:setup(),
+ main:setup(),
io:format("~n~s~n", [banner()]),
io:format("Domino REPL started. Type repl:help() for usage.~n~n"),
ok.
@@ -250,10 +247,10 @@ help() ->
io:format("Quick Start:~n"),
io:format(" repl:start(). %% Initialize system~n"),
io:format(" DB = repl:example_db(). %% Create example database~n"),
- io:format(" repl:q(DB, {scan, employees}). %% Run a query~n~n"),
+ io:format(" repl:q(DB, employees). %% Run a query~n~n"),
io:format("Query Operators (all lazy by default):~n"),
- io:format(" {scan, Name} - Scan relation~n"),
+ io:format(" RelationName - Base relation~n"),
io:format(" {select, Plan, Predicate} - Filter tuples~n"),
io:format(" {project, Plan, [Attrs]} - Project attributes~n"),
io:format(" {join, Left, Right, Attr} - Equijoin~n"),
@@ -281,39 +278,41 @@ help() ->
io:format(" repl:modes() - List all available modes~n~n"),
io:format("Examples:~n"),
+ io:format(" %% Query all employees~n"),
+ io:format(" repl:q(DB, employees).~n~n"),
+
io:format(" %% Filter employees over 30~n"),
- io:format(" repl:q(DB, {select, {scan, employees}, ~n"),
+ io:format(" repl:q(DB, {select, employees, ~n"),
io:format(" fun(T) -> maps:get(age, T) > 30 end}).~n~n"),
io:format(" %% Project name and age~n"),
- io:format(" repl:q(DB, {project, {scan, employees}, [name, age]}).~n~n"),
+ io:format(" repl:q(DB, {project, employees, [name, age]}).~n~n"),
io:format(" %% Join employees with departments~n"),
- io:format(" repl:q(DB, {join, {scan, employees}, ~n"),
- io:format(" {scan, departments}, dept_id}).~n~n"),
+ io:format(" repl:q(DB, {join, employees, departments, dept_id}).~n~n"),
io:format(" %% Rename age to years~n"),
- io:format(" repl:q(DB, {rename, age, years, {scan, employees}}).~n~n"),
+ io:format(" repl:q(DB, {rename, age, years, employees}).~n~n"),
io:format(" %% Materialize first 10 tuples (eager evaluation)~n"),
- io:format(" repl:q(DB, {materialize, {scan, employees}, 10}).~n~n"),
+ io:format(" repl:q(DB, {materialize, employees, 10}).~n~n"),
io:format(" %% Nested materialize - select from materialized data~n"),
- io:format(" repl:q(DB, {select, {materialize, {scan, employees}},~n"),
+ io:format(" repl:q(DB, {select, {materialize, employees},~n"),
io:format(" fun(T) -> maps:get(age, T) > 30 end}).~n~n"),
io:format(" %% Use different visualization modes~n"),
- io:format(" repl:q(DB, {scan, employees}, tree).~n"),
- io:format(" repl:q(DB, {scan, employees}, nested_table).~n"),
- io:format(" repl:q(DB, {scan, employees}, outline).~n~n"),
+ io:format(" repl:q(DB, employees, tree).~n"),
+ io:format(" repl:q(DB, employees, nested_table).~n"),
+ io:format(" repl:q(DB, employees, outline).~n~n"),
io:format(" %% Query without printing, then show results~n"),
- io:format(" Results = repl:query(DB, {scan, employees}).~n"),
+ io:format(" Results = repl:query(DB, employees).~n"),
io:format(" Filtered = lists:filter(fun(T) -> maps:get(age, T) > 30 end, Results).~n"),
io:format(" repl:show(Filtered, tree).~n~n"),
io:format(" %% Explain a query plan~n"),
- io:format(" repl:explain({select, {scan, employees}, ~n"),
+ io:format(" repl:explain({select, employees, ~n"),
io:format(" fun(T) -> maps:get(age, T) > 30 end}).~n~n"),
ok.
diff --git a/src/xml_server.erl b/src/xml_server.erl
index 4eef4c9..eaa08f9 100644
--- a/src/xml_server.erl
+++ b/src/xml_server.erl
@@ -33,7 +33,7 @@
%%% xml_server:start(8080).
%%%
%%% % Client connects and sends:
-%%% % QUERY {scan, employees}\n
+%%% % QUERY employees\n
%%% % NEXT &lt;session-id&gt; 10\n
%%% % NEXT &lt;session-id&gt; 10\n
%%% % CLOSE &lt;session-id&gt;\n
@@ -209,9 +209,12 @@ handle_query(QueryStr, DB, Sessions) ->
{ok, Exprs} = erl_parse:parse_exprs(Tokens),
{value, QueryPlan, _} = erl_eval:exprs(Exprs, erl_eval:new_bindings()),
- % Prepare and execute query to get iterator
+ % Prepare and execute query to get ephemeral relation
PreparedPlan = query_planner:prepare(QueryPlan),
- Iterator = query_planner:execute(DB, PreparedPlan),
+ EphemeralRelation = query_planner:execute(DB, PreparedPlan),
+
+ % Create actual iterator from ephemeral relation's generator
+ Iterator = (EphemeralRelation#relation.generator)(),
% Generate session ID
SessionId = generate_session_id(),
diff --git a/src/xml_server_app.erl b/src/xml_server_app.erl
index d2e1c18..b7b9faa 100644
--- a/src/xml_server_app.erl
+++ b/src/xml_server_app.erl
@@ -12,7 +12,7 @@ start() ->
%% @doc Start XML server on specified port.
start(Port) ->
% Initialize Domino
- operations:setup(),
+ main:setup(),
io:format("~nDomino initialized~n"),
% Create example database