aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--include/operations.hrl39
-rw-r--r--src/generators.erl73
-rw-r--r--src/operations.erl351
-rw-r--r--src/poc.erl167
-rw-r--r--src/query_planner.erl34
-rw-r--r--src/repl.erl46
-rw-r--r--src/visualizer.erl15
-rw-r--r--src/xml_server.erl47
-rw-r--r--test/function_relations_test.erl66
-rw-r--r--test/infinite_relations_test.erl73
-rw-r--r--test/join_test.erl12
-rw-r--r--test/operations_test.erl27
-rw-r--r--test/query_pipeline_test.erl13
13 files changed, 433 insertions, 530 deletions
diff --git a/include/operations.hrl b/include/operations.hrl
index 93b4a74..b027bb0 100644
--- a/include/operations.hrl
+++ b/include/operations.hrl
@@ -3,25 +3,41 @@
-record(database_state, {hash, name, tree, relations, timestamp}).
-record(relation, {
- hash, % Content hash (finite) or generator hash (infinite)
- name, % Relation name (atom)
- tree, % Merkle tree (finite only), undefined for infinite
- schema, % #{attribute_name => type}
- constraints, % #{attribute_name => constraint_spec()} - integrity constraints
- cardinality, % {finite, N} | aleph_zero | continuum
- generator, % Generator function (infinite only), undefined for finite
- provenance % Provenance tracking for updatable views (future)
+ hash, % Content hash (finite) or generator hash (infinite)
+ name, % Relation name (atom)
+ tree, % Merkle tree (mutable only), undefined for immutable/generator-based
+ schema, % #{attribute_name => relation_name}
+ constraints, % #{attribute_name => constraint_spec()} - integrity constraints
+ cardinality, % {finite, N} | aleph_zero | continuum
+ generator, % Generator function (immutable only), undefined for tree-based
+ membership_criteria, % #{attribute_name => constraint_spec()} - domain membership test
+ mutability, % mutable | immutable
+ provenance % Provenance tracking for updatable views (future)
}).
-record(tuple, {hash, relation, attribute_map}).
-record(attribute, {hash, value}).
+%% Specification record for creating immutable relations
+-record(immutable_relation_spec, {
+ name, % Atom - relation name
+ schema, % #{attribute_name => relation_name}
+ generator, % {module, function} - tuple generator
+ membership_criteria, % #{attribute_name => constraint_spec()} - domain membership
+ cardinality % {finite, N} | aleph_zero | continuum
+}).
+
%%% Cardinality Types
--type cardinality() :: {finite, non_neg_integer()} % Finite set with N elements
+-type cardinality() :: {finite, non_neg_integer()} % Finite set with N elements
| aleph_zero % Countably infinite (ℵ₀)
| continuum. % Uncountably infinite (2^ℵ₀)
+%%% Mutability Types
+
+-type mutability() :: mutable % Supports insert/delete (tree-based)
+ | immutable. % Read-only (generator-based or protected)
+
%%% Constraint Types
-type constraint_spec() ::
@@ -33,7 +49,10 @@
| {gte, number()} % Greater than or equal: attr ≥ value
| {in, [term()]} % Membership: attr ∈ list
| {range, number(), number()} % Range: attr ∈ [min, max]
- | {member_of, atom()}. % Type constraint: attr ∈ Relation
+ | {member_of, atom()} % Type constraint: attr ∈ Relation
+ | {'and', [term()]}
+ | is_integer
+ | is_float.
-type constraints() :: #{atom() => constraint_spec()}.
diff --git a/src/generators.erl b/src/generators.erl
index 9e682d3..37facc3 100644
--- a/src/generators.erl
+++ b/src/generators.erl
@@ -25,18 +25,18 @@
%%% </ul>
%%%
%%% Function relations reference domain relations in their schemas:
-%%% ```
-%%% schema => #{a => integers, b => integers, sum => integers}
-%%% '''
+%%% <pre>
+%%% schema =&gt; #{a =&gt; integers, b =&gt; integers, sum =&gt; integers}
+%%% </pre>
%%%
%%% == Reversibility ==
%%%
%%% Function relations support bidirectional querying (like Prolog):
%%% <ul>
-%%% <li>`Plus[1, 2, ?]' → ? = 3 (forward)</li>
-%%% <li>`Plus[1, ?, 3]' → ? = 2 (backward, solve for b)</li>
-%%% <li>`Plus[?, 2, 3]' → ? = 1 (backward, solve for a)</li>
-%%% <li>`Plus[?, ?, 5]' → all pairs (a,b) where a+b=5</li>
+%%% <li>Plus[1, 2, ?] yields ? = 3 (forward)</li>
+%%% <li>Plus[1, ?, 3] yields ? = 2 (backward, solve for b)</li>
+%%% <li>Plus[?, 2, 3] yields ? = 1 (backward, solve for a)</li>
+%%% <li>Plus[?, ?, 5] yields all pairs (a,b) where a+b=5</li>
%%% </ul>
%%%
%%% The generators use constraint analysis to determine which arguments
@@ -45,9 +45,9 @@
%%% == Relational Composition ==
%%%
%%% In the future, function relations will be used through joins:
-%%% ```
+%%% <pre>
%%% Integer[A], Integer[B], Plus[A, B, Sum]
-%%% '''
+%%% </pre>
%%%
%%% This corresponds to a three-way join where the join conditions
%%% provide the constraints to each relation.
@@ -62,12 +62,13 @@
-include("operations.hrl").
-export([
- naturals/1,
- integers/1,
- rationals/1,
+ boolean/1,
+ natural/1,
+ integer/1,
+ rational/1,
make_range_generator/3,
- constrained_naturals/2,
- constrained_integers/2,
+ constrained_natural/2,
+ constrained_integer/2,
%% Function relations
plus/1,
times/1,
@@ -79,6 +80,28 @@
%%% Primitive Generators
%%% ============================================================================
+%% @doc Generator for boolean domain (𝔹 = {true, false}).
+%%
+%% Generates the two boolean values. This is a finite immutable relation.
+%% If constraints specify equality, only generates that value.
+%%
+%% @param Constraints Map of attribute constraints
+%% @returns Generator function producing booleans
+boolean(Constraints) ->
+ case maps:get(value, Constraints, unbounded) of
+ {eq, true} ->
+ make_singleton_generator(#{value => true});
+ {eq, false} ->
+ make_singleton_generator(#{value => false});
+ {in, Values} ->
+ %% Filter to only boolean values
+ BoolValues = [V || V <- Values, V =:= true orelse V =:= false],
+ make_membership_generator(BoolValues, fun(V) -> #{value => V} end);
+ _ ->
+ %% Enumerate all booleans: true, false
+ make_membership_generator([true, false], fun(V) -> #{value => V} end)
+ end.
+
%% @doc Generator for natural numbers (ℕ = {0, 1, 2, 3, ...}).
%%
%% Enumerates natural numbers starting from 0. If constraints specify a range,
@@ -86,7 +109,7 @@
%%
%% @param Constraints Map of attribute constraints
%% @returns Generator function producing naturals
-naturals(Constraints) ->
+natural(Constraints) ->
case extract_bounds(Constraints, value) of
{range, Min, Max} when Min >= 0 ->
make_range_generator(Min, Max, fun(N) -> #{value => N} end);
@@ -130,7 +153,7 @@ naturals(Constraints) ->
%%
%% @param Constraints Map of attribute constraints
%% @returns Generator function producing integers
-integers(Constraints) ->
+integer(Constraints) ->
case extract_bounds(Constraints, value) of
{range, Min, Max} ->
make_integer_range_generator(Min, Max);
@@ -158,7 +181,7 @@ integers(Constraints) ->
%%
%% @param Constraints Map of attribute constraints
%% @returns Generator function producing rationals
-rationals(Constraints) ->
+rational(Constraints) ->
%% Stern-Brocot tree enumeration
%% Start with mediant tree: 0/1, 1/1, then generate mediants
InitialState = {queue:from_list([{0, 1}, {1, 1}]), {0, 1}, {1, 0}},
@@ -320,15 +343,15 @@ make_sb_gen({Queue, {LA, LB}, {RA, RB}}) ->
%%% Constrained Generators (Public API)
%%% ============================================================================
-%% @doc Create a constrained naturals generator.
--spec constrained_naturals(non_neg_integer(), non_neg_integer()) -> generator_fun().
-constrained_naturals(Min, Max) ->
- naturals(#{value => {range, Min, Max}}).
+%% @doc Create a constrained natural generator.
+-spec constrained_natural(non_neg_integer(), non_neg_integer()) -> generator_fun().
+constrained_natural(Min, Max) ->
+ natural(#{value => {range, Min, Max}}).
-%% @doc Create a constrained integers generator.
--spec constrained_integers(integer(), integer()) -> generator_fun().
-constrained_integers(Min, Max) ->
- integers(#{value => {range, Min, Max}}).
+%% @doc Create a constrained integer generator.
+-spec constrained_integer(integer(), integer()) -> generator_fun().
+constrained_integer(Min, Max) ->
+ integer(#{value => {range, Min, Max}}).
%%% ============================================================================
%%% Function Relations
diff --git a/src/operations.erl b/src/operations.erl
index 36fb7e2..4a492f4 100644
--- a/src/operations.erl
+++ b/src/operations.erl
@@ -43,7 +43,7 @@
hash/1,
create_database/1,
create_relation/3,
- create_infinite_relation/2,
+ create_immutable_relation/2,
create_tuple/3,
retract_tuple/3,
retract_tuple/4,
@@ -101,7 +101,7 @@ setup() ->
{type, set}
]),
mnesia:create_table(relation, [
- {attributes, [hash, name, tree, schema, constraints, cardinality, generator, provenance]},
+ {attributes, [hash, name, tree, schema, constraints, cardinality, generator, membership_criteria, mutability, provenance]},
{disc_copies, [node()]},
{type, set}
]),
@@ -140,11 +140,11 @@ hash(Value) ->
%% All operations execute within a Mnesia transaction for ACID guarantees.
%%
%% == Example ==
-%% ```
+%% <pre>
%% DB = operations:create_database(my_db),
%% {DB1, _} = operations:create_relation(DB, users, #{name => string, age => integer}),
%% {DB2, _} = operations:create_tuple(DB1, users, #{name => "Alice", age => 30}).
-%% '''
+%% </pre>
%%
%% @param Database `#database_state{}' record
%% @param RelationName Atom naming the relation
@@ -154,6 +154,22 @@ hash(Value) ->
%% @see create_relation/3
%% @see get_tuples_iterator/2
create_tuple(Database, RelationName, Tuple) when is_map(Tuple), is_record(Database, database_state) ->
+ %% Check if relation is immutable before attempting insert
+ CurrentRelationHash = maps:get(RelationName, Database#database_state.relations),
+ [RelationRecord] = mnesia:dirty_read(relation, CurrentRelationHash),
+
+ 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)
+create_tuple_internal(Database, RelationName, Tuple, _RelationRecord) ->
F = fun() ->
%% Step 1: Store each attribute value and build attribute_map
AttributeMap = maps:map(fun(_AttrName, Value) ->
@@ -385,11 +401,11 @@ retract_relation(Database, Name, Transact) ->
%% contain different tuples.
%%
%% == Example ==
-%% ```
+%% <pre>
%% DB = operations:create_database(my_db),
%% Schema = #{name => string, age => integer, email => string},
%% {DB1, _Rel} = operations:create_relation(DB, users, Schema).
-%% '''
+%% </pre>
%%
%% @param Database `#database_state{}' record
%% @param Name Atom naming the new relation
@@ -412,10 +428,12 @@ create_relation(Database, Name, Definition) when is_record(Database, database_st
name = Name,
tree = undefined,
schema = Definition,
- constraints = #{}, % No constraints by default
- cardinality = {finite, 0}, % Empty relation
- generator = undefined, % Finite relation has no generator
- provenance = {base, Name} % Base relation
+ constraints = #{}, % No constraints by default
+ cardinality = {finite, 0}, % Empty relation
+ generator = undefined, % Finite relation has no generator
+ membership_criteria = #{}, % No membership criteria by default
+ mutability = mutable, % Mutable by default
+ provenance = {base, Name} % Base relation
},
mnesia:write(relation, NewRelation, write),
@@ -456,9 +474,9 @@ create_relation(Database, Name, Definition) when is_record(Database, database_st
%% All changes are committed to Mnesia within a transaction.
%%
%% == Example ==
-%% ```
+%% <pre>
%% DB = operations:create_database(my_app).
-%% '''
+%% </pre>
%%
%% @param Name Atom identifying the database
%% @returns `#database_state{}' record
@@ -466,17 +484,65 @@ create_relation(Database, Name, Definition) when is_record(Database, database_st
%% @see setup/0
%% @see create_relation/3
create_database(Name) ->
- Entry = #database_state{
+ DB = #database_state{
name = Name,
hash = <<>>,
tree = undefined,
relations = #{},
timestamp = erlang:timestamp()
},
+ %% Standard immutable relations (domains and their predicates)
+ StandardTypeRelations = [
+ %% Boolean domain - finite, immutable
+ #immutable_relation_spec{
+ name = boolean,
+ schema = #{value => boolean},
+ generator = {generators, boolean},
+ membership_criteria = #{value => {in, [true, false]}},
+ cardinality = {finite, 2}
+ },
+ %% Natural numbers - infinite, immutable
+ #immutable_relation_spec{
+ name = natural,
+ schema = #{value => natural},
+ generator = {generators, natural},
+ membership_criteria = #{value => {'and', {gte, 0}, is_integer}},
+ cardinality = aleph_zero
+ },
+ %% Integers - infinite, immutable
+ #immutable_relation_spec{
+ name = integer,
+ schema = #{value => integer},
+ generator = {generators, integer},
+ membership_criteria = #{value => is_integer},
+ cardinality = aleph_zero
+ },
+ %% Rationals - infinite, immutable
+ #immutable_relation_spec{
+ name = rational,
+ schema = #{numerator => integer, denominator => integer},
+ generator = {generators, rational},
+ membership_criteria = #{denominator => {neq, 0}},
+ cardinality = aleph_zero
+ }
+ %% Reals - TODO: implement
+ %% #immutable_relation_spec{
+ %% name = reals,
+ %% schema = #{value => reals},
+ %% generator = {generators, reals},
+ %% membership_criteria = #{value => is_float},
+ %% cardinality = continuum
+ %% }
+ ],
+ Folder = fun (Elem, AccDB) ->
+ {NextDB, _} = operations:create_immutable_relation(AccDB, Elem),
+ NextDB
+ end,
+ StandardDB = lists:foldl(Folder, DB, StandardTypeRelations),
{atomic, ok} = mnesia:transaction(fun () ->
- mnesia:write(database_state, Entry, write)
- end),
- Entry.
+ mnesia:write(database_state, StandardDB, write)
+ end),
+ StandardDB.
%% @doc Create an infinite relation with a generator.
%%
@@ -493,46 +559,49 @@ create_database(Name) ->
%% </ul>
%%
%% == Example ==
-%% ```
-%% %% Create infinite relation of natural numbers
-%% {DB1, Naturals} = operations:create_infinite_relation(DB, #{
-%% name => naturals,
-%% schema => #{value => natural},
-%% cardinality => aleph_zero,
-%% generator => {primitive, naturals},
-%% constraints => #{value => {gte, 0}}
+%% <pre>
+%% %% Create immutable relation of natural numbers
+%% {DB1, Naturals} = operations:create_immutable_relation(DB, #immutable_relation_spec{
+%% name = naturals,
+%% schema = #{value => naturals},
+%% generator = {generators, naturals},
+%% membership_criteria = #{value => {'and', {gte, 0}, is_integer}},
+%% cardinality = aleph_zero
%% }).
%%
%% %% Query with bounds
%% Iterator = operations:get_tuples_iterator(DB1, naturals,
%% #{value => {range, 0, 100}}).
-%% '''
+%% </pre>
%%
%% @param Database `#database_state{}' record
-%% @param Spec Map with keys: name, schema, cardinality, generator, constraints
-%% @returns `{UpdatedDatabase, NewRelation}' tuple
+%% @param Specification `#immutable_relation_spec{}' record
+%% @returns `{UpdatedDatabase, NewRelation}'
%%
%% @see create_relation/3
%% @see get_tuples_iterator/3
-create_infinite_relation(Database, Spec) when is_record(Database, database_state) ->
- Name = maps:get(name, Spec),
- Schema = maps:get(schema, Spec),
- Cardinality = maps:get(cardinality, Spec, aleph_zero),
- GeneratorSpec = maps:get(generator, Spec),
- Constraints = maps:get(constraints, Spec, #{}),
+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,
F = fun() ->
- %% Hash is based on generator specification
- RelationHash = hash({Name, Schema, GeneratorSpec, Cardinality}),
+ RelationHash = hash({Name, Schema, undefined}),
NewRelation = #relation{
hash = RelationHash,
name = Name,
- tree = undefined, % Infinite relations have no merkle tree
+ tree = undefined, % Immutable relations have no merkle tree
schema = Schema,
- constraints = Constraints,
+ constraints = #{}, % No constraints by default
cardinality = Cardinality,
- generator = GeneratorSpec,
+ generator = Generator,
+ membership_criteria = MembershipCriteria,
+ mutability = immutable, % Always immutable
provenance = {base, Name}
},
mnesia:write(relation, NewRelation, write),
@@ -641,13 +710,13 @@ hashes_from_tuple(RelationHash) ->
%% 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
@@ -672,20 +741,31 @@ take(Database, RelationName, N) when is_record(Database, database_state), is_int
%% Create take relation
TakeName = list_to_atom(atom_to_list(RelationName) ++ "_take_" ++ integer_to_list(N)),
+ TakeHash = hash({take, RelationName, N}),
+
TakeRelation = #relation{
- hash = hash({take, RelationName, N}),
+ 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}
},
- %% Note: We don't persist take relations to Mnesia for now
- %% They are ephemeral views
- {Database, TakeRelation}
+ %% 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.
@@ -702,14 +782,14 @@ take(Database, RelationName, N) when is_record(Database, database_state), is_int
%% == Options Format ==
%%
%% Nested format (preferred):
-%% ```
+%% <pre>
%% #{constraints => #{age => {gt, 30}}}
-%% '''
+%% </pre>
%%
%% Flat format (backward compatible):
-%% ```
+%% <pre>
%% #{age => {gt, 30}}
-%% '''
+%% </pre>
%%
%% == Constraints ==
%%
@@ -723,7 +803,7 @@ take(Database, RelationName, N) when is_record(Database, database_state), is_int
%% </ul>
%%
%% == Example ==
-%% ```
+%% <pre>
%% %% Iterate employees with age > 30 (metadata always included)
%% Iterator = operations:get_tuples_iterator(DB, employees,
%% #{constraints => #{age => {gt, 30}}}).
@@ -734,7 +814,7 @@ take(Database, RelationName, N) when is_record(Database, database_state), is_int
%% %% provenance => #{id => {employees, id}, ...},
%% %% lineage => {select, Fun, {base, employees}}
%% %% }}
-%% '''
+%% </pre>
%%
%% @param Database `#database_state{}' record
%% @param RelationName Atom naming the relation to iterate
@@ -758,15 +838,28 @@ get_tuples_iterator(Database, RelationName, Options) when is_record(Database, da
%% Metadata tracking is ALWAYS enabled
case Relation#relation.cardinality of
- {finite, _} ->
- %% Finite relation: use tuple hashes
+ {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);
_ ->
- %% Infinite relation: use generator
- Generator = instantiate_generator(Relation#relation.generator, Constraints),
- spawn(fun() -> generator_iterator_loop(Generator, RelationName, true) 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
end
end.
@@ -786,11 +879,11 @@ get_tuples_iterator(Database, RelationName, Options) when is_record(Database, da
%% <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
@@ -842,8 +935,7 @@ close_iterator(IteratorPid) ->
ok.
%% @private
-%% @doc Iterator loop implementation (internal).
-%%
+%% Iterator loop implementation (internal).
%% This is the process loop for the Volcano Iterator Model. It maintains a list
%% of tuple hashes and resolves them lazily when requested.
%%
@@ -886,11 +978,11 @@ tuple_iterator_loop([TupleHash | Rest], RelationName, EnableProvenance) ->
%% of lazy evaluation. Only use for small relations or testing.
%%
%% == Example ==
-%% ```
+%% <pre>
%% Iterator = operations:get_tuples_iterator(DB, users),
%% AllTuples = operations:collect_all(Iterator).
%% %% Iterator is automatically closed
-%% '''
+%% </pre>
%%
%% @param IteratorPid Pid returned from `get_tuples_iterator/2'
%% @returns List of tuple maps, `{error, Reason, PartialResults}' on timeout
@@ -921,11 +1013,11 @@ collect_all(IteratorPid, Acc) ->
%% satisfy the predicate function.
%%
%% == Example ==
-%% ```
+%% <pre>
%% BaseIter = get_tuples_iterator(DB, employees, #{}),
-%% FilteredIter = select_iterator(BaseIter, fun(E) -> maps:get(age, E) > 30 end),
+%% FilteredIter = select_iterator(BaseIter, fun(E) -&gt; maps:get(age, E) &gt; 30 end),
%% Tuples = collect_all(FilteredIter).
-%% '''
+%% </pre>
%%
%% @param ChildIterator Iterator process to filter
%% @param Predicate Function: tuple() -> boolean()
@@ -967,11 +1059,11 @@ select_loop(ChildPid, Predicate) ->
%% attributes. This is the relational projection operator (π).
%%
%% == Example ==
-%% ```
+%% <pre>
%% BaseIter = get_tuples_iterator(DB, employees, #{}),
%% ProjectedIter = project_iterator(BaseIter, [name, age]),
%% Tuples = collect_all(ProjectedIter).
-%% '''
+%% </pre>
%%
%% @param ChildIterator Iterator process to project
%% @param Attributes List of attribute names to keep
@@ -1030,13 +1122,13 @@ project_loop(ChildPid, Attributes) ->
%% produces finite tuples (via constraints or take).
%%
%% == Example ==
-%% ```
+%% <pre>
%% BaseIter = get_tuples_iterator(DB, employees, #{}),
-%% SortedIter = sort_iterator(BaseIter, fun(A, B) ->
-%% maps:get(age, A) =< maps:get(age, B)
+%% SortedIter = sort_iterator(BaseIter, fun(A, B) -&gt;
+%% maps:get(age, A) =&lt; maps:get(age, B)
%% end),
%% Tuples = collect_all(SortedIter).
-%% '''
+%% </pre>
%%
%% @param ChildIterator Iterator process to sort
%% @param CompareFun Function: (tuple(), tuple()) -> boolean()
@@ -1075,11 +1167,11 @@ sorted_emit_loop([Tuple | Rest]) ->
%% then closes the child and signals done.
%%
%% == Example ==
-%% ```
+%% <pre>
%% BaseIter = get_tuples_iterator(DB, naturals, #{value => {range, 0, 1000}}),
%% LimitedIter = take_iterator(BaseIter, 10),
%% Tuples = collect_all(LimitedIter). % Gets exactly 10 tuples
-%% '''
+%% </pre>
%%
%% @param ChildIterator Iterator process to limit
%% @param N Maximum number of tuples to emit
@@ -1132,12 +1224,12 @@ take_iter_loop(ChildPid, _Limit, _Count, _OriginalN) ->
%% Future: Hash join for better performance.
%%
%% == Example ==
-%% ```
+%% <pre>
%% %% Join employees with departments on dept_id
%% LeftIter = operations:get_tuples_iterator(DB, employees, #{}),
%% RightIter = operations:get_tuples_iterator(DB, departments, #{}),
%% JoinIter = operations:equijoin_iterator(LeftIter, RightIter, dept_id)
-%% '''
+%% </pre>
%%
%% @param LeftIterator Iterator for left relation
%% @param RightIterator Iterator for right relation
@@ -1153,13 +1245,13 @@ equijoin_iterator(LeftIterator, RightIterator, JoinAttribute) ->
%% is evaluated. If true, the tuples are merged and emitted.
%%
%% == Example ==
-%% ```
-%% %% Join where employee.age > department.min_age
-%% Predicate = fun(EmpTuple, DeptTuple) ->
-%% maps:get(age, EmpTuple) > maps:get(min_age, DeptTuple)
+%% <pre>
+%% %% Join where employee.age &gt; department.min_age
+%% Predicate = fun(EmpTuple, DeptTuple) -&gt;
+%% maps:get(age, EmpTuple) &gt; maps:get(min_age, DeptTuple)
%% end,
%% JoinIter = operations:theta_join_iterator(LeftIter, RightIter, Predicate)
-%% '''
+%% </pre>
%%
%% @param LeftIterator Iterator for left relation
%% @param RightIterator Iterator for right relation
@@ -1179,17 +1271,17 @@ theta_join_iterator(LeftIterator, RightIterator, Predicate) ->
%% iterator and stores them as a new finite relation in the database.
%%
%% == Example ==
-%% ```
+%% <pre>
%% %% Build lazy pipeline
%% Pipeline = get_tuples_iterator(DB, employees, #{})
-%% |> select_iterator(fun(E) -> maps:get(age, E) > 30 end)
-%% |> sort_iterator(fun(A, B) -> maps:get(age, A) =< maps:get(age, B) end)
-%% |> take_iterator(10)
-%% |> project_iterator([name, age]),
+%% |&gt; select_iterator(fun(E) -&gt; maps:get(age, E) &gt; 30 end)
+%% |&gt; sort_iterator(fun(A, B) -&gt; maps:get(age, A) =&lt; maps:get(age, B) end)
+%% |&gt; take_iterator(10)
+%% |&gt; project_iterator([name, age]),
%%
%% %% Materialize into new relation
%% {DB1, SeniorEmployees} = materialize(DB, Pipeline, senior_employees).
-%% '''
+%% </pre>
%%
%% @param Database Current database state
%% @param SourceIterator Iterator process (pipeline endpoint)
@@ -1219,12 +1311,12 @@ materialize(Database, SourceIterator, ResultName) when is_record(Database, datab
end.
%% @private
-%% @doc Infer schema from a tuple.
+%% Infer schema from a tuple.
infer_schema_from_tuple(Tuple) when is_map(Tuple) ->
maps:map(fun(_AttrName, Value) -> infer_type(Value) end, Tuple).
%% @private
-%% @doc Infer type from value.
+%% Infer type from value.
infer_type(Value) when is_integer(Value) -> integer;
infer_type(Value) when is_float(Value) -> float;
infer_type(Value) when is_binary(Value) -> binary;
@@ -1234,7 +1326,7 @@ infer_type(Value) when is_boolean(Value) -> boolean;
infer_type(_) -> term.
%% @private
-%% @doc Insert multiple tuples into a relation.
+%% Insert multiple tuples into a relation.
insert_all_tuples(DB, RelationName, Tuples) ->
lists:foldl(
fun(Tuple, {DBacc, _RelAcc}) ->
@@ -1248,45 +1340,40 @@ insert_all_tuples(DB, RelationName, Tuples) ->
%%% Generator Support Functions
%%% ============================================================================
-%% @private
-%% @doc Instantiate a generator from a generator specification.
-%%
+%% 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}).
+%% 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
-%% @doc Generator iterator loop for infinite relations.
-%%
+%% Generator iterator loop for infinite relations.
%% Similar to tuple_iterator_loop but works with generator functions instead
%% of tuple hashes.
-%%
-%% @param Generator Generator function
generator_iterator_loop(Generator, RelationName, EnableProvenance) ->
receive
{next, Caller} ->
@@ -1306,8 +1393,7 @@ generator_iterator_loop(Generator, RelationName, EnableProvenance) ->
end.
%% @private
-%% @doc Equijoin loop: nested loop join on attribute equality.
-%%
+%% Equijoin loop: nested loop join on attribute equality.
%% Strategy:
%% 1. Collect all tuples from right iterator into memory
%% 2. For each left tuple, scan right tuples for matches
@@ -1375,8 +1461,7 @@ equijoin_emit_buffered(LeftIter, RightTuples, JoinAttr, []) ->
equijoin_emit_loop(LeftIter, RightTuples, JoinAttr).
%% @private
-%% @doc Theta-join loop: nested loop join with predicate.
-%%
+%% Theta-join loop: nested loop join with predicate.
%% @param LeftIter Left iterator process
%% @param RightIter Right iterator process
%% @param Pred Predicate function fun(LeftTuple, RightTuple) -> boolean()
@@ -1427,8 +1512,7 @@ theta_join_emit_buffered(LeftIter, RightTuples, Pred, []) ->
theta_join_emit_loop(LeftIter, RightTuples, Pred).
%% @private
-%% @doc Wrap existing lineage with a new operation.
-%%
+%% 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.
%%
@@ -1457,8 +1541,7 @@ wrap_lineage(Tuple, OpInfo) ->
end.
%% @private
-%% @doc Add base provenance and lineage to a tuple from a relation.
-%%
+%% 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",
@@ -1481,8 +1564,7 @@ add_base_provenance(Tuple, RelationName) ->
}, Tuple).
%% @private
-%% @doc Merge two tuples for join result, including metadata.
-%%
+%% 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.
%%
@@ -1560,18 +1642,3 @@ merge_tuples(LeftTuple, RightTuple, JoinInfo) ->
_ -> maps:put(meta, MergedMeta, MergedData)
end.
-%% @private
-%% @doc Create a take generator that limits another generator.
-make_take_generator(_RelationName, N, _Constraints) ->
- %% Simplified version: just count to N
- make_take_gen(0, N).
-
-make_take_gen(Current, Max) when Current < Max ->
- fun(next) ->
- Tuple = #{value => Current},
- NextGen = make_take_gen(Current + 1, Max),
- {value, Tuple, NextGen}
- end;
-make_take_gen(_, _) ->
- fun(_) -> done end.
-
diff --git a/src/poc.erl b/src/poc.erl
deleted file mode 100644
index cee0965..0000000
--- a/src/poc.erl
+++ /dev/null
@@ -1,167 +0,0 @@
--module(poc).
--export([test/0, setup/0]).
-
-test() ->
- io:format("~nVolcano Iterator~n~n"),
- %% Setup in-memory "database" (simulating Mnesia)
- io:format("Setting up content-addressed storage...~n"),
- Storage = setup(),
- %% Insert data with automatic deduplication
- io:format("Inserting products (note 'Soap' is stored only once)...~n"),
- {Storage2, SoapHash} = store_value(Storage, "Soap"),
- {Storage3, Price1Hash} = store_value(Storage2, 1.0),
- {Storage4, BazookaHash} = store_value(Storage3, "Bazooka"),
- {Storage5, Price2Hash} = store_value(Storage4, 413.0),
- {Storage6, _SoapHash2} = store_value(Storage5, "Soap"), % Duplicate on purpose
- {Storage7, Price3Hash} = store_value(Storage6, 50.0),
- io:format("Unique values stored: ~p~n", [maps:size(maps:get(values, Storage7))]),
- io:format("(Expected: 5, not 6 becase 'Soap' got deduplicated)~n~n"),
-
- Tuple1 = #{name => SoapHash, price => Price1Hash},
- Tuple2 = #{name => BazookaHash, price => Price2Hash},
- Tuple3 = #{name => SoapHash, price => Price3Hash},
- Tuples = [Tuple1, Tuple2, Tuple3],
-
- io:format("Created 3 product tuples (referencing hashes)~n"),
- io:format("Tuple1: #{name => ~s, price => ~s}~n",
- [short_hash(SoapHash), short_hash(Price1Hash)]),
- io:format("Tuple2: #{name => ~s, price => ~s}~n",
- [short_hash(BazookaHash), short_hash(Price2Hash)]),
- io:format("Tuple3: #{name => ~s, price => ~s}~n~n",
- [short_hash(SoapHash), short_hash(Price3Hash)]),
-
- io:format("Building query pipeline (Volcano Model):~n"),
- io:format("Scan -> SELECT(price =< 100) -> Project(name)~n~n"),
-
- ScanPid = spawn(fun() -> scan_iterator(Tuples, Storage7) end),
-
- SelectPid = spawn(fun() ->
- select_iterator(ScanPid, Storage7, fun(#{price := P}) -> P =< 100.0 end)
- end),
-
- ProjectPid = spawn(fun() ->
- project_iterator(SelectPid, [name])
- end),
-
- io:format("Pulling results (on-demand, lazy evaluation):~n"),
- Results = pull_all(ProjectPid),
-
- io:format("~n"),
- lists:foreach(fun(#{name := Name}) ->
- io:format("-> #{name => ~p}~n", [Name])
- end, Results),
- ok.
-
-setup() ->
- #{values => #{}}. % Hash => Value map
-
-store_value(Storage, Value) ->
- Hash = value_hash(Value),
- Values = maps:get(values, Storage),
- case maps:is_key(Hash, Values) of
- true ->
- io:format("[DEDUP] Value ~p already exists (hash: ~s)~n",
- [Value, short_hash(Hash)]),
- {Storage, Hash};
- false ->
- io:format("[STORE] Value ~p (hash: ~s)~n",
- [Value, short_hash(Hash)]),
- NewValues = maps:put(Hash, Value, Values),
- {Storage#{values => NewValues}, Hash}
- end.
-
-get_value(Storage, Hash) ->
- Values = maps:get(values, Storage),
- maps:get(Hash, Values).
-
-value_hash(Value) ->
- %% Simple hash for demo (in production: crypto:hash(sha256, term_to_binary(Value)))
- erlang:phash2(Value).
-
-short_hash(Hash) ->
- lists:flatten(io_lib:format("~8.16.0B", [Hash])).
-
-scan_iterator([], _Storage) ->
- receive
- {next, Caller} -> Caller ! done
- end;
-scan_iterator([TupleWithHashes | Rest], Storage) ->
- receive
- {next, Caller} ->
- %% Unpacks all the hashes from each tuple
- %% later we gotta use this on the merkle tree
- %% for now it's just silly and inneficient
- ResolvedTuple = maps:map(fun(_Attr, Hash) ->
- get_value(Storage, Hash)
- end, TupleWithHashes),
- io:format("[SCAN] Emitting tuple: ~p~n", [ResolvedTuple]),
- Caller ! {tuple, ResolvedTuple},
- scan_iterator(Rest, Storage)
- end.
-
-select_iterator(SourcePid, Storage, Predicate) ->
- receive
- {next, Caller} ->
- SourcePid ! {next, self()},
- receive
- done ->
- Caller ! done;
- {tuple, Tuple} ->
- case Predicate(Tuple) of
- true ->
- io:format("[SELECT] Pass: ~p~n", [Tuple]),
- Caller ! {tuple, Tuple},
- select_iterator(SourcePid, Storage, Predicate);
- false ->
- io:format("[SELECT] Reject: ~p~n", [Tuple]),
- %% Ignore this dude, and lock the process again
- select_iterator_loop(SourcePid, Storage, Predicate, Caller)
- end
- end
- end.
-
-select_iterator_loop(SourcePid, Storage, Predicate, Caller) ->
- SourcePid ! {next, self()},
- receive
- done ->
- Caller ! done;
- {tuple, Tuple} ->
- case Predicate(Tuple) of
- true ->
- io:format("[SELECT] Pass: ~p~n", [Tuple]),
- Caller ! {tuple, Tuple},
- select_iterator(SourcePid, Storage, Predicate);
- false ->
- io:format("[SELECT] Reject: ~p~n", [Tuple]),
- select_iterator_loop(SourcePid, Storage, Predicate, Caller)
- end
- end.
-
-project_iterator(SourcePid, Attributes) ->
- receive
- {next, Caller} ->
- SourcePid ! {next, self()},
- receive
- done ->
- Caller ! done;
- {tuple, Tuple} ->
- ProjectedTuple = maps:with(Attributes, Tuple),
- io:format("[PROJECT] ~p -> ~p~n", [Tuple, ProjectedTuple]),
- Caller ! {tuple, ProjectedTuple},
- project_iterator(SourcePid, Attributes)
- end
- end.
-
-pull_all(IteratorPid) ->
- pull_all(IteratorPid, []).
-
-pull_all(IteratorPid, Acc) ->
- IteratorPid ! {next, self()},
- receive
- done ->
- lists:reverse(Acc);
- {tuple, Tuple} ->
- pull_all(IteratorPid, [Tuple | Acc])
- after 1000 ->
- {error, timeout}
- end.
diff --git a/src/query_planner.erl b/src/query_planner.erl
index e6f2324..9d9d2bc 100644
--- a/src/query_planner.erl
+++ b/src/query_planner.erl
@@ -7,8 +7,7 @@
%%% == Query DSL Syntax ==
%%%
%%% The DSL uses nested tuples to represent relational operations:
-%%%
-%%% ```
+%%% <pre>
%%% % Scan relation (base case)
%%% {scan, employees}
%%%
@@ -16,7 +15,7 @@
%%% {relation, employees}
%%%
%%% % Selection (filter)
-%%% {select, {scan, employees}, fun(T) -> maps:get(age, T) > 30 end}
+%%% {select, {scan, employees}, fun(T) -&gt; maps:get(age, T) &gt; 30 end}
%%%
%%% % Projection
%%% {project, {scan, employees}, [name, age]}
@@ -28,10 +27,10 @@
%%% {theta_join,
%%% {scan, employees},
%%% {scan, departments},
-%%% fun(L, R) -> maps:get(dept_id, L) =:= maps:get(id, R) end}
+%%% fun(L, R) -&gt; maps:get(dept_id, L) =:= maps:get(id, R) end}
%%%
%%% % Sort
-%%% {sort, {scan, employees}, fun(A, B) -> maps:get(age, A) =< maps:get(age, B) end}
+%%% {sort, {scan, employees}, fun(A, B) -&gt; maps:get(age, A) =&lt; maps:get(age, B) end}
%%%
%%% % Take (limit)
%%% {take, {scan, employees}, 10}
@@ -46,23 +45,22 @@
%%% {scan, employees},
%%% {scan, departments},
%%% dept_id},
-%%% fun(T) -> maps:get(age, T) > 30 end},
+%%% 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, {scan, employees},
-%%% fun(T) -> maps:get(age, T) > 30 end}).
+%%% 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
@@ -236,8 +234,18 @@ compile_to_iterator(DB, {sort, SubPlan, Comparator}) ->
operations:sort_iterator(SubIter, Comparator);
compile_to_iterator(DB, {take, SubPlan, N}) ->
- SubIter = compile_to_iterator(DB, SubPlan),
- operations:take_iterator(SubIter, 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;
compile_to_iterator(DB, {rename, OldAttr, NewAttr, SubPlan}) ->
SubIter = compile_to_iterator(DB, SubPlan),
diff --git a/src/repl.erl b/src/repl.erl
index dc1e2c6..c61ebfe 100644
--- a/src/repl.erl
+++ b/src/repl.erl
@@ -5,8 +5,7 @@
%%% results and working with prepared statements.
%%%
%%% == Quick Start ==
-%%%
-%%% ```
+%%% <pre>
%%% % Start the system
%%% repl:start().
%%%
@@ -14,20 +13,19 @@
%%% DB = repl:example_db().
%%%
%%% % Execute a query
-%%% repl:q(DB, {select, {relation, employees}, fun(T) -> maps:get(age, T) > 30 end}).
+%%% repl:q(DB, {select, {relation, employees}, fun(T) -&gt; maps:get(age, T) &gt; 30 end}).
%%%
%%% % Explain a query plan
%%% repl:explain({project, {relation, employees}, [name, age]}).
-%%% ```
+%%% </pre>
%%%
%%% == Query Syntax Examples ==
-%%%
-%%% ```
+%%% <pre>
%%% % Scan relation
%%% {scan, employees}
%%%
%%% % Filter
-%%% {select, {scan, employees}, fun(T) -> maps:get(age, T) > 30 end}
+%%% {select, {scan, employees}, fun(T) -&gt; maps:get(age, T) &gt; 30 end}
%%%
%%% % Project
%%% {project, {scan, employees}, [name, age]}
@@ -47,11 +45,11 @@
%%% {scan, employees},
%%% {scan, departments},
%%% dept_id},
-%%% fun(T) -> maps:get(age, T) > 30 end},
+%%% fun(T) -&gt; maps:get(age, T) &gt; 30 end},
%%% [name, dept_name, age]},
-%%% fun(A, B) -> maps:get(age, A) =< maps:get(age, B) end},
+%%% fun(A, B) -&gt; maps:get(age, A) =&lt; maps:get(age, B) end},
%%% 5}
-%%% ```
+%%% </pre>
%%%
%%% @author Nekoma Team
%%% @copyright 2025
@@ -121,19 +119,15 @@ example_db() ->
{DB8, _} = operations:create_tuple(DB7, departments, #{dept_id => 10, dept_name => "Engineering", budget => 100000}),
{DB9, _} = operations:create_tuple(DB8, departments, #{dept_id => 20, dept_name => "Sales", budget => 80000}),
{DB10, _} = operations:create_tuple(DB9, departments, #{dept_id => 30, dept_name => "Marketing", budget => 90000}),
- {DB11, _} = operations:create_infinite_relation(DB10, #{
- name => naturals,
- schema => #{value => integer},
- cardinality => aleph_zero,
- generator => {primitive, naturals},
- constraints => #{value => {gte, 0}}
- }),
+
+ %% Note: natural, integer, rational, and boolean are now built-in to every database
io:format("Example database created with:~n"),
io:format(" - employees (5 tuples)~n"),
- io:format(" - departments (3 tuples)~n~n"),
+ io:format(" - departments (3 tuples)~n"),
+ io:format(" - Built-in immutable relations: boolean, natural, integer, rational~n~n"),
- DB11.
+ DB10.
%% @doc Execute a query and pretty-print results (default table mode).
%%
@@ -178,11 +172,11 @@ q(DB, Plan, Mode) ->
%% @returns List of result tuples
%%
%% == Example ==
-%% ```
+%% <pre>
%% Results = repl:query(DB, {scan, employees}).
-%% FilteredResults = lists:filter(fun(T) -> maps:get(age, T) > 30 end, Results).
+%% FilteredResults = lists:filter(fun(T) -&gt; maps:get(age, T) &gt; 30 end, Results).
%% repl:show(FilteredResults, tree).
-%% ```
+%% </pre>
-spec query(term(), query_planner:query_plan()) -> [map()].
query(DB, Plan) ->
PreparedPlan = query_planner:prepare(Plan),
@@ -190,7 +184,7 @@ query(DB, Plan) ->
%% @doc Display results using default visualization mode (table).
%%
-%% Takes a list of tuples (e.g., from `query/2`) and displays them
+%% Takes a list of tuples (e.g., from query/2) and displays them
%% using the table visualization mode.
%%
%% @param Results List of result tuples
@@ -201,7 +195,7 @@ show(Results) ->
%% @doc Display results using specified visualization mode.
%%
-%% Takes a list of tuples (e.g., from `query/2`) and displays them
+%% Takes a list of tuples (e.g., from query/2) and displays them
%% using the specified visualization mode.
%%
%% @param Results List of result tuples
@@ -209,10 +203,10 @@ show(Results) ->
%% @returns ok
%%
%% == Example ==
-%% ```
+%% <pre>
%% Results = repl:query(DB, {scan, employees}),
%% repl:show(Results, tree).
-%% ```
+%% </pre>
-spec show([map()], visualizer:render_mode()) -> ok.
show(Results, Mode) ->
visualizer:render(Results, Mode).
diff --git a/src/visualizer.erl b/src/visualizer.erl
index 83080f1..75f85d1 100644
--- a/src/visualizer.erl
+++ b/src/visualizer.erl
@@ -5,19 +5,18 @@
%%%
%%% == Modes ==
%%%
-%%% - `table` - Classic flat table (default)
-%%% - `nested_table` - Tables within table cells
-%%% - `tree` - Hierarchical tree view with indentation
-%%% - `linked_tables` - Separate linked tables for nested relations
-%%% - `outline` - Indented outline format
+%%% - table - Classic flat table (default)
+%%% - nested_table - Tables within table cells
+%%% - tree - Hierarchical tree view with indentation
+%%% - linked_tables - Separate linked tables for nested relations
+%%% - outline - Indented outline format
%%%
%%% == Usage ==
-%%%
-%%% ```
+%%% <pre>
%%% Results = query_planner:collect(DB, Plan),
%%% visualizer:render(Results, tree).
%%% visualizer:render(Results, nested_table).
-%%% ```
+%%% </pre>
%%%
%%% @author Nekoma Team
%%% @copyright 2025
diff --git a/src/xml_server.erl b/src/xml_server.erl
index eee4771..4eef4c9 100644
--- a/src/xml_server.erl
+++ b/src/xml_server.erl
@@ -7,40 +7,37 @@
%%% == Protocol ==
%%%
%%% Client sends commands as line-delimited strings:
-%%%
-%%% ```
-%%% QUERY <erlang-term-string> - Execute a query plan, returns session ID
-%%% NEXT <session-id> <count> - Get next N tuples from iterator
-%%% CLOSE <session-id> - Close iterator and free resources
-%%% ```
+%%% <pre>
+%%% QUERY &lt;erlang-term-string&gt; - Execute a query plan, returns session ID
+%%% NEXT &lt;session-id&gt; &lt;count&gt; - Get next N tuples from iterator
+%%% CLOSE &lt;session-id&gt; - Close iterator and free resources
+%%% </pre>
%%%
%%% Server responds with XML:
-%%%
-%%% ```
-%%% <?xml version="1.0"?>
-%%% <response>
-%%% <session>abc123</session>
-%%% <tuples>
-%%% <tuple>
-%%% <attribute name="id">1</attribute>
-%%% <attribute name="name">Alice</attribute>
-%%% </tuple>
-%%% </tuples>
-%%% </response>
-%%% ```
+%%% <pre>
+%%% &lt;?xml version="1.0"?&gt;
+%%% &lt;response&gt;
+%%% &lt;session&gt;abc123&lt;/session&gt;
+%%% &lt;tuples&gt;
+%%% &lt;tuple&gt;
+%%% &lt;attribute name="id"&gt;1&lt;/attribute&gt;
+%%% &lt;attribute name="name"&gt;Alice&lt;/attribute&gt;
+%%% &lt;/tuple&gt;
+%%% &lt;/tuples&gt;
+%%% &lt;/response&gt;
+%%% </pre>
%%%
%%% == Usage ==
-%%%
-%%% ```
+%%% <pre>
%%% % Start server on port 8080
%%% xml_server:start(8080).
%%%
%%% % Client connects and sends:
%%% % QUERY {scan, employees}\n
-%%% % NEXT <session-id> 10\n
-%%% % NEXT <session-id> 10\n
-%%% % CLOSE <session-id>\n
-%%% ```
+%%% % NEXT &lt;session-id&gt; 10\n
+%%% % NEXT &lt;session-id&gt; 10\n
+%%% % CLOSE &lt;session-id&gt;\n
+%%% </pre>
%%%
%%% @author Nekoma Team
%%% @copyright 2025
diff --git a/test/function_relations_test.erl b/test/function_relations_test.erl
index a9164bf..a6ab2d2 100644
--- a/test/function_relations_test.erl
+++ b/test/function_relations_test.erl
@@ -38,51 +38,43 @@ setup() ->
operations:setup(),
DB = operations:create_database(test_db),
- % First set up the basic number types (integers, naturals, etc)
- {DB1, _Integers} = operations:create_infinite_relation(DB, #{
- name => integers,
- schema => #{}, % empty schema - this is a base type
- cardinality => aleph_zero,
- generator => {primitive, integers}
+ % Integers and naturals are now built-in to every database
+ % Just create the function relations using the new format
+
+ % Create function relations - note how their schemas reference the domain relations
+ {DB1, _Plus} = operations:create_immutable_relation(DB, #immutable_relation_spec{
+ name = plus,
+ schema = #{a => integer, b => integer, sum => integer},
+ generator = {generators, plus},
+ membership_criteria = #{},
+ cardinality = aleph_zero
}),
- {DB2, _Naturals} = operations:create_infinite_relation(DB1, #{
- name => naturals,
- schema => #{},
- cardinality => aleph_zero,
- generator => {primitive, naturals}
+ {DB2, _Times} = operations:create_immutable_relation(DB1, #immutable_relation_spec{
+ name = times,
+ schema = #{a => integer, b => integer, product => integer},
+ generator = {generators, times},
+ membership_criteria = #{},
+ cardinality = aleph_zero
}),
- % Now create the function relations - note how their schemas reference the domain relations above
- {DB3, _Plus} = operations:create_infinite_relation(DB2, #{
- name => plus,
- schema => #{a => integers, b => integers, sum => integers},
- cardinality => aleph_zero,
- generator => {primitive, plus}
+ {DB3, _Minus} = operations:create_immutable_relation(DB2, #immutable_relation_spec{
+ name = minus,
+ schema = #{a => integer, b => integer, difference => integer},
+ generator = {generators, minus},
+ membership_criteria = #{},
+ cardinality = aleph_zero
}),
- {DB4, _Times} = operations:create_infinite_relation(DB3, #{
- name => times,
- schema => #{a => integers, b => integers, product => integers},
- cardinality => aleph_zero,
- generator => {primitive, times}
+ {DB4, _Divide} = operations:create_immutable_relation(DB3, #immutable_relation_spec{
+ name = divide,
+ schema = #{a => integer, b => integer, quotient => integer, remainder => natural},
+ generator = {generators, divide},
+ membership_criteria = #{},
+ cardinality = aleph_zero
}),
- {DB5, _Minus} = operations:create_infinite_relation(DB4, #{
- name => minus,
- schema => #{a => integers, b => integers, difference => integers},
- cardinality => aleph_zero,
- generator => {primitive, minus}
- }),
-
- {DB6, _Divide} = operations:create_infinite_relation(DB5, #{
- name => divide,
- schema => #{a => integers, b => integers, quotient => integers, remainder => naturals},
- cardinality => aleph_zero,
- generator => {primitive, divide}
- }),
-
- DB6.
+ DB4.
cleanup(_DB) ->
ok.
diff --git a/test/infinite_relations_test.erl b/test/infinite_relations_test.erl
index 39fe29a..5f87acb 100644
--- a/test/infinite_relations_test.erl
+++ b/test/infinite_relations_test.erl
@@ -37,35 +37,22 @@ strip_meta(Tuples) when is_list(Tuples) ->
%%% Tests
test_create_naturals(DB) ->
- %% Create infinite relation of natural numbers
- %% Domain relations have empty schemas (base case)
- {DB1, Naturals} = operations:create_infinite_relation(DB, #{
- name => naturals,
- schema => #{},
- cardinality => aleph_zero,
- generator => {primitive, naturals},
- constraints => #{value => {gte, 0}}
- }),
+ %% Naturals are now built-in to every database
+ %% Just retrieve the relation to verify it exists
+ {ok, NaturalsHash} = operations:get_relation_hash(DB, natural),
+ [NaturalsRel] = mnesia:dirty_read(relation, NaturalsHash),
[
- ?_assert(is_record(Naturals, relation)),
- ?_assertEqual(naturals, Naturals#relation.name),
- ?_assertEqual(aleph_zero, Naturals#relation.cardinality),
- ?_assertEqual({primitive, naturals}, Naturals#relation.generator)
+ ?_assert(is_record(NaturalsRel, relation)),
+ ?_assertEqual(natural, NaturalsRel#relation.name),
+ ?_assertEqual(aleph_zero, NaturalsRel#relation.cardinality),
+ ?_assertEqual({generators, natural}, NaturalsRel#relation.generator)
].
test_naturals_with_constraints(DB) ->
- %% Create naturals relation
- {DB1, _Naturals} = operations:create_infinite_relation(DB, #{
- name => naturals,
- schema => #{},
- cardinality => aleph_zero,
- generator => {primitive, naturals},
- constraints => #{value => {gte, 0}}
- }),
-
+ %% Naturals are built-in, just query them
%% Query with range constraint [0, 9]
- Iterator = operations:get_tuples_iterator(DB1, naturals, #{value => {range, 0, 9}}),
+ Iterator = operations:get_tuples_iterator(DB, natural, #{value => {range, 0, 9}}),
Tuples = operations:collect_all(Iterator),
DataOnly = strip_meta(Tuples),
@@ -78,16 +65,12 @@ test_naturals_with_constraints(DB) ->
].
test_integers_generator(DB) ->
- %% Create integers relation
- {DB1, Integers} = operations:create_infinite_relation(DB, #{
- name => integers,
- schema => #{},
- cardinality => aleph_zero,
- generator => {primitive, integers}
- }),
+ %% Integers are built-in
+ {ok, IntegersHash} = operations:get_relation_hash(DB, integer),
+ [Integers] = mnesia:dirty_read(relation, IntegersHash),
%% Query with range constraint [-5, 5]
- Iterator = operations:get_tuples_iterator(DB1, integers, #{value => {range, -5, 5}}),
+ Iterator = operations:get_tuples_iterator(DB, integer, #{value => {range, -5, 5}}),
Tuples = operations:collect_all(Iterator),
DataOnly = strip_meta(Tuples),
@@ -101,16 +84,12 @@ test_integers_generator(DB) ->
].
test_rationals_generator(DB) ->
- %% Create rationals relation
- {DB1, Rationals} = operations:create_infinite_relation(DB, #{
- name => rationals,
- schema => #{},
- cardinality => aleph_zero,
- generator => {primitive, rationals}
- }),
+ %% Rationals are built-in
+ {ok, RationalsHash} = operations:get_relation_hash(DB, rational),
+ [Rationals] = mnesia:dirty_read(relation, RationalsHash),
%% Take first 10 rationals using constraints
- Iterator = operations:get_tuples_iterator(DB1, rationals, #{}),
+ Iterator = operations:get_tuples_iterator(DB, rational, #{}),
%% Just get a few tuples to verify generator works
{ok, First} = operations:next_tuple(Iterator),
@@ -126,22 +105,14 @@ test_rationals_generator(DB) ->
].
test_take_operator(DB) ->
- %% Create naturals
- {DB1, _} = operations:create_infinite_relation(DB, #{
- name => naturals,
- schema => #{},
- cardinality => aleph_zero,
- generator => {primitive, naturals},
- constraints => #{value => {gte, 0}}
- }),
-
+ %% Naturals are built-in, just use them
%% Take 100 naturals
- {_DB2, Naturals100} = operations:take(DB1, naturals, 100),
+ {_DB2, Naturals100} = operations:take(DB, natural, 100),
%% Verify result is finite
[
?_assertEqual({finite, 100}, Naturals100#relation.cardinality),
- ?_assertEqual({take, naturals, 100}, Naturals100#relation.generator)
+ ?_assertEqual({take, natural, 100}, Naturals100#relation.generator)
].
test_finite_relation_cardinality(DB) ->
@@ -149,7 +120,7 @@ test_finite_relation_cardinality(DB) ->
{DB1, _} = operations:create_relation(DB, users, #{name => string, age => integer}),
%% Add tuple
- {DB2, UpdatedRel} = operations:create_tuple(DB1, users, #{name => "Alice", age => 30}),
+ {_DB2, UpdatedRel} = operations:create_tuple(DB1, users, #{name => "Alice", age => 30}),
%% Cardinality should be 1
[
diff --git a/test/join_test.erl b/test/join_test.erl
index 8abdc0b..de62127 100644
--- a/test/join_test.erl
+++ b/test/join_test.erl
@@ -207,19 +207,13 @@ test_theta_join_complex_predicate(DB) ->
%%% Integration Tests
test_join_with_infinite_relation(DB) ->
- % Create integers relation
- {DB1, _} = operations:create_infinite_relation(DB, #{
- name => integers,
- schema => #{},
- cardinality => aleph_zero,
- generator => {primitive, integers}
- }),
+ % Integers are built-in, no need to create them
% Get small set of integers
- IntIter = operations:get_tuples_iterator(DB1, integers, #{value => {range, 1, 5}}),
+ IntIter = operations:get_tuples_iterator(DB, integer, #{value => {range, 1, 5}}),
% Get employees
- EmpIter = operations:get_tuples_iterator(DB1, employees, #{}),
+ EmpIter = operations:get_tuples_iterator(DB, employees, #{}),
% Join where employee.id matches integer.value
Predicate = fun(Emp, Int) ->
diff --git a/test/operations_test.erl b/test/operations_test.erl
index a69c4fa..31bde43 100644
--- a/test/operations_test.erl
+++ b/test/operations_test.erl
@@ -38,7 +38,11 @@ database_creation_test_() ->
[?_test(begin
DB = operations:create_database(my_db),
?assertEqual(my_db, DB#database_state.name),
- ?assertEqual(#{}, DB#database_state.relations),
+ % Database now comes with built-in infinite relations
+ Relations = operations:get_relations(DB),
+ ?assert(lists:member(natural, Relations)),
+ ?assert(lists:member(integer, Relations)),
+ ?assert(lists:member(rational, Relations)),
?assertNotEqual(undefined, DB#database_state.timestamp)
end)]}.
@@ -59,8 +63,12 @@ relation_creation_test_() ->
?assertEqual(users, Relation#relation.name),
?assertEqual(Schema, Relation#relation.schema),
- % Verify database was updated
- ?assertEqual([users], operations:get_relations(DB1)),
+ % Verify database was updated (includes built-in relations)
+ Relations = operations:get_relations(DB1),
+ ?assert(lists:member(users, Relations)),
+ ?assert(lists:member(natural, Relations)),
+ ?assert(lists:member(integer, Relations)),
+ ?assert(lists:member(rational, Relations)),
{ok, RelationHash} = operations:get_relation_hash(DB1, users),
?assertEqual(Relation#relation.hash, RelationHash)
end)]}.
@@ -238,14 +246,19 @@ retract_relation_test_() ->
[?_test(begin
DB = sample_database_with_tuples(),
- % Verify relation exists
- ?assertEqual([users], operations:get_relations(DB)),
+ % Verify relation exists (along with built-in relations)
+ Relations = operations:get_relations(DB),
+ ?assert(lists:member(users, Relations)),
% Retract relation
{atomic, DB1} = operations:retract_relation(DB, users),
- % Verify relation was removed
- ?assertEqual([], operations:get_relations(DB1)),
+ % Verify users relation was removed (built-in relations remain)
+ Relations1 = operations:get_relations(DB1),
+ ?assertNot(lists:member(users, Relations1)),
+ ?assert(lists:member(natural, Relations1)),
+ ?assert(lists:member(integer, Relations1)),
+ ?assert(lists:member(rational, Relations1)),
?assertEqual({error, not_found}, operations:get_relation_hash(DB1, users)),
% Verify database hash changed
diff --git a/test/query_pipeline_test.erl b/test/query_pipeline_test.erl
index 389ff7c..a76c2ff 100644
--- a/test/query_pipeline_test.erl
+++ b/test/query_pipeline_test.erl
@@ -139,18 +139,11 @@ test_materialize_finite(DB) ->
].
test_materialize_infinite(DB) ->
- %% Create infinite naturals
- {DB1, _} = operations:create_infinite_relation(DB, #{
- name => naturals,
- schema => #{},
- cardinality => aleph_zero,
- generator => {primitive, naturals},
- constraints => #{value => {gte, 0}}
- }),
+ %% Naturals are built-in, no need to create them
%% Take 100 and materialize
- Pipeline = operations:get_tuples_iterator(DB1, naturals, #{value => {range, 0, 99}}),
- {_DB2, Naturals100} = operations:materialize(DB1, Pipeline, naturals_100),
+ Pipeline = operations:get_tuples_iterator(DB, natural, #{value => {range, 0, 99}}),
+ {_DB2, Naturals100} = operations:materialize(DB, Pipeline, naturals_100),
[
?_assertEqual({finite, 100}, Naturals100#relation.cardinality),