aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarcos Magueta <maguetamarcos@gmail.com>2026-03-05 23:42:15 -0300
committerMarcos Magueta <maguetamarcos@gmail.com>2026-03-05 23:42:15 -0300
commit8c640a262969ef44cd8bc0e0abe95e2a54f4c3f5 (patch)
tree84f9ddd9e41236fb7a6f540aab07e65db9bc93cb
parent295c6a5a1f2b0db9d756ffe7bf413553a0c02737 (diff)
downloadSakura-8c640a262969ef44cd8bc0e0abe95e2a54f4c3f5.tar.gz
Sakura-8c640a262969ef44cd8bc0e0abe95e2a54f4c3f5.zip
ocaml re-migration
Diffstat (limited to '')
-rw-r--r--.build.yml15
-rwxr-xr-x.direnv/bin/nix-direnv-reload19
-rw-r--r--.envrc2
-rw-r--r--.github/workflows/build.yml61
-rw-r--r--.github/workflows/ci.yml43
-rw-r--r--.github/workflows/docs.yml86
-rw-r--r--.gitignore7
-rw-r--r--.ocamlformat0
-rw-r--r--LICENSE73
-rw-r--r--README.org162
-rw-r--r--bin/dune7
-rw-r--r--bin/main.ml11
-rw-r--r--config/shell.config31
-rw-r--r--config/sys.config24
-rw-r--r--config/vm.args7
-rw-r--r--deps/libressl.nix13
-rw-r--r--deps/merklecpp.nix19
-rw-r--r--deps/ppx_protocol_conv.nix27
-rw-r--r--deps/ppx_protocol_conv_xml_light.nix27
-rw-r--r--deps/relational_engine_lib.nix28
-rw-r--r--docs/architecture.plantuml72
-rw-r--r--docs/code.org272
-rw-r--r--docs/command-interface.puml58
-rw-r--r--docs/constraint-classification.puml82
-rw-r--r--docs/constraint-enforcement-strategies.puml42
-rw-r--r--docs/constraints.org97
-rw-r--r--docs/data-model.puml101
-rw-r--r--docs/distribution-architecture.puml63
-rw-r--r--docs/features.org429
-rw-r--r--docs/infinite-relations.puml177
-rw-r--r--docs/infinite_relations_design.org348
-rw-r--r--docs/iterator-pipeline.puml45
-rw-r--r--docs/karuta-interface-layers.puml65
-rw-r--r--docs/merkle-tree-hierarchy.puml50
-rw-r--r--docs/module-dependencies.puml39
-rw-r--r--docs/provenance-lineage.puml69
-rw-r--r--docs/query-execution-flow.puml54
-rw-r--r--docs/reactive-queries.puml51
-rw-r--r--docs/reliability_roadmap.org223
-rw-r--r--docs/roadmap.puml72
-rw-r--r--docs/system-overview.puml81
-rw-r--r--docs/temporal-queries.puml61
-rw-r--r--docs/transaction-algo.org12
-rw-r--r--docs/type-checking-flow.puml108
-rw-r--r--docs/type-hierarchy.puml289
-rw-r--r--dune-project29
-rw-r--r--flake.lock89
-rw-r--r--flake.nix345
-rw-r--r--include/operations.hrl94
-rw-r--r--lib/core_functions.ml11
-rw-r--r--lib/core_types.ml3
-rw-r--r--lib/dune40
-rw-r--r--lib/interop.ml9
-rw-r--r--rebar-deps.nix44
-rw-r--r--rebar.config103
-rw-r--r--rebar.lock14
-rw-r--r--sakura.opam31
-rw-r--r--shared/librelational_engine/CMakeLists.txt11
-rw-r--r--shared/librelational_engine/library.cpp23
-rw-r--r--shared/librelational_engine/library.h8
-rw-r--r--sources/Geometry64x64.pngbin0 -> 1017 bytes
-rw-r--r--src/constraint.erl1179
-rw-r--r--src/generators.erl584
-rw-r--r--src/main.app.src15
-rw-r--r--src/main.erl66
-rw-r--r--src/operations.erl1484
-rw-r--r--src/query_planner.erl601
-rw-r--r--src/relation.erl42
-rw-r--r--src/relational_operators.erl490
-rw-r--r--src/repl.erl326
-rw-r--r--src/visualizer.erl348
-rw-r--r--test/dune6
-rw-r--r--test/test_relational_engine.ml0
-rw-r--r--treefmt.nix10
74 files changed, 761 insertions, 8866 deletions
diff --git a/.build.yml b/.build.yml
new file mode 100644
index 0000000..ffbf56e
--- /dev/null
+++ b/.build.yml
@@ -0,0 +1,15 @@
+image: nixos/unstable
+oauth: pages.sr.ht/PAGES:RW
+sources:
+ - git@git.sr.ht:~mmagueta/relational-engine
+environment:
+ NIX_CONFIG: "experimental-features = nix-command flakes"
+tasks:
+ - check_branch: |
+ cd relational-engine
+ if [ "$(git rev-parse master)" != "$(git rev-parse HEAD)" ]; then \
+ complete-build; \
+ fi
+ - build: |
+ cd relational-engine
+ nix flake check \ No newline at end of file
diff --git a/.direnv/bin/nix-direnv-reload b/.direnv/bin/nix-direnv-reload
new file mode 100755
index 0000000..f65a008
--- /dev/null
+++ b/.direnv/bin/nix-direnv-reload
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+set -e
+if [[ ! -d "/Users/mmagueta/Developer/Nekoma/sakura-ocaml" ]]; then
+ echo "Cannot find source directory; Did you move it?"
+ echo "(Looking for "/Users/mmagueta/Developer/Nekoma/sakura-ocaml")"
+ echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
+ exit 1
+fi
+
+# rebuild the cache forcefully
+_nix_direnv_force_reload=1 direnv exec "/Users/mmagueta/Developer/Nekoma/sakura-ocaml" true
+
+# Update the mtime for .envrc.
+# This will cause direnv to reload again - but without re-building.
+touch "/Users/mmagueta/Developer/Nekoma/sakura-ocaml/.envrc"
+
+# Also update the timestamp of whatever profile_rc we have.
+# This makes sure that we know we are up to date.
+touch -r "/Users/mmagueta/Developer/Nekoma/sakura-ocaml/.envrc" "/Users/mmagueta/Developer/Nekoma/sakura-ocaml/.direnv"/*.rc
diff --git a/.envrc b/.envrc
index a5dbbcb..3550a30 100644
--- a/.envrc
+++ b/.envrc
@@ -1 +1 @@
-use flake .
+use flake
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index 5754292..0000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,61 +0,0 @@
-name: "[Domino] Build"
-
-on:
- push:
- branches: [ "master" ]
- paths:
- - .github/workflows/**
- - config/**
- - docs/**
- - include/**
- - test/**
- - src/**
- - flake.nix
- - flake.lock
- - rebar.config
- - rebar-deps.nix
- - rebar.lock
-
- pull_request:
- branches: [ "master" ]
- paths:
- - .github/workflows/**
- - config/**
- - docs/**
- - include/**
- - test/**
- - src/**
- - flake.nix
- - flake.lock
- - rebar.config
- - rebar-deps.nix
- - rebar.lock
-
-permissions:
- contents: read
-
-jobs:
- build:
- name: "[Domino] Nix Build"
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v6
- with:
- lfs: true
-
- - name: Install Nix
- uses: cachix/install-nix-action@v31
- with:
- # Mostly to avoid GitHub rate limiting
- extra_nix_config: |
- access-tokens = github.com=${{ github.token }}
-
- - name: Install Nix Cache
- uses: DeterminateSystems/magic-nix-cache-action@main
-
- # Actual Pipelines
- - name: "Build"
- run: |
- nix build
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index b2ec401..0000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-name: CI
-
-on:
- pull_request:
-
-jobs:
- test:
- name: Test on OTP ${{matrix.otp}} / rebar3 ${{matrix.rebar3}}
- runs-on: ubuntu-latest
-
- strategy:
- matrix:
- otp: ['26', '27']
- rebar3: ['3.23']
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Setup Erlang/OTP
- uses: erlef/setup-beam@v1
- with:
- otp-version: ${{matrix.otp}}
- rebar3-version: ${{matrix.rebar3}}
-
- - name: Restore dependencies cache
- uses: actions/cache@v4
- with:
- path: |
- _build
- rebar.lock
- key: ${{ runner.os }}-rebar3-${{ hashFiles('rebar.config') }}
- restore-keys: |
- ${{ runner.os }}-rebar3-
-
- - name: Compile
- run: rebar3 compile
-
- - name: Run tests
- run: rebar3 eunit
-
- - name: Run dialyzer
- run: rebar3 dialyzer
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
deleted file mode 100644
index 0d3aa58..0000000
--- a/.github/workflows/docs.yml
+++ /dev/null
@@ -1,86 +0,0 @@
-name: Documentation
-
-on:
- push:
- branches:
- - master
- pull_request:
- branches:
- - master
- paths:
- - .github/workflows/docs.yml
- - include/**
- - src/**
-
-permissions:
- contents: read
- pages: write
- id-token: write
-
-concurrency:
- group: "pages"
- cancel-in-progress: false
-
-jobs:
- build:
- name: Build documentation
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Setup Erlang/OTP
- uses: erlef/setup-beam@v1
- with:
- otp-version: "27"
- rebar3-version: "3.23"
-
- - name: Restore dependencies cache
- uses: actions/cache@v4
- with:
- path: |
- _build
- rebar.lock
- key: ${{ runner.os }}-rebar3-${{ hashFiles('rebar.config') }}
- restore-keys: |
- ${{ runner.os }}-rebar3-
-
- - name: Compile
- run: rebar3 compile
-
- - name: Generate EDoc
- run: rebar3 edoc
-
- - name: Fix index.html for GitHub Pages
- run: |
- if [ ! -f doc/index.html ]; then
- cp doc/overview-summary.html doc/index.html
- echo "Copied overview-summary.html to index.html"
- else
- echo "index.html already exists, skipping"
- fi
-
- - name: Setup Pages
- uses: actions/configure-pages@v4
- if: github.event_name != 'pull_request'
-
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v3
- if: github.event_name != 'pull_request'
- with:
- path: "doc"
-
- deploy:
- name: Deploy to GitHub Pages
- if: github.event_name != 'pull_request'
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- runs-on: ubuntu-latest
- needs: build
-
- steps:
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 2f4880d..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,7 +0,0 @@
-.direnv
-_build
-Mnesia.*
-
-# EDoc generated files
-doc/
-!doc/overview.edoc
diff --git a/.ocamlformat b/.ocamlformat
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.ocamlformat
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 7bfa10f..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,73 +0,0 @@
-Apache License
-Version 2.0, January 2004
-http://www.apache.org/licenses/
-
-TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-1. Definitions.
-
-"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
-
-"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
-
-"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
-
-"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
-
-"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
-
-"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
-
-"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
-
-"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
-
-"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
-
-"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
-
-2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
-
-3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
-
-4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
-
- (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
-
- You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
-
-5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
-
-6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
-
-7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
-
-8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
-
-9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
-
-END OF TERMS AND CONDITIONS
-
-APPENDIX: How to apply the Apache License to your work.
-
-To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
-
-Copyright 2025 mmagueta
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
diff --git a/README.org b/README.org
index cf42f5c..737ae19 100644
--- a/README.org
+++ b/README.org
@@ -1,139 +1,27 @@
-#+TITLE: Sakura
+#+TITLE: Relational Engine
* About
-
-Sakura is a semantic-relational algebra engine inspired by
-E. F. Codd's RM/T and D. McGoveran's (as understood by Fabian Pascal)
-SRDM. It models relations in both intension and extension, providing
-their manipulation as first-class elements of an algebra.
-
-* Motivation
-
-We believe the relational model was not ever fully implemented. The
-fundamental necessity of expressing intent and binding meaning
-enclosed to a conceptual model in data is seldom achieved by any
-self-claimed relational database vendor. The emergence of different
-physically concerned paradigms in the industry demonstrate not only a
-large scale amnesia about fundamentals, but also a lethal
-manifestation of problems that should not exist otherwise. The
-relational data model as an applied theory is an universally rich and
-concrete approach to reality that bases itself greatly in
-moderate-realism applied to mathematics, as described in =An
-Aristotelian Realist Philosophy of Mathematics: Mathematics as the
-Science of Quantity and Structure= by J. Franklin. We believe that
-unleashing the full potential of the relational model as refined over
-the years despite overwhelming disregard of the industry as a whole,
-will promote efficiency to a new scale never seen, that was very much
-so cut short by relational pretenders and theoretically unsound
-approaches. Our goal is to eliminate the application-database
-distinction, while maintaining a clear separation between a first
-order predicate logic algebra engine and a higher order predicate
-logic decision engine (application) via an embedded data sublanguage
-with Sakura as a possible auxiliary runtime.
-
-* Current State
-
-** What Works Today
-
-- Base finite relations: create, insert, retract, clear
-- Built-in immutable/generated domains on database creation:
- =integer=, =natural=, =rational=, =string=, =atom=, =term=
-- Query DSL in =query_planner= and =repl=:
- - base relation scan (using relation atom)
- - =select=, =project=, =join=, =theta_join=, =sort=, =take=, =rename=
- - =materialize= (plan-level, with caveats)
-- Volcano-style iterator execution model (process-per-operator)
-- Visual output modes in =visualizer= (table/tree/nested views)
-- XML TCP server protocol commands: =QUERY=, =NEXT=, =CLOSE=
-
-** Known Gaps / Reliability Risks
-
-- Lack of constraints greater than 1OPs;
-- =repl:start/0= is destructive: it calls =main:setup/0=, which recreates schema;
-- =xml_server= =SCHEMA= path is currently fragile with constraints representation;
-- Constraint/intension representation is not yet fully normalized across modules;
-- Planner contains dual compilation styles (iterator path + relation path);
-- Blocking operators (for example =sort=) over infinite relations can time out unless bounded with =take= first;
-- There is currently no substantial automated test suite in-repo despite CI hooks;
-
-See =docs/reliability_roadmap.org= for the detailed remediation plan.
-
-* Architecture (High Level)
-
-- =src/operations.erl= :: core storage/versioning and iterator primitives
-- =src/constraint.erl= :: domain constraints, inference, validation
-- =src/generators.erl= :: infinite relation generators
-- =src/relational_operators.erl= :: pure relation-to-relation algebra operators
-- =src/query_planner.erl= :: tuple DSL parsing/validation/execution planning
-- =src/repl.erl= :: shell-facing query convenience API
-- =src/xml_server.erl= :: TCP/XML query interface
-- =src/visualizer.erl= :: renderers for result tuples
-
-Data model records are defined in =include/operations.hrl=.
-
-* Query DSL
-
-Plans are Erlang tuples:
-
-- base relation: =employees=
-- selection: ={select, employees, fun(T) -> maps:get(age, T) > 30 end}=
-- projection: ={project, employees, [name, age]}=
-- equijoin: ={join, employees, departments, dept_id}=
-- theta join: ={theta_join, Left, Right, Pred2}=
-- sort: ={sort, Plan, Comparator2}=
-- take: ={take, Plan, N}=
-- rename: ={rename, old_attr, new_attr, Plan}=
-
-* Running
-
-** Compile
-
-#+begin_src sh
-rebar3 compile
-#+end_src
-
-** REPL
-
-#+begin_src erlang
-repl:start().
-DB = repl:example_db().
-repl:q(DB, employees).
-repl:q(DB, {take, {sort, employees,
- fun(A,B) -> maps:get(age,A) =< maps:get(age,B) end}, 3}).
-#+end_src
-
-** XML Server (manual)
-
-Start server from Erlang shell, then send line commands over TCP:
-
-- =QUERY <plan>=
-- =NEXT <session_id> <n>=
-- =CLOSE <session_id>=
-
-* Design Intent
-
-Sakura aims to treat relations as algebraic manipulation units with both:
-
-- extension (actual tuples, finite or generated)
-- intension (schema + constraints + derivation meaning)
-
-The current engine already exposes the extension side robustly for
-many operations, despite the lack of constraints greater than 1OP.
-The main active work is making intension handling fully consistent and
-reliable across planners/operators/protocols.
-
-* Roadmap and Docs
-
-- =docs/reliability_roadmap.org= :: precise reliability + correctness roadmap
-- =docs/code.org= :: code-oriented notes
-- =docs/features.org= :: feature notes (may contain older planning context)
-- =docs/constraints.org= :: constraint model notes
-
-* References
-
-- Codd, E. F. (1970), "A Relational Model of Data for Large Shared Data Banks"
-- Codd, E. F. (1979), "Extending the Database Relational Model to Capture More Meaning"
-
-* License
-
-See =LICENSE=.
+This is a relational engine.
+
+* Roadmap
+- [ ] State
+ - [ ] Add the schema modifications to the commit state hash
+ - [ ] Save a history of the schema
+- [ ] Filesystem [0/6]
+ - [ ] Durability
+ - [ ] Immutable storage
+ - [ ] ARIES
+ - [ ] Merkle construction per virtual file
+ - [ ] Branching
+ - [ ] Branch merge strategies [0/3]
+ - [ ] Selected merge
+ - [ ] Union merge
+ - [ ] Biased union merge
+- [ ] Language [0/4]
+ - [ ] Lexer
+ - [ ] Parser
+ - [ ] AST
+ - [ ] Unification and query terms
+- [ ] Server [0/2]
+ - [ ] Authentication
+ - [ ] Multi-client coordination
diff --git a/bin/dune b/bin/dune
new file mode 100644
index 0000000..9c5253a
--- /dev/null
+++ b/bin/dune
@@ -0,0 +1,7 @@
+(executable
+ (public_name sakura)
+ (name main)
+ (modules main)
+ (libraries relational_engine)
+ (package relational_engine)
+ (modes byte exe))
diff --git a/bin/main.ml b/bin/main.ml
new file mode 100644
index 0000000..90d4f24
--- /dev/null
+++ b/bin/main.ml
@@ -0,0 +1,11 @@
+let main () =
+ let digest = Relational_engine.Interop.Sha256.compute_hash (Bytes.of_string "sakura") in
+ print_endline digest
+
+let () =
+ (* let response = *)
+ (* Relational_engine.Relation.write_and_retrieve() *)
+ (* in match response with *)
+ (* | Ok response -> print_endline response *)
+ (* | Error err -> print_endline err; *)
+ main ()
diff --git a/config/shell.config b/config/shell.config
deleted file mode 100644
index cfee2fa..0000000
--- a/config/shell.config
+++ /dev/null
@@ -1,31 +0,0 @@
-[
- {kernel, [
- {logger_level, debug},
- {logger, [
- {handler, default, logger_std_h, #{
- level => debug,
- config => #{
- type => standard_io
- },
- formatter => {logger_formatter, #{
- legacy_header => false,
- single_line => false,
- max_size => unlimited,
- depth => unlimited,
- chars_limit => unlimited,
- template => [time, " ", color_start, "[", level, "]", color_end, " ",
- {mfa, [mfa, ":", line], ""}, " ", msg, "\n"],
- time_designator => $T,
- time_offset => "Z"
- }}
- }}
- ]}
- ]},
- {sasl, [
- {sasl_error_logger, {file, "log/sasl.log"}},
- {errlog_type, error},
- {error_logger_mf_dir, "log"},
- {error_logger_mf_maxbytes, 10485760},
- {error_logger_mf_maxfiles, 5}
- ]}
-].
diff --git a/config/sys.config b/config/sys.config
deleted file mode 100644
index a63cf9e..0000000
--- a/config/sys.config
+++ /dev/null
@@ -1,24 +0,0 @@
-[
- {kernel, [
- {logger_level, info},
- {logger, [
- {handler, default, logger_std_h, #{
- level => info,
- config => #{
- type => standard_io
- },
- formatter => {logger_formatter, #{
- single_line => false,
- template => [
- "{\"timestamp\":\"", time, "\",",
- "\"level\":\"", level, "\",",
- "\"pid\":\"", pid, "\",",
- "\"mfa\":\"", mfa, "\",",
- "\"line\":", line, ",",
- "\"msg\":", msg, "}\n"
- ]
- }}
- }}
- ]}
- ]}
-].
diff --git a/config/vm.args b/config/vm.args
deleted file mode 100644
index 524f9f2..0000000
--- a/config/vm.args
+++ /dev/null
@@ -1,7 +0,0 @@
-## Name of the node
--sname sakura
-
-## Extra options
--heart
--env HEART_BEAT_TIMEOUT 20
-+K true
diff --git a/deps/libressl.nix b/deps/libressl.nix
new file mode 100644
index 0000000..a517dab
--- /dev/null
+++ b/deps/libressl.nix
@@ -0,0 +1,13 @@
+{ pkgs, ... }:
+pkgs.stdenv.mkDerivation {
+ name = "libressl-lib";
+ src = ./.;
+ buildInputs = with pkgs; [ libressl.dev pkg-config ];
+ installPhase = ''
+ mkdir -p $out/lib
+ mkdir -p $out/include
+ libressl_path=$(${pkgs.pkg-config}/bin/pkg-config --libs ${pkgs.libressl.dev}/lib/pkgconfig/libssl.pc | awk '{sub(/^-L/, ""); sub(/ -lssl$/, ""); print}')
+ cp -r $libressl_path/* $out/lib
+ cp -r ${pkgs.libressl.dev}/include/* $out/include
+ '';
+}
diff --git a/deps/merklecpp.nix b/deps/merklecpp.nix
new file mode 100644
index 0000000..d130be6
--- /dev/null
+++ b/deps/merklecpp.nix
@@ -0,0 +1,19 @@
+{ pkgs, ... }:
+pkgs.stdenv.mkDerivation rec {
+ name = "merklecpp";
+
+ src = pkgs.fetchFromGitHub {
+ owner = "microsoft";
+ repo = "merklecpp";
+ rev = "master";
+ sha256 = "jJJ6FEn+AkMknqun0GkXh5DSXsJnFbdE7U8BQ7Dd/Fw=";
+ };
+
+ prePatch = "";
+ configurePhase = "";
+ buildPhase = "";
+ installPhase = ''
+ mkdir -p $out/include
+ mv merklecpp.h $out/include/merklecpp.h
+ '';
+}
diff --git a/deps/ppx_protocol_conv.nix b/deps/ppx_protocol_conv.nix
new file mode 100644
index 0000000..856904e
--- /dev/null
+++ b/deps/ppx_protocol_conv.nix
@@ -0,0 +1,27 @@
+{ lib, ocamlPackages, fetchFromGitHub, defaultVersion ? "5.2.3" }:
+ocamlPackages.buildDunePackage {
+ duneVersion = "3";
+
+ pname = "ppx_protocol_conv";
+ version = defaultVersion;
+
+ minimalOCamlVersion = "4.07";
+
+ src = fetchFromGitHub {
+ owner = "andersfugmann";
+ repo = "ppx_protocol_conv";
+ rev = "425a7a1a26c26fcb740e5574e252043fd03eff46";
+ sha256 = "sha256-G6zMgHioPUCh2i50cjQ6talN8CYO6UhEaWgi0xe3CWA=";
+ };
+
+ buildInputs = [ocamlPackages.ppxlib ocamlPackages.ppx_sexp_conv ocamlPackages.sexplib ocamlPackages.alcotest ];
+
+ propagatedBuildInputs = [ ocamlPackages.ppxlib ocamlPackages.ppx_sexp_conv ocamlPackages.sexplib ocamlPackages.alcotest ];
+
+ doCheck = true;
+
+ meta = {
+ license = lib.licenses.bsd3;
+ homepage = "https://github.com/andersfugmann/ppx_protocol_conv";
+ };
+}
diff --git a/deps/ppx_protocol_conv_xml_light.nix b/deps/ppx_protocol_conv_xml_light.nix
new file mode 100644
index 0000000..a1f3e35
--- /dev/null
+++ b/deps/ppx_protocol_conv_xml_light.nix
@@ -0,0 +1,27 @@
+{ lib, ppx_protocol_conv, ocamlPackages, fetchFromGitHub, defaultVersion ? "5.2.3" }:
+ocamlPackages.buildDunePackage {
+ duneVersion = "3";
+
+ pname = "ppx_protocol_conv_xml_light";
+ version = defaultVersion;
+
+ minimalOCamlVersion = "4.08";
+
+ src = fetchFromGitHub {
+ owner = "andersfugmann";
+ repo = "ppx_protocol_conv";
+ rev = "425a7a1a26c26fcb740e5574e252043fd03eff46";
+ sha256 = "sha256-G6zMgHioPUCh2i50cjQ6talN8CYO6UhEaWgi0xe3CWA=";
+ };
+
+ buildInputs = [ppx_protocol_conv ocamlPackages.xml-light ocamlPackages.ppx_sexp_conv ocamlPackages.sexplib ocamlPackages.alcotest ];
+
+ propagatedBuildInputs = [ ocamlPackages.xml-light ocamlPackages.ppxlib ocamlPackages.ppx_sexp_conv ocamlPackages.sexplib ocamlPackages.alcotest ];
+
+ doCheck = true;
+
+ meta = {
+ license = lib.licenses.bsd3;
+ homepage = "https://github.com/andersfugmann/ppx_protocol_conv";
+ };
+}
diff --git a/deps/relational_engine_lib.nix b/deps/relational_engine_lib.nix
new file mode 100644
index 0000000..475ce00
--- /dev/null
+++ b/deps/relational_engine_lib.nix
@@ -0,0 +1,28 @@
+{ pkgs, libressl ? pkgs.callPackage ./libressl.nix { }
+# , merklecpp ? pkgs.callPackage ./merklecpp.nix { }
+, ... }:
+let shared_lib_extension = if pkgs.stdenv.isDarwin then "dylib" else "so";
+in pkgs.stdenv.mkDerivation {
+ name = "librelational_engine";
+ src = ../shared/librelational_engine;
+ nativeBuildInputs = [ pkgs.cmake pkgs.gcc ];
+ buildInputs = [
+ libressl
+ # merklecpp
+ ];
+ env = {
+ # MERKLECPP_INCLUDE_PATH = "${merklecpp}/include";
+ LIBRESSL_INCLUDE_PATH = "${libressl}/include";
+ LIBRESSL_LIB_PATH = "${libressl}/lib";
+ OS_LIB_EXTENSION = shared_lib_extension;
+ };
+ buildPhase = ''
+ mkdir -p $out/lib
+ mkdir -p $out/include
+ cmake --build . --target relational_engine
+ '';
+ installPhase = ''
+ mv librelational_engine.${shared_lib_extension} $out/lib/librelational_engine.${shared_lib_extension}
+ cp $src/library.h $out/include/library.h
+ '';
+}
diff --git a/docs/architecture.plantuml b/docs/architecture.plantuml
new file mode 100644
index 0000000..90985ba
--- /dev/null
+++ b/docs/architecture.plantuml
@@ -0,0 +1,72 @@
+@startuml
+allowmixing
+
+package Client {
+ object "Query Interface" as query_interface
+}
+
+package Algebra {
+ object "Unifier" as unifier
+ object "Bytecode Builder" as bytecode_builder
+ object "Parser" as parser
+ object "Lexer" as lexer
+}
+
+query_interface --> lexer
+lexer --> parser
+parser --> unifier
+unifier --> bytecode_builder
+
+package Query {
+ object "Schema Builder" as schema_builder
+ object "Facts Builder" as facts_builder
+ object "Optimizer" as optimizer
+ object "Commands Generator" as commands_generator
+ object "B+ Tree" as b_plus_tree
+}
+
+bytecode_builder --> optimizer
+optimizer --> commands_generator
+facts_builder --> commands_generator
+schema_builder --> commands_generator
+commands_generator --> b_plus_tree
+b_plus_tree --> query_interface
+
+package Coordinator {
+ object "File Historical Hashes" as file_historical_hashes
+ object "Location Hashes" as location_hashes
+ object "Command Parser" as command_parser
+ object "Curator" as curator
+}
+
+commands_generator <-- command_parser
+
+package Disk {
+ object "Merkle Tree Root Generator" as merkle_tree
+ object "WAL Reader" as wal_reader
+ object "WAL Writer" as wal_writer
+ object "ARIES" as aries
+ object "SHA256 Hasher" as sha
+ object "Executor" as executor
+ map storage {
+ write => +λ
+ read => +λ
+ }
+ object "Courier" as courier
+ database Wal
+}
+
+executor *--> merkle_tree
+executor *--> sha
+curator --> command_parser
+command_parser --> executor
+location_hashes *--> curator
+file_historical_hashes *--> curator
+executor --> aries
+aries --> wal_reader
+aries --> wal_writer
+wal_writer --> Wal
+wal_reader --> Wal
+courier <-- wal_reader
+courier --> storage
+@enduml \ No newline at end of file
diff --git a/docs/code.org b/docs/code.org
deleted file mode 100644
index 30ff9c9..0000000
--- a/docs/code.org
+++ /dev/null
@@ -1,272 +0,0 @@
-#+TITLE: Code Documentation
-
-* Overview
-
-Domino is a content-addressed, immutable relational database engine written in Erlang. The system uses cryptographic hashing (SHA-256) and Merkle trees to provide version control, deduplication, and efficient synchronization capabilities.
-
-* Architecture
-
-** Design Patterns
-
-- *Content-Addressed Storage* :: All data (attributes, tuples, relations, databases) are identified by their cryptographic hash
-- *Immutable Data Structures* :: All operations create new versions rather than modifying existing data
-- *Merkle Trees* :: Relations and databases use merkle trees for efficient diffing and synchronization
-- *Volcano Iterator Model* :: Query execution uses lazy evaluation with process-based iterators
-- *Actor Model* :: Iterators are implemented as independent Erlang processes
-- *Transactional Storage* :: Mnesia provides ACID guarantees for all mutations
-
-* Module Structure
-
-** operations.erl
-
-The core database operations module and main API.
-
-*** Public API
-
-**** Database & Relation Management
-- =setup/0= :: Initialize Mnesia schema and create all tables
-- =create_database/1= :: Create a new database state
-- =create_relation/3= :: Add a relation to a database with schema
-- =get_relations/1= :: List all relation names in a database
-- =get_relation_hash/2= :: Get relation hash by name
-
-**** Tuple Operations
-- =create_tuple/3= :: Insert a tuple into a relation (transactional)
-- =retract_tuple/3= :: Remove a tuple from a relation (default: transactional)
-- =retract_tuple/4= :: Remove a tuple with optional transaction control
-
-**** Relation Operations
-- =clear_relation/2= :: Remove all tuples from a relation (default: transactional)
-- =clear_relation/3= :: Clear relation with optional transaction control
-- =retract_relation/2= :: Remove a relation from database (default: transactional)
-- =retract_relation/3= :: Remove relation with optional transaction control
-
-**** Iterator Pattern (Volcano Model)
-- =get_tuples_iterator/2= :: Create lazy iterator process for relation tuples
-- =next_tuple/1= :: Pull next tuple from iterator (returns ={ok, Tuple} | done=)
-- =close_iterator/1= :: Terminate iterator process
-- =collect_all/1= :: Helper to consume entire iterator into list
-
-**** Utility Functions
-- =hash/1= :: SHA-256 hash of term
-- =hashes_from_tuple/1= :: Extract all tuple hashes from a relation
-
-** relation.erl
-
-Simple helper module for building tuples and relations conceptually.
-
-*** Public API
-- =new_attribute/2= :: Create attribute map: =#{name => Name, value => Value}=
-- =new_tuple/1= :: Convert list of attributes to map
-- =new_relation/1= :: Convert list of tuples to sets:set
-
-** poc.erl
-
-Proof-of-concept demonstrating the Volcano Iterator Model for query execution.
-
-*** Key Concepts
-- Content-addressed storage simulation using in-memory maps
-- Lazy iterator chain: Scan → Select → Project
-- Message-passing protocol for tuple streaming
-- Automatic deduplication via hashing
-
-** main.erl
-
-OTP application entry point. Starts Mnesia on application startup.
-
-* Data Structures
-
-All core records are defined in =include/operations.hrl=.
-
-** attribute
-#+BEGIN_SRC erlang
--record(attribute, {
- hash, % SHA-256 hash of value
- value % Actual value (any Erlang term)
-}).
-#+END_SRC
-
-** tuple
-#+BEGIN_SRC erlang
--record(tuple, {
- hash, % SHA-256 hash of entire tuple
- relation, % Relation name (atom)
- attribute_map % #{attr_name => value_hash}
-}).
-#+END_SRC
-
-** relation
-#+BEGIN_SRC erlang
--record(relation, {
- hash, % hash({name, schema, tree_root})
- name, % Relation name (atom)
- tree, % Merkle tree of tuple hashes
- schema % Schema definition map
-}).
-#+END_SRC
-
-** database_state
-#+BEGIN_SRC erlang
--record(database_state, {
- hash, % Merkle tree root hash
- name, % Database name (atom)
- tree, % Merkle tree of relations
- relations, % #{relation_name => relation_hash}
- timestamp % Creation/modification timestamp
-}).
-#+END_SRC
-
-* Data Flow
-
-** Creating a Tuple
-
-1. Store each attribute value separately (enables deduplication)
-2. Build attribute_map with hashes: =#{name => AttrHash1, age => AttrHash2}=
-3. Create tuple record with hash
-4. Insert tuple hash into relation's merkle tree
-5. Compute new relation hash from tree root
-6. Update database merkle tree
-7. Return updated database and relation
-
-All operations occur within a Mnesia transaction.
-
-** Querying Tuples
-
-1. Lookup relation hash from database
-2. Read relation from Mnesia
-3. Extract tuple hashes from merkle tree
-4. Spawn iterator process with hash list
-5. Client pulls tuples via message passing
-6. Iterator resolves: hash → tuple → attribute hashes → actual values
-7. Return materialized tuple map
-
-* Usage Examples
-
-** Basic Workflow
-
-#+BEGIN_SRC erlang
-%% Setup
-operations:setup().
-
-%% Create database
-DB = operations:create_database(my_app).
-
-%% Create relation with schema
-{DB1, _Rel} = operations:create_relation(DB, users, #{
- name => string,
- age => integer
-}).
-
-%% Insert tuples
-{DB2, _} = operations:create_tuple(DB1, users, #{name => "Alice", age => 30}).
-{DB3, _} = operations:create_tuple(DB2, users, #{name => "Bob", age => 25}).
-
-%% Query tuples
-Iterator = operations:get_tuples_iterator(DB3, users).
-Tuples = operations:collect_all(Iterator).
-%% Returns: [#{name => "Alice", age => 30}, #{name => "Bob", age => 25}]
-
-%% Remove tuple
-{ok, RelHash} = operations:get_relation_hash(DB3, users).
-[TupleHash|_] = operations:hashes_from_tuple(RelHash).
-{atomic, {DB4, _}} = operations:retract_tuple(DB3, RelHash, TupleHash).
-
-%% Clear relation (remove all tuples, keep schema)
-{atomic, {DB5, _}} = operations:clear_relation(DB4, RelHash).
-
-%% Remove relation entirely
-{atomic, DB6} = operations:retract_relation(DB5, users).
-#+END_SRC
-
-** Iterator Protocol
-
-#+BEGIN_SRC erlang
-Iterator = operations:get_tuples_iterator(DB, users).
-
-%% Pull tuples one by one
-case operations:next_tuple(Iterator) of
- {ok, Tuple} -> process_tuple(Tuple);
- done -> io:format("No more tuples~n")
-end.
-
-%% Or collect all at once
-AllTuples = operations:collect_all(Iterator).
-
-%% Clean up
-operations:close_iterator(Iterator).
-#+END_SRC
-
-* Key Implementation Details
-
-** Immutability & Versioning
-
-Every modification creates a new hash. Old versions remain accessible via their hash. Database evolution is tracked via timestamp and hash chain.
-
-** Deduplication
-
-Identical attribute values are stored once (content-addressed). Example: 1000 users with age=30 store the value "30" only once.
-
-** Transaction Semantics
-
-All mutations are wrapped in =mnesia:transaction/1=. Dirty reads are used for immutable data lookups.
-
-** Merkle Tree Strategy
-
-- Relations store tuple hashes as keys: ={TupleHash, <<>>}=
-- Databases store relation names → hashes: ={RelationName, RelationHash}=
-- Enables efficient diff computation between versions
-- Foundation for distributed synchronization
-
-** Iterator Pattern
-
-- Decouples query execution from data access
-- Enables pipelined query operators (future: select, project, join)
-- Memory bounded (doesn't materialize entire relation)
-- Process-based concurrency via Erlang actors
-
-** Schema Flexibility
-
-Schema stored as map (e.g., =#{name => string, age => integer}=). Currently not enforced, but provides foundation for future domain validation.
-
-* Dependencies
-
-- =merklet= (2.0.0) :: Merkle tree implementation for set reconciliation
-- =mnesia= :: Distributed database for persistent storage
-- =crypto= :: SHA-256 hashing
-
-* Testing
-
-The test suite (=test/operations_test.erl=) validates:
-
-- Functional correctness (CRUD operations)
-- Hash determinism and consistency
-- Transactional integrity
-- Iterator protocol compliance
-- State evolution tracking
-- Cross-database consistency
-
-Run tests with:
-
-#+BEGIN_SRC sh
-rebar3 eunit
-#+END_SRC
-
-* Current State
-
-** Implemented
-- Core data model (attribute, tuple, relation, database)
-- Content-addressed storage with SHA-256
-- Immutable versioning
-- Merkle tree integration
-- Iterator-based lazy evaluation
-- Transactional CRUD operations
-- Comprehensive test suite
-
-** Not Yet Implemented
-- Relational algebra operators (σ, π, ⋈, ∪, ∩, −)
-- Schema validation and type checking
-- Infinite relations (aleph₀)
-- Join indexing
-- Query optimization
-- Distributed synchronization
-- REPL/query console
diff --git a/docs/command-interface.puml b/docs/command-interface.puml
deleted file mode 100644
index 4c0319a..0000000
--- a/docs/command-interface.puml
+++ /dev/null
@@ -1,58 +0,0 @@
-@startuml command-interface
-!theme plain
-title Command-Based Interface Flow
-
-participant "Karuta" as K
-participant "domino:execute/2" as API
-participant "Command Validator" as V
-participant "operations.erl" as Ops
-database "Mnesia" as DB
-
-K -> API: execute(DB0, {create, relation, employee, Schema})
-activate API
-API -> V: validate({create, relation, ...})
-activate V
-V -> V: check schema format
-V -> V: verify types
-V --> API: ok
-deactivate V
-
-API -> Ops: create_relation(DB0, employee, Schema)
-activate Ops
-Ops -> DB: transaction(fn -> ... end)
-activate DB
-DB -> DB: insert relation record
-DB -> DB: update database tree
-DB --> Ops: {atomic, {NewDB, Hash}}
-deactivate DB
-Ops --> API: {NewDB, Hash}
-deactivate Ops
-
-API --> K: {ok, DB1, {relation_created, employee, Hash}}
-deactivate API
-
-K -> API: execute(DB1, {insert, tuple, employee, Data})
-activate API
-API -> V: validate({insert, tuple, ...})
-activate V
-V -> V: check schema compliance
-V -> V: verify constraints
-V --> API: ok
-deactivate V
-
-API -> Ops: create_tuple(DB1, employee, Data)
-activate Ops
-Ops -> DB: transaction(fn -> ... end)
-activate DB
-DB -> DB: store attribute values
-DB -> DB: create tuple record
-DB -> DB: update merkle tree
-DB --> Ops: {atomic, {NewDB2, TupleHash}}
-deactivate DB
-Ops --> API: {NewDB2, TupleHash}
-deactivate Ops
-
-API --> K: {ok, DB2, {tuple_inserted, TupleHash}}
-deactivate API
-
-@enduml
diff --git a/docs/constraint-classification.puml b/docs/constraint-classification.puml
deleted file mode 100644
index 09094f1..0000000
--- a/docs/constraint-classification.puml
+++ /dev/null
@@ -1,82 +0,0 @@
-@startuml constraint-classification
-!theme plain
-title Constraint Classification (McGoveran Framework)
-
-package "Database Constraints" {
- class RelationConstraints {
- Which relations must/can exist
- Example: referential integrity
- }
-
- class CardinalityConstraints {
- Relation size limits
- Examples:
- - exactly N tuples
- - at most M tuples
- }
-}
-
-package "Relation Constraints" {
- package "Tuple Constraints" {
- class AttributeConstraints {
- Domain constraints per attribute
- Examples:
- - age >= 18
- - salary > 0
- - name ≠ ""
- }
-
- class TupleLevelConstraints {
- Cross-attribute rules
- Example:
- if salary > 200k then age >= 30
- }
- }
-
- package "Relation-Level Constraints" {
- class KeyConstraints {
- Uniqueness + not null
- - Primary keys
- - Candidate keys
- - Composite keys
- }
-
- class FunctionalDependencies {
- A → B
- Example: ZIP → {city, state}
- }
-
- class ForeignKeys {
- Inclusion dependencies
- Example: employee.dept_id ⊆ department.id
- }
- }
-}
-
-package "Transition Constraints" {
- class StateTransitionRules {
- Before → After constraints
- Examples:
- - salary (monotonic increasing)
- - created_at (immutable)
- }
-
- class TemporalConstraints {
- Time-based rules
- Example: end_date >= start_date
- }
-}
-
-package "Multi-Relation Constraints" {
- class AggregateConstraints {
- Cross-relation aggregates
- Example: sum(dept_salaries) <= dept_budget
- }
-
- class MutualExclusion {
- Disjoint sets
- Example: managers ∩ ICs = ∅
- }
-}
-
-@enduml
diff --git a/docs/constraint-enforcement-strategies.puml b/docs/constraint-enforcement-strategies.puml
deleted file mode 100644
index 6e0305d..0000000
--- a/docs/constraint-enforcement-strategies.puml
+++ /dev/null
@@ -1,42 +0,0 @@
-@startuml constraint-enforcement-strategies
-!theme plain
-title Constraint Enforcement Strategies
-
-state "Operation Request" as Request
-state "Immediate Validation" as Immediate
-state "Deferred Validation" as Deferred
-state "Execute Operation" as Execute
-state "Transaction Commit" as Commit
-state "Rollback" as Rollback
-state "Success" as Success
-
-Request --> Immediate: Strategy 1:\nEager
-Request --> Execute: Strategy 2:\nDeferred
-
-Immediate --> Execute: Valid
-Immediate --> Rollback: Invalid
-
-Execute --> Deferred: At commit
-Deferred --> Commit: Valid
-Deferred --> Rollback: Invalid
-
-Commit --> Success
-Rollback --> Request: Retry
-
-note right of Immediate
- Immediate Checking:
- - Validate before execute
- - Simple reasoning
- - May reject valid intermediate states
- - Lower performance
-end note
-
-note right of Deferred
- Deferred Checking:
- - Validate at transaction end
- - Allows intermediate invalid states
- - Better performance
- - Complex error handling
-end note
-
-@enduml
diff --git a/docs/constraints.org b/docs/constraints.org
deleted file mode 100644
index b3978e6..0000000
--- a/docs/constraints.org
+++ /dev/null
@@ -1,97 +0,0 @@
-#+TITLE: Constraints
-
-* Preamble
-Under an ontological commitment to objects, with an objects-properties
-modeling in a conceptual model, there are primitive and compound objects:
-
-** Primitive objects
-These are things we directly observe their existence independent of their individual properties
-
-** Compound objects
-These are collective properties observed through their relationships in groups or multigroups
-
-* Entity Properties
-These are the properties derived by observation or measurement
-procedures. They can be further divided in:
-- Quantitative :: which include ordinal and cardinal properties; for
- example, customer rating and price of a product.
-- Qualitative :: observed only, for example an enum
-
-* 1st Order Properties
-They are inferred properties not directly observable in entities. They
-are abstractions generalized from multiple observed or measured
-properties that share:
-- The same kind of values;
-- The same allowable manipulations, and;
-- Meaningful comparability.
-
-They arise when several concrete properties, for example salary,
-commission and department budget, meet the above criteria. They are
-understood as deriving from a more general underlying property, in the
-example aforementioned, compensation, budget or whatever. That
-underlying abstraction is the 1OP.
-
-** 1st Order Properties in Context
-These are directly observed or measured properties of entities that
-appear in a specific group in context. In other words, they are
-concrete properties associated with entities of a particular type
-whithin a context of a specific group or multigroup. The context is
-essential because the underlying 1OP may manifest as different 1OPiCs
-depending on the entity type (employee VS department) or group
-membership (sales employees VS marketing employees).
-
-They are related to 1OPs insofar as they are contextual instantiations
-of a 1OP, therefore, all 1OPiCs of the same kind share comparable
-value sets and allow operations because they derive from the same
-root. To illustrate with one example: salary and commission are 1OPiC
-of the 1OP Compensation in the context of employees.
-
-* Constraint Algebra
-The costrait module attempts to implement constraints as relations, in
-other words, it follows the principle that Entity/1OPs/1OPiCs should
-be a relation or domain.
-
-** Core Distinction: Domains vs Relations
-- Domain (#domain{}): A VALUE SET with membership criteria (e.g., integers, rationals)
-- Relation (#relation{}): A SET OF TUPLES
-`less_than` is NOT a domain. It is a binary relation (subset of D × D).
-
-** 1st Order Properties (1OPs)
-A 1OP is a direct property (attribute) of individual entities. 1OPs can be:
-- Observed/measured: directly observed from entities (e.g., `salary`, `commission`)
-- Derived/inferred: computed from other 1OPs (e.g., `compensation` = salary + commission)
-
-** Types
-Domain constraints: ARE the domains themselves (membership criteria)
-- `integer` domain: membership test is_integer/1
-- `rational` domain: numerator, denominator (integers), denominator ≠ 0
-Attribute constraints (1OP constraints): Restrict attribute values
-- Specify which domain the attribute draws from
-- Add constraints beyond domain membership
-- Example: `salary` draws from `rational` + [gt(0), lt(1000000)]
-
-** 1st Order Properties in Context (1OPiCs)
-A 1OPiC is a 1OP that is DERIVED from other observed/measured 1OPs within
-the context of a specific relation.
-Examples in `employees` relation:
-- `salary`: observed/measured 1OP (draws from `money` domain)
-- `commission`: observed/measured 1OP (draws from `money` domain)
-- `compensation`: 1OPiC derived as salary + commission in context of employees
-The schema entry `#{compensation => money}` declares which domain the
-derived 1OPiC draws its values from.
-
-** Binary Relations
-Comparisons are proper relations (not domains!):
-```
-Less_Than_Integer = {(a, b) | a, b in Z, a < b}
-```
-To express "x < 5", we check: `(x, 5) in Less_Than_Integer`
-
-** First-Order Logic Interface
-Constraints become membership tests in relations:
-- `x < 5` becomes `(x, 5) in less_than`
-- `x > 3 AND x < 10` becomes `(3, x) in less_than AND (x, 10) in less_than`
-
-** Negation
-Negation requires an explicit universe: `not(R, Universe)` computes
-the complement of R within Universe.
diff --git a/docs/data-model.puml b/docs/data-model.puml
deleted file mode 100644
index 59e2cff..0000000
--- a/docs/data-model.puml
+++ /dev/null
@@ -1,101 +0,0 @@
-@startuml data-model
-!theme plain
-title Data Model Core Abstractions
-
-class attribute {
- + hash: SHA256
- + value: term()
-}
-
-class tuple {
- + hash: SHA256
- + relation: atom()
- + attribute_map: #{name => hash}
-}
-
-class relation {
- + hash: SHA256
- + name: atom()
- + tree: merkle_tree()
- + schema: #{attr => type_relation}
- + constraints: constraints()
- + cardinality: {finite, N} | aleph_zero | continuum
- + generator: function() | undefined
- + provenance: provenance()
-}
-
-class type_relation {
- + name: atom()
- + erlang_type: type()
- + membership_predicate: function()
- + unfold_generator: function()
- + cardinality: aleph_zero | continuum
-}
-
-class database_state {
- + hash: SHA256
- + name: atom()
- + tree: merkle_tree()
- + relations: #{name => hash}
- + type_relations: #{name => type_relation}
- + timestamp: integer()
-}
-
-database_state "1" *-- "many" relation : contains
-database_state "1" *-- "many" type_relation : defines types
-relation "1" *-- "many" tuple : contains
-tuple "1" *-- "many" attribute : references
-relation --> type_relation : schema references
-
-note right of attribute
- Content-Addressed Storage:
- - SHA-256 hash of value
- - Automatic deduplication
- - Immutable once stored
-
- Value Validation:
- Value must be member of
- type relation specified in schema
-end note
-
-note right of relation
- Storage Strategy:
- - Finite: Merkle tree (materialized)
- - Infinite: Generator (lazy)
-
- Type Relations:
- - Primitives: natural, integer, real, string
- - Composite: views over other relations
-end note
-
-note left of type_relation
- **Schema Philosophy:**
- Attributes are typed by type relations (not primitives).
- Types are relations (stored as infinite relations).
- Schema validation = membership checking.
-
- **Type Relations are Relations:**
- They define domains via:
- 1. Erlang type constraint
- 2. Additional predicates
- 3. Unfold generator for enumeration
-
- Examples:
- - natural[X] :- integer[X], X >= 0
- - complex[X] :- X is (A,B), real[A], real[B]
-end note
-
-note bottom of database_state
- **Schema Example:**
- employee relation schema:
- #{
- name => non_empty_string, % type relation
- age => natural, % type relation
- salary => positive_integer % type relation
- }
-
- Each schema value is a reference to a type relation,
- NOT a primitive type string like "integer"
-end note
-
-@enduml
diff --git a/docs/distribution-architecture.puml b/docs/distribution-architecture.puml
deleted file mode 100644
index fd2fd8c..0000000
--- a/docs/distribution-architecture.puml
+++ /dev/null
@@ -1,63 +0,0 @@
-@startuml distribution-architecture
-!theme plain
-title Future Distribution Architecture (Proposed)
-
-node "Node 1" {
- component "Domino Instance 1" as D1
- database "Mnesia Replica 1" as M1
- component "Merkle Tree 1" as MT1
-}
-
-node "Node 2" {
- component "Domino Instance 2" as D2
- database "Mnesia Replica 2" as M2
- component "Merkle Tree 2" as MT2
-}
-
-node "Node 3" {
- component "Domino Instance 3" as D3
- database "Mnesia Replica 3" as M3
- component "Merkle Tree 3" as MT3
-}
-
-cloud "Sync Protocol" {
- [Hash Comparison] as HC
- [Delta Transfer] as DT
- [Conflict Resolution] as CR
-}
-
-D1 --> MT1
-D2 --> MT2
-D3 --> MT3
-
-MT1 --> M1
-MT2 --> M2
-MT3 --> M3
-
-MT1 <--> HC: compare hashes
-MT2 <--> HC
-MT3 <--> HC
-
-HC --> DT: if different
-DT --> CR: if conflict
-CR --> M1: apply changes
-CR --> M2
-CR --> M3
-
-note right of HC
- Merkle tree enables efficient sync:
- 1. Compare root hashes
- 2. If different, compare subtrees
- 3. Transfer only changed branches
- 4. O(log n) comparison complexity
-end note
-
-note bottom of CR
- Conflict Resolution Strategies:
- - Last-write-wins (timestamp)
- - CRDTs (commutative operations)
- - Consensus (Raft/Paxos)
- - Application-defined merge
-end note
-
-@enduml
diff --git a/docs/features.org b/docs/features.org
deleted file mode 100644
index 98350c5..0000000
--- a/docs/features.org
+++ /dev/null
@@ -1,429 +0,0 @@
-#+TITLE: Domino Features & Roadmap
-
-* Extended Relational Model (RM/T)
- :PROPERTIES:
- :CATEGORY: semantic-modeling
- :END:
- An extended relational database engine based on E. F. Codd's RM/T (1979).
- Captures more meaning through entity types, surrogates, and molecular semantics.
-
-** Implemented Features
-
-*** DONE Content-Addressed Storage
- CLOSED: [2025-01-21]
- - [X] SHA-256 surrogates for all entities
- - [X] System-controlled entity identifiers (E-domain)
- - [X] Automatic deduplication via content addressing
- - [X] Immutable data structures
-
-*** DONE Entity Types (E-Relations)
- CLOSED: [2025-01-21]
- - [X] Kernel entity types (database, relation)
- - [X] E-relations for entity existence assertion
- - [X] Surrogate-based entity identification
- - [X] Entity integrity enforcement
-
-*** DONE Property Relations (P-Relations)
- CLOSED: [2025-01-21]
- - [X] Immediate property storage
- - [X] Single-valued property support
- - [X] Property integrity (Rule 4)
- - [X] Foreign key references via surrogates
-
-*** DONE Merkle Tree Integration
- CLOSED: [2025-01-21]
- - [X] Relations as Merkle trees
- - [X] Database state versioning
- - [X] Efficient state diffing
- - [X] Content-addressed relation hashing
-
-*** DONE Volcano Iterator Model
- CLOSED: [2025-01-21]
- - [X] Lazy tuple evaluation
- - [X] Process-based iterators
- - [X] Message-passing protocol
- - [X] Memory-bounded queries
-
-*** DONE Transactional Operations
- CLOSED: [2025-01-21]
- - [X] ACID guarantees via Mnesia
- - [X] Atomic tuple insertion
- - [X] Tuple retraction with transactions
- - [X] Relation clear and retract operations
-
-*** DONE Comprehensive Test Suite
- CLOSED: [2025-01-21]
- - [X] Database creation tests
- - [X] Relation and tuple operations
- - [X] Hash consistency validation
- - [X] Iterator protocol tests
- - [X] Retraction and truncate tests
- - [X] Join operation tests
- - [X] Provenance tracking tests
-
-*** DONE Attribute-Level Provenance Tracking
- CLOSED: [2025-01-22]
- - [X] Nested metadata structure (meta.provenance)
- - [X] Track immediate source of each attribute
- - [X] Always enabled - every tuple includes metadata
- - [X] Automatic provenance merging in joins
- - [X] Conflict resolution for duplicate attributes
- - [X] Clean separation of data attributes and metadata
- - [X] Foundation for tuple-level lineage tracking (implemented)
-
-** Planned RM/T Extensions
-
-*** TODO Characteristic Entities
- Multi-valued properties as subordinate entities.
- - [ ] Characteristic entity types (C-relations)
- - [ ] Characteristic graph (CG-relation)
- - [ ] Characteristic tree hierarchy
- - [ ] Characteristic integrity (Rule 5)
- - [ ] Example: Job history as characteristic of Employee
-
-*** TODO Associative Entities
- Relationships as first-class entities with properties.
- - [ ] Associative entity types (A-relations)
- - [ ] Association graph (AG-relation)
- - [ ] Association integrity (Rule 6)
- - [ ] Higher-order associations
- - [ ] Example: Assignment(employee, project, start_date)
-
-*** TODO Generalization Hierarchies
- Entity subtyping and inheritance.
- - [ ] Unconditional gen inclusion (UGI-relation)
- - [ ] Alternative gen inclusion (AGI-relation)
- - [ ] Subtype integrity (Rule 7)
- - [ ] Property inheritance
- - [ ] Multiple inheritance support
- - [ ] Example: Engineer/Secretary/Trucker subtypes of Employee
-
-*** TODO Cover Aggregation
- Collections and groupings.
- - [ ] Cover membership relation (KG-relation)
- - [ ] Cover entity types
- - [ ] Dynamic membership
- - [ ] Heterogeneous covers
- - [ ] Example: Convoys, Teams, Departments
-
-*** TODO Event Precedence
- Temporal ordering and scripts.
- - [ ] Event entity types (T-relations)
- - [ ] Unconditional successor (US-relation)
- - [ ] Alternative successor (AS-relation)
- - [ ] Unconditional predecessor (UP-relation)
- - [ ] Alternative predecessor (AP-relation)
- - [ ] Example: Order → Shipment → Invoice
-
-*** TODO Extended Operators
- Operators for molecular semantics.
- - [ ] OUTER NATURAL JOIN
- - [ ] OUTER UNION
- - [ ] OUTER THETA-JOIN
- - [ ] MAYBE operators (null handling)
- - [ ] Graph operators (OPEN, CLOSE, STEP)
-
-*** TODO RM/T Catalog
- Extensible schema catalog.
- - [ ] CATR (relations catalog)
- - [ ] CATRA (relation-attribute)
- - [ ] CATA (attributes catalog)
- - [ ] CATAD (attribute-domain)
- - [ ] CATD (domains catalog)
- - [ ] CATC (categories catalog)
- - [ ] CATRC (relation-category)
-
-*** TODO Property Graph (PG-relation)
- Link P-relations to E-relations.
- - [ ] Property molecule types
- - [ ] Dynamic property schema
- - [ ] Property graph traversal
-
-** Classical Relational Algebra
-
-*** TODO Selection (σ)
- Filter tuples by predicate.
- - [ ] THETA-SELECT with comparison operators
- - [ ] Support for <, ≤, =, ≥, >, ≠
- - [ ] TRUE and MAYBE versions (null handling)
-
-*** TODO Projection (π)
- Project relation onto subset of attributes.
- - [ ] Remove specified columns
- - [ ] Eliminate duplicate rows
- - [ ] Preserve set semantics
-
-*** TODO Cartesian Product (×)
- Cross product of two relations.
- - [ ] Unrestricted concatenation
- - [ ] Basis for joins
-
-*** DONE Natural Join (⋈)
- CLOSED: [2025-01-22]
- Join on common attributes.
- - [X] Equi-join with duplicate removal (equijoin_iterator)
- - [X] Nested loop join strategy
- - [X] Attribute conflict resolution (right_ prefix)
- - [X] Attribute-level provenance tracking
- - [X] Comprehensive test coverage
-
-*** DONE Theta Join (⋈θ)
- CLOSED: [2025-01-22]
- Join with arbitrary comparison.
- - [X] Support all theta operators via predicate functions
- - [X] Generalizes natural join
- - [X] Nested loop join with custom predicates
- - [X] Provenance merging from both sides
-
-*** TODO Union (∪)
- Set union of compatible relations.
- - [ ] Union-compatible constraint
- - [ ] Duplicate removal
-
-*** TODO Intersection (∩)
- Set intersection.
- - [ ] Union-compatible constraint
-
-*** TODO Difference (−)
- Set difference.
- - [ ] Union-compatible constraint
-
-*** TODO Divide (÷)
- Relational division.
- - [ ] Algebraic universal quantifier
-
-*** TODO Rename (ρ)
- Attribute renaming.
- - [ ] Enable self-joins
-
-** Advanced Features
-
-*** DONE Tuple-Level Lineage Tracking
- CLOSED: [2025-01-23]
- Track complete operational history of each tuple through query pipelines.
-
-**** Status: IMPLEMENTED
-
-**** API Usage
-
- Metadata tracking is ALWAYS enabled. All tuples include the 'meta' field:
- #+BEGIN_SRC erlang
- %% Simple query (metadata automatically included)
- Iter = operations:get_tuples_iterator(DB, employees, #{}).
-
- %% With constraints (metadata still included)
- Iter = operations:get_tuples_iterator(DB, employees,
- #{constraints => #{age => {gt, 30}}}).
-
- %% Results always have:
- %% #{id => 1, name => "Alice",
- %% meta => #{
- %% provenance => #{id => {employees, id}, name => {employees, name}},
- %% lineage => {base, employees}
- %% }}
- #+END_SRC
-
-**** Provenance vs. Lineage
-
- - *Provenance* (IMPLEMENTED): Tracks immediate source of each attribute
- #+BEGIN_SRC erlang
- meta => #{
- provenance => #{
- name => {employees, name},
- dept_name => {departments, dept_name}
- }
- }
- #+END_SRC
-
- - *Lineage* (IMPLEMENTED): Tracks complete operation tree that created the tuple
- #+BEGIN_SRC erlang
- meta => #{
- provenance => #{...}, % Attribute sources
- lineage => {project, [name, dept_name],
- {join, dept_id,
- {select, fun(T) -> maps:get(age, T) > 30 end,
- {base, employees}},
- {base, departments}}}
- }
- #+END_SRC
-
-**** Type Definition
-
- #+BEGIN_SRC erlang
- -type lineage_op() ::
- {base, atom()} % Base relation
- | {select, fun(), lineage_op()} % Filter operation
- | {project, [atom()], lineage_op()} % Projection
- | {join, atom(), lineage_op(), lineage_op()} % Join on attribute
- | {theta_join, fun(), lineage_op(), lineage_op()} % Join with predicate
- | {sort, fun(), lineage_op()} % Sort operation
- | {take, pos_integer(), lineage_op()} % Limit operation
- | {aggregate, atom(), atom(), lineage_op()}. % Aggregation (future)
- #+END_SRC
-
-**** Use Cases
-
- - [ ] *Updatable Views*: Propagate updates back to base relations
- - Example: =UPDATE view SET name = "Bob"= → trace lineage → =UPDATE employees SET name = "Bob"=
-
- - [ ] *Incremental View Maintenance*: Identify affected tuples when base data changes
- - Example: If employees tuple changes, lineage identifies which view tuples need recomputation
-
- - [ ] *Audit Trails*: Track how sensitive data was transformed
- - Example: "This salary came from: employees → join(departments) → aggregate(avg)"
-
- - [ ] *Query Explanation*: Debug complex pipelines
- - Example: "Why is this tuple here?" → inspect lineage to see operations applied
-
- - [ ] *Provenance Queries*: Query the lineage itself
- - Example: "Show all tuples derived from employees.id=5"
-
-**** Implementation Plan
-
- 1. [X] Extend =tuple_metadata()= type with lineage field
- 2. [X] Add lineage construction to each iterator operator
- - [X] select_iterator: ={select, Predicate, ChildLineage}=
- - [X] project_iterator: ={project, Attributes, ChildLineage}=
- - [X] equijoin_iterator: ={join, Attr, LeftLineage, RightLineage}=
- - [X] theta_join_iterator: ={theta_join, Pred, LeftLineage, RightLineage}=
- - [X] sort_iterator: ={sort, CompareFun, ChildLineage}=
- - [X] take_iterator: ={take, N, ChildLineage}=
- 3. [X] Implement lineage merging in joins (combine operation trees)
- 4. [ ] Add lineage querying API for provenance analysis (future)
- 5. [ ] Use lineage for updatable view update propagation (future)
- 6. [X] Add comprehensive lineage tests
-
-**** Relation to Existing Code
-
- Relation-level provenance already exists in =#relation.provenance=:
- #+BEGIN_SRC erlang
- -type provenance() ::
- {base, atom()}
- | {join, provenance(), provenance()}
- | {select, provenance(), constraints()}
- | {project, provenance(), [atom()]}
- | {take, provenance(), pos_integer()}.
- #+END_SRC
-
- This tracks where *relations* come from. Lineage is the *tuple-level*
- equivalent, tracking operational history of individual tuples through
- query pipelines.
-
-**** Example: Complete Lineage Chain
-
- #+BEGIN_SRC erlang
- % Query: employees → select(age>30) → join(departments) → project([name, dept_name])
-
- % Resulting tuple with full lineage:
- #{name => "Alice",
- dept_name => "Engineering",
- meta => #{
- provenance => #{
- name => {employees, name},
- dept_name => {departments, dept_name}
- },
- lineage => {
- project, [name, dept_name],
- {join, dept_id,
- {select, fun(T) -> maps:get(age, T) > 30 end,
- {base, employees}},
- {base, departments}}}
- }}
-
- % From this lineage, we can:
- % 1. Trace "Alice" back to employees.name
- % 2. Know she passed the age>30 filter
- % 3. See she was joined with departments on dept_id
- % 4. Understand the projection removed other attributes
- #+END_SRC
-
-*** TODO Null Value Handling
- Three-valued logic for incomplete information.
- - [ ] "Value unknown" null type
- - [ ] "Property inapplicable" null type
- - [ ] E-null for "entity unknown"
- - [ ] Null substitution principle
- - [ ] MAYBE operators
-
-*** TODO Infinite Relations (Aleph₀)
- Countably infinite relation generators.
- - [ ] Integer generator
- - [ ] Arithmetic relations (plus, minus, multiply)
- - [ ] Lazy evaluation with constraints
- - [ ] Partially materialized storage
- - [ ] User-defined generators
-
-*** TODO View Integration
- Merging multiple perspectives.
- - [ ] View definition language
- - [ ] Automatic view maintenance
- - [ ] View update translation
-
-*** TODO Temporal Support
- Time-varying databases.
- - [ ] Transaction time
- - [ ] Valid time
- - [ ] Bitemporal support
- - [ ] Non-forgetting mode
-
-*** TODO Distributed Synchronization
- Merkle tree-based replication.
- - [ ] Efficient diff protocol
- - [ ] Conflict resolution
- - [ ] Eventual consistency
-
-** Developer Experience
-
-*** TODO Query Language
- High-level relational query language.
- - [ ] Algebraic syntax
- - [ ] Tuple calculus variant
- - [ ] Domain calculus variant
-
-*** TODO REPL Console
- Interactive query environment.
- - [ ] Live query evaluation
- - [ ] Schema introspection
- - [ ] Query history
-
-*** TODO Visualization Tools
- Graphical representation.
- - [ ] Relation browser
- - [ ] Join graph visualization
- - [ ] Generalization hierarchy diagrams
- - [ ] Query execution plans
-
-*** TODO Org Babel Integration
- Literate programming support.
- - [ ] Execute relational queries in org-mode
- - [ ] Export results to tables
- - [ ] Reproducible analysis
-
-** Documentation & Validation
-
-*** DONE Code Documentation
- CLOSED: [2025-01-21]
- - [X] Module architecture overview
- - [X] Data structure specifications
- - [X] Usage examples
- - [X] Implementation details
-
-*** TODO Theoretical Validation
- Ensure correctness.
- - [ ] Formal proofs of operator properties
- - [ ] Closure verification
- - [ ] Idempotence tests
- - [ ] Property-based testing with PropEr
- - [ ] Comparison with Date & Darwen semantics
-
-*** TODO Research Papers
- Link to foundational theory.
- - [ ] Codd 1970: "A Relational Model"
- - [ ] Codd 1979: "Extending the Database Relational Model" (RM/T)
- - [ ] Codd 1985: "Does AlOL Need the Relational Algebra?"
- - [ ] Date & Darwen: "Foundation for Future Database Systems"
-
-** References
- - E. F. Codd (1979). "Extending the Database Relational Model to Capture More Meaning"
- - E. F. Codd (1970). "A Relational Model of Data for Large Shared Data Banks"
- - C. J. Date & Hugh Darwen. "Foundation for Future Database Systems: The Third Manifesto"
diff --git a/docs/infinite-relations.puml b/docs/infinite-relations.puml
deleted file mode 100644
index cd6aa0f..0000000
--- a/docs/infinite-relations.puml
+++ /dev/null
@@ -1,177 +0,0 @@
-@startuml infinite-relations
-!theme plain
-title Infinite Type Relations: Unfold Pattern & Membership
-
-abstract class InfiniteTypeRelation {
- + name: atom()
- + cardinality: aleph_zero | continuum
- + erlang_type: type()
- + membership_predicate: function()
- + unfold_generator: function()
- + constraints: constraints()
-}
-
-class Naturals {
- Erlang Type: integer()
- Membership: natural[X] :-
- erlang:is_integer(X),
- X >= 0.
-
- Unfold Generator:
- unfold(0, fun(N) -> N + 1 end)
- → 0, 1, 2, 3, ...
-
- Cardinality: aleph_zero
-}
-
-class Integers {
- Erlang Type: integer()
- Membership: integer[X] :-
- erlang:is_integer(X).
-
- Unfold Generator:
- unfold({0, 1}, fun({N, S}) ->
- case S of
- 1 -> {N, -1};
- -1 -> {N+1, 1}
- end
- end)
- → 0, 1, -1, 2, -2, ...
-
- Cardinality: aleph_zero
-}
-
-class Rationals {
- Erlang Type: {integer(), integer()}
- Membership: rational[X] :-
- X is (P, Q),
- integer[P], integer[Q],
- Q ≠ 0.
-
- Unfold Generator:
- Stern-Brocot tree unfold
- → (0,1), (1,1), (1,2), ...
-
- Cardinality: aleph_zero
-}
-
-class Reals {
- Erlang Type: float()
- Membership: real[X] :-
- erlang:is_float(X).
-
- Unfold Generator:
- IEEE 754 float enumeration
- (impractical, continuum)
-
- Cardinality: continuum
-}
-
-class Strings {
- Erlang Type: string()
- Membership: string[X] :-
- erlang:is_list(X),
- all_chars(X).
-
- Unfold Generator:
- Length-ordered, lexicographic
- → "", "a", "b", ..., "aa", ...
-
- Cardinality: aleph_zero
-}
-
-class Plus {
- Erlang Type: {integer(), integer(), integer()}
- Membership: plus[A, B, Sum] :-
- integer[A], integer[B],
- Sum is A + B.
-
- Unfold Generator:
- Diagonalization of ℤ × ℤ
- → (0,0,0), (0,1,1), (1,0,1), ...
-
- Cardinality: continuum (ℤ × ℤ)
-}
-
-package "Constraint Application" {
- class ConstrainedGenerator {
- Pattern: Apply constraints during unfold
- base_generator: unfold()
- constraints: [constraint()]
- ..
- filter_unfold(Gen, Constraints) ->
- filter(Gen, satisfies_all(Constraints))
- }
-
- note bottom of ConstrainedGenerator
- **Example 1: Bounded Naturals**
- natural[X], X >= 0, X =< 100
- → Generates: 0, 1, 2, ..., 100 (finite!)
-
- **Example 2: Constrained Plus**
- plus[5, B, 12]
- → Solves: B = 7
- → Generates: single tuple (5, 7, 12)
-
- **Example 3: Range Query**
- integer[X], X >= -10, X =< 10
- → Generates: 0, 1, -1, 2, -2, ..., 10, -10
- end note
-}
-
-package "Composite Type Relations (Views)" {
- class Complex {
- Type Definition (Karuta):
- complex[X] :-
- X is (A, B),
- real[A], real[B].
-
- Implementation:
- Unfold real × real with
- product enumeration
-
- Not primitive: Defined as view
- over primitive real relation
- }
-}
-
-InfiniteTypeRelation <|-- Naturals
-InfiniteTypeRelation <|-- Integers
-InfiniteTypeRelation <|-- Rationals
-InfiniteTypeRelation <|-- Reals
-InfiniteTypeRelation <|-- Strings
-InfiniteTypeRelation <|-- Plus
-
-Naturals --> ConstrainedGenerator: apply\nconstraints
-Integers --> ConstrainedGenerator
-Rationals --> ConstrainedGenerator
-Plus --> ConstrainedGenerator
-
-Reals --> Complex: used in\nproduct
-Complex : (view relation)
-
-note right of Naturals
- **Core Pattern:** Infinite relations use unfold
- to generate members. Primitive types are
- infinite relations.
-
- **Membership:** Erlang type + constraints
-
- **Unfold Pattern:**
- unfold(Seed, Step) generates infinite sequence:
- Seed, Step(Seed), Step(Step(Seed)), ...
-
- This enables lazy evaluation and
- constraint-based pruning.
-end note
-
-note left of Plus
- **Function Relations:**
- Mathematical functions like plus, multiply, etc.
- are relations with continuum cardinality.
-
- Membership = evaluation
- Generation = enumeration
-end note
-
-@enduml
diff --git a/docs/infinite_relations_design.org b/docs/infinite_relations_design.org
deleted file mode 100644
index c13e783..0000000
--- a/docs/infinite_relations_design.org
+++ /dev/null
@@ -1,348 +0,0 @@
-#+TITLE: Infinite Relations Design
-#+AUTHOR: Nekoma Team
-#+DATE: 2025-01-22
-
-* Overview
-
-This document specifies the design for infinite relations in Domino, enabling
-countably infinite (ℵ₀) and uncountably infinite (continuum) relations alongside
-finite relations.
-
-** Core Principle
-
-*Everything is a relation.* Types are relations. Functions are relations.
-Numbers are relations. This unifies the type system with the data model.
-
-* Cardinality System
-
-** Cardinality Types
-
-#+BEGIN_SRC erlang
--type cardinality() :: {finite, non_neg_integer()} % Finite set with N elements
- | aleph_zero % Countably infinite (ℵ₀)
- | continuum. % Uncountably infinite (2^ℵ₀)
-#+END_SRC
-
-** Properties
-
-- Finite relations have explicit tuple count: ={finite, 1000}=
-- Countably infinite relations: =aleph_zero=
-- Uncountably infinite relations: =continuum=
-
-* Extended Data Model
-
-** Modified Relation Record
-
-#+BEGIN_SRC erlang
--record(relation, {
- hash, % Content hash (for finite) or generator hash (for infinite)
- name, % Relation name (atom)
- tree, % Merkle tree (finite only), undefined for infinite
- schema, % Schema definition
- cardinality, % {finite, N} | aleph_zero | continuum
- generator, % Generator function (infinite only), undefined for finite
- is_function % Boolean: true if relation represents a function
-}).
-#+END_SRC
-
-** Generator Specification
-
-Generators are functions that produce tuples on-demand based on constraints.
-
-#+BEGIN_SRC erlang
--type generator_spec() ::
- {primitive, atom()} % Built-in: naturals, integers, rationals
- | {function_relation, atom(), arity()} % Built-in function: plus/3, times/3
- | {custom, fun((constraints()) -> tuple())} % User-defined generator
- | {derived, relational_operation()}. % Derived from other relations
-
--type constraints() :: #{
- attribute_name() => constraint_spec()
-}.
-
--type constraint_spec() ::
- {eq, value()} % Equality: age = 30
- | {neq, value()} % Inequality: age ≠ 30
- | {lt, value()} % Less than: age < 30
- | {lte, value()} % Less than or equal
- | {gt, value()} % Greater than
- | {gte, value()} % Greater than or equal
- | {in, [value()]} % Membership: age in [20,30,40]
- | {range, value(), value()} % Range: age in [20..30]
- | {member_of, relation_name()}. % Type constraint: x ∈ Naturals
-#+END_SRC
-
-* Primitive Infinite Relations
-
-** Naturals (ℕ)
-
-#+BEGIN_SRC erlang
-Schema: #{value => natural}
-Cardinality: aleph_zero
-Generator: Enumerates 0, 1, 2, 3, ...
-
-Example tuples:
- #{value => 0}
- #{value => 1}
- #{value => 2}
- ...
-#+END_SRC
-
-** Integers (ℤ)
-
-#+BEGIN_SRC erlang
-Schema: #{value => integer}
-Cardinality: aleph_zero
-Generator: Enumerates 0, 1, -1, 2, -2, 3, -3, ...
-
-Enumeration strategy: Interleave positive and negative
- f(0) = 0
- f(2n) = n (for n > 0)
- f(2n+1) = -n (for n > 0)
-#+END_SRC
-
-** Rationals (ℚ)
-
-#+BEGIN_SRC erlang
-Schema: #{numerator => integer, denominator => natural}
-Cardinality: aleph_zero
-Generator: Stern-Brocot tree or Calkin-Wilf sequence
-
-Constraints:
- - denominator > 0
- - gcd(numerator, denominator) = 1 (reduced form)
-
-Example tuples:
- #{numerator => 0, denominator => 1} % 0/1
- #{numerator => 1, denominator => 1} % 1/1
- #{numerator => 1, denominator => 2} % 1/2
- #{numerator => 2, denominator => 1} % 2/1
- #{numerator => 1, denominator => 3} % 1/3
- ...
-#+END_SRC
-
-** Reals (ℝ) - Theoretical
-
-#+BEGIN_SRC erlang
-Schema: #{value => real}
-Cardinality: continuum
-Generator: Not fully enumerable (conceptual only)
-
-Note: In practice, we can only represent computable reals or
- algebraic reals (still countable but more expressive).
-#+END_SRC
-
-* Function Relations
-
-A relation R is a *function* if: ∀a ∃!b : (a,b) ∈ R
-
-That is, for every input, there exists exactly one output.
-
-** Plus (addition)
-
-#+BEGIN_SRC erlang
-Name: plus
-Schema: #{a => number, b => number, sum => number}
-Cardinality: aleph_zero (when a,b ∈ ℤ or ℚ)
-Is_function: false (it's a 3-way relation, not (a,b) → sum)
-Function view: plus(a, b) = sum where (a, b, sum) ∈ Plus
-
-Generator: For each (a, b) pair, yield #{a => A, b => B, sum => A + B}
-
-Constraints required for iteration:
- - At least two of {a, b, sum} must be bounded
-
-Valid queries:
- - plus where a ∈ [0..10], b ∈ [0..10] ✓ (finite result: 121 tuples)
- - plus where a = 5, b ∈ Naturals ✓ (countable: {5,0,5}, {5,1,6}, ...)
- - plus where sum = 10 ✓ (countable: infinitely many a+b=10)
- - plus with no constraints ✗ (cannot enumerate)
-#+END_SRC
-
-** Times (multiplication)
-
-#+BEGIN_SRC erlang
-Name: times
-Schema: #{a => number, b => number, product => number}
-Cardinality: aleph_zero
-Generator: For each (a, b) pair, yield #{a => A, b => B, product => A * B}
-#+END_SRC
-
-** Successor
-
-#+BEGIN_SRC erlang
-Name: successor
-Schema: #{n => integer, succ => integer}
-Cardinality: aleph_zero
-Is_function: true (n → succ is a function)
-Generator: For each n, yield #{n => N, succ => N + 1}
-#+END_SRC
-
-* Cardinality Algebra
-
-Rules for computing cardinality of relational operations:
-
-** Union (∪)
-
-| Left | Right | Result | Notes |
-|------------+------------+------------+--------------------------|
-| {finite,m} | {finite,n} | {finite,≤m+n} | May have duplicates |
-| {finite,n} | aleph_zero | aleph_zero | Finite + infinite = infinite |
-| aleph_zero | aleph_zero | aleph_zero | ℵ₀ + ℵ₀ = ℵ₀ |
-| aleph_zero | continuum | continuum | Continuum dominates |
-| continuum | continuum | continuum | 2^ℵ₀ + 2^ℵ₀ = 2^ℵ₀ |
-
-** Cartesian Product (×)
-
-| Left | Right | Result | Notes |
-|------------+------------+---------------+-----------------------|
-| {finite,m} | {finite,n} | {finite,m·n} | Product of sizes |
-| {finite,n} | aleph_zero | aleph_zero | n · ℵ₀ = ℵ₀ |
-| aleph_zero | aleph_zero | aleph_zero | ℵ₀ · ℵ₀ = ℵ₀ |
-| aleph_zero | continuum | continuum | ℵ₀ · 2^ℵ₀ = 2^ℵ₀ |
-| continuum | continuum | continuum | 2^ℵ₀ · 2^ℵ₀ = 2^ℵ₀ |
-
-** Natural Join (⋈)
-
-Cardinality depends on join selectivity:
-
-| Left | Right | Result | Notes |
-|------------+------------+------------+------------------------------------|
-| {finite,m} | {finite,n} | {finite,k} | k ≤ min(m,n) typically |
-| {finite,n} | aleph_zero | varies | Could be finite or aleph_zero |
-| aleph_zero | aleph_zero | varies | Depends on join condition |
-
-*Examples:*
-
-#+BEGIN_SRC erlang
-%% Finite ⋈ Infinite (finite result)
-Employees = {finite, 100} % 100 employees
-Naturals = aleph_zero % Infinite numbers
-Employees ⋈ Naturals on employee_id = value
- ⇒ {finite, 100} % At most 100 matches
-
-%% Infinite ⋈ Infinite (infinite result)
-Naturals ⋈ Plus on Naturals.value = Plus.a
- ⇒ aleph_zero % For each natural n, infinite (n,b,n+b) tuples
-
-%% Infinite ⋈ Infinite (finite result with constraints)
-(Naturals where value < 10) ⋈ (Naturals where value < 10)
- ⇒ {finite, 100} % 10 × 10 = 100
-#+END_SRC
-
-** Selection (σ)
-
-| Input | Result | Notes |
-|------------+------------+------------------------------------|
-| {finite,n} | {finite,k} | k ≤ n (filtering reduces size) |
-| aleph_zero | varies | Could be finite or aleph_zero |
-| continuum | varies | Could be any cardinality |
-
-*Examples:*
-
-#+BEGIN_SRC erlang
-σ(Naturals, value < 10) ⇒ {finite, 10} % Bounded selection
-σ(Naturals, value > 5) ⇒ aleph_zero % Infinite result
-σ(Naturals, value % 2 = 0) ⇒ aleph_zero % Even numbers (infinite)
-#+END_SRC
-
-** Projection (π)
-
-| Input | Result | Notes |
-|------------+------------+---------------------------------|
-| {finite,n} | {finite,k} | k ≤ n (duplicates removed) |
-| aleph_zero | aleph_zero | Typically preserves infinity |
-| continuum | continuum | Typically preserves cardinality |
-
-* Constraint System
-
-** Mandatory Constraints
-
-Queries on infinite relations *must* provide constraints that bound the iteration.
-
-#+BEGIN_SRC erlang
-%% INVALID: Cannot iterate infinite relation without constraints
-Iterator = get_tuples_iterator(DB, naturals). ❌
-
-%% VALID: Bounded by constraint
-Iterator = get_tuples_iterator(DB, naturals, #{value => {range, 0, 100}}). ✓
-#+END_SRC
-
-** Constraint Validation
-
-Before executing a query on an infinite relation, validate:
-
-1. *Finite result check*: Do constraints guarantee finite result?
-2. *Bounded iteration check*: Can we enumerate results in finite time?
-
-#+BEGIN_SRC erlang
-validate_constraints(Relation, Constraints) ->
- case Relation#relation.cardinality of
- {finite, _} ->
- ok; % Finite relations don't need constraints
- aleph_zero ->
- case infers_finite_result(Relation, Constraints) of
- true -> ok;
- false -> {error, unbounded_iteration}
- end;
- continuum ->
- {error, uncountable_relation} % Cannot iterate continuum
- end.
-#+END_SRC
-
-** Constraint Inference
-
-Smart inference of finiteness:
-
-#+BEGIN_SRC erlang
-%% These constraints imply finite results:
-#{value => {range, 0, 100}} % Range is finite
-#{value => {in, [1,2,3,4,5]}} % Membership in finite set
-#{value => {lt, 10}} % Bounded above (if naturals)
-
-%% These do NOT imply finite results:
-#{value => {gt, 10}} % Unbounded above
-#{value => {neq, 5}} % Excludes one element
-#+END_SRC
-
-* Implementation Strategy
-
-** Phase 1: Core Infrastructure
-1. Extend =relation= record with =cardinality= and =generator= fields
-2. Implement cardinality algebra functions
-3. Add constraint validation framework
-
-** Phase 2: Primitive Relations
-1. Implement =naturals= generator
-2. Implement =integers= generator (interleaving strategy)
-3. Implement =rationals= generator (Stern-Brocot or Calkin-Wilf)
-
-** Phase 3: Function Relations
-1. Implement =plus= relation
-2. Implement =times= relation
-3. Implement =successor=, =predecessor= relations
-
-** Phase 4: Query Engine
-1. Extend iterator model to support generators
-2. Implement constraint-based enumeration
-3. Add cardinality inference for query results
-
-** Phase 5: Relational Operators
-1. Extend join to handle infinite relations
-2. Extend union with cardinality tracking
-3. Extend selection with constraint propagation
-
-* Open Questions
-
-1. *Partial materialization*: Should we cache generated tuples?
-2. *Generator composition*: How to compose generators for derived relations?
-3. *Constraint solving*: Do we need a full constraint solver (CLP)?
-4. *Termination guarantees*: How to prove queries terminate?
-5. *Physical storage*: How to persist infinite relation definitions?
-
-* References
-
-- Codd, E. F. (1979). "Extending the Database Relational Model"
-- Cantor, G. "On the Cardinality of Sets"
-- Stern-Brocot Tree: Enumeration of rationals
-- Calkin-Wilf Tree: Alternative rational enumeration
diff --git a/docs/iterator-pipeline.puml b/docs/iterator-pipeline.puml
deleted file mode 100644
index 810ef63..0000000
--- a/docs/iterator-pipeline.puml
+++ /dev/null
@@ -1,45 +0,0 @@
-@startuml iterator-pipeline
-!theme plain
-title Volcano Iterator Model - Process Pipeline
-
-component "Scan Iterator\n(Process)" as P1 {
- [Read from Relation] as S1
-}
-
-component "Select Iterator\n(Process)" as P2 {
- [Apply Predicate] as S2
-}
-
-component "Project Iterator\n(Process)" as P3 {
- [Filter Attributes] as S3
-}
-
-component "Sort Iterator\n(Process)" as P4 {
- [Sort All Tuples] as S4
-}
-
-component "Take Iterator\n(Process)" as P5 {
- [Limit N Tuples] as S5
-}
-
-P1 -right-> P2: {next, Pid}\n{tuple, T} | done
-P2 -right-> P3: {next, Pid}\n{tuple, T} | done
-P3 -right-> P4: {next, Pid}\n{tuple, T} | done
-P4 -right-> P5: {next, Pid}\n{tuple, T} | done
-
-note bottom of P1
- Each iterator is an
- independent Erlang process
-
- Message passing protocol:
- - Request: {next, CallerPid}
- - Response: {tuple, Data} | done
-
- Benefits:
- - Lazy evaluation
- - Memory-bounded
- - Composable
- - Concurrent
-end note
-
-@enduml
diff --git a/docs/karuta-interface-layers.puml b/docs/karuta-interface-layers.puml
deleted file mode 100644
index cd5443e..0000000
--- a/docs/karuta-interface-layers.puml
+++ /dev/null
@@ -1,65 +0,0 @@
-@startuml karuta-interface-layers
-!theme plain
-title Karuta Interface Architecture Layers
-
-actor "Karuta Program" as Karuta
-
-package "Language Bindings" {
- [Karuta FFI] as FFI
- [Erlang Ports] as Ports
- [NIF (Native)] as NIF
-}
-
-package "Stable Contract Layer" {
- component "domino.erl" as DominoAPI {
- [create_database/1]
- [execute/2]
- [query/2]
- [transaction/2]
- }
-}
-
-package "Command Processor" {
- [Command Parser] as CmdParser
- [Validator] as Validator
- [Executor] as Executor
-}
-
-package "Implementation Layer" {
- component "operations.erl" as Ops {
- [create_relation/3]
- [create_tuple/3]
- [get_tuples_iterator/2]
- [etc...]
- }
-}
-
-database "Mnesia Storage" as Storage
-
-Karuta --> FFI
-Karuta --> Ports
-Karuta --> NIF
-
-FFI --> DominoAPI
-Ports --> DominoAPI
-NIF --> DominoAPI
-
-DominoAPI --> CmdParser
-CmdParser --> Validator
-Validator --> Executor
-
-Executor --> Ops
-Ops --> Storage
-
-note right of DominoAPI
- Stable, versioned interface
- Command format:
- {Verb, Entity, Name, Parameters}
-
- Examples:
- {create, relation, employee, Schema}
- {insert, tuple, employee, Data}
- {query, relation, employee, Constraints}
-end note
-
-@enduml
diff --git a/docs/merkle-tree-hierarchy.puml b/docs/merkle-tree-hierarchy.puml
deleted file mode 100644
index 4e86e26..0000000
--- a/docs/merkle-tree-hierarchy.puml
+++ /dev/null
@@ -1,50 +0,0 @@
-@startuml merkle-tree-hierarchy
-!theme plain
-title Merkle Tree Hierarchy
-
-package "Database Level" {
- [Database Hash] as DBHash
- database "Merkle Tree of Relations" as DBTree
-}
-
-package "Relation Level" {
- [Relation Hash 1] as R1Hash
- [Relation Hash 2] as R2Hash
- [Relation Hash N] as RNHash
-
- database "Merkle Tree of Tuples (R1)" as R1Tree
- database "Merkle Tree of Tuples (R2)" as R2Tree
-}
-
-package "Tuple Level" {
- [Tuple Hash 1.1] as T11
- [Tuple Hash 1.2] as T12
- [Tuple Hash 2.1] as T21
- [Tuple Hash 2.2] as T22
-}
-
-DBHash --> DBTree
-DBTree --> R1Hash
-DBTree --> R2Hash
-DBTree --> RNHash
-
-R1Hash --> R1Tree
-R2Hash --> R2Tree
-
-R1Tree --> T11
-R1Tree --> T12
-R2Tree --> T21
-R2Tree --> T22
-
-note bottom of DBHash
- Single hash represents
- entire database state
-
- Benefits:
- - Version control
- - Efficient diffing
- - Distributed sync
- - Integrity verification
-end note
-
-@enduml
diff --git a/docs/module-dependencies.puml b/docs/module-dependencies.puml
deleted file mode 100644
index dcc955b..0000000
--- a/docs/module-dependencies.puml
+++ /dev/null
@@ -1,39 +0,0 @@
-@startuml module-dependencies
-!theme plain
-title Module Dependencies
-
-package "Entry Points" {
- [main.erl] as Main
- [repl.erl] as REPL
-}
-
-package "Server Layer" {
- [xml_server_app.erl] as XMLApp
- [xml_server.erl] as XMLServer
-}
-
-package "Query Layer" {
- [query_planner.erl] as Planner
-}
-
-package "Core Layer" {
- [operations.erl] as Ops
-}
-
-package "Support Modules" {
- [relation.erl] as Relation
- [generators.erl] as Generators
- [visualizer.erl] as Visualizer
-}
-
-Main --> XMLApp
-XMLApp --> XMLServer
-XMLServer --> Planner
-REPL --> Ops
-
-Planner --> Ops
-Ops --> Relation
-Ops --> Generators
-Ops --> Visualizer
-
-@enduml
diff --git a/docs/provenance-lineage.puml b/docs/provenance-lineage.puml
deleted file mode 100644
index 4e8f68a..0000000
--- a/docs/provenance-lineage.puml
+++ /dev/null
@@ -1,69 +0,0 @@
-@startuml provenance-lineage
-!theme plain
-title Provenance and Lineage Tracking
-
-component "Base Tuple\n(employee)" as T1
-
-note right of T1
- **Data:**
- id = 1
- name = "Alice"
- age = 30
- salary = 100000
-
- **Meta:**
- provenance:
- id: (employee, id)
- name: (employee, name)
- age: (employee, age)
- salary: (employee, salary)
- lineage: (base, employee)
-end note
-
-component "After Select\n(age > 25)" as T2
-
-note right of T2
- **Data:**
- id = 1
- name = "Alice"
- age = 30
- salary = 100000
-
- **Meta:**
- provenance: (same as above)
- lineage:
- select
- fun(T) -> age > 25 end
- (base, employee)
-end note
-
-component "After Project\n[name, age]" as T3
-
-note right of T3
- **Data:**
- name = "Alice"
- age = 30
-
- **Meta:**
- provenance:
- name: (employee, name)
- age: (employee, age)
- lineage:
- project
- [name, age]
- (select, _, (base, employee))
-end note
-
-T1 -down-> T2: select operation\nadds to lineage
-T2 -down-> T3: project operation\nfilters provenance
-
-note bottom of T3
- **Benefits:**
- - Data source tracking
- - Query result explanation
- - Reproducibility
- - Debugging support
- - Foundation for updatable views
-end note
-
-@enduml
diff --git a/docs/query-execution-flow.puml b/docs/query-execution-flow.puml
deleted file mode 100644
index 998b0e2..0000000
--- a/docs/query-execution-flow.puml
+++ /dev/null
@@ -1,54 +0,0 @@
-@startuml query-execution-flow
-!theme plain
-title Query Execution Flow
-
-actor Client
-participant "Query Planner" as Planner
-participant "Scan Iterator" as Scan
-participant "Select Iterator" as Select
-participant "Project Iterator" as Project
-participant "Operations API" as Ops
-database "Mnesia" as DB
-
-Client -> Planner: query({project,\n {select, {scan, employee}, Pred},\n [name, age]})
-activate Planner
-Planner -> Planner: parse & validate
-Planner -> Scan: spawn_iterator(employee)
-activate Scan
-Planner -> Select: spawn_iterator(Scan, Pred)
-activate Select
-Planner -> Project: spawn_iterator(Select, [name, age])
-activate Project
-Planner --> Client: {ok, Iterator}
-deactivate Planner
-
-Client -> Project: next_tuple()
-Project -> Select: next_tuple()
-Select -> Scan: next_tuple()
-Scan -> Ops: get_tuples_iterator(employee)
-Ops -> DB: transaction(read tuple)
-DB --> Ops: {ok, Tuple}
-Ops --> Scan: {ok, Tuple}
-Scan --> Select: {ok, Tuple}
-Select -> Select: apply Predicate
-Select --> Project: {ok, Tuple}
-Project -> Project: filter attributes
-Project --> Client: {ok, #{name => "Alice", age => 30}}
-
-Client -> Project: next_tuple()
-Project -> Select: next_tuple()
-Select -> Scan: next_tuple()
-Scan -> Ops: get_tuples_iterator(employee)
-Ops -> DB: transaction(read tuple)
-DB --> Ops: done
-Ops --> Scan: done
-Scan --> Select: done
-Select --> Project: done
-Project --> Client: done
-
-Client -> Project: close_iterator()
-deactivate Project
-deactivate Select
-deactivate Scan
-
-@enduml
diff --git a/docs/reactive-queries.puml b/docs/reactive-queries.puml
deleted file mode 100644
index bcba5d5..0000000
--- a/docs/reactive-queries.puml
+++ /dev/null
@@ -1,51 +0,0 @@
-@startuml reactive-queries
-!theme plain
-title Reactive Queries (Live Subscriptions)
-
-participant "Client" as C
-participant "Subscription Manager" as SM
-participant "Domino" as D
-database "employee relation" as E
-
-C -> SM: subscribe(query: "age > 30")
-activate SM
-SM -> D: register_subscription(employee, predicate)
-activate D
-D -> E: initial query
-E --> D: [{name: "Alice", age: 35}]
-D --> SM: initial results
-SM --> C: [{name: "Alice", age: 35}]
-deactivate D
-deactivate SM
-
-... time passes ...
-
-C -> D: insert(employee, {name: "Bob", age: 32})
-activate D
-D -> D: check affected subscriptions
-D -> D: evaluate predicate on new tuple\n(32 > 30? yes)
-D -> SM: notify(subscription_id, {inserted: "Bob"})
-activate SM
-SM -> C: delta: {inserted: {name: "Bob", age: 32}}
-deactivate SM
-deactivate D
-
-C -> D: insert(employee, {name: "Charlie", age: 25})
-activate D
-D -> D: evaluate predicate\n(25 > 30? no)
-D -> D: no notification needed
-deactivate D
-
-note right of SM
- Subscription tracks:
- - Relation(s) to watch
- - Filter predicate
- - Client callback
-
- On mutation:
- 1. Check which subscriptions affected
- 2. Evaluate predicate on changed tuples
- 3. Send delta (not full re-query)
-end note
-
-@enduml
diff --git a/docs/reliability_roadmap.org b/docs/reliability_roadmap.org
deleted file mode 100644
index d47a00e..0000000
--- a/docs/reliability_roadmap.org
+++ /dev/null
@@ -1,223 +0,0 @@
-#+TITLE: Sakura Reliability Roadmap (Precise)
-
-* Scope
-
-Build Sakura into a correct and efficient first-order predicate logic (FOPL)
-relational algebra engine where relations are first-class values with:
-
-- *Extension* :: denotation as a tuple set (finite stored set or generated stream)
-- *Intension* :: logical meaning (schema + normalized constraints + derivation)
-
-* Formal Position on Issue =#48=
-
-Issue =#48= is *not* semantic redundancy.
-
-- =constraint:create_1op/3= is an attribute-constraint *definition* constructor.
-- =constraint:add_attribute_constraint/3= is a relation-level *binding* operation.
-
-They are distinct layers. The defect is API ambiguity and weak invariants.
-
-- [ ] Require key/record coherence: key =:= =#attribute_constraint.attribute=.
-- [ ] Provide one canonical "declare and bind" path to prevent invalid states.
-
-* Relation Contract (Non-Negotiable)
-
-** C1 Representation Contract
-
-- [ ] Exactly one extension mode per relation:
- - finite stored: =tree =/= undefined= and =generator= is factory for tree iterator
- - generated: =tree == undefined= and =generator= is mandatory
-- [ ] Intension is always =#relation_constraints{}=, never raw =#{}=.
-- [ ] =membership_criteria= is either removed (Issue =#47=) or treated as a derived cache from constraints, never a second source of truth.
-
-** C2 Identity Contract
-
-- [ ] Relation hash function is canonical and identical across all mutation paths.
-- [ ] Hash input includes the same semantic fields in the same order, with explicit versioned format.
-- [ ] Any relation update that changes intension or extension must change hash deterministically.
-
-** C3 Algebraic Closure Contract
-
-- [ ] Every algebra operator returns a relation with valid extension + intension.
-- [ ] Constraint propagation rules are explicit and tested per operator.
-- [ ] Result schema and constraints are compatible and normalized.
-
-* Current Critical Correctness Gaps
-
-- [ ] Non-canonical relation hash updates in =src/operations.erl=.
-- [ ] Split semantics between =constraints= and =membership_criteria=.
-- [ ] Mixed constraints shapes (=#relation_constraints{}= and raw maps).
-- [ ] Placeholder substitution search in =constraint:find_satisfying_substitution/4=.
-- [ ] Derived infinite/finite relations not consistently honoring constraints (=#13=, =#14=).
-- [ ] Dual planner paths with mismatched behavior in =src/query_planner.erl=.
-- [ ] Iterator close protocol mismatch (=stop= vs =close= forms).
-- [ ] XML schema endpoint assumes map constraints and crashes.
-
-* Workstreams
-
-** W0 Safety and Determinism (P0)
-
-- [ ] Fix =xml_server= SCHEMA serialization for record constraints (=src/xml_server.erl=).
-- [ ] Replace ad-hoc prints with =logger= (=#39=), remove debug output in constraint lookup.
-- [ ] Make =repl:start/0= non-destructive; add explicit destructive reset command.
-- [ ] Standardize iterator termination protocol across all iterator loops.
-- [ ] Normalize constraints type in all constructors (planner/operators/operations).
-
-*Acceptance criteria*
-
-- [ ] No SCHEMA crash under any built-in relation set.
-- [ ] No unsolicited stdout noise during normal queries.
-- [ ] No relation loss on default REPL startup.
-- [ ] No orphan iterator processes after collect/close paths.
-
-** W1 FOPL Semantics Core (P1)
-
-- [ ] Implement full variable binding search and substitution for partial constraints.
-- [ ] Define and enforce formula AST for =and/or/not/exists/forall=.
-- [ ] Separate logical membership from physical storage existence checks.
-- [ ] Decide and document two-valued logic policy (no NULL) or explicit three-valued policy.
-- [ ] Complete 1OPiC modeling (=#42=) and baseline 1OP constraints (=#28=).
-
-*Acceptance criteria*
-
-- [ ] Quantified constraints produce correct truth values on finite fixtures.
-- [ ] Membership equivalence tests pass for stored vs generated extension of same intension.
-- [ ] Constraint violation diagnostics identify failing attribute/formula branch.
-
-** W2 Constraint Model Consistency (P1)
-
-- [ ] Resolve =#48= with scope-safe API (define vs bind) and strict invariant checks.
-- [ ] Implement relation creation with constraints in one call (=#15=).
-- [ ] Decide fate of =membership_criteria= (=#47=): remove or strict derivation cache.
-- [ ] Add migration helper to convert legacy constraint maps to =#relation_constraints{}=.
-- [ ] Establish extension/intension introspection API for debugging and tooling.
-
-*Acceptance criteria*
-
-- [ ] No public path can construct mismatched attribute constraint key/record pairs.
-- [ ] New relation creation API supports schema + constraints atomically.
-- [ ] Intension is serialized/deserialized in one normalized shape everywhere.
-
-** W3 Planner Unification (P1)
-
-- [ ] Remove dual compile paths; keep one logical -> rewrite -> physical pipeline.
-- [ ] Add semantic validation (schema compatibility, attribute existence, constraint shape).
-- [ ] Add rewrite rules: projection pruning, predicate pushdown, limited join reordering.
-- [ ] Add typed planner errors for REPL/XML consumers.
-
-*Acceptance criteria*
-
-- [ ] Same query plan always yields same output schema and row semantics across interfaces.
-- [ ] Planner rejects invalid plans with stable machine-readable reasons.
-
-** W4 Algebra Completeness and Performance (P2)
-
-- [ ] Implement union/intersection/difference with strict compatibility checks.
-- [ ] Implement semijoin/antijoin/division.
-- [ ] Add hash join and merge join; keep nested loop fallback.
-- [ ] Enforce bounded execution preconditions for blocking operators over infinite relations.
-- [ ] Improve tuple hash retrieval path performance (=#7=).
-
-*Acceptance criteria*
-
-- [ ] Algebra law regression tests pass for finite relations (where laws apply).
-- [ ] Join strategy selection is observable and benchmarked.
-- [ ] Infinite + blocking plans fail fast with actionable diagnostics unless bounded.
-
-** W5 Versioning and Updatable Views (P2)
-
-- [ ] Implement branch metadata and lineage (=#43=).
-- [ ] Implement fast-forward merge semantics and conflict checks (=#44=).
-- [ ] Implement provenance-safe updatable ephemeral views (=#26=).
-
-*Acceptance criteria*
-
-- [ ] Branch/merge operations are deterministic and test-covered.
-- [ ] Updatable views reject ambiguous write-back paths.
-
-** W6 Interfaces and Hardening (P2)
-
-- [ ] Replace XML =erl_eval= execution with safe parsed DSL/AST.
-- [ ] Version protocol responses and provide structured errors.
-- [ ] Add explain-analyze metrics (rows, time, strategy, spills/materialization).
-- [ ] Add visualizer mode for relation semantics (schema, constraints, lineage, extension mode).
-- [ ] Integrate or deprecate =relation.erl= helper model.
-
-*Acceptance criteria*
-
-- [ ] No arbitrary code execution path through external query interface.
-- [ ] Interface responses are stable under malformed input and concurrency.
-
-* GitHub Issue Mapping
-
-** Core reliability and semantics
-
-- [ ] =#39= logging
-- [ ] =#15= define constraints at finite relation creation
-- [ ] =#14= derived finite from infinite honors/inherits constraints
-- [ ] =#13= derived infinite honors constraints
-- [ ] =#7= tuple hash retrieval performance
-
-** Constraint/data model
-
-- [ ] =#48= clarify define/bind API and enforce invariants
-- [ ] =#47= remove/replace membership_criteria split
-- [ ] =#42= proper 1OPiC model
-- [ ] =#28 #29 #30 #31= 1st/2nd/3rd/4th order property constraints
-
-** Versioning and updates
-
-- [ ] =#43= branching
-- [ ] =#44= fast-forward merge
-- [ ] =#26= updatable views
-
-** Optional research tracks
-
-- [ ] =#45= nullary relations vs booleans
-- [ ] =#40= state monad
-- [ ] =#41= C node for hot loops
-- [ ] =#19= chessboard integration
-
-* Verification Matrix (Must Exist in CI)
-
-- [ ] Unit tests: constraints AST eval, membership semantics, hash determinism
-- [ ] Property tests: algebra laws, hash stability, propagation invariants
-- [ ] Integration tests: REPL flows, XML QUERY/NEXT/CLOSE/SCHEMA, malformed inputs
-- [ ] Performance tests: join algorithms, bounded infinite queries, Merkle hash retrieval
-- [ ] Concurrency tests: iterator lifecycle and server session cleanup
-
-* Milestones and Exit Criteria
-
-** M1 (Safety Baseline)
-
-- Deliver W0
-- Close target issues: =#39 #15 #14 #13 #7=
-- Exit: no destructive default startup, no XML schema crash, no protocol-level iterator leaks
-
-** M2 (Semantic Correctness)
-
-- Deliver W1 + W2
-- Close target issues: =#48 #47 #42 #28= (with foundations for =#29 #30 #31=)
-- Exit: extension/intension invariants hold across create/query/derive flows
-
-** M3 (Planner and Performance)
-
-- Deliver W3 + W4
-- Exit: unified planner, bounded infinite behavior, measurable join improvements
-
-** M4 (Versioned Workflows)
-
-- Deliver W5 + W6
-- Close target issues: =#43 #44 #26=
-- Exit: deterministic branch/merge and safe updatable views with stable interfaces
-
-* Execution Order
-
-1. W0 Safety and Determinism
-2. W1 FOPL Semantics Core
-3. W2 Constraint Model Consistency
-4. W3 Planner Unification
-5. W4 Algebra Completeness and Performance
-6. W5 Versioning and Updatable Views
-7. W6 Interfaces and Hardening
-8. Optional research tracks (=#45 #40 #41 #19=)
diff --git a/docs/roadmap.puml b/docs/roadmap.puml
deleted file mode 100644
index 42ad8c2..0000000
--- a/docs/roadmap.puml
+++ /dev/null
@@ -1,72 +0,0 @@
-@startuml roadmap
-!theme plain
-title Suggested Implementation Roadmap
-
-package "Phase 1: Stable Interface (MVP)" {
- [domino.erl wrapper] as P1_1
- [Command interface] as P1_2
- [Basic constraint validation] as P1_3
- [Karuta FFI bindings] as P1_4
-}
-
-package "Phase 2: Type System" {
- [Fake-relations primitives] as P2_1
- [Constraint-based refinement] as P2_2
- [Subtype checking] as P2_3
- [Type inference] as P2_4
-}
-
-package "Phase 3: Advanced Constraints" {
- [Foreign keys] as P3_1
- [Functional dependencies] as P3_2
- [Tuple/relation-level constraints] as P3_3
- [Transition constraints] as P3_4
-}
-
-package "Phase 4: Query Features" {
- [Updatable views] as P4_1
- [Materialized views] as P4_2
- [Query optimization] as P4_3
- [Explain plans] as P4_4
-}
-
-package "Phase 5: Distribution" {
- [Merkle sync] as P5_1
- [Multi-node replication] as P5_2
- [Partition strategies] as P5_3
- [Consistency protocols] as P5_4
-}
-
-package "Phase 6: Advanced Features" {
- [Temporal queries] as P6_1
- [Reactive queries] as P6_2
- [Constraint solving] as P6_3
- [Datalog engine] as P6_4
-}
-
-P1_1 -down-> P2_1
-P1_2 -down-> P2_2
-P1_3 -down-> P3_1
-P1_4 -down-> P2_1
-
-P2_1 -down-> P3_1
-P2_2 -down-> P3_2
-P2_3 -down-> P3_3
-P2_4 -down-> P4_1
-
-P3_1 -down-> P4_1
-P3_2 -down-> P4_2
-P3_3 -down-> P4_3
-P3_4 -down-> P4_4
-
-P4_1 -down-> P5_1
-P4_2 -down-> P5_2
-P4_3 -down-> P5_3
-P4_4 -down-> P6_1
-
-P5_1 -down-> P6_1
-P5_2 -down-> P6_2
-P5_3 -down-> P6_3
-P5_4 -down-> P6_4
-
-@enduml
diff --git a/docs/system-overview.puml b/docs/system-overview.puml
deleted file mode 100644
index 8d76fc5..0000000
--- a/docs/system-overview.puml
+++ /dev/null
@@ -1,81 +0,0 @@
-@startuml system-overview
-!theme plain
-title Domino System Overview
-
-package "Client Interfaces" {
- [XML Server\n(TCP/IP Port 8080)] as XML
- [REPL\n(Interactive Shell)] as REPL
- [Direct API\n(Erlang Apps)] as API
-}
-
-package "Query Processing Layer" {
- package "Query Planner" {
- [DSL Parser] as Parser
- [Plan Validation\n& Compilation] as Validator
- }
-
- package "Volcano Iterator Model" {
- [Scan Iterator] as Scan
- [Select Iterator] as Select
- [Project Iterator] as Project
- [Sort Iterator] as Sort
- [Take Iterator] as Take
- [Join Iterators] as Join
- }
-}
-
-package "Operations Layer (API)" {
- [Database Management] as DBMgmt
- [Relation Operations] as RelOps
- [Tuple Operations] as TupleOps
- [Iterator Management] as IterMgmt
-}
-
-package "Data Model Layer" {
- [Core Abstractions\nattribute, tuple,\nrelation, database_state] as CoreAbs
- [Finite Relations\n(Merkle Trees)] as Finite
- [Infinite Relations\n(Generators)] as Infinite
-}
-
-package "Content-Addressed Storage" {
- database "Mnesia" {
- [attribute table] as AttrTable
- [tuple table] as TupleTable
- [relation table] as RelTable
- [database_state table] as DBTable
- }
-}
-
-XML --> Parser
-REPL --> Parser
-API --> Parser
-
-Parser --> Validator
-Validator --> Scan
-
-Scan --> Select
-Select --> Project
-Project --> Sort
-Sort --> Take
-Join --> Take
-
-Scan --> TupleOps
-Select --> IterMgmt
-Project --> IterMgmt
-
-DBMgmt --> CoreAbs
-RelOps --> CoreAbs
-TupleOps --> CoreAbs
-IterMgmt --> CoreAbs
-
-CoreAbs --> Finite
-CoreAbs --> Infinite
-
-Finite --> AttrTable
-Finite --> TupleTable
-Finite --> RelTable
-Finite --> DBTable
-
-Infinite --> AttrTable
-
-@enduml
diff --git a/docs/temporal-queries.puml b/docs/temporal-queries.puml
deleted file mode 100644
index 7d72ed5..0000000
--- a/docs/temporal-queries.puml
+++ /dev/null
@@ -1,61 +0,0 @@
-@startuml temporal-queries
-!theme plain
-title Temporal Queries with Immutable Database States
-
-database "DB @ t0" as DB0
-note bottom of DB0
- hash = abc123...
- timestamp = 2024-01-01 10:00
-
- relations:
- employee => hash_e0
-end note
-
-database "DB @ t1" as DB1
-note bottom of DB1
- hash = def456...
- timestamp = 2024-01-01 11:00
-
- relations:
- employee => hash_e1
-end note
-
-database "DB @ t2" as DB2
-note bottom of DB2
- hash = ghi789...
- timestamp = 2024-01-01 12:00
-
- relations:
- employee => hash_e2
-end note
-
-database "Current DB" as DB3
-note bottom of DB3
- hash = jkl012...
- timestamp = 2024-01-01 13:00
-
- relations:
- employee => hash_e3
-end note
-
-DB0 -right-> DB1: insert tuple
-DB1 -right-> DB2: update salary
-DB2 -right-> DB3: delete tuple
-
-note top of DB0
- **Time Travel Queries:**
-
- query_at_time(2024-01-01 10:00)
- → returns DB0
-
- query_at_time(2024-01-01 12:00)
- → returns DB2
-
- diff(DB0, DB3)
- → (inserted: [...], deleted: [...], modified: [...])
-
- history(employee, id: 1)
- → [(t0, state0), (t1, state1), (t2, state2)]
-end note
-
-@enduml
diff --git a/docs/transaction-algo.org b/docs/transaction-algo.org
new file mode 100644
index 0000000..22903ec
--- /dev/null
+++ b/docs/transaction-algo.org
@@ -0,0 +1,12 @@
+* Check if temporary file with the command hash with exists
+To hash the command, we must hash its bytes without the timestamp,
+since it will always vary given it's a transactional time, and add
+instead the commit HEAD of the state of the database.
+
+If such file exists, load the transaction and storage offsets
+alongside the command and attempt to write it as a recovery.
+* Get the transaction offset of the transaction graph file AND storage file
+* Write the offsets to a tmp file
+* Write the command with a flag **flushed** set to false to a tmp file
+* Attempt to execute the command
+* Delete the temporary file
diff --git a/docs/type-checking-flow.puml b/docs/type-checking-flow.puml
deleted file mode 100644
index d1e6654..0000000
--- a/docs/type-checking-flow.puml
+++ /dev/null
@@ -1,108 +0,0 @@
-@startuml type-checking-flow
-!theme plain
-title Type Checking as Relation Membership Checking
-
-participant "Karuta Program" as K
-participant "Type Checker" as TC
-participant "Domino" as D
-database "Type Relations\n(Infinite Relations)" as TR
-
-note over K,TR
- **Type Checking:** Check if value is member of type relation
- **Type Relations:** Primitives stored as infinite relations
- **Membership:** Erlang type check + constraint satisfaction
-end note
-
-K -> TC: insert(employee, {age: 25, name: "Alice"})
-activate TC
-
-TC -> D: check: natural[25]?
-activate D
-D -> TR: membership query: natural[25]
-activate TR
-TR -> TR: 1. Erlang type check:\n is_integer(25) → true
-TR -> TR: 2. Constraint check:\n 25 >= 0 → true
-TR --> D: true (25 ∈ natural)
-deactivate TR
-D --> TC: member ✓
-deactivate D
-
-TC -> D: check: non_empty_string["Alice"]?
-activate D
-D -> TR: membership query: non_empty_string["Alice"]
-activate TR
-TR -> TR: 1. Check string["Alice"]:\n is_list("Alice") ∧ all_chars → true
-TR -> TR: 2. Constraint check:\n length("Alice") > 0 → true
-TR --> D: true ("Alice" ∈ non_empty_string)
-deactivate TR
-D --> TC: member ✓
-deactivate D
-
-TC --> K: type_check passed ✓\n{age: 25, name: "Alice"} ∈ employee
-deactivate TC
-
-== Type Check Failure Example ==
-
-K -> TC: insert(employee, {age: -5, name: ""})
-activate TC
-
-TC -> D: check: natural[-5]?
-activate D
-D -> TR: membership query: natural[-5]
-activate TR
-TR -> TR: 1. Erlang type check:\n is_integer(-5) → true
-TR -> TR: 2. Constraint check:\n -5 >= 0 → false ✗
-TR --> D: false (-5 ∉ natural)
-deactivate TR
-D --> TC: not_member ✗
-deactivate D
-
-TC --> K: type_error: -5 ∉ natural\nExpected: natural[Age]\nGot: Age = -5
-deactivate TC
-
-== Composite Type Example ==
-
-K -> TC: check: complex[(3.14, 2.71)]?
-activate TC
-
-TC -> D: check: complex[(3.14, 2.71)]?
-activate D
-note over D
- complex[X] :-
- X is (A, B),
- real[A], real[B].
-end note
-
-D -> TR: membership query: real[3.14]
-activate TR
-TR -> TR: is_float(3.14) → true
-TR --> D: true (3.14 ∈ real)
-deactivate TR
-
-D -> TR: membership query: real[2.71]
-activate TR
-TR -> TR: is_float(2.71) → true
-TR --> D: true (2.71 ∈ real)
-deactivate TR
-
-D --> TC: member ✓\n(3.14, 2.71) ∈ complex
-deactivate D
-
-TC --> K: type_check passed ✓
-deactivate TC
-
-note right of TR
- **Type Relation Storage:**
- - Primitives: infinite relations
- - Membership: predicate evaluation
- - No materialization needed
- - Lazy constraint checking
-
- **Example:**
- natural[X] is stored as:
- - erlang_type: integer()
- - constraint: X >= 0
- - generator: unfold(0, +1)
-end note
-
-@enduml
diff --git a/docs/type-hierarchy.puml b/docs/type-hierarchy.puml
deleted file mode 100644
index c53a965..0000000
--- a/docs/type-hierarchy.puml
+++ /dev/null
@@ -1,289 +0,0 @@
-@startuml type-hierarchy
-!theme plain
-title Type System: Stratified Universes & Types as Relations
-
-package "Universe Stratification" {
- class "Universe₀" as U0 {
- Base values
- Examples: 3, -5, 3.14, true, "hello"
- The set of all runtime values
- }
-
- class "Universe₁" as U1 {
- Type relations (classify values)
- Examples: natural, integer, real, boolean
- Each type is a relation over Universe₀
- Type relations are infinite relations
- }
-
- class "Universe₂" as U2 {
- Type of type relations
- The classifier of type relations
- Prevents self-reference paradoxes
- }
-
- class "Universe₃" as U3 {
- Meta-type level
- Type : Universe₂ : Universe₃
- }
-
- U0 : Universe₁
- U1 : Universe₂
- U2 : Universe₃
- U3 : Universe₄
-
- note right of U1
- **Stratification prevents Russell's Paradox**
- Type relations live in Universe₁
- and classify values in Universe₀.
- A type cannot contain itself.
-
- natural : Universe₁
- 3 ∈ natural (3 ∈ Universe₀)
- But: natural ∉ natural ✗ (universe mismatch)
- end note
-}
-
-package "Supertypes (Universe₁)" {
- class Number {
- Membership: number[X] where X : Universe₀
- Definition: Union of all numeric types
- number[X] :- natural[X]; integer[X];
- rational[X]; real[X].
- Allows polymorphism over numeric types
- Universe level: Universe₁
- }
-
- class Scalar {
- Membership: scalar[X] where X : Universe₀
- Definition: Atomic (non-composite) values
- scalar[X] :- number[X]; boolean[X]; string[X].
- Universe level: Universe₁
- }
-}
-
-package "Numeric Type Relations (Universe₁)" {
- class Naturals {
- Membership: natural[X] where X : Universe₀
- Criteria:
- - X is erlang:integer()
- - X >= 0
- Cardinality: aleph_zero
- Generator: 0, 1, 2, 3, ...
- Universe level: Universe₁
- }
-
- class Integers {
- Membership: integer[X] where X : Universe₀
- Criteria:
- - X is erlang:integer()
- Cardinality: aleph_zero
- Generator: 0, 1, -1, 2, -2, ...
- Universe level: Universe₁
- }
-
- class Rationals {
- Membership: rational[X] where X : Universe₀
- Criteria:
- - X is (P, Q)
- - integer[P], integer[Q]
- - Q ≠ 0
- Cardinality: aleph_zero
- Generator: Stern-Brocot tree
- Universe level: Universe₁
- }
-
- class Reals {
- Membership: real[X] where X : Universe₀
- Criteria:
- - X is erlang:float()
- Cardinality: continuum
- Generator: IEEE 754 floats
- Universe level: Universe₁
- }
-}
-
-package "Other Primitive Types (Universe₁)" {
- class Strings {
- Membership: string[X] where X : Universe₀
- Criteria:
- - X is erlang:string()
- Cardinality: aleph_zero
- Ordering: by length, then lexicographic
- Universe level: Universe₁
- }
-
- class Booleans {
- Membership: boolean[X] where X : Universe₀
- Criteria:
- - X is erlang:boolean()
- - X ∈ {true, false}
- Cardinality: 2
- Universe level: Universe₁
- }
-}
-
-package "Constrained Type Relations (Universe₁)" {
- class PositiveIntegers {
- Definition: positive_int[X] where X : Universe₀
- Criteria:
- - integer[X]
- - X > 0
- Relation: positive_int ⊆ integer
- Universe level: Universe₁
- }
-
- class BoundedInt {
- Definition: bounded_int[X, Min, Max]
- where X : Universe₀
- Criteria:
- - integer[X]
- - Min ≤ X ≤ Max
- Relation: bounded_int ⊆ integer
- Universe level: Universe₁
- }
-
- class NonEmptyStrings {
- Definition: non_empty_string[X]
- where X : Universe₀
- Criteria:
- - string[X]
- - length(X) > 0
- Relation: non_empty_string ⊆ string
- Universe level: Universe₁
- }
-}
-
-package "Composite Type Relations (Universe₁)" {
- class Complex {
- Definition (Karuta-style):
- complex[X] where X : Universe₀
- complex[X] :-
- X is (A, B),
- real[A], real[B].
-
- Interpretation:
- - (A, B) where A is real part
- - B is imaginary part
- Relation: complex ⊆ real × real
- Universe level: Universe₁
- }
-
- class Employee {
- Definition:
- employee[X] where X : Universe₀
- employee[X] :-
- X is (Name, Age, Salary),
- non_empty_string[Name],
- positive_int[Age],
- positive_int[Salary].
-
- Relation: employee ⊆ string × ℕ⁺ × ℕ⁺
- Universe level: Universe₁
- }
-}
-
-' Universe stratification relationships
-U1 --> U0 : classifies values in
-U2 --> U1 : types type relations
-U3 --> U2 : meta-level
-
-' All type relations live in Universe₁
-U1 ..> Number : contains
-U1 ..> Naturals : contains
-U1 ..> Integers : contains
-U1 ..> Rationals : contains
-U1 ..> Reals : contains
-U1 ..> Booleans : contains
-U1 ..> Strings : contains
-U1 ..> Scalar : contains
-
-' Type relation hierarchy (within Universe₁)
-Number <|-- Naturals : ⊇
-Number <|-- Integers : ⊇
-Number <|-- Rationals : ⊇
-Number <|-- Reals : ⊇
-
-Number <|-- Scalar : ⊇
-Booleans <|-- Scalar : ⊇
-Strings <|-- Scalar : ⊇
-
-' Subset relations among numeric types
-Naturals .up.|> Integers : ⊆
-Integers .up.|> Rationals : ⊆
-Rationals .up.|> Reals : ⊆
-
-' Constrained types
-Integers .down.|> PositiveIntegers : ⊇
-Integers .down.|> BoundedInt : ⊇
-Strings .down.|> NonEmptyStrings : ⊇
-
-' Composite types (also in Universe₁)
-Number .down.|> Complex : number × number
-Complex : (view: pair of numbers)
-
-Scalar .down.|> Employee : scalar × scalar × scalar
-Employee : (view: string × nat × nat)
-
-note right of Complex
- **Views as Type Relations:**
- Types can be defined as relational views
- over other type relations.
-
- Example using supertype:
- complex[C] :- C is (R, I),
- number[R], number[I].
-
- Or more specifically:
- complex[C] :- C is (R, I),
- real[R], real[I].
-
- Views can use supertypes for flexibility!
- All views live in Universe₁.
-end note
-
-note left of Number
- **Core Principle:** Every type is a relation
- Type checking = membership checking in relations
- T1 <: T2 iff T1 ⊆ T2 (subset relation)
-
- **Supertypes enable polymorphism:**
- A function can accept number[X] and work
- with naturals, integers, rationals, or reals.
-
- **Universe Levels:**
- All type relations: Universe₁
- All values: Universe₀
- Type of types: Universe₂
-
- **Erlang Type Mapping:**
- natural → integer() + (≥ 0)
- integer → integer()
- rational → {integer(), integer()}
- real → float()
- string → string()
- boolean → boolean()
-end note
-
-note bottom of U0
- **Avoiding Russell's Paradox:**
-
- Like F*, Domino uses predicative universe
- stratification. The key invariant:
-
- Type u#i : Type u#(i+1)
-
- In Domino:
- - Values (3, "hello", true) : Universe₀
- - Type relations (natural, string) : Universe₁
- - Type : Universe₂
-
- This prevents:
- natural ∈ natural (universe mismatch!)
-
- A type relation cannot be a member of itself.
- All type relations classify values in Universe₀
- but exist at Universe₁.
-end note
-
-@enduml
diff --git a/dune-project b/dune-project
new file mode 100644
index 0000000..a5631f0
--- /dev/null
+++ b/dune-project
@@ -0,0 +1,29 @@
+(lang dune 3.16)
+(map_workspace_root false)
+
+(using ctypes 0.3)
+
+(name relational_engine)
+
+(generate_opam_files true)
+
+(source
+ (github username/reponame))
+
+(authors "Author Name")
+
+(maintainers "Maintainer Name")
+
+(license LICENSE)
+
+(documentation https://url/to/documentation)
+
+(package
+ (name relational_engine)
+ (synopsis "A short synopsis")
+ (description "A longer description")
+ (depends ocaml dune)
+ (tags
+ (topics "to describe" your project)))
+
+; See the complete stanza docs at https://dune.readthedocs.io/en/stable/reference/dune-project/index.html
diff --git a/flake.lock b/flake.lock
index 3c091ca..e43eb45 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,92 +1,73 @@
{
"nodes": {
- "flake-parts": {
+ "flake-utils": {
"inputs": {
- "nixpkgs-lib": "nixpkgs-lib"
+ "systems": "systems"
},
"locked": {
- "lastModified": 1763759067,
- "narHash": "sha256-LlLt2Jo/gMNYAwOgdRQBrsRoOz7BPRkzvNaI/fzXi2Q=",
- "owner": "hercules-ci",
- "repo": "flake-parts",
- "rev": "2cccadc7357c0ba201788ae99c4dfa90728ef5e0",
- "type": "github"
- },
- "original": {
- "owner": "hercules-ci",
- "repo": "flake-parts",
- "type": "github"
- }
- },
- "nixpkgs": {
- "locked": {
- "lastModified": 1764667669,
- "narHash": "sha256-7WUCZfmqLAssbDqwg9cUDAXrSoXN79eEEq17qhTNM/Y=",
- "owner": "nixos",
- "repo": "nixpkgs",
- "rev": "418468ac9527e799809c900eda37cbff999199b6",
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
- "owner": "nixos",
- "ref": "nixos-unstable",
- "repo": "nixpkgs",
+ "owner": "numtide",
+ "repo": "flake-utils",
"type": "github"
}
},
- "nixpkgs-lib": {
+ "nix-filter": {
"locked": {
- "lastModified": 1761765539,
- "narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=",
- "owner": "nix-community",
- "repo": "nixpkgs.lib",
- "rev": "719359f4562934ae99f5443f20aa06c2ffff91fc",
+ "lastModified": 1757882181,
+ "narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=",
+ "owner": "numtide",
+ "repo": "nix-filter",
+ "rev": "59c44d1909c72441144b93cf0f054be7fe764de5",
"type": "github"
},
"original": {
- "owner": "nix-community",
- "repo": "nixpkgs.lib",
+ "owner": "numtide",
+ "repo": "nix-filter",
"type": "github"
}
},
- "nixpkgs_2": {
+ "nixpkgs": {
"locked": {
- "lastModified": 1761236834,
- "narHash": "sha256-+pthv6hrL5VLW2UqPdISGuLiUZ6SnAXdd2DdUE+fV2Q=",
- "owner": "nixos",
+ "lastModified": 1772624091,
+ "narHash": "sha256-QKyJ0QGWBn6r0invrMAK8dmJoBYWoOWy7lN+UHzW1jc=",
+ "owner": "NixOS",
"repo": "nixpkgs",
- "rev": "d5faa84122bc0a1fd5d378492efce4e289f8eac1",
+ "rev": "80bdc1e5ce51f56b19791b52b2901187931f5353",
"type": "github"
},
"original": {
- "owner": "nixos",
- "ref": "nixpkgs-unstable",
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
- "flake-parts": "flake-parts",
- "nixpkgs": "nixpkgs",
- "treefmt-nix": "treefmt-nix"
+ "flake-utils": "flake-utils",
+ "nix-filter": "nix-filter",
+ "nixpkgs": "nixpkgs"
}
},
- "treefmt-nix": {
- "inputs": {
- "nixpkgs": "nixpkgs_2"
- },
+ "systems": {
"locked": {
- "lastModified": 1762938485,
- "narHash": "sha256-AlEObg0syDl+Spi4LsZIBrjw+snSVU4T8MOeuZJUJjM=",
- "owner": "numtide",
- "repo": "treefmt-nix",
- "rev": "5b4ee75aeefd1e2d5a1cc43cf6ba65eba75e83e4",
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
- "owner": "numtide",
- "repo": "treefmt-nix",
+ "owner": "nix-systems",
+ "repo": "default",
"type": "github"
}
}
diff --git a/flake.nix b/flake.nix
index 14cc8de..c81b6b0 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,82 +1,295 @@
{
- description = "Sakura's flakes";
+ description = "Sakura Relational Engine";
+ # Flake dependency specification
+ #
+ # To update all flake inputs:
+ #
+ # $ nix flake update --commit-lockfile
+ #
+ # To update individual flake inputs:
+ #
+ # $ nix flake lock --update-input <input> ... --commit-lockfile
+ #
inputs = {
- nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
-
- flake-parts = {
- url = "github:hercules-ci/flake-parts";
- };
-
- treefmt-nix.url = "github:numtide/treefmt-nix";
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ # Convenience functions for writing flakes
+ flake-utils.url = "github:numtide/flake-utils";
+ # Precisely filter files copied to the nix store
+ nix-filter.url = "github:numtide/nix-filter";
};
- outputs = { self, nixpkgs, flake-parts, treefmt-nix, ... }@inputs:
- flake-parts.lib.mkFlake { inherit inputs; } {
- systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
-
- perSystem = { pkgs, system, ... }:
- let
- erlangLatest = pkgs.erlang_28;
-
- rebar_config = builtins.readFile ./rebar.config;
- match = builtins.match ".*release,.+sakura, \"([0-9.]+)\".*" rebar_config;
- pversion = builtins.head match;
- in {
- # This sets `pkgs` to a nixpkgs with allowUnfree option set.
- _module.args.pkgs = import nixpkgs {
- inherit system;
- config.allowUnfree = true;
+ outputs = { self, nixpkgs, flake-utils, nix-filter }:
+ # Construct an output set that supports a number of default systems
+ flake-utils.lib.eachDefaultSystem (system:
+ let
+ # Legacy packages that have not been converted to flakes
+ legacyPackages = nixpkgs.legacyPackages.${system};
+ # OCaml packages available on nixpkgs
+ # Library functions from nixpkgs
+ lib = legacyPackages.lib;
+ ocamlPackages = legacyPackages.ocamlPackages;
+ # merklecpp = legacyPackages.callPackage ./deps/merklecpp.nix { };
+ libressl = legacyPackages.callPackage ./deps/libressl.nix { };
+ ppx_protocol_conv =
+ legacyPackages.callPackage ./deps/ppx_protocol_conv.nix {
+ lib = legacyPackages.lib;
+ fetchFromGitHub = legacyPackages.fetchFromGitHub;
+ ocamlPackages = ocamlPackages; };
+ ppx_protocol_conv_xml_light =
+ legacyPackages.callPackage ./deps/ppx_protocol_conv_xml_light.nix {
+ lib = legacyPackages.lib;
+ ppx_protocol_conv = ppx_protocol_conv;
+ fetchFromGitHub = legacyPackages.fetchFromGitHub;
+ ocamlPackages = ocamlPackages; };
+ librelational_engine =
+ legacyPackages.callPackage ./deps/relational_engine_lib.nix {
+ libressl = libressl;
+ # merklecpp = merklecpp;
+ };
+ # Filtered sources (prevents unecessary rebuilds)
+ sources = {
+ ocaml = nix-filter.lib {
+ root = ./.;
+ include = [
+ ".ocamlformat"
+ "dune-project"
+ (nix-filter.lib.inDirectory "bin")
+ (nix-filter.lib.inDirectory "lib")
+ (nix-filter.lib.inDirectory "test")
+ ];
};
- # nix build
- packages = {
- default =
- let
- deps = import ./rebar-deps.nix {
- inherit (pkgs) fetchHex fetchFromGitHub fetchgit;
- builder = pkgs.beamPackages.buildRebar3;
- };
- in
- pkgs.beamPackages.rebar3Relx {
- pname = "sakura";
- version = pversion;
- src = pkgs.lib.cleanSource ./.;
- releaseType = "release";
- profile = "prod";
- buildInputs = (
- with pkgs;
- [
- coreutils
- gawk
- gnugrep
- openssl
- ]
- ++ lib.optional stdenv.isLinux [
- liburing
- ]
- );
- beamDeps = builtins.attrValues deps;
- };
+ nix = nix-filter.lib {
+ root = ./.;
+ include = [ (nix-filter.lib.matchExt "nix") ];
};
+ };
+ in {
+ # Exposed packages that can be built or run with `nix build` or
+ # `nix run` respectively:
+ #
+ # $ nix build .#<name>
+ # $ nix run .#<name> -- <args?>
+ #
+ packages = {
+ # The package that will be built or run by default. For example:
+ #
+ # $ nix build
+ # $ nix run -- <args?>
+ #
+ default = self.packages.${system}.relational_engine;
+
+ relational_engine = ocamlPackages.buildDunePackage {
+ pname = "relational_engine";
+ version = "0.1.0";
+ duneVersion = "3";
+ src = sources.ocaml;
- # Define devShell for Erlang + Rebar3
- devShells = {
- default = pkgs.mkShell {
- packages = with pkgs; [
- erlang-language-platform
- rebar3
- ] ++ [erlangLatest];
+ env = {
+ # MERKLECPP_INCLUDE_PATH = "${merklecpp}/include";
+ LIBRESSL_INCLUDE_PATH = "${libressl}/include";
+ LIBRESSL_LIB_PATH = "${libressl}/lib";
+ OS_LIB_EXTENSION =
+ if legacyPackages.stdenv.isDarwin then "dylib" else "so";
+ LIBRELATIONAL_ENGINE_LIB_PATH = "${librelational_engine}/lib";
+ LIBRELATIONAL_ENGINE_INCLUDE_PATH = "${librelational_engine}/include";
};
+
+ nativeInputs = [ ];
+
+ buildInputs = [ ppx_protocol_conv_xml_light
+ ppx_protocol_conv ]
+ ++ (with ocamlPackages; [
+ ctypes
+ ctypes-foreign
+ data-encoding
+ ppx_inline_test
+ ppx_deriving
+ ppx_sexp_conv
+ lwt
+ lwt-exit
+ ]);
+
+ strictDeps = true;
+
};
+ };
+ # Flake checks
+ #
+ # $ nix flake check
+ #
+ checks = {
+ # Run tests for the `relational_engine` package
+ relational_engine = let
+ # Patches calls to dune commands to produce log-friendly output
+ # when using `nix ... --print-build-log`. Ideally there would be
+ # support for one or more of the following:
+ #
+ # In Dune:
+ #
+ # - have workspace-specific dune configuration files
+ #
+ # In NixPkgs:
+ #
+ # - allow dune flags to be set in in `ocamlPackages.buildDunePackage`
+ # - alter `ocamlPackages.buildDunePackage` to use `--display=short`
+ # - alter `ocamlPackages.buildDunePackage` to allow `--config-file=FILE` to be set
+ patchDuneCommand =
+ let subcmds = [ "build" "test" "runtest" "install" ];
+ in lib.replaceStrings
+ (lib.lists.map (subcmd: "dune ${subcmd}") subcmds)
+ (lib.lists.map (subcmd: "dune ${subcmd} --display=short")
+ subcmds);
+
+ in self.packages.${system}.relational_engine.overrideAttrs
+ (oldAttrs: {
+ name = "check-${oldAttrs.name}";
+ doCheck = true;
+ buildPhase = patchDuneCommand oldAttrs.buildPhase;
+ checkPhase = patchDuneCommand oldAttrs.checkPhase;
+ # skip installation (this will be tested in the `relational_engine-app` check)
+ installPhase = "touch $out";
+ });
+
+ # Check Dune and OCaml formatting
+ dune-fmt = legacyPackages.runCommand "check-dune-fmt" {
+ nativeBuildInputs = [
+ ocamlPackages.dune_3
+ ocamlPackages.ocaml
+ legacyPackages.ocamlformat
+ ];
+ } ''
+ echo "checking dune and ocaml formatting"
+ dune build \
+ --display=short \
+ --no-print-directory \
+ --root="${sources.ocaml}" \
+ --build-dir="$(pwd)/_build" \
+ @fmt
+ touch $out
+ '';
+
+ dune-test = legacyPackages.runCommand "check-dune-test" {
+ nativeBuildInputs = [
+ ocamlPackages.dune_3
+ ocamlPackages.ocaml
+ legacyPackages.ocamlformat
+ ocamlPackages.ppx_inline_test
+ ocamlPackages.ppx_deriving
+ ocamlPackages.ppx_sexp_conv
+ ocamlPackages.lwt
+ ocamlPackages.lwt-exit
+ ppx_protocol_conv
+ ppx_protocol_conv_xml_light
+ ];
+ } ''
+ echo "checking dune and ocaml formatting"
+ dune build \
+ --display=short \
+ --no-print-directory \
+ --root="${sources.ocaml}" \
+ --build-dir="$(pwd)/_build" \
+ @fmt
+ touch $out
+ '';
+
+ # Check documentation generation
+ dune-doc = legacyPackages.runCommand "check-dune-doc" {
+ ODOC_WARN_ERROR = "true";
+ nativeBuildInputs =
+ [ ocamlPackages.dune_3 ocamlPackages.ocaml ocamlPackages.odoc ];
+ } ''
+ echo "checking ocaml documentation"
+ dune build \
+ --display=short \
+ --no-print-directory \
+ --root="${sources.ocaml}" \
+ --build-dir="$(pwd)/_build" \
+ @doc
+ touch $out
+ '';
+
+ # Check Nix formatting
+ # nixpkgs-fmt = legacyPackages.runCommand "check-nixpkgs-fmt" {
+ # nativeBuildInputs = [ legacyPackages.nixpkgs-fmt ];
+ # } ''
+ # echo "checking nix formatting"
+ # nixpkgs-fmt --check ${sources.nix}
+ # touch $out
+ # '';
+ };
+
+ # Development shells
+ #
+ # $ nix develop .#<name>
+ # $ nix develop .#<name> --command dune build @test
+ #
+ # [Direnv](https://direnv.net/) is recommended for automatically loading
+ # development environments in your shell. For example:
+ #
+ # $ echo "use flake" > .envrc && direnv allow
+ # $ dune build @test
+ #
+ devShells = {
+ default = legacyPackages.mkShell {
+
+ env = {
+ # MERKLECPP_INCLUDE_PATH = "${merklecpp}/include";
+ LIBRESSL_INCLUDE_PATH = "${libressl}/include";
+ LIBRESSL_LIB_PATH = "${libressl}/lib";
+ OS_LIB_EXTENSION =
+ if legacyPackages.stdenv.isDarwin then "dylib" else "so";
+ LIBRELATIONAL_ENGINE_LIB_PATH = "${librelational_engine}/lib";
+ LIBRELATIONAL_ENGINE_INCLUDE_PATH = "${librelational_engine}/include";
+ };
+
+ # Development tools
+ packages = [
+ # Source file formatting
+ legacyPackages.nixpkgs-fmt
+ legacyPackages.ocamlformat
+ # For `dune build --watch ...`
+ # legacyPackages.fswatch
+ # For `dune build @doc`
+ ocamlPackages.odoc
+ # OCaml editor support
+ ocamlPackages.ocaml-lsp
+ # Nicely formatted types on hover
+ ocamlPackages.ocamlformat-rpc-lib
+ # Fancy REPL thing
+ ocamlPackages.utop
+ # Libraries
+ # ocamlPackages.menhir
+ ocamlPackages.ctypes
+ ocamlPackages.ctypes-foreign
+ ocamlPackages.data-encoding
+ ocamlPackages.ppx_inline_test
+ ocamlPackages.ppx_deriving
+ ocamlPackages.ppx_sexp_conv
+ legacyPackages.cmake
+ legacyPackages.gcc
+ # legacyPackages.nixfmt-classic
+ ocamlPackages.lwt
+ ocamlPackages.lwt-exit
+ ppx_protocol_conv
+ ppx_protocol_conv_xml_light
+ ocamlPackages.earlybird
+ ];
+
- # Formatter setup for Nix files
- formatter = treefmt-nix.lib.evalModule pkgs ./treefmt.nix;
+ shellHook = ''
+ # echo MERKLECPP_INCLUDE_PATH=$MERKLECPP_INCLUDE_PATH
+ echo LIBRESSL_INCLUDE_PATH=$LIBRESSL_INCLUDE_PATH
+ echo LIBRESSL_LIB_PATH=$LIBRESSL_LIB_PATH
+ # echo OS_LIB_EXTENSION=$OS_LIB_EXTENSION
+ echo LIBRELATIONAL_ENGINE_LIB_PATH=$LIBRELATIONAL_ENGINE_LIB_PATH
+ echo LIBRELATIONAL_ENGINE_INCLUDE_PATH=$LIBRELATIONAL_ENGINE_INCLUDE_PATH
+ '';
- # nix flake check --all-systems
- checks = {
- formatting = treefmt-nix.lib.evalModule pkgs ./treefmt.nix;
+ # Tools from packages
+ inputsFrom = [ self.packages.${system}.relational_engine ];
};
};
- };
+ });
}
diff --git a/include/operations.hrl b/include/operations.hrl
deleted file mode 100644
index 52f2f4c..0000000
--- a/include/operations.hrl
+++ /dev/null
@@ -1,94 +0,0 @@
-%%% Core Records
-
--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 (mutable only), undefined for immutable/generator-based
- schema, % #{attribute_name => relation_name}
- constraints, % #relation_constraints{} - logical formulas over membership tests
- cardinality, % {finite, N} | aleph_zero | continuum
- generator, % Generator function (immutable only), undefined for tree-based
- membership_criteria, % #{attribute_name => constraint:relational_constraint()} - domain membership test
- provenance, % attribute_provenance() - maps attribute to source relation
- lineage % lineage_op() - operation tree showing derivation
-}).
-
--record(tuple, {hash, relation, attribute_map}).
--record(attribute, {hash, value}).
-
-%% Specification record for creating immutable relations
--record(domain, {
- name, % Atom - relation name
- schema, % #{attribute_name => relation_name}
- generator, % {module, function} - tuple generator
- membership_criteria, % #{attribute_name => constraint:relational_constraint()} - domain membership
- cardinality % {finite, N} | aleph_zero | continuum
-}).
-
-%% Relation constraints - unified logical formulas.
-%% Every formula is evaluated as a membership test expression over tuple bindings.
--record(relation_constraints, {
- constraints :: [{atom(), term()}]
-}).
-
-%%% Cardinality Types
-
--type cardinality() :: {finite, non_neg_integer()} % Finite set with N elements
- | aleph_zero % Countably infinite (ℵ₀)
- | continuum. % Uncountably infinite (2^ℵ₀)
-
--type constraints() :: #{atom() => constraint:relational_constraint()}.
-
--type boundedness_constraints() :: #{atom() => constraints()}.
-
-%%% Generator Types
-
--type generator_fun() :: fun((constraints()) -> generator_result()).
-
--type generator_result() ::
- done % No more tuples
- | {value, map(), generator_fun()} % Tuple and continuation
- | {error, term()}. % Error
-
--type generator_spec() ::
- {primitive, atom()} % Built-in: naturals, integers, rationals
- | {custom, generator_fun()} % User-defined generator
- | {take, atom(), pos_integer()} % Take N from relation
- | {derived, relational_op()}. % Derived from relational operation
-
--type relational_op() ::
- {select, atom(), constraints()}
- | {project, atom(), [atom()]}
- | {join, atom(), atom(), join_spec()}.
-
--type join_spec() ::
- {equijoin, atom()} % Natural join on attribute
- | {theta, atom(), atom(), atom()}. % Theta join: op(attr1, attr2)
-
-%%% Provenance Types (for updatable views)
-
-%% Relation-level provenance (tracks operation lineage)
--type provenance() :: undefined
- | {base, atom()} % Base relation
- | {join, provenance(), provenance()}
- | {select, provenance(), constraints()}
- | {project, provenance(), [atom()]}
- | {take, provenance(), pos_integer()}.
-
-%% Attribute-level provenance (tracks data lineage for each attribute)
-%% Maps attribute name to its source (relation, original_attribute_name)
--type attribute_provenance() :: #{atom() => {atom(), atom()}}.
-
-%% Tuple-level lineage (tracks complete operational history)
-%% Operation tree showing how the tuple was derived
--type lineage_op() ::
- {base, atom()} % Base relation
- | {select, fun(), lineage_op()} % Filter operation
- | {project, [atom()], lineage_op()} % Projection
- | {join, atom(), lineage_op(), lineage_op()} % Join on attribute
- | {theta_join, fun(), lineage_op(), lineage_op()} % Join with predicate
- | {sort, fun(), lineage_op()} % Sort operation
- | {take, pos_integer(), lineage_op()} % Limit operation
- | {aggregate, atom(), atom(), lineage_op()}. % Aggregation (future)
diff --git a/lib/core_functions.ml b/lib/core_functions.ml
new file mode 100644
index 0000000..083e0cf
--- /dev/null
+++ b/lib/core_functions.ml
@@ -0,0 +1,11 @@
+module Types = Types_generated
+
+module Functions (F : Ctypes.FOREIGN) = struct
+ open F
+
+ let compute_hash =
+ foreign "compute_hash"
+ (F.(@->) (Ctypes.ptr Ctypes.char)
+ (F.(@->) Ctypes.string (F.returning Ctypes.void)))
+
+end
diff --git a/lib/core_types.ml b/lib/core_types.ml
new file mode 100644
index 0000000..45f5720
--- /dev/null
+++ b/lib/core_types.ml
@@ -0,0 +1,3 @@
+module Types (F : Ctypes.TYPE) = struct
+ let hash_size = F.constant "HASH_SIZE" F.int
+end
diff --git a/lib/dune b/lib/dune
new file mode 100644
index 0000000..1397a97
--- /dev/null
+++ b/lib/dune
@@ -0,0 +1,40 @@
+(library
+ (name relational_engine)
+ (libraries
+ ctypes
+ ctypes-foreign
+ data-encoding
+ lwt
+ lwt.unix
+ lwt-exit
+ ppx_protocol_conv_xml_light
+ sexplib)
+ (preprocess
+ (pps ppx_deriving.show ppx_sexp_conv ppx_protocol_conv))
+ (c_library_flags
+ (-L %{env:LIBRELATIONAL_ENGINE_LIB_PATH=})
+ (-lrelational_engine))
+ ;; (foreign_stubs
+ ;; (language cxx)
+ ;; (names merklecpp_stubs)
+ ;; (flags
+ ;; (:standard -lssl -lcrypto)
+ ;; (-L %{env:LIBRESSL_LIB_PATH=})
+ ;; (-I %{env:LIBRESSL_INCLUDE_PATH=})
+ ;; (-I %{env:MERKLECPP_INCLUDE_PATH=})))
+ (ctypes
+ (external_library_name relational_engine)
+ (headers
+ (include %{env:LIBRELATIONAL_ENGINE_INCLUDE_PATH=}/library.h))
+ (build_flags_resolver
+ (vendored
+ (c_flags -L %{env:LIBRELATIONAL_ENGINE_LIB_PATH=})))
+ (type_description
+ (instance Types)
+ (functor core_types))
+ (function_description
+ (concurrency unlocked)
+ (instance Functions)
+ (functor core_functions))
+ (generated_types Types_generated)
+ (generated_entry_point C)))
diff --git a/lib/interop.ml b/lib/interop.ml
new file mode 100644
index 0000000..3b7a3d2
--- /dev/null
+++ b/lib/interop.ml
@@ -0,0 +1,9 @@
+open Ctypes
+
+module Sha256 = struct
+ let compute_hash content =
+ let size_with_null_terminator = C.Types.hash_size + 1 in
+ let buf = Ctypes.allocate_n char ~count:size_with_null_terminator in
+ let () = C.Functions.compute_hash buf (Bytes.to_string content) in
+ Ctypes.coerce (ptr char) Ctypes.string buf
+end
diff --git a/rebar-deps.nix b/rebar-deps.nix
deleted file mode 100644
index c34883c..0000000
--- a/rebar-deps.nix
+++ /dev/null
@@ -1,44 +0,0 @@
-# Generated by rebar3_nix
-let fetchOnly = { src, ... }: src;
-in { builder ? fetchOnly, fetchHex, fetchgit, fetchFromGitHub, overrides ? (x: y: { }) }:
-let
- self = packages // (overrides self packages);
- packages = with self; {
- quickrand = builder {
- name = "quickrand";
- version = "2.0.7";
- src = fetchHex {
- pkg = "quickrand";
- version = "2.0.7";
- sha256 = "sha256-uKy/iaIkvCF8MHDKi+vG6yNtvn+XZ5k7J0CE6gRNNfA=";
- };
- beamDeps = [ ];
- };
- uuid_erl = builder {
- name = "uuid_erl";
- version = "2.0.7";
- src = fetchHex {
- pkg = "uuid_erl";
- version = "2.0.7";
- sha256 = "sha256-Tkxco0YdxHxeFX7UKqOYGgU7ehhnkq+XKiexSpSJMk4=";
- };
- beamDeps = [ quickrand ];
- postPatch = ''
- # The package is named uuid_erl but the app.src uses {application, uuid, ...}
- # Rename the .app.src file and update the application name to match the package name
- mv src/uuid.app.src src/uuid_erl.app.src
- sed -i 's/{application, *uuid,/{application,uuid_erl,/g' src/uuid_erl.app.src
- '';
- };
- merklet = builder {
- name = "merklet";
- version = "2.0.0";
- src = fetchHex {
- pkg = "merklet";
- version = "2.0.0";
- sha256 = "sha256-teho2ll5+bPD5Oi4FxqlwWeGjjVJW4FzHJPYOC/dMng=";
- };
- beamDeps = [ ];
- };
- };
-in self
diff --git a/rebar.config b/rebar.config
deleted file mode 100644
index 584e37b..0000000
--- a/rebar.config
+++ /dev/null
@@ -1,103 +0,0 @@
-%% == Compiler Options ==
-{erl_opts, [
- debug_info,
- report,
- verbose,
- warn_unused_import,
- warn_export_vars
-]}.
-
-%% == Dependencies and Plugins ==
-{deps, [
- {merklet, "2.0.0"},
- {uuid_erl, "2.0.7"}
-]}.
-
-{plugins, [
- {covertool, "2.0.7"},
- {rebar3_nix, ".*", {git, "https://github.com/erlang-nix/rebar3_nix.git", {tag, "v0.1.1"}}}
-]}.
-
-%% == Common Test Options ==
-{ct_opts, []}.
-
-%% == Releases ==
-{relx, [
- {release, {sakura, "0.1.0"}, [
- merklet,
- uuid_erl
- ]},
-
- {mode, dev},
- {extended_start_script, true},
-
- %% automatically picked up if the files
- %% exist but can be set manually, which
- %% is required if the names aren't exactly
- %% sys.config and vm.args
- {sys_config, "./config/sys.config"},
- {vm_args, "./config/vm.args"},
-
- {overlay, [
- {mkdir, "log/sasl"},
- {mkdir, "log/errors"}
- ]}
-]}.
-
-%% == Profiles ==
-{profiles, [
- %% prod is the default mode when prod
- %% profile is used, so does not have
- %% to be explicitly included like this
- {prod, [
- {relx, [
- {include_erts, true},
- {sys_config, "./config/sys.config"},
- %% https://rebar3.org/docs/deployment/releases/#modes
- {mode, prod}
- ]}
- ]},
-
- {test, [
- {deps, [
- meck,
- proper
- ]}
- ]}
-]}.
-
-%% == Shell ==
-%% apps to auto-boot with `rebar3 shell'; defaults to apps
-%% specified in a `relx' tuple, if any.
-{shell, [
- %% We have a special config just to run the app inside
- %% a shell, helps with debugging and local testing.
- {config, "config/shell.config"},
- {apps, [main]}
-
- %% If you need any environment variables to be set inside
- %% the debug shell as well.
- %%{env_file, "deployment/default.env"}]
-]}.
-
-%% == Dialyzer Settings ==
-{dialyzer, [
- {plt_extra_apps, [merklet, uuid_erl, quickrand]},
- {warnings, [unknown]},
- {exclude_mods, [
-
- ]}
-]}.
-
-%% == EDOC Settings ==
-{edoc_opts, [
- {doclet, edoc_doclet},
- {source_path, ["src"]},
- {app_default, "https://www.erlang.org/doc/man"},
- {preprocess, true},
- {includes, ["include"]},
- {dir, "doc"},
- {overview, "doc/overview.edoc"},
- {title, "Sakura - Extended Relational Database Engine"},
- {pretty_printer, erl_pp}
-]}.
diff --git a/rebar.lock b/rebar.lock
deleted file mode 100644
index b0ee48b..0000000
--- a/rebar.lock
+++ /dev/null
@@ -1,14 +0,0 @@
-{"1.2.0",
-[{<<"merklet">>,{pkg,<<"merklet">>,<<"2.0.0">>},0},
- {<<"quickrand">>,{pkg,<<"quickrand">>,<<"2.0.7">>},1},
- {<<"uuid_erl">>,{pkg,<<"uuid_erl">>,<<"2.0.7">>},0}]}.
-[
-{pkg_hash,[
- {<<"merklet">>, <<"9E53AA5F83B4CE33A8EE0BEDC798AA54A1750A8FD92D649FC04BA7F7B8E65E78">>},
- {<<"quickrand">>, <<"D2BD76676A446E6A058D678444B7FDA1387B813710D1AF6D6E29BB92186C8820">>},
- {<<"uuid_erl">>, <<"B2078D2CC814F53AFA52D36C91E08962C7E7373585C623F4C0EA6DFB04B2AF94">>}]},
-{pkg_hash_ext,[
- {<<"merklet">>, <<"B5E868DA5979F9B3C3E4E8B8171AA5C167868E35495B81731C93D8382FDD3278">>},
- {<<"quickrand">>, <<"B8ACBF89A224BC217C3070CA8BEBC6EB236DBE7F9767993B274084EA044D35F0">>},
- {<<"uuid_erl">>, <<"4E4C5CA3461DC47C5E157ED42AA3981A053B7A186792AF972A27B14A9489324E">>}]}
-].
diff --git a/sakura.opam b/sakura.opam
new file mode 100644
index 0000000..b296c11
--- /dev/null
+++ b/sakura.opam
@@ -0,0 +1,31 @@
+# This file is generated by dune, edit dune-project instead
+opam-version: "2.0"
+synopsis: "A short synopsis"
+description: "A longer description"
+maintainer: ["Maintainer Name"]
+authors: ["Author Name"]
+license: "LICENSE"
+tags: ["topics" "to describe" "your" "project"]
+homepage: "https://github.com/username/reponame"
+doc: "https://url/to/documentation"
+bug-reports: "https://github.com/username/reponame/issues"
+depends: [
+ "ocaml"
+ "dune" {>= "3.16"}
+ "odoc" {with-doc}
+]
+build: [
+ ["dune" "subst"] {dev}
+ [
+ "dune"
+ "build"
+ "-p"
+ name
+ "-j"
+ jobs
+ "@install"
+ "@runtest" {with-test}
+ "@doc" {with-doc}
+ ]
+]
+dev-repo: "git+https://github.com/username/reponame.git"
diff --git a/shared/librelational_engine/CMakeLists.txt b/shared/librelational_engine/CMakeLists.txt
new file mode 100644
index 0000000..22c8444
--- /dev/null
+++ b/shared/librelational_engine/CMakeLists.txt
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.29)
+project(relational_engine)
+
+include_directories($ENV{LIBRESSL_INCLUDE_PATH})
+
+add_library(relational_engine SHARED library.cpp)
+
+target_link_libraries(relational_engine
+ $ENV{LIBRESSL_LIB_PATH}/libssl.$ENV{OS_LIB_EXTENSION}
+ $ENV{LIBRESSL_LIB_PATH}/libcrypto.$ENV{OS_LIB_EXTENSION}
+) \ No newline at end of file
diff --git a/shared/librelational_engine/library.cpp b/shared/librelational_engine/library.cpp
new file mode 100644
index 0000000..a9b4c81
--- /dev/null
+++ b/shared/librelational_engine/library.cpp
@@ -0,0 +1,23 @@
+#include <iostream>
+#include <openssl/sha.h>
+#include <iomanip>
+#include <sstream>
+#include <cstring>
+
+#include <sys/stat.h>
+
+extern "C" {
+#include "library.h"
+
+ extern void compute_hash(char* computed_hash, const char *content) {
+ unsigned char hash[SHA256_DIGEST_LENGTH];
+ SHA256_CTX sha256;
+ SHA256_Init(&sha256);
+ SHA256_Update(&sha256, content, std::strlen(content));
+ SHA256_Final(hash, &sha256);
+ std::stringstream ss;
+ for (int i = 0; i < SHA256_DIGEST_LENGTH; i++)
+ ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(hash[i]);
+ std::memcpy(computed_hash, ss.str().c_str(), HASH_SIZE);
+ }
+}
diff --git a/shared/librelational_engine/library.h b/shared/librelational_engine/library.h
new file mode 100644
index 0000000..cae22cb
--- /dev/null
+++ b/shared/librelational_engine/library.h
@@ -0,0 +1,8 @@
+#ifndef RELATIONAL_ENGINE_LIBRARY_H
+#define RELATIONAL_ENGINE_LIBRARY_H
+
+#define HASH_SIZE 64
+
+extern void compute_hash(char*, const char*);
+
+#endif //RELATIONAL_ENGINE_LIBRARY_H
diff --git a/sources/Geometry64x64.png b/sources/Geometry64x64.png
new file mode 100644
index 0000000..d256e49
--- /dev/null
+++ b/sources/Geometry64x64.png
Binary files differ
diff --git a/src/constraint.erl b/src/constraint.erl
deleted file mode 100644
index 6391217..0000000
--- a/src/constraint.erl
+++ /dev/null
@@ -1,1179 +0,0 @@
--module(constraint).
-
--include("../include/operations.hrl").
-
--export([
- less_than/2,
- less_than_or_equal/2,
- greater_than/2,
- greater_than_or_equal/2,
- equal/2,
- not_equal/2,
- plus/2,
- times/2,
- minus/2,
- divide/2,
- member/2,
- member/3,
- 'not'/2,
- 'and'/2,
- 'or'/2,
- resolve/4,
- validate_tuple/3,
- validate_tuple_against_schema/3,
- build_membership_criteria/2,
- lt/2,
- lte/2,
- gt/2,
- gte/2,
- eq/2,
- neq/2,
- between/2,
- example_supertype_exclusive_constraints/0,
- example_order_item_references_constraints/0,
- example_manager_constraints/0,
- example_ticket_status_exclusive_constraints/0,
- example_weak_entity_dependency_constraints/0,
- example_department_has_employee_constraints/0,
- example_symmetry_antisymmetry_constraints/0,
- run_example_supertype_exclusive/0,
- run_example_order_item_references/0,
- run_example_manager_constraints/0,
- run_example_ticket_status_exclusive/0,
- run_example_weak_entity_dependency/0,
- run_example_department_has_employee/0,
- run_example_symmetry_antisymmetry/0,
- run_all_examples/0,
- empty_constraints/0,
- merge_constraints/2,
- filter_constraints/2,
- rename_constraint_attrs/2,
- infer_constraints_from_pred/2,
- get_domain_from_db/2
-]).
-
--type relational_constraint() ::
- {member_of, relation_name() | #relation{} | #domain{}, binding()}
- | {'not', relational_constraint()}
- | {'not', relational_constraint(), relation_name()}
- | {'and', [relational_constraint()]}
- | {'or', [relational_constraint()]}
- | {exists, atom(), relation_name(), relational_constraint()}
- | {forall, atom(), relation_name(), relational_constraint()}.
-
--type relation_name() :: atom().
--type binding() :: #{atom() => term() | {var, atom()} | {const, term()}}.
-
--export_type([relational_constraint/0, binding/0]).
-
--spec less_than(atom(), cardinality()) -> #relation{}.
-less_than(DomainName, Cardinality) ->
- #relation{
- hash = undefined,
- name = less_than,
- tree = undefined,
- schema = #{left => DomainName, right => DomainName},
- constraints = empty_constraints(),
- cardinality = Cardinality,
- generator = {comparison, '<', DomainName},
- membership_criteria = #{
- intension => fun(#{left := L, right := R}) -> L < R end
- },
- provenance = undefined,
- lineage = {base, less_than}
- }.
-
--spec less_than_or_equal(atom(), cardinality()) -> #relation{}.
-less_than_or_equal(DomainName, Cardinality) ->
- #relation{
- hash = undefined,
- name = less_than_or_equal,
- tree = undefined,
- schema = #{left => DomainName, right => DomainName},
- constraints = empty_constraints(),
- cardinality = Cardinality,
- generator = {comparison, '=<', DomainName},
- membership_criteria = #{
- intension => fun(#{left := L, right := R}) -> L =< R end
- },
- provenance = undefined,
- lineage = {base, less_than_or_equal}
- }.
-
--spec greater_than(atom(), cardinality()) -> #relation{}.
-greater_than(DomainName, Cardinality) ->
- #relation{
- hash = undefined,
- name = greater_than,
- tree = undefined,
- schema = #{left => DomainName, right => DomainName},
- constraints = empty_constraints(),
- cardinality = Cardinality,
- generator = {comparison, '>', DomainName},
- membership_criteria = #{
- intension => fun(#{left := L, right := R}) -> L > R end
- },
- provenance = undefined,
- lineage = {base, greater_than}
- }.
-
--spec greater_than_or_equal(atom(), cardinality()) -> #relation{}.
-greater_than_or_equal(DomainName, Cardinality) ->
- #relation{
- hash = undefined,
- name = greater_than_or_equal,
- tree = undefined,
- schema = #{left => DomainName, right => DomainName},
- constraints = empty_constraints(),
- cardinality = Cardinality,
- generator = {comparison, '>=', DomainName},
- membership_criteria = #{
- intension => fun(#{left := L, right := R}) -> L >= R end
- },
- provenance = undefined,
- lineage = {base, greater_than_or_equal}
- }.
-
--spec equal(atom(), cardinality()) -> #relation{}.
-equal(DomainName, Cardinality) ->
- #relation{
- hash = undefined,
- name = equal,
- tree = undefined,
- schema = #{left => DomainName, right => DomainName},
- constraints = empty_constraints(),
- cardinality = Cardinality,
- generator = {comparison, '=:=', DomainName},
- membership_criteria = #{
- intension => fun(#{left := L, right := R}) -> L =:= R end
- },
- provenance = undefined,
- lineage = {base, equal}
- }.
-
--spec not_equal(atom(), cardinality()) -> #relation{}.
-not_equal(DomainName, Cardinality) ->
- #relation{
- hash = undefined,
- name = not_equal,
- tree = undefined,
- schema = #{left => DomainName, right => DomainName},
- constraints = empty_constraints(),
- cardinality = Cardinality,
- generator = {comparison, '/=', DomainName},
- membership_criteria = #{
- intension => fun(#{left := L, right := R}) -> L /= R end
- },
- provenance = undefined,
- lineage = {base, not_equal}
- }.
-
--spec plus(atom(), cardinality()) -> #relation{}.
-plus(DomainName, Cardinality) ->
- #relation{
- hash = undefined,
- name = plus,
- tree = undefined,
- schema = #{a => DomainName, b => DomainName, sum => DomainName},
- constraints = empty_constraints(),
- cardinality = Cardinality,
- generator = {arithmetic, plus, DomainName},
- membership_criteria = #{
- intension => fun(#{a := A, b := B, sum := S}) ->
- is_number(A) andalso is_number(B) andalso is_number(S) andalso (A + B =:= S)
- end
- },
- provenance = undefined,
- lineage = {base, plus}
- }.
-
--spec times(atom(), cardinality()) -> #relation{}.
-times(DomainName, Cardinality) ->
- #relation{
- hash = undefined,
- name = times,
- tree = undefined,
- schema = #{a => DomainName, b => DomainName, product => DomainName},
- constraints = empty_constraints(),
- cardinality = Cardinality,
- generator = {arithmetic, times, DomainName},
- membership_criteria = #{
- intension => fun(#{a := A, b := B, product := P}) ->
- is_number(A) andalso is_number(B) andalso is_number(P) andalso (A * B =:= P)
- end
- },
- provenance = undefined,
- lineage = {base, times}
- }.
-
--spec minus(atom(), cardinality()) -> #relation{}.
-minus(DomainName, Cardinality) ->
- #relation{
- hash = undefined,
- name = minus,
- tree = undefined,
- schema = #{a => DomainName, b => DomainName, difference => DomainName},
- constraints = empty_constraints(),
- cardinality = Cardinality,
- generator = {arithmetic, minus, DomainName},
- membership_criteria = #{
- intension => fun(#{a := A, b := B, difference := D}) ->
- is_number(A) andalso is_number(B) andalso is_number(D) andalso (A - B =:= D)
- end
- },
- provenance = undefined,
- lineage = {base, minus}
- }.
-
--spec divide(atom(), cardinality()) -> #relation{}.
-divide(DomainName, Cardinality) ->
- #relation{
- hash = undefined,
- name = divide,
- tree = undefined,
- schema = #{a => DomainName, b => DomainName, quotient => DomainName, remainder => DomainName},
- constraints = empty_constraints(),
- cardinality = Cardinality,
- generator = {arithmetic, divide, DomainName},
- membership_criteria = #{
- intension => fun(#{a := A, b := B, quotient := Q, remainder := R}) ->
- is_integer(A) andalso is_integer(B) andalso is_integer(Q) andalso is_integer(R)
- andalso B =/= 0
- andalso (A div B =:= Q)
- andalso (A rem B =:= R)
- end
- },
- provenance = undefined,
- lineage = {base, divide}
- }.
-
--spec member(map(), #domain{} | #relation{}) -> boolean().
-member(Tuple, #relation{membership_criteria = #{intension := Intension}}) ->
- Intension(Tuple);
-member(Tuple, #relation{membership_criteria = #{test := TestFun}}) ->
- TestFun(Tuple);
-member(Tuple, #relation{hash = RelationHash, tree = Tree}) when Tree =/= undefined ->
- BindingHashes = binding_to_hashes(Tuple),
- TupleHashes = operations:hashes_from_tuple(RelationHash),
- lists:any(
- fun(TupleHash) ->
- tuple_hash_matches_binding(TupleHash, BindingHashes)
- end,
- TupleHashes);
-member(_Tuple, #relation{tree = undefined, generator = Generator}) when Generator =/= undefined ->
- false;
-member(_Tuple, #relation{}) ->
- false;
-member(Tuple, #domain{membership_criteria = #{test := TestFun}}) ->
- TestFun(Tuple);
-member(Tuple, #domain{membership_criteria = Criteria}) when is_map(Criteria) ->
- validate_tuple_against_criteria(Tuple, Criteria).
-
-binding_to_hashes(Binding) when is_map(Binding) ->
- maps:map(
- fun(_Attr, Value) ->
- operations:value_hash(Value)
- end,
- Binding).
-
-tuple_hash_matches_binding(TupleHash, BindingHashes) ->
- [#tuple{attribute_map = AttributeMap}] = mnesia:dirty_read(tuple, TupleHash),
- tuple_matches_binding_hashes(BindingHashes, AttributeMap).
-
-tuple_matches_binding_hashes(BindingHashes, TupleAttributeMap)
- when is_map(BindingHashes), is_map(TupleAttributeMap) ->
- maps:fold(
- fun(Key, ValueHash, Acc) ->
- Acc andalso maps:get(Key, TupleAttributeMap, undefined) =:= ValueHash
- end,
- true,
- BindingHashes).
-
--spec member(binding(), #domain{} | #relation{}, map()) -> {true, map()} | false.
-member(Binding, Relation, Substitution) ->
- BoundTuple = apply_substitution(Binding, Substitution),
- case find_variables(BoundTuple) of
- [] ->
- case member(BoundTuple, Relation) of
- true -> {true, Substitution};
- false -> false
- end;
- _ ->
- false
- end.
-
--spec 'not'(#relation{}, #relation{}) -> #relation{}.
-'not'(R, Universe) ->
- RName = get_name(R),
- UniverseName = get_name(Universe),
- ResultName = list_to_atom("not_" ++ atom_to_list(RName) ++ "_in_" ++ atom_to_list(UniverseName)),
- #relation{
- hash = undefined,
- name = ResultName,
- tree = undefined,
- schema = get_schema(R),
- constraints = empty_constraints(),
- cardinality = complement_cardinality(R, Universe),
- generator = {complement, R, Universe},
- membership_criteria = #{
- intension => fun(Tuple) ->
- member(Tuple, Universe) andalso not member(Tuple, R)
- end
- },
- provenance = undefined,
- lineage = {complement, get_lineage(R), get_lineage(Universe)}
- }.
-
--spec 'and'(#relation{}, #relation{}) -> #relation{}.
-'and'(R1, R2) ->
- Schema1 = get_schema(R1),
- Schema2 = get_schema(R2),
- case schemas_compatible(Schema1, Schema2) of
- false ->
- error({incompatible_schemas, Schema1, Schema2});
- true ->
- R1Name = get_name(R1),
- R2Name = get_name(R2),
- ResultName = list_to_atom(atom_to_list(R1Name) ++ "_and_" ++ atom_to_list(R2Name)),
- #relation{
- hash = undefined,
- name = ResultName,
- tree = undefined,
- schema = merge_schemas(Schema1, Schema2),
- constraints = empty_constraints(),
- cardinality = intersection_cardinality(R1, R2),
- generator = {intersection, R1, R2},
- membership_criteria = #{
- intension => fun(Tuple) ->
- member(Tuple, R1) andalso member(Tuple, R2)
- end
- },
- provenance = undefined,
- lineage = {intersection, get_lineage(R1), get_lineage(R2)}
- }
- end.
-
--spec 'or'(#relation{}, #relation{}) -> #relation{}.
-'or'(R1, R2) ->
- Schema1 = get_schema(R1),
- Schema2 = get_schema(R2),
- case schemas_compatible(Schema1, Schema2) of
- false ->
- error({incompatible_schemas, Schema1, Schema2});
- true ->
- R1Name = get_name(R1),
- R2Name = get_name(R2),
- ResultName = list_to_atom(atom_to_list(R1Name) ++ "_or_" ++ atom_to_list(R2Name)),
- #relation{
- hash = undefined,
- name = ResultName,
- tree = undefined,
- schema = merge_schemas(Schema1, Schema2),
- constraints = empty_constraints(),
- cardinality = union_cardinality(R1, R2),
- generator = {union, R1, R2},
- membership_criteria = #{
- intension => fun(Tuple) ->
- member(Tuple, R1) orelse member(Tuple, R2)
- end
- },
- provenance = undefined,
- lineage = {union, get_lineage(R1), get_lineage(R2)}
- }
- end.
-
--spec resolve(#database_state{}, atom(), #domain{} | #relation{} | relational_constraint(), map()) ->
- {ok, boolean()} | {error, term()}.
-resolve(_Database, domain, #domain{} = Domain, Values) ->
- {ok, member(Values, Domain)};
-resolve(_Database, relation, #relation{} = Relation, Values) ->
- {ok, member(Values, Relation)};
-resolve(Database, constraint, Constraint, Binding) ->
- evaluate_constraint(Database, Constraint, Binding).
-
--spec validate_tuple(#database_state{}, map(), #relation{}) -> ok | {error, term()}.
-validate_tuple(Database, Tuple, #relation{schema = Schema, constraints = Constraints}) ->
- case validate_tuple_against_schema(Database, Tuple, Schema) of
- ok ->
- validate_tuple_against_constraints(Database, Tuple, Constraints);
- Error ->
- Error
- end.
-
--spec validate_tuple_against_schema(#database_state{}, map(), map()) -> ok | {error, term()}.
-validate_tuple_against_schema(Database, Tuple, Schema) ->
- SchemaAttrs = maps:keys(Schema),
- TupleAttrs = maps:keys(Tuple),
- Missing = SchemaAttrs -- TupleAttrs,
- case Missing of
- [] ->
- Extra = TupleAttrs -- SchemaAttrs,
- case Extra of
- [] ->
- validate_all_attributes(Database, Tuple, Schema);
- _ ->
- {error, {extra_attributes, Extra}}
- end;
- _ ->
- {error, {missing_attributes, Missing}}
- end.
-
-validate_all_attributes(Database, Tuple, Schema) ->
- maps:fold(
- fun(AttrName, DomainName, Acc) ->
- case Acc of
- {error, _} = Err ->
- Err;
- ok ->
- Value = maps:get(AttrName, Tuple),
- case get_domain_from_db(Database, DomainName) of
- {ok, Domain} ->
- case member(#{value => Value}, Domain) of
- true -> ok;
- false -> {error, {not_in_domain, AttrName, Value, DomainName}}
- end;
- {error, not_found} ->
- {error, {unknown_domain, AttrName, DomainName}}
- end
- end
- end,
- ok,
- Schema
- ).
-
-validate_tuple_against_constraints(_Database, _Tuple, undefined) ->
- ok;
-validate_tuple_against_constraints(Database, Tuple, Constraints) ->
- Normalized = normalize_constraints(Constraints),
- NamedConstraints = Normalized#relation_constraints.constraints,
- case evaluate_named_constraints(Database, NamedConstraints, Tuple) of
- {true, _Violations} ->
- ok;
- {false, Violations} ->
- {error, {constraint_violations, Violations}}
- end.
-
-evaluate_named_constraints(_Database, [], _Context) ->
- {true, []};
-evaluate_named_constraints(Database, NamedConstraints, Context) ->
- ViolationsRev = lists:foldl(
- fun({Name, Formula}, Acc) ->
- {Result, Diagnostics} = evaluate_constraint_with_diagnostics(Database, Formula, Context),
- case Result of
- true ->
- Acc;
- false ->
- [#{constraint_name => Name,
- formula => Formula,
- diagnostics => Diagnostics} | Acc]
- end
- end,
- [],
- NamedConstraints),
- case ViolationsRev of
- [] -> {true, []};
- _ -> {false, lists:reverse(ViolationsRev)}
- end.
-
--spec evaluate_constraint_list(#database_state{}, [term()], map()) -> {boolean(), [map()]}.
-evaluate_constraint_list(_Database, [], _Context) ->
- {true, []};
-evaluate_constraint_list(Database, Constraints, Context) ->
- {AllTrue, DiagnosticsRev} = lists:foldl(
- fun(ConstraintTerm, {AccTrue, AccDiags}) ->
- {Result, Diags} = evaluate_constraint_with_diagnostics(Database, ConstraintTerm, Context),
- {AccTrue andalso Result, lists:reverse(Diags) ++ AccDiags}
- end,
- {true, []},
- Constraints
- ),
- {AllTrue, lists:reverse(DiagnosticsRev)}.
-
--spec evaluate_constraint_with_diagnostics(#database_state{}, term(), map()) -> {boolean(), [map()]}.
-evaluate_constraint_with_diagnostics(Database, {member_of, RelName, Binding}, Context)
- when is_atom(RelName), is_map(Binding) ->
- BoundBinding = bind_variables(Binding, Context),
- case get_domain_from_db(Database, RelName) of
- {ok, Relation} ->
- case member(BoundBinding, Relation) of
- true -> {true, []};
- false ->
- {false, [#{kind => member_of_false, relation => RelName, binding => BoundBinding}]}
- end;
- {error, not_found} ->
- {false, [#{kind => relation_not_found, relation => RelName, binding => BoundBinding}]}
- end;
-evaluate_constraint_with_diagnostics(_Database, {member_of, #relation{} = Rel, Binding}, Context) ->
- BoundBinding = bind_variables(Binding, Context),
- case member(BoundBinding, Rel) of
- true -> {true, []};
- false -> {false, [#{kind => member_of_false, relation => get_name(Rel), binding => BoundBinding}]}
- end;
-evaluate_constraint_with_diagnostics(_Database, {member_of, #domain{} = Rel, Binding}, Context) ->
- BoundBinding = bind_variables(Binding, Context),
- case member(BoundBinding, Rel) of
- true -> {true, []};
- false -> {false, [#{kind => member_of_false, relation => get_name(Rel), binding => BoundBinding}]}
- end;
-evaluate_constraint_with_diagnostics(Database, {'and', Constraints}, Context) ->
- evaluate_constraint_list(Database, Constraints, Context);
-evaluate_constraint_with_diagnostics(Database, {'or', Constraints}, Context) ->
- {AnyTrue, DiagnosticsRev} = lists:foldl(
- fun(ConstraintTerm, {AccTrue, AccDiags}) ->
- {Result, Diags} = evaluate_constraint_with_diagnostics(Database, ConstraintTerm, Context),
- {AccTrue orelse Result, lists:reverse(Diags) ++ AccDiags}
- end,
- {false, []},
- Constraints
- ),
- {AnyTrue, lists:reverse(DiagnosticsRev)};
-evaluate_constraint_with_diagnostics(Database, {'not', Constraint}, Context) ->
- {Result, Diagnostics} = evaluate_constraint_with_diagnostics(Database, Constraint, Context),
- {not Result, Diagnostics};
-evaluate_constraint_with_diagnostics(Database, {'not', Constraint, _Universe}, Context) ->
- {Result, Diagnostics} = evaluate_constraint_with_diagnostics(Database, Constraint, Context),
- {not Result, Diagnostics};
-evaluate_constraint_with_diagnostics(Database, {exists, VarName, QuantifierRel, Constraint}, Context)
- when is_atom(VarName), is_atom(QuantifierRel) ->
- evaluate_quantifier_with_diagnostics(Database, exists, VarName, QuantifierRel, Constraint, Context);
-evaluate_constraint_with_diagnostics(Database, {forall, VarName, QuantifierRel, Constraint}, Context)
- when is_atom(VarName), is_atom(QuantifierRel) ->
- evaluate_quantifier_with_diagnostics(Database, forall, VarName, QuantifierRel, Constraint, Context);
-evaluate_constraint_with_diagnostics(_Database, Constraint, _Context) ->
- {false, [#{kind => unsupported_constraint, constraint => Constraint}]}.
-
-evaluate_quantifier_with_diagnostics(Database, Quantifier, VarName, QuantifierRel, Constraint, Context) ->
- case get_domain_from_db(Database, QuantifierRel) of
- {error, not_found} ->
- {false, [#{kind => quantifier_relation_not_found,
- quantifier => Quantifier,
- variable => VarName,
- relation => QuantifierRel}]};
- {ok, Relation} ->
- case Relation#relation.cardinality of
- {finite, _} ->
- evaluate_finite_quantifier(Database, Quantifier, VarName, QuantifierRel, Constraint, Context);
- _ ->
- {false, [#{kind => unbounded_quantifier,
- quantifier => Quantifier,
- variable => VarName,
- relation => QuantifierRel}]}
- end
- end.
-
-evaluate_finite_quantifier(Database, Quantifier, VarName, QuantifierRel, Constraint, Context) ->
- Iterator = operations:get_tuples_iterator(Database, QuantifierRel),
- case operations:collect_all(Iterator) of
- {error, Reason, _Partial} ->
- {false, [#{kind => quantifier_iteration_error,
- quantifier => Quantifier,
- variable => VarName,
- relation => QuantifierRel,
- reason => Reason}]};
- Tuples when is_list(Tuples) ->
- evaluate_quantifier_tuples(Database, Quantifier, VarName, Constraint, Context, Tuples)
- end.
-
-evaluate_quantifier_tuples(_Database, exists, _VarName, _Constraint, _Context, []) ->
- {false, []};
-evaluate_quantifier_tuples(_Database, forall, _VarName, _Constraint, _Context, []) ->
- {true, []};
-evaluate_quantifier_tuples(Database, Quantifier, VarName, Constraint, Context, Tuples) ->
- {ResultsRev, DiagnosticsRev} = lists:foldl(
- fun(TupleValueMap, {AccResults, AccDiags}) ->
- case extract_quantifier_value(TupleValueMap) of
- {ok, QuantifiedValue} ->
- ScopedContext = Context#{VarName => QuantifiedValue},
- {Result, Diags} = evaluate_constraint_with_diagnostics(Database, Constraint, ScopedContext),
- {[Result | AccResults], lists:reverse(Diags) ++ AccDiags};
- {error, Reason} ->
- {[false | AccResults], [#{kind => invalid_quantifier_tuple,
- variable => VarName,
- tuple => TupleValueMap,
- reason => Reason} | AccDiags]}
- end
- end,
- {[], []},
- Tuples
- ),
- Results = lists:reverse(ResultsRev),
- Diagnostics = lists:reverse(DiagnosticsRev),
- case Quantifier of
- exists -> {lists:any(fun(X) -> X end, Results), Diagnostics};
- forall -> {lists:all(fun(X) -> X end, Results), Diagnostics}
- end.
-
-extract_quantifier_value(TupleValueMap) when is_map(TupleValueMap) ->
- case maps:to_list(TupleValueMap) of
- [{_Attr, Value}] ->
- {ok, Value};
- [] ->
- {error, empty_tuple};
- _ ->
- {error, non_unary_quantifier_relation}
- end.
-
--spec build_membership_criteria(#database_state{}, map()) -> map().
-build_membership_criteria(Database, Schema) ->
- maps:fold(
- fun(AttrName, DomainName, Acc) ->
- case get_domain_from_db(Database, DomainName) of
- {ok, Domain} ->
- Test = case Domain#relation.membership_criteria of
- #{test := TestFun} -> TestFun;
- _ -> fun(_) -> true end
- end,
- Acc#{AttrName => #{domain => DomainName, test => Test}};
- {error, not_found} ->
- Acc#{AttrName => #{domain => DomainName, test => fun(_) -> true end}}
- end
- end,
- #{},
- Schema
- ).
-
--spec lt(term(), term()) -> relational_constraint().
-lt(Left, Right) ->
- {member_of, less_than, #{left => Left, right => Right}}.
-
--spec lte(term(), term()) -> relational_constraint().
-lte(Left, Right) ->
- {member_of, less_than_or_equal, #{left => Left, right => Right}}.
-
--spec gt(term(), term()) -> relational_constraint().
-gt(Left, Right) ->
- {member_of, greater_than, #{left => Left, right => Right}}.
-
--spec gte(term(), term()) -> relational_constraint().
-gte(Left, Right) ->
- {member_of, greater_than_or_equal, #{left => Left, right => Right}}.
-
--spec eq(term(), term()) -> relational_constraint().
-eq(Left, Right) ->
- {member_of, equal, #{left => Left, right => Right}}.
-
--spec neq(term(), term()) -> relational_constraint().
-neq(Left, Right) ->
- {member_of, not_equal, #{left => Left, right => Right}}.
-
--spec between(term(), term()) -> relational_constraint().
-between(Min, Max) ->
- {'and', [gte({var, value}, Min), lt({var, value}, Max)]}.
-
--spec empty_constraints() -> #relation_constraints{}.
-empty_constraints() ->
- #relation_constraints{constraints = []}.
-
--spec example_supertype_exclusive_constraints() -> #{atom() => #relation_constraints{}}.
-example_supertype_exclusive_constraints() ->
- #{
- b => #relation_constraints{constraints = [
- {b_requires_a, {member_of, a, #{id => {var, id}}}},
- {b_excludes_c, {'not', {member_of, c, #{id => {var, id}}}}}
- ]},
- c => #relation_constraints{constraints = [
- {c_requires_a, {member_of, a, #{id => {var, id}}}},
- {c_excludes_b, {'not', {member_of, b, #{id => {var, id}}}}}
- ]}
- }.
-
--spec example_order_item_references_constraints() -> #relation_constraints{}.
-example_order_item_references_constraints() ->
- #relation_constraints{constraints = [
- {order_item_order_fk, {member_of, order, #{id => {var, order_id}}}},
- {order_item_product_fk, {member_of, product, #{id => {var, product_id}}}}
- ]}.
-
--spec example_manager_constraints() -> #relation_constraints{}.
-example_manager_constraints() ->
- #relation_constraints{constraints = [
- {managed_employee_exists, {member_of, employee, #{id => {var, employee_id}}}},
- {manager_exists, {member_of, employee, #{id => {var, manager_id}}}},
- {no_self_management, neq({var, employee_id}, {var, manager_id})}
- ]}.
-
--spec example_ticket_status_exclusive_constraints() -> #{atom() => #relation_constraints{}}.
-example_ticket_status_exclusive_constraints() ->
- #{
- open_ticket => #relation_constraints{constraints = [
- {open_excludes_closed, {'not', {member_of, closed_ticket, #{ticket_id => {var, ticket_id}}}}}
- ]},
- closed_ticket => #relation_constraints{constraints = [
- {closed_excludes_open, {'not', {member_of, open_ticket, #{ticket_id => {var, ticket_id}}}}}
- ]}
- }.
-
--spec example_weak_entity_dependency_constraints() -> #relation_constraints{}.
-example_weak_entity_dependency_constraints() ->
- #relation_constraints{constraints = [
- {dependent_person_fk, {member_of, person, #{id => {var, person_id}}}}
- ]}.
-
--spec example_department_has_employee_constraints() -> #relation_constraints{}.
-example_department_has_employee_constraints() ->
- #relation_constraints{constraints = [
- {department_has_employee, {member_of, employee_dept, #{dept_id => {var, id}}}}
- ]}.
-
--spec example_symmetry_antisymmetry_constraints() -> #{atom() => #relation_constraints{}}.
-example_symmetry_antisymmetry_constraints() ->
- #{
- married_to => #relation_constraints{constraints = [
- {marriage_is_symmetric, {member_of, married_to, #{person_a => {var, person_b}, person_b => {var, person_a}}}}
- ]},
- reports_to => #relation_constraints{constraints = [
- {reports_to_no_self, neq({var, employee_id}, {var, manager_id})},
- {reports_to_antisymmetric,
- {'or', [
- eq({var, employee_id}, {var, manager_id}),
- {'not', {member_of, reports_to,
- #{employee_id => {var, manager_id}, manager_id => {var, employee_id}}}}
- ]}}
- ]}
- }.
-
--spec run_example_supertype_exclusive() -> map().
-run_example_supertype_exclusive() ->
- main:setup(),
- DB0 = operations:create_database(example_supertype_exclusive),
- {DB1, _} = operations:create_relation(DB0, a, #{id => integer}),
- {DB2, _} = operations:create_relation(DB1, b, #{id => integer}),
- {DB3, _} = operations:create_relation(DB2, c, #{id => integer}),
- ConstraintsByRel = example_supertype_exclusive_constraints(),
- {DB4, _} = operations:update_relation_constraints(DB3, b, maps:get(b, ConstraintsByRel)),
- {DB5, _} = operations:update_relation_constraints(DB4, c, maps:get(c, ConstraintsByRel)),
- {DB6, _} = operations:create_tuple(DB5, a, #{id => 1}),
- InB = operations:create_tuple(DB6, b, #{id => 1}),
- DBAfterB = db_after(InB),
- InCWithB = operations:create_tuple(DBAfterB, c, #{id => 1}),
- {DB7, _} = operations:create_tuple(DBAfterB, a, #{id => 3}),
- InCWithAAndNoB = operations:create_tuple(DB7, c, #{id => 3}),
- InCWithoutA = operations:create_tuple(DBAfterB, c, #{id => 2}),
- #{accepted_in_b => InB,
- accepted_in_c_with_a_only => InCWithAAndNoB,
- rejected_c_overlap_b => InCWithB,
- rejected_c_missing_a => InCWithoutA}.
-
--spec run_example_order_item_references() -> map().
-run_example_order_item_references() ->
- main:setup(),
- DB0 = operations:create_database(example_order_item_references),
- {DB1, _} = operations:create_relation(DB0, order, #{id => integer}),
- {DB2, _} = operations:create_relation(DB1, product, #{id => integer}),
- {DB3, _} = operations:create_relation(DB2, order_item, #{order_id => integer, product_id => integer}),
- {DB4, _} = operations:update_relation_constraints(DB3, order_item, example_order_item_references_constraints()),
- {DB5, _} = operations:create_tuple(DB4, order, #{id => 10}),
- {DB6, _} = operations:create_tuple(DB5, product, #{id => 20}),
- Valid = operations:create_tuple(DB6, order_item, #{order_id => 10, product_id => 20}),
- DBAfterValid = db_after(Valid),
- MissingOrder = operations:create_tuple(DBAfterValid, order_item, #{order_id => 999, product_id => 20}),
- MissingProduct = operations:create_tuple(DBAfterValid, order_item, #{order_id => 10, product_id => 999}),
- #{accepted_valid_order_item => Valid,
- rejected_missing_order => MissingOrder,
- rejected_missing_product => MissingProduct}.
-
--spec run_example_manager_constraints() -> map().
-run_example_manager_constraints() ->
- main:setup(),
- DB0 = operations:create_database(example_manager_constraints),
- {DB1, _} = operations:create_relation(DB0, employee, #{id => integer}),
- {DB2, _} = operations:create_relation(DB1, manager_of, #{employee_id => integer, manager_id => integer}),
- {DB3, _} = operations:update_relation_constraints(DB2, manager_of, example_manager_constraints()),
- {DB4, _} = operations:create_tuple(DB3, employee, #{id => 1}),
- {DB5, _} = operations:create_tuple(DB4, employee, #{id => 2}),
- Valid = operations:create_tuple(DB5, manager_of, #{employee_id => 1, manager_id => 2}),
- DBAfterValid = db_after(Valid),
- Self = operations:create_tuple(DBAfterValid, manager_of, #{employee_id => 1, manager_id => 1}),
- MissingEmployee = operations:create_tuple(DBAfterValid, manager_of, #{employee_id => 3, manager_id => 2}),
- MissingManager = operations:create_tuple(DBAfterValid, manager_of, #{employee_id => 1, manager_id => 3}),
- #{accepted_valid_manager_pair => Valid,
- rejected_self_management => Self,
- rejected_missing_employee => MissingEmployee,
- rejected_missing_manager => MissingManager}.
-
--spec run_example_ticket_status_exclusive() -> map().
-run_example_ticket_status_exclusive() ->
- main:setup(),
- DB0 = operations:create_database(example_ticket_status_exclusive),
- {DB1, _} = operations:create_relation(DB0, open_ticket, #{ticket_id => integer}),
- {DB2, _} = operations:create_relation(DB1, closed_ticket, #{ticket_id => integer}),
- ConstraintsByRel = example_ticket_status_exclusive_constraints(),
- {DB3, _} = operations:update_relation_constraints(DB2, open_ticket, maps:get(open_ticket, ConstraintsByRel)),
- {DB4, _} = operations:update_relation_constraints(DB3, closed_ticket, maps:get(closed_ticket, ConstraintsByRel)),
- Open7 = operations:create_tuple(DB4, open_ticket, #{ticket_id => 7}),
- DBAfterOpen7 = db_after(Open7),
- Closed7 = operations:create_tuple(DBAfterOpen7, closed_ticket, #{ticket_id => 7}),
- Closed8 = operations:create_tuple(DBAfterOpen7, closed_ticket, #{ticket_id => 8}),
- DBAfterClosed8 = db_after(Closed8),
- Open8 = operations:create_tuple(DBAfterClosed8, open_ticket, #{ticket_id => 8}),
- #{accepted_open_7 => Open7,
- rejected_closed_7 => Closed7,
- accepted_closed_8 => Closed8,
- rejected_open_8 => Open8}.
-
--spec run_example_weak_entity_dependency() -> map().
-run_example_weak_entity_dependency() ->
- main:setup(),
- DB0 = operations:create_database(example_weak_entity_dependency),
- {DB1, _} = operations:create_relation(DB0, person, #{id => integer}),
- {DB2, _} = operations:create_relation(DB1, dependent, #{person_id => integer, dep_name => string}),
- {DB3, _} = operations:update_relation_constraints(DB2, dependent, example_weak_entity_dependency_constraints()),
- {DB4, _} = operations:create_tuple(DB3, person, #{id => 1}),
- Valid = operations:create_tuple(DB4, dependent, #{person_id => 1, dep_name => "kid"}),
- DBAfterValid = db_after(Valid),
- MissingParent = operations:create_tuple(DBAfterValid, dependent, #{person_id => 2, dep_name => "kid2"}),
- #{accepted_existing_parent => Valid,
- rejected_missing_parent => MissingParent}.
-
--spec run_example_department_has_employee() -> map().
-run_example_department_has_employee() ->
- main:setup(),
- DB0 = operations:create_database(example_department_has_employee),
- {DB1, _} = operations:create_relation(DB0, employee_dept, #{dept_id => integer, employee_id => integer}),
- {DB2, _} = operations:create_relation(DB1, department, #{id => integer}),
- {DB3, _} = operations:update_relation_constraints(DB2, department, example_department_has_employee_constraints()),
- {DB4, _} = operations:create_tuple(DB3, employee_dept, #{dept_id => 1, employee_id => 99}),
- Dept1 = operations:create_tuple(DB4, department, #{id => 1}),
- DBAfterDept1 = db_after(Dept1),
- Dept2 = operations:create_tuple(DBAfterDept1, department, #{id => 2}),
- #{accepted_department_with_employee => Dept1,
- rejected_department_without_employee => Dept2}.
-
--spec run_example_symmetry_antisymmetry() -> map().
-run_example_symmetry_antisymmetry() ->
- main:setup(),
- DB0 = operations:create_database(example_symmetry_antisymmetry),
- {DB1, _} = operations:create_relation(DB0, married_to, #{person_a => integer, person_b => integer}),
- {DB2, _} = operations:create_relation(DB1, reports_to, #{employee_id => integer, manager_id => integer}),
- {DB3, _} = operations:create_tuple(DB2, married_to, #{person_a => 2, person_b => 1}),
- {DB4, _} = operations:create_tuple(DB3, married_to, #{person_a => 1, person_b => 2}),
- ConstraintsByRel = example_symmetry_antisymmetry_constraints(),
- {DB5, _} = operations:update_relation_constraints(DB4, married_to, maps:get(married_to, ConstraintsByRel)),
- {DB6, _} = operations:update_relation_constraints(DB5, reports_to, maps:get(reports_to, ConstraintsByRel)),
- MissingReverseMarriage = operations:create_tuple(DB6, married_to, #{person_a => 3, person_b => 4}),
- AcceptedWithReverse = operations:create_tuple(DB6, married_to, #{person_a => 2, person_b => 1}),
- Reports12 = operations:create_tuple(DB6, reports_to, #{employee_id => 1, manager_id => 2}),
- DBAfterReports12 = db_after(Reports12),
- Reports21 = operations:create_tuple(DBAfterReports12, reports_to, #{employee_id => 2, manager_id => 1}),
- Reports33 = operations:create_tuple(DBAfterReports12, reports_to, #{employee_id => 3, manager_id => 3}),
- #{rejected_missing_reverse_marriage => MissingReverseMarriage,
- accepted_with_existing_reverse_marriage => AcceptedWithReverse,
- accepted_reports_1_to_2 => Reports12,
- rejected_reports_2_to_1 => Reports21,
- rejected_reports_3_to_3 => Reports33}.
-
--spec run_all_examples() -> map().
-run_all_examples() ->
- #{
- supertype_exclusive => run_example_supertype_exclusive(),
- order_item_references => run_example_order_item_references(),
- manager_constraints => run_example_manager_constraints(),
- ticket_status_exclusive => run_example_ticket_status_exclusive(),
- weak_entity_dependency => run_example_weak_entity_dependency(),
- department_has_employee => run_example_department_has_employee(),
- symmetry_antisymmetry => run_example_symmetry_antisymmetry()
- }.
-
-db_after({DB, _Relation}) -> DB.
-
--spec merge_constraints(#relation_constraints{} | undefined, #relation_constraints{} | undefined) -> #relation_constraints{}.
-merge_constraints(C1, C2) ->
- N1 = normalize_constraints(C1),
- N2 = normalize_constraints(C2),
- #relation_constraints{constraints = N1#relation_constraints.constraints ++ N2#relation_constraints.constraints}.
-
--spec filter_constraints(#relation_constraints{} | undefined, [atom()]) -> #relation_constraints{}.
-filter_constraints(Constraints, Attrs) ->
- Normalized = normalize_constraints(Constraints),
- Keep = [
- {Name, Formula} || {Name, Formula} <- Normalized#relation_constraints.constraints,
- lists:all(fun(V) -> lists:member(V, Attrs) end, vars_in_constraint(Formula))
- ],
- #relation_constraints{constraints = Keep}.
-
--spec rename_constraint_attrs(#relation_constraints{} | undefined, map()) -> #relation_constraints{}.
-rename_constraint_attrs(Constraints, RenameMappings) ->
- Normalized = normalize_constraints(Constraints),
- Renamed = [{Name, rename_vars_in_constraint(Formula, RenameMappings)} ||
- {Name, Formula} <- Normalized#relation_constraints.constraints],
- #relation_constraints{constraints = Renamed}.
-
--spec infer_constraints_from_pred(term(), map()) -> #relation_constraints{}.
-infer_constraints_from_pred(Predicate, _Schema) when is_function(Predicate) ->
- empty_constraints();
-infer_constraints_from_pred({attr, AttrName, Op, Value}, _Schema) ->
- Name = list_to_atom(atom_to_list(AttrName) ++ "_" ++ atom_to_list(Op)),
- #relation_constraints{constraints = [{Name, op_to_constraint(Op, AttrName, Value)}]};
-infer_constraints_from_pred({'and', Predicates}, Schema) ->
- lists:foldl(
- fun(Pred, Acc) ->
- merge_constraints(Acc, infer_constraints_from_pred(Pred, Schema))
- end,
- empty_constraints(),
- Predicates
- );
-infer_constraints_from_pred({'or', Predicates}, Schema) ->
- OrParts = [
- constraint_from_predicate(P, Schema) || P <- Predicates,
- constraint_from_predicate(P, Schema) =/= undefined
- ],
- case OrParts of
- [] -> empty_constraints();
- _ -> #relation_constraints{constraints = [{inferred_or, {'or', OrParts}}]}
- end;
-infer_constraints_from_pred(_, _Schema) ->
- empty_constraints().
-
-constraint_from_predicate({attr, AttrName, Op, Value}, _Schema) ->
- op_to_constraint(Op, AttrName, Value);
-constraint_from_predicate({'and', Predicates}, Schema) ->
- {'and', [C || C <- [constraint_from_predicate(P, Schema) || P <- Predicates], C =/= undefined]};
-constraint_from_predicate({'or', Predicates}, Schema) ->
- {'or', [C || C <- [constraint_from_predicate(P, Schema) || P <- Predicates], C =/= undefined]};
-constraint_from_predicate(_, _Schema) ->
- undefined.
-
-op_to_constraint(lt, Attr, Value) -> lt({var, Attr}, Value);
-op_to_constraint(lte, Attr, Value) -> lte({var, Attr}, Value);
-op_to_constraint(gt, Attr, Value) -> gt({var, Attr}, Value);
-op_to_constraint(gte, Attr, Value) -> gte({var, Attr}, Value);
-op_to_constraint(eq, Attr, Value) -> eq({var, Attr}, Value);
-op_to_constraint(neq, Attr, Value) -> neq({var, Attr}, Value);
-op_to_constraint(between, Attr, {Min, Max}) -> {'and', [gte({var, Attr}, Min), lt({var, Attr}, Max)]};
-op_to_constraint(_, _Attr, _Value) -> {'and', []}.
-
-normalize_constraints(undefined) ->
- empty_constraints();
-normalize_constraints(#relation_constraints{} = Constraints) ->
- NormalizedEntries = normalize_named_constraints(Constraints#relation_constraints.constraints),
- assert_unique_constraint_names(NormalizedEntries),
- Constraints#relation_constraints{constraints = NormalizedEntries};
-normalize_constraints(_) ->
- empty_constraints().
-
-normalize_named_constraints(Entries) ->
- [normalize_named_constraint(Entry) || Entry <- Entries].
-
-normalize_named_constraint({Name, Formula}) when is_atom(Name) ->
- {Name, Formula};
-normalize_named_constraint(InvalidEntry) ->
- error({invalid_constraint_entry, InvalidEntry}).
-
-assert_unique_constraint_names(NamedConstraints) ->
- Names = [Name || {Name, _Formula} <- NamedConstraints],
- case has_duplicates(Names) of
- true ->
- error({duplicate_constraint_names, Names});
- false ->
- ok
- end.
-
-has_duplicates([]) ->
- false;
-has_duplicates([Name | Rest]) ->
- lists:member(Name, Rest) orelse has_duplicates(Rest).
-
-vars_in_constraint({member_of, _Rel, Binding}) when is_map(Binding) ->
- maps:fold(
- fun(_K, V, Acc) ->
- Acc ++ vars_in_term(V)
- end,
- [],
- Binding);
-vars_in_constraint({'and', Cs}) -> lists:usort(lists:flatmap(fun vars_in_constraint/1, Cs));
-vars_in_constraint({'or', Cs}) -> lists:usort(lists:flatmap(fun vars_in_constraint/1, Cs));
-vars_in_constraint({'not', C}) -> vars_in_constraint(C);
-vars_in_constraint({'not', C, _}) -> vars_in_constraint(C);
-vars_in_constraint({exists, Var, _Rel, C}) -> lists:delete(Var, vars_in_constraint(C));
-vars_in_constraint({forall, Var, _Rel, C}) -> lists:delete(Var, vars_in_constraint(C));
-vars_in_constraint(_) -> [].
-
-vars_in_term({var, Name}) when is_atom(Name) -> [Name];
-vars_in_term({const, _}) -> [];
-vars_in_term(T) when is_tuple(T) ->
- lists:flatmap(fun vars_in_term/1, tuple_to_list(T));
-vars_in_term(T) when is_list(T) ->
- lists:flatmap(fun vars_in_term/1, T);
-vars_in_term(T) when is_map(T) ->
- maps:fold(fun(_K, V, Acc) -> Acc ++ vars_in_term(V) end, [], T);
-vars_in_term(_) -> [].
-
-rename_vars_in_constraint({member_of, RelName, Binding}, Mappings) ->
- RenamedBinding = maps:map(
- fun(_K, {var, VarName}) ->
- {var, maps:get(VarName, Mappings, VarName)};
- (_K, V) ->
- V
- end,
- Binding
- ),
- {member_of, RelName, RenamedBinding};
-rename_vars_in_constraint({'and', Constraints}, Mappings) ->
- {'and', [rename_vars_in_constraint(C, Mappings) || C <- Constraints]};
-rename_vars_in_constraint({'or', Constraints}, Mappings) ->
- {'or', [rename_vars_in_constraint(C, Mappings) || C <- Constraints]};
-rename_vars_in_constraint({'not', Constraint}, Mappings) ->
- {'not', rename_vars_in_constraint(Constraint, Mappings)};
-rename_vars_in_constraint({'not', Constraint, Universe}, Mappings) ->
- {'not', rename_vars_in_constraint(Constraint, Mappings), Universe};
-rename_vars_in_constraint({exists, Var, Rel, Constraint}, Mappings) ->
- {exists, maps:get(Var, Mappings, Var), Rel, rename_vars_in_constraint(Constraint, Mappings)};
-rename_vars_in_constraint({forall, Var, Rel, Constraint}, Mappings) ->
- {forall, maps:get(Var, Mappings, Var), Rel, rename_vars_in_constraint(Constraint, Mappings)};
-rename_vars_in_constraint(Other, _Mappings) ->
- Other.
-
-validate_tuple_against_criteria(Tuple, Criteria) when is_map(Criteria) ->
- maps:fold(
- fun(Attr, Constraint, Acc) ->
- Acc andalso validate_attribute(maps:get(Attr, Tuple, undefined), Constraint)
- end,
- true,
- Criteria
- ).
-
-validate_attribute(undefined, _) -> false;
-validate_attribute(Value, #{test := Test}) -> Test(#{value => Value});
-validate_attribute(Value, Constraint) when is_function(Constraint, 1) -> Constraint(Value);
-validate_attribute(_, _) -> true.
-
-apply_substitution(Binding, Substitution) ->
- maps:map(
- fun(_K, {var, VarName}) ->
- maps:get(VarName, Substitution, {var, VarName});
- (_K, V) ->
- V
- end,
- Binding
- ).
-
-find_variables(Binding) ->
- maps:fold(
- fun(_K, {var, VarName}, Acc) -> [VarName | Acc];
- (_K, _, Acc) -> Acc
- end,
- [],
- Binding
- ).
-
-get_schema(#domain{schema = S}) -> S;
-get_schema(#relation{schema = S}) -> S.
-
-get_name(#domain{name = N}) -> N;
-get_name(#relation{name = N}) -> N.
-
-get_lineage(#relation{lineage = L}) -> L;
-get_lineage(#domain{name = N}) -> {base, N}.
-
-schemas_compatible(S1, S2) ->
- maps:keys(S1) =:= maps:keys(S2).
-
-merge_schemas(S1, _S2) ->
- S1.
-
-get_cardinality(#domain{cardinality = C}) -> C;
-get_cardinality(#relation{cardinality = C}) -> C.
-
-complement_cardinality(R, Universe) ->
- RC = get_cardinality(R),
- UC = get_cardinality(Universe),
- case {RC, UC} of
- {{finite, N}, {finite, M}} -> {finite, max(0, M - N)};
- {{finite, _}, Infinite} -> Infinite;
- _ -> UC
- end.
-
-intersection_cardinality(R1, R2) ->
- C1 = get_cardinality(R1),
- C2 = get_cardinality(R2),
- case {C1, C2} of
- {{finite, N}, _} -> {finite, N};
- {_, {finite, M}} -> {finite, M};
- _ -> min_cardinality(C1, C2)
- end.
-
-union_cardinality(R1, R2) ->
- C1 = get_cardinality(R1),
- C2 = get_cardinality(R2),
- case {C1, C2} of
- {{finite, N}, {finite, M}} -> {finite, N + M};
- _ -> max_cardinality(C1, C2)
- end.
-
-min_cardinality({finite, N}, {finite, M}) -> {finite, min(N, M)};
-min_cardinality({finite, N}, _) -> {finite, N};
-min_cardinality(_, {finite, M}) -> {finite, M};
-min_cardinality(aleph_zero, _) -> aleph_zero;
-min_cardinality(_, aleph_zero) -> aleph_zero;
-min_cardinality(continuum, continuum) -> continuum.
-
-max_cardinality({finite, N}, {finite, M}) -> {finite, max(N, M)};
-max_cardinality(continuum, _) -> continuum;
-max_cardinality(_, continuum) -> continuum;
-max_cardinality(aleph_zero, _) -> aleph_zero;
-max_cardinality(_, aleph_zero) -> aleph_zero.
-
-evaluate_constraint(Database, {member_of, RelName, Binding}, Context) when is_atom(RelName) ->
- BoundBinding = bind_variables(Binding, Context),
- case get_domain_from_db(Database, RelName) of
- {ok, Relation} ->
- {ok, member(BoundBinding, Relation)};
- {error, not_found} ->
- {error, {relation_not_found, RelName}}
- end;
-evaluate_constraint(_Database, {member_of, #relation{} = Rel, Binding}, Context) ->
- BoundBinding = bind_variables(Binding, Context),
- {ok, member(BoundBinding, Rel)};
-evaluate_constraint(_Database, {member_of, #domain{} = Rel, Binding}, Context) ->
- BoundBinding = bind_variables(Binding, Context),
- {ok, member(BoundBinding, Rel)};
-evaluate_constraint(Database, {'not', Constraint, _Universe}, Context) ->
- case evaluate_constraint(Database, Constraint, Context) of
- {ok, Bool} -> {ok, not Bool};
- Error -> Error
- end;
-evaluate_constraint(Database, {'and', Constraints}, Context) ->
- Results = [evaluate_constraint(Database, C, Context) || C <- Constraints],
- case lists:all(fun({ok, true}) -> true; (_) -> false end, Results) of
- true -> {ok, true};
- false -> {ok, false}
- end;
-evaluate_constraint(Database, {'or', Constraints}, Context) ->
- Results = [evaluate_constraint(Database, C, Context) || C <- Constraints],
- case lists:any(fun({ok, true}) -> true; (_) -> false end, Results) of
- true -> {ok, true};
- false -> {ok, false}
- end;
-evaluate_constraint(Database, {'not', Constraint}, Context) ->
- {Result, _Diagnostics} = evaluate_constraint_with_diagnostics(Database, {'not', Constraint}, Context),
- {ok, Result};
-evaluate_constraint(Database, {exists, VarName, QuantifierRel, Constraint}, Context) ->
- {Result, _Diagnostics} = evaluate_constraint_with_diagnostics(Database, {exists, VarName, QuantifierRel, Constraint}, Context),
- {ok, Result};
-evaluate_constraint(Database, {forall, VarName, QuantifierRel, Constraint}, Context) ->
- {Result, _Diagnostics} = evaluate_constraint_with_diagnostics(Database, {forall, VarName, QuantifierRel, Constraint}, Context),
- {ok, Result};
-evaluate_constraint(_Database, _, _) ->
- {error, unsupported_constraint}.
-
-bind_variables(Binding, Context) ->
- maps:map(
- fun(_K, {var, VarName}) ->
- maps:get(VarName, Context, maps:get(value, Context, undefined));
- (_K, {const, Value}) ->
- Value;
- (_K, V) ->
- V
- end,
- Binding
- ).
-
--spec get_domain_from_db(#database_state{}, atom()) -> {ok, #relation{}} | {error, not_found}.
-get_domain_from_db(Database, DomainName) ->
- case maps:find(DomainName, Database#database_state.relations) of
- {ok, DomainHash} ->
- [Domain] = mnesia:dirty_read(relation, DomainHash),
- {ok, Domain};
- error ->
- {error, not_found}
- end.
diff --git a/src/generators.erl b/src/generators.erl
deleted file mode 100644
index 37facc3..0000000
--- a/src/generators.erl
+++ /dev/null
@@ -1,584 +0,0 @@
-%%% @doc Generators for Infinite Relations
-%%%
-%%% This module provides generator functions for primitive infinite relations.
-%%%
-%%% == Domain Relations (Base Types) ==
-%%%
-%%% Domain relations are unary relations representing mathematical sets:
-%%% <ul>
-%%% <li>Naturals (ℕ): {(0), (1), (2), (3), ...}</li>
-%%% <li>Integers (ℤ): {(0), (1), (-1), (2), (-2), ...}</li>
-%%% <li>Rationals (ℚ): {(0/1), (1/1), (1/2), (2/1), ...}</li>
-%%% </ul>
-%%%
-%%% Domain relations have empty schemas (`schema => #{}') as they are the
-%%% base case in the type system.
-%%%
-%%% == Function Relations ==
-%%%
-%%% Function relations represent mathematical operations as infinite relations:
-%%% <ul>
-%%% <li>Plus: {(a, b, sum) | a ∈ ℤ, b ∈ ℤ, sum = a + b}</li>
-%%% <li>Times: {(a, b, product) | a ∈ ℤ, b ∈ ℤ, product = a × b}</li>
-%%% <li>Minus: {(a, b, difference) | a ∈ ℤ, b ∈ ℤ, difference = a - b}</li>
-%%% <li>Divide: {(a, b, q, r) | a ∈ ℤ, b ∈ ℤ\{0}, a = b×q + r}</li>
-%%% </ul>
-%%%
-%%% Function relations reference domain relations in their schemas:
-%%% <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, ?] 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
-%%% are bound and compute the missing values accordingly.
-%%%
-%%% == 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.
-%%%
-%%% Generators produce tuples lazily based on constraints, enabling
-%%% memory-bounded iteration over infinite relations.
-%%%
-%%% @author Nekoma Team
-%%% @copyright 2025
-
--module(generators).
--include("operations.hrl").
-
--export([
- boolean/1,
- natural/1,
- integer/1,
- rational/1,
- make_range_generator/3,
- constrained_natural/2,
- constrained_integer/2,
- %% Function relations
- plus/1,
- times/1,
- minus/1,
- divide/1
-]).
-
-%%% ============================================================================
-%%% 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,
-%% only generates numbers within that range.
-%%
-%% @param Constraints Map of attribute constraints
-%% @returns Generator function producing naturals
-natural(Constraints) ->
- case extract_bounds(Constraints, value) of
- {range, Min, Max} when Min >= 0 ->
- make_range_generator(Min, Max, fun(N) -> #{value => N} end);
- {range, Min, Max} when Min < 0, Max >= 0 ->
- %% Negative min for naturals: adjust to start at 0
- make_range_generator(0, Max, fun(N) -> #{value => N} end);
- {range, _Min, Max} when Max < 0 ->
- %% Entire range is negative: no naturals exist
- fun(_) -> done end;
- {gte, Min} when Min >= 0 ->
- make_unbounded_generator(Min, fun(N) -> #{value => N} end);
- {gte, Min} when Min < 0 ->
- %% Negative lower bound for naturals: start at 0
- make_unbounded_generator(0, fun(N) -> #{value => N} end);
- {lte, Max} when Max >= 0 ->
- make_range_generator(0, Max, fun(N) -> #{value => N} end);
- {lte, Max} when Max < 0 ->
- %% Upper bound is negative: no naturals exist
- fun(_) -> done end;
- {eq, Value} when Value >= 0 ->
- make_singleton_generator(#{value => Value});
- {eq, _Value} ->
- %% Negative value: not a natural
- fun(_) -> done end;
- {in, List} ->
- make_membership_generator(List, fun(N) -> #{value => N} end);
- unbounded ->
- make_unbounded_generator(0, fun(N) -> #{value => N} end);
- {error, Reason} ->
- fun(_) -> {error, Reason} end
- end.
-
-%% @doc Generator for integers (ℤ = {..., -2, -1, 0, 1, 2, ...}).
-%%
-%% Enumerates integers using interleaving strategy:
-%% 0, 1, -1, 2, -2, 3, -3, ...
-%%
-%% Mapping: f(0) = 0
-%% f(2n) = n for n > 0
-%% f(2n+1) = -n for n > 0
-%%
-%% @param Constraints Map of attribute constraints
-%% @returns Generator function producing integers
-integer(Constraints) ->
- case extract_bounds(Constraints, value) of
- {range, Min, Max} ->
- make_integer_range_generator(Min, Max);
- {gte, Min} ->
- make_unbounded_integer_generator(Min, infinity);
- {lte, Max} ->
- make_unbounded_integer_generator(neg_infinity, Max);
- {eq, Value} ->
- make_singleton_generator(#{value => Value});
- {in, List} ->
- make_membership_generator(List, fun(N) -> #{value => N} end);
- unbounded ->
- make_interleaved_integer_generator();
- {error, Reason} ->
- fun(_) -> {error, Reason} end
- end.
-
-%% @doc Generator for rational numbers (ℚ) using Stern-Brocot tree.
-%%
-%% Enumerates all rational numbers in reduced form (p/q where gcd(p,q) = 1)
-%% using the Stern-Brocot tree, which systematically generates all positive
-%% rationals without repetition.
-%%
-%% Schema: #{numerator => integer, denominator => natural}
-%%
-%% @param Constraints Map of attribute constraints
-%% @returns Generator function producing rationals
-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}},
- make_stern_brocot_generator(InitialState, Constraints).
-
-%%% ============================================================================
-%%% Helper: Constraint Extraction
-%%% ============================================================================
-
-%% @doc Extract bounds from constraints for a given attribute.
-%%
-%% @param Constraints Constraint map
-%% @param Attr Attribute name
-%% @returns {range, Min, Max} | {gte, Min} | {lte, Max} | {eq, Value} | unbounded
-extract_bounds(Constraints, Attr) ->
- case maps:get(Attr, Constraints, undefined) of
- undefined ->
- unbounded;
- {range, Min, Max} ->
- {range, Min, Max};
- {gte, Min} ->
- {gte, Min};
- {lte, Max} ->
- {lte, Max};
- {gt, Min} ->
- {gte, Min + 1};
- {lt, Max} ->
- {lte, Max - 1};
- {eq, Value} ->
- {eq, Value};
- {in, List} ->
- {in, List};
- Other ->
- {error, {unsupported_constraint, Other}}
- end.
-
-%%% ============================================================================
-%%% Generator Constructors
-%%% ============================================================================
-
-%% @doc Create a generator for a finite range [Min, Max].
-make_range_generator(Min, Max, MapFun) when Min =< Max ->
- make_range_gen(Min, Max, MapFun).
-
-make_range_gen(Current, Max, MapFun) when Current =< Max ->
- fun(next) ->
- Tuple = MapFun(Current),
- NextGen = make_range_gen(Current + 1, Max, MapFun),
- {value, Tuple, NextGen}
- end;
-make_range_gen(_, _, _) ->
- fun(_) -> done end.
-
-%% @doc Create an unbounded generator starting from Min.
-make_unbounded_generator(Start, MapFun) ->
- make_unbounded_gen(Start, MapFun).
-
-make_unbounded_gen(Current, MapFun) ->
- fun(next) ->
- Tuple = MapFun(Current),
- NextGen = make_unbounded_gen(Current + 1, MapFun),
- {value, Tuple, NextGen}
- end.
-
-%% @doc Create a singleton generator (single value).
-make_singleton_generator(Tuple) ->
- fun(next) -> {value, Tuple, fun(_) -> done end} end.
-
-%% @doc Create a generator from a membership list.
-make_membership_generator([], _MapFun) ->
- fun(_) -> done end;
-make_membership_generator([H|T], MapFun) ->
- fun(next) ->
- Tuple = MapFun(H),
- NextGen = make_membership_generator(T, MapFun),
- {value, Tuple, NextGen}
- end.
-
-%% @doc Create integer range generator.
-make_integer_range_generator(Min, Max) when Min =< Max ->
- make_range_generator(Min, Max, fun(N) -> #{value => N} end);
-make_integer_range_generator(_, _) ->
- fun(_) -> done end.
-
-%% @doc Create interleaved integer generator: 0, 1, -1, 2, -2, 3, -3, ...
-make_interleaved_integer_generator() ->
- make_interleaved_gen(0).
-
-make_interleaved_gen(0) ->
- fun(next) ->
- {value, #{value => 0}, make_interleaved_gen(1)}
- end;
-make_interleaved_gen(N) ->
- fun(next) ->
- Positive = #{value => N},
- NextGen1 = fun(next) ->
- Negative = #{value => -N},
- NextGen2 = make_interleaved_gen(N + 1),
- {value, Negative, NextGen2}
- end,
- {value, Positive, NextGen1}
- end.
-
-%% @doc Unbounded integer generator with optional min/max bounds.
-make_unbounded_integer_generator(neg_infinity, infinity) ->
- make_interleaved_integer_generator();
-make_unbounded_integer_generator(Min, infinity) when is_integer(Min) ->
- make_unbounded_generator(Min, fun(N) -> #{value => N} end);
-make_unbounded_integer_generator(neg_infinity, Max) when is_integer(Max) ->
- %% Generate from Max down to -infinity
- make_decreasing_gen(Max);
-make_unbounded_integer_generator(Min, Max) when is_integer(Min), is_integer(Max) ->
- make_integer_range_generator(Min, Max).
-
-make_decreasing_gen(Current) ->
- fun(next) ->
- Tuple = #{value => Current},
- NextGen = make_decreasing_gen(Current - 1),
- {value, Tuple, NextGen}
- end.
-
-%% @doc Stern-Brocot tree generator for rationals.
-%%
-%% The Stern-Brocot tree enumerates all positive rationals exactly once.
-%% Each rational p/q is generated in reduced form.
-%%
-%% Algorithm:
-%% 1. Start with two fractions: 0/1 (left ancestor) and 1/0 (right ancestor)
-%% 2. The mediant of a/b and c/d is (a+c)/(b+d)
-%% 3. Build tree by computing mediants
-make_stern_brocot_generator(State, _Constraints) ->
- make_sb_gen(State).
-
-make_sb_gen({Queue, {LA, LB}, {RA, RB}}) ->
- fun(next) ->
- case queue:out(Queue) of
- {empty, _} ->
- done;
- {{value, {P, Q}}, RestQueue} ->
- %% Current rational is P/Q
- Tuple = #{numerator => P, denominator => Q},
-
- %% Generate children (mediants)
- %% Left child: mediant of left ancestor and current
- %% Right child: mediant of current and right ancestor
- LeftChild = {LA + P, LB + Q},
- RightChild = {P + RA, Q + RB},
-
- %% Enqueue children
- NewQueue = queue:in(RightChild, queue:in(LeftChild, RestQueue)),
-
- %% Continue with updated state
- NextGen = make_sb_gen({NewQueue, {LA, LB}, {RA, RB}}),
- {value, Tuple, NextGen}
- end
- end.
-
-%%% ============================================================================
-%%% Constrained Generators (Public API)
-%%% ============================================================================
-
-%% @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 integer generator.
--spec constrained_integer(integer(), integer()) -> generator_fun().
-constrained_integer(Min, Max) ->
- integer(#{value => {range, Min, Max}}).
-
-%%% ============================================================================
-%%% Function Relations
-%%% ============================================================================
-
-%% @doc Plus relation: {(a, b, sum) | sum = a + b}.
-%%
-%% Generates tuples for the addition relation. Requires at least 2 of 3
-%% attributes to be constrained for bounded generation.
-%%
-%% Constraint strategies:
-%% <ul>
-%% <li>a, b constrained → compute sum</li>
-%% <li>a, sum constrained → compute b = sum - a</li>
-%% <li>b, sum constrained → compute a = sum - b</li>
-%% <li>sum constrained only → generate all (a,b) pairs that sum to value</li>
-%% <li>Less than 1 constrained → error (unbounded)</li>
-%% </ul>
-%%
-%% @param Constraints Map of attribute constraints
-%% @returns Generator function producing plus tuples
-plus(Constraints) ->
- ABounds = extract_bounds(Constraints, a),
- BBounds = extract_bounds(Constraints, b),
- SumBounds = extract_bounds(Constraints, sum),
-
- case {ABounds, BBounds, SumBounds} of
- %% Both operands constrained - generate sums
- {{range, AMin, AMax}, {range, BMin, BMax}, _} ->
- make_plus_ab_generator(AMin, AMax, BMin, BMax);
-
- %% a and sum constrained - compute b
- {{range, AMin, AMax}, _, {range, SMin, SMax}} ->
- make_plus_a_sum_generator(AMin, AMax, SMin, SMax);
-
- %% b and sum constrained - compute a
- {_, {range, BMin, BMax}, {range, SMin, SMax}} ->
- make_plus_b_sum_generator(BMin, BMax, SMin, SMax);
-
- %% Only sum constrained - generate all pairs
- {unbounded, unbounded, {eq, SumValue}} ->
- make_plus_sum_only_generator(SumValue);
-
- {unbounded, unbounded, {range, SMin, SMax}} ->
- make_plus_sum_range_generator(SMin, SMax);
-
- %% Insufficient constraints
- _ ->
- fun(_) -> {error, {unbounded_plus, Constraints}} end
- end.
-
-%% @doc Times relation: {(a, b, product) | product = a * b}.
-%%
-%% Generates tuples for the multiplication relation.
-%%
-%% @param Constraints Map of attribute constraints
-%% @returns Generator function producing times tuples
-times(Constraints) ->
- ABounds = extract_bounds(Constraints, a),
- BBounds = extract_bounds(Constraints, b),
- ProductBounds = extract_bounds(Constraints, product),
-
- case {ABounds, BBounds, ProductBounds} of
- %% Both operands constrained - generate products
- {{range, AMin, AMax}, {range, BMin, BMax}, _} ->
- make_times_ab_generator(AMin, AMax, BMin, BMax);
-
- %% Insufficient constraints
- _ ->
- fun(_) -> {error, {unbounded_times, Constraints}} end
- end.
-
-%% @doc Minus relation: {(a, b, difference) | difference = a - b}.
-%%
-%% @param Constraints Map of attribute constraints
-%% @returns Generator function producing minus tuples
-minus(Constraints) ->
- ABounds = extract_bounds(Constraints, a),
- BBounds = extract_bounds(Constraints, b),
- DiffBounds = extract_bounds(Constraints, difference),
-
- case {ABounds, BBounds, DiffBounds} of
- {{range, AMin, AMax}, {range, BMin, BMax}, _} ->
- make_minus_ab_generator(AMin, AMax, BMin, BMax);
-
- _ ->
- fun(_) -> {error, {unbounded_minus, Constraints}} end
- end.
-
-%% @doc Divide relation: {(a, b, quotient) | quotient = a / b, b ≠ 0}.
-%%
-%% @param Constraints Map of attribute constraints
-%% @returns Generator function producing divide tuples
-divide(Constraints) ->
- ABounds = extract_bounds(Constraints, a),
- BBounds = extract_bounds(Constraints, b),
-
- case {ABounds, BBounds} of
- {{range, AMin, AMax}, {range, BMin, BMax}} when BMin =/= 0 orelse BMax =/= 0 ->
- make_divide_ab_generator(AMin, AMax, BMin, BMax);
-
- _ ->
- fun(_) -> {error, {unbounded_divide, Constraints}} end
- end.
-
-%%% ============================================================================
-%%% Function Relation Generator Helpers
-%%% ============================================================================
-
-%% Plus: a and b constrained
-make_plus_ab_generator(AMin, AMax, BMin, BMax) ->
- Pairs = [{A, B} || A <- lists:seq(AMin, AMax), B <- lists:seq(BMin, BMax)],
- make_plus_pairs_gen(Pairs).
-
-make_plus_pairs_gen([]) ->
- fun(_) -> done end;
-make_plus_pairs_gen([{A, B} | Rest]) ->
- fun(next) ->
- Tuple = #{a => A, b => B, sum => A + B},
- NextGen = make_plus_pairs_gen(Rest),
- {value, Tuple, NextGen}
- end.
-
-%% Plus: a and sum constrained, compute b
-make_plus_a_sum_generator(AMin, AMax, SMin, SMax) ->
- Pairs = [{A, S} ||
- A <- lists:seq(AMin, AMax),
- S <- lists:seq(SMin, SMax),
- (S - A) >= 0], % b must be non-negative if we want naturals
- make_plus_a_sum_gen(Pairs).
-
-make_plus_a_sum_gen([]) ->
- fun(_) -> done end;
-make_plus_a_sum_gen([{A, Sum} | Rest]) ->
- fun(next) ->
- B = Sum - A,
- Tuple = #{a => A, b => B, sum => Sum},
- NextGen = make_plus_a_sum_gen(Rest),
- {value, Tuple, NextGen}
- end.
-
-%% Plus: b and sum constrained, compute a
-make_plus_b_sum_generator(BMin, BMax, SMin, SMax) ->
- Pairs = [{B, S} ||
- B <- lists:seq(BMin, BMax),
- S <- lists:seq(SMin, SMax),
- (S - B) >= 0],
- make_plus_b_sum_gen(Pairs).
-
-make_plus_b_sum_gen([]) ->
- fun(_) -> done end;
-make_plus_b_sum_gen([{B, Sum} | Rest]) ->
- fun(next) ->
- A = Sum - B,
- Tuple = #{a => A, b => B, sum => Sum},
- NextGen = make_plus_b_sum_gen(Rest),
- {value, Tuple, NextGen}
- end.
-
-%% Plus: only sum constrained - generate all non-negative pairs
-make_plus_sum_only_generator(SumValue) when SumValue >= 0 ->
- Pairs = [{A, SumValue - A} || A <- lists:seq(0, SumValue)],
- make_plus_pairs_gen(Pairs);
-make_plus_sum_only_generator(_SumValue) ->
- %% Negative sum: no non-negative pairs exist
- fun(_) -> done end.
-
-%% Plus: sum range constrained
-make_plus_sum_range_generator(SMin, SMax) ->
- %% Adjust range to non-negative values (naturals only)
- AdjustedMin = max(0, SMin),
- Pairs = [{A, B, S} ||
- S <- lists:seq(AdjustedMin, SMax),
- A <- lists:seq(0, S),
- B <- [S - A]],
- make_plus_triple_gen(Pairs).
-
-make_plus_triple_gen([]) ->
- fun(_) -> done end;
-make_plus_triple_gen([{A, B, Sum} | Rest]) ->
- fun(next) ->
- Tuple = #{a => A, b => B, sum => Sum},
- NextGen = make_plus_triple_gen(Rest),
- {value, Tuple, NextGen}
- end.
-
-%% Times: a and b constrained
-make_times_ab_generator(AMin, AMax, BMin, BMax) ->
- Pairs = [{A, B} || A <- lists:seq(AMin, AMax), B <- lists:seq(BMin, BMax)],
- make_times_pairs_gen(Pairs).
-
-make_times_pairs_gen([]) ->
- fun(_) -> done end;
-make_times_pairs_gen([{A, B} | Rest]) ->
- fun(next) ->
- Tuple = #{a => A, b => B, product => A * B},
- NextGen = make_times_pairs_gen(Rest),
- {value, Tuple, NextGen}
- end.
-
-%% Minus: a and b constrained
-make_minus_ab_generator(AMin, AMax, BMin, BMax) ->
- Pairs = [{A, B} || A <- lists:seq(AMin, AMax), B <- lists:seq(BMin, BMax)],
- make_minus_pairs_gen(Pairs).
-
-make_minus_pairs_gen([]) ->
- fun(_) -> done end;
-make_minus_pairs_gen([{A, B} | Rest]) ->
- fun(next) ->
- Tuple = #{a => A, b => B, difference => A - B},
- NextGen = make_minus_pairs_gen(Rest),
- {value, Tuple, NextGen}
- end.
-
-%% Divide: a and b constrained (integer division)
-make_divide_ab_generator(AMin, AMax, BMin, BMax) ->
- Pairs = [{A, B} ||
- A <- lists:seq(AMin, AMax),
- B <- lists:seq(BMin, BMax),
- B =/= 0], % Avoid division by zero
- make_divide_pairs_gen(Pairs).
-
-make_divide_pairs_gen([]) ->
- fun(_) -> done end;
-make_divide_pairs_gen([{A, B} | Rest]) ->
- fun(next) ->
- Tuple = #{a => A, b => B, quotient => A div B, remainder => A rem B},
- NextGen = make_divide_pairs_gen(Rest),
- {value, Tuple, NextGen}
- end.
diff --git a/src/main.app.src b/src/main.app.src
deleted file mode 100644
index 34e2f46..0000000
--- a/src/main.app.src
+++ /dev/null
@@ -1,15 +0,0 @@
-{application, main, [
- {description, "A relational algebra engine"},
- {vsn, "0.1.0"},
- {registered, []},
- {mod, {main, []}},
- {applications, [
- kernel,
- stdlib,
- mnesia
- ]},
- {env, []},
- {modules, []},
- {licenses, ["Apache-2.0"]},
- {links, []}
- ]}.
diff --git a/src/main.erl b/src/main.erl
deleted file mode 100644
index 323d054..0000000
--- a/src/main.erl
+++ /dev/null
@@ -1,66 +0,0 @@
--module(main).
--behaviour(application).
-
--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, 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
- io:format("Starting app...~n"),
-
- %% Ensure Mnesia is started
- mnesia:start(),
- setup(),
- _ = uuid(),
-
- {ok, self()}.
-
-stop(_State) ->
- io:format("Stopping app...~n"),
- ok.
diff --git a/src/operations.erl b/src/operations.erl
deleted file mode 100644
index de61fd8..0000000
--- a/src/operations.erl
+++ /dev/null
@@ -1,1484 +0,0 @@
-%%% @doc Extended Relational Database Operations (RM/T)
-%%%
-%%% This module implements core database operations for Sakura, an extended
-%%% relational database engine based on E. F. Codd's RM/T (1979).
-%%%
-%%% == Architecture ==
-%%%
-%%% The system uses:
-%%% <ul>
-%%% <li><b>Content-Addressed Storage</b> - All entities identified by SHA-256 hashes</li>
-%%% <li><b>Immutable Data Structures</b> - Operations create new versions</li>
-%%% <li><b>Merkle Trees</b> - Efficient versioning and diffing via merklet</li>
-%%% <li><b>Mnesia Backend</b> - ACID transactions and persistent storage</li>
-%%% <li><b>Volcano Iterator Model</b> - Lazy evaluation with process-based iterators</li>
-%%% </ul>
-%%%
-%%% == Data Model ==
-%%%
-%%% The system maintains four tables in Mnesia:
-%%% <ul>
-%%% <li>`attribute' - Stores atomic values with their hashes</li>
-%%% <li>`tuple' - Stores tuples as maps from attribute names to value hashes</li>
-%%% <li>`relation' - Stores relations with merkle trees of tuple hashes</li>
-%%% <li>`database_state' - Stores database snapshots with relation merkle trees</li>
-%%% </ul>
-%%%
-%%% == RM/T Integrity Rules ==
-%%%
-%%% <ul>
-%%% <li><b>Rule 3 (Entity Integrity)</b> - E-relations accept insertions/deletions, not updates</li>
-%%% <li><b>Rule 4 (Property Integrity)</b> - Tuples require entity existence in E-relation</li>
-%%% </ul>
-%%%
-%%% @author Nekoma Team
-%%% @copyright 2025
-%%% @version 0.1.0
-%%% @reference E. F. Codd (1979). "Extending the Database Relational Model to Capture More Meaning"
-%%% @reference E. F. Codd (1970). "A Relational Model of Data for Large Shared Data Banks"
-
--module(operations).
--include("../include/operations.hrl").
--export([
- create_database/1,
- value_hash/1,
- create_relation/3,
- create_immutable_relation/2,
- update_relation_constraints/3,
- 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_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,
- %% Iterator operators (lazy)
- select_iterator/2,
- project_iterator/2,
- sort_iterator/2,
- take_iterator/2,
- %% Join operators
- equijoin_iterator/3,
- theta_join_iterator/3,
- %% Materialization (eager)
- materialize/3
- ]).
-
-%% @private
-%% @doc Compute SHA-256 hash of an Erlang term.
-%%
-%% This is the foundational hashing function used throughout the system for
-%% content addressing. All entities (attributes, tuples, relations, databases)
-%% are identified by hashes computed with this function.
-%%
-%% @param Value Any Erlang term to hash
-%% @returns Binary SHA-256 hash (32 bytes)
-%%
-%% @see create_tuple/3
-%% @see create_relation/3
-hash(Value) ->
- crypto:hash(sha256, term_to_binary(Value)).
-
-%% @doc Hash a value using Sakura's canonical content hash.
-%%
-%% Public wrapper around the internal hash function, used by other modules
-%% that need to compare values against stored attribute hashes.
-%%
-%% @param Value Any Erlang term
-%% @returns Binary SHA-256 hash
--spec value_hash(term()) -> binary().
-value_hash(Value) ->
- hash(Value).
-
-%% @doc Insert a tuple into a relation and update the database state.
-%%
-%% This is the primary operation for adding data to the database. It:
-%% <ol>
-%% <li>Stores each attribute value with its hash (automatic deduplication)</li>
-%% <li>Creates a tuple record mapping attribute names to value hashes</li>
-%% <li>Inserts the tuple hash into the relation's merkle tree</li>
-%% <li>Computes new relation hash from updated tree</li>
-%% <li>Updates database merkle tree with new relation hash</li>
-%% <li>Computes new database hash</li>
-%% </ol>
-%%
-%% 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
-%% @param Tuple Map of attribute names to values (e.g., `#{name => "Alice", age => 30}')
-%% @returns `{UpdatedDatabase, UpdatedRelation}' tuple
-%%
-%% @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),
- %% Validate tuple against relation schema before insertion
- case constraint:validate_tuple(Database, Tuple, RelationRecord) of
- ok ->
- create_tuple_internal(Database, RelationName, Tuple, RelationRecord);
- {error, Reason} ->
- {error, {constraint_violation, Reason}}
- end.
-
-%% @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
- AttributeMap = maps:map(fun(_AttrName, Value) ->
- ValueHash = hash(Value),
- mnesia:write(attribute, #attribute{hash = ValueHash, value = Value}, write),
- ValueHash
- end, Tuple),
-
- %% Step 2: Compute tuple hash and store tuple
- TupleHash = hash(Tuple),
- mnesia:write(tuple, #tuple{hash = TupleHash, relation = RelationName, attribute_map = AttributeMap}, write),
-
- %% Step 3: Get current relation hash from database
- CurrentRelationHash = maps:get(RelationName, Database#database_state.relations),
-
- %% Step 4: Read current relation
- [RelationRecord] = mnesia:read(relation, CurrentRelationHash),
-
- %% Step 5: Insert tuple hash into relation merkle tree
- %% We don't need to store the content of a tuple hash, just the key
- %% merklet can spot the difference in both keys and values
- %% sets:set follows the same strategy of an empty value
- NewRelationTree = merklet:insert({TupleHash, <<>>}, RelationRecord#relation.tree),
-
- %% Step 6: Compute new relation hash as the root of the new merkle tree and store
- {_,NewRelationTreeHash,_,_} = NewRelationTree,
- NewRelationHash = hash({RelationRecord#relation.name, RelationRecord#relation.schema, NewRelationTreeHash}),
-
- %% Update cardinality: increment by 1
- NewCardinality = case RelationRecord#relation.cardinality of
- {finite, N} -> {finite, N + 1};
- Other -> Other % Shouldn't happen for finite relations
- end,
-
- UpdatedRelation = #relation{
- hash = NewRelationHash,
- name = RelationName,
- tree = NewRelationTree,
- schema = RelationRecord#relation.schema,
- constraints = RelationRecord#relation.constraints,
- cardinality = NewCardinality,
- generator = RelationRecord#relation.generator,
- provenance = RelationRecord#relation.provenance,
- lineage = RelationRecord#relation.lineage,
- membership_criteria = RelationRecord#relation.membership_criteria
- },
- mnesia:write(relation, UpdatedRelation, write),
-
- %% Step 7: Update database relations map
- NewRelations = maps:put(RelationName, NewRelationHash, Database#database_state.relations),
-
- %% Step 8: Update database tree (for diffing)
- RelationKey = atom_to_binary(RelationName, utf8),
- NewDatabaseTree = merklet:insert({RelationKey, NewRelationHash}, Database#database_state.tree),
-
- %% Step 9: Compute new database hash and store
- {_,NewDatabaseHash,_,_} = NewDatabaseTree,
- UpdatedDatabase = #database_state{
- name = Database#database_state.name,
- hash = NewDatabaseHash,
- tree = NewDatabaseTree,
- relations = NewRelations,
- timestamp = erlang:timestamp()
- },
- mnesia:write(database_state, UpdatedDatabase, write),
-
- {UpdatedDatabase, UpdatedRelation}
- end,
-
- {atomic, Result} = mnesia:transaction(F),
- Result.
-
-%% @doc Clear all tuples from a relation (truncate).
-%%
-%% Wrapper for `clear_relation/3' with transactions enabled.
-%%
-%% @param Database `#database_state{}' record
-%% @param RelationHash Binary hash identifying the relation
-%% @returns `{atomic, {UpdatedDatabase, UpdatedRelation}}' or `{aborted, Reason}'
-%%
-%% @see clear_relation/3
-%% @see retract_relation/2
-clear_relation(Database, RelationHash) ->
- clear_relation(Database, RelationHash, true).
-
-%% @doc Clear all tuples from a relation with optional transaction control.
-%%
-%% Removes all tuples from the relation but preserves its schema. The relation's
-%% merkle tree is set to `undefined', and a new relation hash is computed.
-%%
-%% @param Database `#database_state{}' record
-%% @param RelationHash Binary hash identifying the relation
-%% @param Transact Boolean - if `true', wraps operation in Mnesia transaction
-%% @returns When `Transact =:= true': `{atomic, {UpdatedDatabase, UpdatedRelation}}'
-%% When `Transact =:= false': `{UpdatedDatabase, UpdatedRelation}'
-%%
-%% @see retract_relation/2
-clear_relation(Database, RelationHash, Transact) ->
- [Relation] = mnesia:dirty_read(relation, RelationHash),
- NewRelationHash = hash({Relation#relation.name, Relation#relation.schema, undefined}),
- UpdatedRelation =
- Relation#relation{hash = NewRelationHash,
- tree = undefined},
- DatabaseTreeWithoutTuples = merklet:insert({atom_to_binary(Relation#relation.name, utf8), NewRelationHash}, Database#database_state.tree),
- {_,NewDatabaseHash,_,_} = DatabaseTreeWithoutTuples,
- NewRelations = maps:put(Relation#relation.name, NewRelationHash, Database#database_state.relations),
- UpdatedDatabase =
- Database#database_state{timestamp = erlang:timestamp(),
- hash = NewDatabaseHash,
- tree = DatabaseTreeWithoutTuples,
- relations = NewRelations},
- TX = fun () ->
- mnesia:write(relation, UpdatedRelation, write),
- mnesia:write(database_state, UpdatedDatabase, write),
- {UpdatedDatabase, UpdatedRelation}
- end,
- case Transact of
- true -> mnesia:transaction(TX);
- false -> {UpdatedDatabase, UpdatedRelation}
- end.
-
-%% @doc Remove a tuple from a relation.
-%%
-%% Wrapper for `retract_tuple/4' with transactions enabled.
-%%
-%% @param Database `#database_state{}' record
-%% @param RelationHash Binary hash identifying the relation
-%% @param TupleHash Binary hash identifying the tuple to remove
-%% @returns `{atomic, {UpdatedDatabase, UpdatedRelation}}' or `{aborted, Reason}'
-%%
-%% @see retract_tuple/4
-%% @see create_tuple/3
-retract_tuple(Database, RelationHash, TupleHash) ->
- retract_tuple(Database, RelationHash, TupleHash, true).
-
-%% @doc Remove a tuple from a relation with optional transaction control.
-%%
-%% Deletes a specific tuple from the relation's merkle tree and recomputes
-%% the relation and database hashes.
-%%
-%% @param Database `#database_state{}' record
-%% @param RelationHash Binary hash identifying the relation
-%% @param TupleHash Binary hash identifying the tuple to remove
-%% @param Transact Boolean - if `true', wraps operation in Mnesia transaction
-%% @returns When `Transact =:= true': `{atomic, {UpdatedDatabase, UpdatedRelation}}'
-%% When `Transact =:= false': `{UpdatedDatabase, UpdatedRelation}'
-%%
-%% @see create_tuple/3
-retract_tuple(Database, RelationHash, TupleHash, Transact)
- when is_record(Database, database_state) ->
- [Relation] = mnesia:dirty_read(relation, RelationHash),
- RelationTreeWithoutTuple = merklet:delete(TupleHash, Relation#relation.tree),
- {_,NewRelationTreeHash,_,_} = RelationTreeWithoutTuple,
- NewRelationHash = hash({Relation#relation.name, Relation#relation.schema, NewRelationTreeHash}),
- DatabaseTreeWithoutTuple = merklet:insert({atom_to_binary(Relation#relation.name, utf8), NewRelationHash}, Database#database_state.tree),
- {_,NewDatabaseHash,_,_} = DatabaseTreeWithoutTuple,
- NewRelations = maps:put(Relation#relation.name, NewRelationHash, Database#database_state.relations),
- UpdatedDatabase =
- Database#database_state{timestamp = erlang:timestamp(),
- hash = NewDatabaseHash,
- tree = DatabaseTreeWithoutTuple,
- relations = NewRelations},
- UpdatedRelation =
- Relation#relation{hash = NewRelationHash,
- tree = RelationTreeWithoutTuple},
- TX = fun () ->
- mnesia:write(relation, UpdatedRelation, write),
- mnesia:write(database_state, UpdatedDatabase, write),
- {UpdatedDatabase, UpdatedRelation}
- end,
- case Transact of
- true -> mnesia:transaction(TX);
- false -> {UpdatedDatabase, UpdatedRelation}
- end.
-
-%% @doc Remove a relation from the database.
-%%
-%% Wrapper for `retract_relation/3' with transactions enabled.
-%%
-%% @param Database `#database_state{}' record
-%% @param Name Atom naming the relation to remove
-%% @returns `{atomic, UpdatedDatabase}' or `{aborted, Reason}'
-%%
-%% @see retract_relation/3
-%% @see clear_relation/2
-retract_relation(Database, Name) ->
- retract_relation(Database, Name, true).
-
-%% @doc Remove a relation from the database with optional transaction control.
-%%
-%% First clears all tuples from the relation, then removes the relation entirely
-%% from the database's relation map and merkle tree.
-%%
-%% @param Database `#database_state{}' record
-%% @param Name Atom naming the relation to remove
-%% @param Transact Boolean - if `true', wraps operation in Mnesia transaction
-%% @returns When `Transact =:= true': `{atomic, UpdatedDatabase}'
-%% When `Transact =:= false': `UpdatedDatabase'
-%%
-%% @see create_relation/3
-retract_relation(Database, Name, Transact) ->
- RelationHash = maps:get(Name, Database#database_state.relations, {error, retract_relation_could_not_find}),
- {DatabaseWithClearedRelation, _ClearedRelation} = clear_relation(Database, RelationHash, false),
- NewTree = merklet:delete(atom_to_binary(Name, utf8), DatabaseWithClearedRelation#database_state.tree),
- NewHash = case NewTree of
- undefined -> <<>>;
- {_,H,_,_} -> H
- end,
- UpdatedDatabase =
- DatabaseWithClearedRelation#database_state{relations = maps:remove(Name, DatabaseWithClearedRelation#database_state.relations),
- hash = NewHash,
- tree = NewTree,
- timestamp = erlang:timestamp()},
- TX = fun () ->
- mnesia:write(database_state, UpdatedDatabase, write),
- UpdatedDatabase
- end,
- case Transact of
- true -> mnesia:transaction(TX);
- false -> UpdatedDatabase
- end.
-
-%% @doc Create a new relation in the database with a schema definition.
-%%
-%% Creates an empty relation (no tuples) with the specified schema. The schema
-%% is stored but not yet enforced - it serves as documentation and provides a
-%% foundation for future domain validation.
-%%
-%% <b>Important:</b> The relation's identity is tied to its name. Two relations
-%% with identical schemas but different names are semantically distinct and can
-%% 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
-%% @param Definition Map specifying schema (e.g., `#{name => string, age => integer}')
-%% @returns `{UpdatedDatabase, NewRelation}' tuple
-%%
-%% @see create_database/1
-%% @see create_tuple/3
-%% @see retract_relation/2
-create_relation(Database, Name, Definition) ->
- F = fun() ->
- %% Create relation with empty tree
- %% The meaning of a relation needs to be attached to its name
- %% It may sound nominalistic but that is a physical concern.
- %% 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,
-
- %% Build membership criteria from schema (1OPiC declarations)
- MembershipCriteria = constraint:build_membership_criteria(Database, Definition),
-
- NewRelation = #relation{
- hash = RelationHash,
- name = Name,
- tree = undefined,
- schema = Definition,
- constraints = constraint:empty_constraints(), % No constraints by default
- cardinality = {finite, 0}, % Empty relation
- generator = GeneratorFun, % Function generator for finite relations
- membership_criteria = MembershipCriteria, % Domain membership from schema
- provenance = build_base_provenance(Definition, Name), % Base relation provenance
- lineage = {base, Name} % Base relation lineage
- },
- mnesia:write(relation, NewRelation, write),
-
- %% Update database relations map
- %% TODO: maybe we can purge this later after including the name on the merkle trees
- NewRelations = maps:put(Name, RelationHash, Database#database_state.relations),
-
- %% Update database tree (for diffing)
- %% Note that the name here is important as a key because
- %% we gotta go from a name to a hash within a database state
- NewTree = merklet:insert({atom_to_binary(Name), RelationHash}, Database#database_state.tree),
- {_,NewHash,_,_} = NewTree,
-
- UpdatedDatabase = #database_state{
- name = Database#database_state.name,
- hash = NewHash,
- tree = NewTree,
- relations = NewRelations,
- timestamp = erlang:timestamp()
- },
- mnesia:write(database_state, UpdatedDatabase, write),
-
- {UpdatedDatabase, NewRelation}
- end,
- {atomic, Result} = mnesia:transaction(F),
- Result.
-
-%% @doc Update a relation's constraints.
-%%
-%% This function updates the constraints on an existing relation and returns
-%% an updated database state with the new relation version. The merklet tree
-%% automatically replaces the old relation hash with the new one.
-%%
-%% == Example ==
-%% <pre>
-%% %% Create constraints
-%% AgeConstraint = constraint:create_1op(age, integer, [constraint:gte(18), constraint:lt(70)]),
-%% SalaryConstraint = constraint:create_1op(salary, integer, [constraint:between(30000, 200000)]),
-%% C1 = constraint:add_attribute_constraint(undefined, age, AgeConstraint),
-%% C2 = constraint:add_attribute_constraint(C1, salary, SalaryConstraint),
-%%
-%% %% Update relation with constraints
-%% {DB2, UpdatedRel} = operations:update_relation_constraints(DB1, employees, C2).
-%% </pre>
-%%
-%% @param Database Current database state
-%% @param RelationName Name of the relation to update
-%% @param Constraints The #relation_constraints{} record to set
-%% @returns {UpdatedDatabase, UpdatedRelation}
-update_relation_constraints(Database, RelationName, Constraints)
- when is_record(Database, database_state),
- is_atom(RelationName),
- is_record(Constraints, relation_constraints) ->
- F = fun() ->
- %% Get current relation hash from database state
- CurrentRelationHash = maps:get(RelationName, Database#database_state.relations),
-
- %% Read current relation from mnesia
- case mnesia:read(relation, CurrentRelationHash) of
- [] ->
- %% Relation not found, might be using wrong database version
- mnesia:abort({relation_not_found, RelationName, CurrentRelationHash});
- [CurrentRelation] ->
- update_relation_with_constraints(CurrentRelation, RelationName, Constraints, Database)
- end
- end,
- {atomic, Result} = mnesia:transaction(F),
- Result.
-
-%% @private
-update_relation_with_constraints(CurrentRelation, RelationName, Constraints, Database) ->
- %% Create updated relation with new constraints
- %% The hash changes because constraints are part of the relation definition
- NewRelationHash = hash({CurrentRelation#relation.name,
- CurrentRelation#relation.schema,
- Constraints}),
-
- UpdatedRelation = CurrentRelation#relation{
- hash = NewRelationHash,
- constraints = Constraints
- },
-
- %% Write updated relation
- mnesia:write(relation, UpdatedRelation, write),
-
- %% Update database relations map
- NewRelations = maps:put(RelationName, NewRelationHash, Database#database_state.relations),
-
- %% Update database tree (merklet:insert automatically replaces old value)
- NewTree = merklet:insert({atom_to_binary(RelationName), NewRelationHash},
- Database#database_state.tree),
- {_, NewHash, _, _} = NewTree,
-
- UpdatedDatabase = #database_state{
- name = Database#database_state.name,
- hash = NewHash,
- tree = NewTree,
- relations = NewRelations,
- timestamp = erlang:timestamp()
- },
- mnesia:write(database_state, UpdatedDatabase, write),
-
- {UpdatedDatabase, UpdatedRelation}.
-
-%% @doc Create a new database state.
-%%
-%% Initializes an empty database with no relations. The database starts with:
-%% <ul>
-%% <li>Empty relation map</li>
-%% <li>Undefined merkle tree (will be created when first relation is added)</li>
-%% <li>Empty hash</li>
-%% <li>Timestamp of creation</li>
-%% </ul>
-%%
-%% 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
-%%
-%% @see setup/0
-%% @see create_relation/3
-create_database(Name) ->
- DB = #database_state{
- name = Name,
- hash = <<>>,
- tree = undefined,
- relations = #{},
- timestamp = erlang:timestamp()
- },
- %% Standard immutable relations (domains and their predicates)
- %% TODO: Since 1OP constraints were added, the constraints passed to the base models now must be supported.
- %% The idea is to have the generators be able to produce the constrained subsets
- StandardTypeRelations = [
- #domain{
- name = integer,
- schema = #{value => integer},
- generator = fun() -> spawn(fun() -> generator_iterator_loop(generators:integer(#{})) end) end,
- membership_criteria = #{test => fun(#{value := V}) -> is_integer(V) end},
- cardinality = aleph_zero
- },
- #domain{
- name = natural,
- schema = #{value => natural},
- generator = fun() -> spawn(fun() -> generator_iterator_loop(generators:natural(#{})) end) end,
- membership_criteria = #{test => fun(#{value := V}) -> is_integer(V) andalso V >= 0 end},
- cardinality = aleph_zero
- },
- #domain{
- name = rational,
- schema = #{numerator => integer, denominator => integer},
- generator = fun() -> spawn(fun() -> generator_iterator_loop(generators:rational(#{})) end) end,
- membership_criteria = #{test => fun(#{numerator := N, denominator := D}) ->
- is_integer(N) andalso is_integer(D) andalso D /= 0
- end
- },
- cardinality = aleph_zero
- },
- #domain{
- name = string,
- schema = #{value => string},
- generator = undefined,
- membership_criteria = #{test => fun(#{value := V}) -> is_list(V) orelse is_binary(V) end},
- cardinality = aleph_zero
- },
- #domain{
- name = atom,
- schema = #{value => atom},
- generator = undefined,
- membership_criteria = #{test => fun(#{value := V}) -> is_atom(V) end},
- cardinality = aleph_zero
- },
- #domain{
- name = term,
- schema = #{value => term},
- generator = undefined,
- membership_criteria = #{test => fun(_) -> true end},
- cardinality = aleph_zero
- },
- constraint:less_than(term, aleph_zero),
- constraint:less_than_or_equal(term, aleph_zero),
- constraint:greater_than(term, aleph_zero),
- constraint:greater_than_or_equal(term, aleph_zero),
- constraint:equal(term, aleph_zero),
- constraint:not_equal(term, aleph_zero),
- constraint:plus(integer, aleph_zero),
- constraint:times(integer, aleph_zero),
- constraint:minus(integer, aleph_zero),
- constraint:divide(integer, aleph_zero)
- ],
- 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, StandardDB, write)
- end),
- StandardDB.
-
-%% @doc Create an infinite relation with a generator.
-%%
-%% Creates a relation with infinite cardinality (aleph_zero or continuum).
-%% The relation is defined by a generator function that produces tuples
-%% lazily based on constraints.
-%%
-%% == Primitive Infinite Relations ==
-%%
-%% <ul>
-%% <li>`naturals' - Natural numbers: {0, 1, 2, 3, ...}</li>
-%% <li>`integers' - Integers: {0, 1, -1, 2, -2, ...}</li>
-%% <li>`rationals' - Rationals via Stern-Brocot tree</li>
-%% </ul>
-%%
-%% == Example ==
-%% <pre>
-%% %% Create immutable relation of natural numbers
-%% {DB1, Naturals} = operations:create_immutable_relation(DB, #domain{
-%% 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 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, 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}),
- NewRelation = #relation{
- hash = RelationHash,
- name = Name,
- tree = undefined, % Immutable 'relations' have no merkle tree
- schema = Schema,
- constraints = constraint:empty_constraints(), % TODO: No constraints unless by domain extension
- cardinality = Cardinality,
- generator = Generator,
- membership_criteria = MembershipCriteria,
- provenance = build_base_provenance(Schema, Name), % Base relation provenance
- lineage = {base, Name} % Base relation lineage
- },
- mnesia:write(relation, NewRelation, write),
- NewRelations = maps:put(Name, RelationHash, Database#database_state.relations),
- NewTree = merklet:insert({atom_to_binary(Name), RelationHash}, Database#database_state.tree),
- {_,NewHash,_,_} = NewTree,
- UpdatedDatabase = #database_state{
- name = Database#database_state.name,
- hash = NewHash,
- tree = NewTree,
- relations = NewRelations,
- timestamp = erlang:timestamp()
- },
- mnesia:write(database_state, UpdatedDatabase, write),
- {UpdatedDatabase, NewRelation}
- end,
- {atomic, Result} = mnesia:transaction(F),
- Result;
-
-create_immutable_relation(Database, Specification)
- when is_record(Database, database_state)
- andalso is_record(Specification, relation) ->
- F = fun() ->
- RelationHash = hash({Specification#relation.name, Specification#relation.schema, undefined}),
- UpdatedRelation = Specification#relation{hash = RelationHash},
- mnesia:write(relation, UpdatedRelation, write),
- NewRelations = maps:put(Specification#relation.name, RelationHash, Database#database_state.relations),
- NewTree = merklet:insert({atom_to_binary(Specification#relation.name), RelationHash}, Database#database_state.tree),
- {_,NewHash,_,_} = NewTree,
- UpdatedDatabase = #database_state{
- name = Database#database_state.name,
- hash = NewHash,
- tree = NewTree,
- relations = NewRelations,
- timestamp = erlang:timestamp()
- },
- mnesia:write(database_state, UpdatedDatabase, write),
-
- {UpdatedDatabase, Specification}
- end,
- {atomic, Result} = mnesia:transaction(F),
- Result.
-
-%% @doc Get relation hash by name.
-%%
-%% Looks up a relation's hash in the database's relation map. This hash
-%% can be used to retrieve the full relation record from Mnesia.
-%%
-%% @param Database `#database_state{}' record
-%% @param RelationName Atom naming the relation
-%% @returns `{ok, Hash}' on success, `{error, not_found}' if relation doesn't exist
-%%
-%% @see get_relations/1
-%% @see hashes_from_tuple/1
-get_relation_hash(Database, RelationName) ->
- case maps:find(RelationName, Database#database_state.relations) of
- error -> {error, not_found};
- 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:
-%% <ol>
-%% <li>Reading the tuple record from Mnesia (contains attribute_map)</li>
-%% <li>For each attribute, reading the actual value using its hash</li>
-%% <li>Building a map with attribute names and materialized values</li>
-%% </ol>
-%%
-%% Uses dirty reads since all data is immutable.
-%%
-%% @param TupleHash Binary hash identifying the tuple
-%% @returns Map with attribute names and actual values (e.g., `#{name => "Alice", age => 30}')
-%%
-%% @see get_tuples_iterator/2
-%% @see create_tuple/3
-resolve_tuple(TupleHash) ->
- [#tuple{attribute_map = AttributeMap}] = mnesia:dirty_read(tuple, TupleHash),
- maps:map(fun(_AttrName, ValueHash) ->
- [#attribute{value = Value}] = mnesia:dirty_read(attribute, ValueHash),
- Value
- end, AttributeMap).
-
-%% @doc Extract all tuple hashes from a relation.
-%%
-%% Retrieves the list of all tuple hashes stored in the relation's merkle tree.
-%% Returns an empty list if the relation has no tuples (tree is `undefined').
-%%
-%% Uses dirty reads since the relation data is immutable.
-%%
-%% @param RelationHash Binary hash identifying the relation
-%% @returns List of binary tuple hashes, or empty list `[]' if relation is empty
-%%
-%% @see get_tuples_iterator/2
-%% @see create_tuple/3
-hashes_from_tuple(RelationHash) ->
- [Relation] = mnesia:dirty_read(relation, RelationHash),
- case Relation#relation.tree of
- undefined -> [];
- Tree -> merklet:keys(Tree)
- end.
-
-%% @doc Create an iterator for streaming tuples from a relation with constraints.
-%%
-%% Extended version of get_tuples_iterator/2 that accepts boundedness constraints.
-%% For infinite relations, constraints are mandatory to ensure termination.
-%%
-%% <b>All tuples include metadata tracking</b> with a nested `meta' field containing:
-%% <ul>
-%% <li>`provenance' - Tracks immediate source of each attribute</li>
-%% <li>`lineage' - Tracks complete operational history</li>
-%% </ul>
-%%
-%% == Options Format ==
-%%
-%% Nested format (preferred):
-%% <pre>
-%% #{constraints => #{age => {gt, 30}}}
-%% </pre>
-%%
-%% Flat format (backward compatible):
-%% <pre>
-%% #{age => {gt, 30}}
-%% </pre>
-%%
-%% == Constraints ==
-%%
-%% Constraints bound the iteration over infinite relations:
-%% <ul>
-%% <li>`{range, Min, Max}' - Range constraint</li>
-%% <li>`{eq, Value}' - Equality constraint</li>
-%% <li>`{lt, Value}', `{lte, Value}' - Upper bounds</li>
-%% <li>`{gt, Value}', `{gte, Value}' - Lower bounds</li>
-%% <li>`{in, List}' - Membership constraint</li>
-%% </ul>
-%%
-%% == Example ==
-%% <pre>
-%% %% Iterate employees with age > 30 (metadata always included)
-%% Iterator = operations:get_tuples_iterator(DB, employees,
-%% #{constraints => #{age => {gt, 30}}}).
-%%
-%% %% Results will have:
-%% %% #{id => 1, name => "Alice", age => 35,
-%% %% meta => #{
-%% %% provenance => #{id => {employees, id}, ...},
-%% %% lineage => {select, Fun, {base, employees}}
-%% %% }}
-%% </pre>
-%%
-%% @param Database `#database_state{}' record
-%% @param RelationName Atom naming the relation to iterate
-%% @returns Pid of iterator process
-%%
-%% @see get_tuples_iterator/2
-%% @see take/3
-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, relation_not_found, RelationName});
- RelationHash ->
- [Relation] = mnesia:dirty_read(relation, RelationHash),
- case Relation#relation.cardinality of
- {finite, _} when Relation#relation.tree =/= undefined ->
- TupleHashes = hashes_from_tuple(RelationHash),
- spawn(fun() -> tuple_iterator_loop(TupleHashes) end);
- _ ->
- get_iterator_from_generator(Relation#relation.name, Relation#relation.generator)
- end
- end.
-
-%% @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.
-%%
-%% Sends a `{next, self()}' message to the iterator process and waits for
-%% the response. The iterator will materialize the tuple on demand.
-%%
-%% Timeout is set to 5 seconds to prevent indefinite blocking.
-%%
-%% @param IteratorPid Pid returned from `get_tuples_iterator/2'
-%% @returns `{ok, Tuple}' with materialized tuple map,
-%% `done' when no more tuples remain, or
-%% `{error, timeout}' if iterator doesn't respond
-%%
-%% @see get_tuples_iterator/2
-%% @see close_iterator/1
-next_tuple(IteratorPid) ->
- IteratorPid ! {next, self()},
- receive
- {tuple, Tuple} -> {ok, Tuple};
- done -> done;
- {error, Reason} -> {error, Reason}
- after 5000 ->
- {error, timeout}
- end.
-
-%% @doc Close iterator process.
-%%
-%% Sends a `stop' message to terminate the iterator process. This should be
-%% called when you're done consuming tuples to avoid leaving processes running.
-%%
-%% @param IteratorPid Pid returned from `get_tuples_iterator/2'
-%% @returns `ok'
-%%
-%% @see get_tuples_iterator/2
-%% @see collect_all/1
-close_iterator(IteratorPid) ->
- IteratorPid ! stop,
- ok.
-
-%% @private
-%% 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.
-%%
-%% Protocol:
-%% <ul>
-%% <li>`{next, Caller}' - Resolve next tuple and send `{tuple, ResolvedTuple}' or `done'</li>
-%% <li>`stop' - Terminate the process</li>
-%% </ul>
-%%
-%% @param TupleHashes List of binary tuple hashes to stream
-tuple_iterator_loop([]) ->
- receive
- {next, Caller} ->
- Caller ! done;
- stop ->
- ok
- end;
-tuple_iterator_loop([TupleHash | Rest]) ->
- receive
- {next, Caller} ->
- %% Resolve tuple on demand
- ResolvedTuple = resolve_tuple(TupleHash),
- Caller ! {tuple, ResolvedTuple},
- tuple_iterator_loop(Rest);
- stop ->
- ok
- end.
-
-%% @doc Helper to collect all tuples from an iterator into a list.
-%%
-%% This is a convenience function primarily used for testing. It exhausts the
-%% iterator by repeatedly calling `next_tuple/1' until `done' is received,
-%% accumulating all tuples into a list.
-%%
-%% <b>Warning:</b> This materializes all tuples in memory, defeating the purpose
-%% 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
-%%
-%% @see get_tuples_iterator/2
-%% @see next_tuple/1
-collect_all(IteratorPid) ->
- collect_all(IteratorPid, []).
-
-collect_all(IteratorPid, Acc) ->
- case next_tuple(IteratorPid) of
- {ok, Tuple} -> collect_all(IteratorPid, [Tuple | Acc]);
- done ->
- close_iterator(IteratorPid),
- lists:reverse(Acc); % Preserve order
- {error, Reason} ->
- close_iterator(IteratorPid),
- {error, Reason, lists:reverse(Acc)}
- end.
-
-%% @doc Select iterator: Filters tuples based on a predicate.
-%%
-%% Lazy operator that wraps a child iterator and only emits tuples that
-%% satisfy the predicate function.
-%%
-%% == Example ==
-%% <pre>
-%% BaseIter = get_tuples_iterator(DB, employees, #{}),
-%% 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()
-%% @returns Iterator process (Pid)
-select_iterator(ChildIterator, Predicate) when is_function(Predicate, 1) ->
- spawn(fun() -> select_loop(ChildIterator, Predicate) end).
-
-select_loop(ChildPid, Predicate) ->
- receive
- {next, Caller} ->
- case next_tuple(ChildPid) of
- {ok, Tuple} ->
- case Predicate(Tuple) of
- true ->
- Caller ! {tuple, Tuple},
- select_loop(ChildPid, Predicate);
- false ->
- self() ! {next, Caller},
- select_loop(ChildPid, Predicate)
- end;
- done ->
- close_iterator(ChildPid),
- Caller ! done;
- {error, Reason} ->
- close_iterator(ChildPid),
- Caller ! {error, Reason}
- end;
- stop ->
- close_iterator(ChildPid),
- ok
- end.
-
-%% @doc Project iterator: Projects tuples onto specified attributes.
-%%
-%% Lazy operator that transforms tuples by keeping only the specified
-%% 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
-%% @returns Iterator process (Pid)
-project_iterator(ChildIterator, Attributes) when is_list(Attributes) ->
- spawn(fun() -> project_loop(ChildIterator, Attributes) end).
-
-project_loop(ChildPid, Attributes) ->
- receive
- {next, Caller} ->
- case next_tuple(ChildPid) of
- {ok, Tuple} ->
- % Project data attributes only - no metadata handling
- ProjectedTuple = maps:with(Attributes, Tuple),
- Caller ! {tuple, ProjectedTuple},
- project_loop(ChildPid, Attributes);
- done ->
- close_iterator(ChildPid),
- Caller ! done;
- {error, Reason} ->
- close_iterator(ChildPid),
- Caller ! {error, Reason}
- end;
- stop ->
- close_iterator(ChildPid),
- ok
- end.
-
-%% @doc Sort iterator: Sorts all tuples from child iterator.
-%%
-%% <b>Blocking operator</b> - must consume ALL tuples from child before
-%% emitting any output. Materializes and sorts tuples in memory.
-%%
-%% <b>Warning:</b> Do not use on unbounded streams! Ensure child iterator
-%% produces finite tuples (via constraints or take).
-%%
-%% == Example ==
-%% <pre>
-%% BaseIter = get_tuples_iterator(DB, employees, #{}),
-%% 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()
-%% @returns Iterator process (Pid)
-sort_iterator(ChildIterator, CompareFun) when is_function(CompareFun, 2) ->
- spawn(fun() ->
- %% Sync all state blocking the pipeline
- AllTuples = collect_all(ChildIterator),
- %% Sort in memory
- SortedTuples = lists:sort(CompareFun, AllTuples),
- sorted_emit_loop(SortedTuples)
- end).
-
-sorted_emit_loop([]) ->
- receive
- {next, Caller} -> Caller ! done;
- stop -> ok
- end;
-sorted_emit_loop([Tuple | Rest]) ->
- receive
- {next, Caller} ->
- Caller ! {tuple, Tuple},
- sorted_emit_loop(Rest);
- stop -> ok
- end.
-
-%% @doc Take iterator: Limits output to N tuples.
-%%
-%% Lazy operator that emits at most N tuples from the child iterator,
-%% 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
-%% @returns Iterator process (Pid)
-take_iterator(ChildIterator, N) when is_integer(N), N > 0 ->
- spawn(fun() -> take_iter_loop(ChildIterator, N, 0, N) end).
-
-take_iter_loop(ChildPid, Limit, Count, OriginalN) when Count < Limit ->
- receive
- {next, Caller} ->
- case next_tuple(ChildPid) of
- {ok, Tuple} ->
- %% Pass through tuple without metadata injection
- Caller ! {tuple, Tuple},
- take_iter_loop(ChildPid, Limit, Count + 1, OriginalN);
- done ->
- close_iterator(ChildPid),
- Caller ! done;
- {error, Reason} ->
- close_iterator(ChildPid),
- Caller ! {error, Reason}
- end;
- stop ->
- close_iterator(ChildPid),
- ok
- end;
-take_iter_loop(ChildPid, _Limit, _Count, _OriginalN) ->
- %% Reached limit - close child and signal done
- close_iterator(ChildPid),
- receive
- {next, Caller} -> Caller ! done;
- stop -> ok
- end.
-
-%%% ============================================================================
-%%% Join Operators
-%%% ============================================================================
-
-%% @doc Equijoin iterator: Natural join on matching attribute values.
-%%
-%% Performs an equijoin between two relations on a common attribute.
-%% For each tuple from the left iterator, finds all matching tuples from
-%% the right iterator where the specified attributes are equal.
-%%
-%% The result tuples merge attributes from both sides. If attribute names
-%% conflict, right side attributes are prefixed with "right_".
-%%
-%% Implementation: Nested loop join (simple but works for small-medium datasets).
-%% 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
-%% @param JoinAttribute Attribute name to join on (must exist in both relations)
-%% @returns Iterator process that emits joined tuples
-equijoin_iterator(LeftIterator, RightIterator, JoinAttribute) ->
- spawn(fun() -> equijoin_loop(LeftIterator, RightIterator, JoinAttribute) end).
-
-%% @doc Theta-join iterator: Join with arbitrary predicate.
-%%
-%% Performs a theta-join between two relations using a user-provided
-%% predicate function. For each pair of tuples (left, right), the predicate
-%% is evaluated. If true, the tuples are merged and emitted.
-%%
-%% == Example ==
-%% <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
-%% @param Predicate Function `fun(LeftTuple, RightTuple) -> boolean()'
-%% @returns Iterator process that emits joined tuples
-theta_join_iterator(LeftIterator, RightIterator, Predicate) ->
- spawn(fun() -> theta_join_loop(LeftIterator, RightIterator, Predicate) end).
-
-%%% ============================================================================
-%%% Materialization (Eager - Relation Creation)
-%%% ============================================================================
-
-%% @doc Materialize an iterator pipeline into a new relation.
-%%
-%% This is the boundary between lazy evaluation (iterator pipeline) and
-%% eager materialization (stored relation). Consumes all tuples from the
-%% iterator and stores them as a new finite relation in the database.
-%%
-%% == Example ==
-%% <pre>
-%% %% Build lazy pipeline
-%% Pipeline = get_tuples_iterator(DB, employees, #{})
-%% |&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)
-%% @param ResultName Atom naming the new relation
-%% @returns `{UpdatedDatabase, NewRelation}' tuple
-materialize(Database, SourceIterator, ResultName) when is_record(Database, database_state) ->
- %% 1. Pull all tuples from pipeline
- AllTuples = collect_all(SourceIterator),
-
- case AllTuples of
- [] ->
- %% Empty result - create empty relation
- Schema = #{}, % Empty schema
- create_relation(Database, ResultName, Schema);
-
- [FirstTuple | _] ->
- %% 2. Infer schema from first tuple
- Schema = infer_schema_from_tuple(FirstTuple),
-
- %% 3. Create new relation
- {DB1, _Relation} = create_relation(Database, ResultName, Schema),
-
- %% 4. Insert all tuples
- {FinalDB, FinalRelation} = create_all_tuples(DB1, ResultName, AllTuples),
-
- {FinalDB, FinalRelation}
- end.
-
-%% @private
-%% 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
-%% 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;
-infer_type(Value) when is_list(Value) -> string;
-infer_type(Value) when is_atom(Value) -> atom;
-infer_type(Value) when is_boolean(Value) -> boolean;
-infer_type(_) -> term.
-
-%% @private
-%% Insert multiple tuples into a relation.
-create_all_tuples(DB, RelationName, Tuples) ->
- lists:foldl(
- fun(Tuple, {DBacc, _RelAcc}) ->
- create_tuple(DBacc, RelationName, Tuple)
- end,
- {DB, undefined},
- Tuples
- ).
-
-%%% ============================================================================
-%%% Generator Support Functions
-%%% ============================================================================
-
-
-%% @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) ->
- receive
- {next, Caller} ->
- case Generator(next) of
- done ->
- Caller ! done;
- {value, Tuple, NextGenerator} ->
- Caller ! {tuple, Tuple},
- generator_iterator_loop(NextGenerator);
- {error, Reason} ->
- Caller ! {error, Reason}
- end;
- stop ->
- ok
- end.
-
-%% @private
-%% 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
-%% 3. Emit merged tuple when join attribute values match
-%%
-%% @param LeftIter Left iterator process
-%% @param RightIter Right iterator process
-%% @param JoinAttr Attribute to join on
-equijoin_loop(LeftIter, RightIter, JoinAttr) ->
- % Materialize right side (for nested loop)
- RightTuples = collect_all(RightIter),
- equijoin_emit_loop(LeftIter, RightTuples, JoinAttr).
-
-equijoin_emit_loop(LeftIter, RightTuples, JoinAttr) ->
- receive
- {next, Caller} ->
- case next_tuple(LeftIter) of
- done ->
- Caller ! done,
- ok; % Stop after sending done
- {ok, LeftTuple} ->
- % Find all right tuples that match on join attribute
- case maps:get(JoinAttr, LeftTuple, undefined) of
- undefined ->
- % Left tuple doesn't have join attribute - try next
- self() ! {next, Caller},
- equijoin_emit_loop(LeftIter, RightTuples, JoinAttr);
- LeftValue ->
- % Find matching right tuples
- Matches = [merge_tuples(LeftTuple, RightTuple)
- || RightTuple <- RightTuples,
- maps:get(JoinAttr, RightTuple, undefined) =:= LeftValue],
- case Matches of
- [] ->
- % No matches - try next left tuple
- self() ! {next, Caller},
- equijoin_emit_loop(LeftIter, RightTuples, JoinAttr);
- [FirstMatch | RestMatches] ->
- % Emit first match, buffer rest
- Caller ! {tuple, FirstMatch},
- equijoin_emit_buffered(LeftIter, RightTuples, JoinAttr, RestMatches)
- end
- end;
- {error, Reason} ->
- close_iterator(LeftIter),
- Caller ! {error, Reason},
- ok % Stop after sending error
- end;
- stop ->
- close_iterator(LeftIter),
- ok
- end.
-
-equijoin_emit_buffered(LeftIter, RightTuples, JoinAttr, [Match | Rest]) ->
- receive
- {next, Caller} ->
- Caller ! {tuple, Match},
- equijoin_emit_buffered(LeftIter, RightTuples, JoinAttr, Rest);
- stop ->
- close_iterator(LeftIter),
- ok
- end;
-equijoin_emit_buffered(LeftIter, RightTuples, JoinAttr, []) ->
- % Buffer empty - continue with next left tuple
- equijoin_emit_loop(LeftIter, RightTuples, JoinAttr).
-
-%% @private
-%% 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()
-theta_join_loop(LeftIter, RightIter, Pred) ->
- % Materialize right side
- RightTuples = collect_all(RightIter),
- theta_join_emit_loop(LeftIter, RightTuples, Pred).
-
-theta_join_emit_loop(LeftIter, RightTuples, Pred) ->
- receive
- {next, Caller} ->
- case next_tuple(LeftIter) of
- done ->
- Caller ! done;
- {ok, LeftTuple} ->
- % Find all right tuples that satisfy predicate
- Matches = [merge_tuples(LeftTuple, RightTuple)
- || RightTuple <- RightTuples,
- Pred(LeftTuple, RightTuple)],
- case Matches of
- [] ->
- % No matches - continue
- self() ! {next, Caller},
- theta_join_emit_loop(LeftIter, RightTuples, Pred);
- [FirstMatch | RestMatches] ->
- Caller ! {tuple, FirstMatch},
- theta_join_emit_buffered(LeftIter, RightTuples, Pred, RestMatches)
- end;
- {error, Reason} ->
- close_iterator(LeftIter),
- Caller ! {error, Reason}
- end;
- stop ->
- close_iterator(LeftIter),
- ok
- end.
-
-theta_join_emit_buffered(LeftIter, RightTuples, Pred, [Match | Rest]) ->
- receive
- {next, Caller} ->
- Caller ! {tuple, Match},
- theta_join_emit_buffered(LeftIter, RightTuples, Pred, Rest);
- stop ->
- close_iterator(LeftIter),
- ok
- end;
-theta_join_emit_buffered(LeftIter, RightTuples, Pred, []) ->
- theta_join_emit_loop(LeftIter, RightTuples, Pred).
-
-%% @private
-%% @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 conflict, add right attribute
- maps:put(RightKey, RightValue, LeftTupleAcc);
- _ ->
- % Different attributes
- maps:put(RightKey, RightValue, LeftTupleAcc)
- end
- end,
- LeftTuple,
- RightTuple
- ).
-
-%% @private
-%% 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,
- #{},
- Schema
- ).
-
-%% 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.
-
-%% @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
deleted file mode 100644
index d995f88..0000000
--- a/src/query_planner.erl
+++ /dev/null
@@ -1,601 +0,0 @@
-%%% @doc Query Planner and Prepared Statement Execution
-%%%
-%%% Provides a Lisp-inspired tuple-based DSL for constructing and executing
-%%% relational algebra queries. Queries are represented as nested Erlang tuples
-%%% that can be validated, optimized, and compiled into iterator pipelines.
-%%%
-%%% == Query DSL Syntax ==
-%%%
-%%% The DSL uses nested tuples to represent relational operations:
-%%% <pre>
-%%% % Base relation (just use the relation name)
-%%% employees
-%%%
-%%% % Selection (filter)
-%%% {select, employees, fun(T) -&gt; maps:get(age, T) &gt; 30 end}
-%%%
-%%% % Projection
-%%% {project, employees, [name, age]}
-%%%
-%%% % Equijoin (natural join on common attribute)
-%%% {join, employees, departments, dept_id}
-%%%
-%%% % Theta join (join with arbitrary predicate)
-%%% {theta_join,
-%%% employees,
-%%% departments,
-%%% fun(L, R) -&gt; maps:get(dept_id, L) =:= maps:get(id, R) end}
-%%%
-%%% % Sort
-%%% {sort, employees, fun(A, B) -&gt; maps:get(age, A) =&lt; maps:get(age, B) end}
-%%%
-%%% % Take (limit)
-%%% {take, employees, 10}
-%%%
-%%% % Rename attribute
-%%% {rename, old_name, new_name, employees}
-%%%
-%%% % Composite query
-%%% {project,
-%%% {select,
-%%% {join, employees, departments, dept_id},
-%%% fun(T) -&gt; maps:get(age, T) &gt; 30 end},
-%%% [name, dept_name]}
-%%% </pre>
-%%%
-%%% == Usage ==
-%%% <pre>
-%%% % Prepare a query plan
-%%% Plan = query_planner:prepare({select, employees,
-%%% fun(T) -&gt; maps:get(age, T) &gt; 30 end}).
-%%%
-%%% % Execute and get iterator
-%%% Iter = query_planner:execute(DB, Plan).
-%%%
-%%% % Collect all results
-%%% Results = query_planner:collect(DB, Plan).
-%%% </pre>
-%%%
-%%% @author Nekoma Team
-%%% @copyright 2025
-
--module(query_planner).
-
--export([prepare/1,
- execute/2,
- collect/2,
- explain/1]).
-
--include("operations.hrl").
-
-%%% Types
-
--type query_plan() ::
- atom() % Relation name
- | {select, query_plan(), fun((map()) -> boolean())} % Filter
- | {project, query_plan(), [atom()]} % Projection
- | {join, query_plan(), query_plan(), atom()} % Equijoin
- | {theta_join, query_plan(), query_plan(),
- fun((map(), map()) -> boolean())} % Theta join
- | {sort, query_plan(), fun((map(), map()) -> boolean())} % Sort
- | {take, query_plan(), pos_integer()} % Limit
- | {rename, atom(), atom(), query_plan()} % Rename attribute
- | {materialize, query_plan()} % Materialize all
- | {materialize, query_plan(), pos_integer()}. % Materialize N tuples
-
--export_type([query_plan/0]).
-
-%%% Public API
-
-%% @doc Prepare and validate a query plan.
-%%
-%% Takes a query plan tuple and validates its structure. Returns the
-%% plan unchanged if valid, or throws an error if invalid.
-%%
-%% @param Plan The query plan to prepare
-%% @returns The validated query plan
-%% @throws {invalid_plan, Reason}
--spec prepare(query_plan()) -> query_plan().
-prepare(Plan) ->
- case validate_plan(Plan) of
- ok -> Plan;
- {error, Reason} -> error({invalid_plan, Reason})
- end.
-
-%% @doc Execute a prepared query plan against a database.
-%%
-%% Returns an ephemeral relation for top-level operations that can spawn
-%% multiple independent iterators. The relation's generator creates fresh
-%% iterator pipelines on demand, allowing reproducible results.
-%%
-%% @param DB The database state
-%% @param Plan The prepared query plan
-%% @returns Ephemeral relation record
--spec execute(term(), query_plan()) -> #relation{}.
-execute(DB, Plan) ->
- create_ephemeral_relation(DB, Plan).
-
-%% @doc Execute a query plan and collect all results.
-%%
-%% Convenience function that executes the plan and collects all
-%% tuples into a list. Uses the ephemeral relation to create an
-%% iterator and automatically closes it.
-%%
-%% If the iterator times out (e.g., when sorting an infinite relation),
-%% returns partial results with a warning.
-%%
-%% @param DB The database state
-%% @param Plan The prepared query plan
-%% @returns List of result tuples, or partial results on timeout
--spec collect(term(), query_plan()) -> [map()].
-collect(DB, Plan) ->
- EphemeralRelation = execute(DB, Plan),
- Iter = (EphemeralRelation#relation.generator)(),
- Results = operations:collect_all(Iter),
- operations:close_iterator(Iter),
- case Results of
- {error, timeout, PartialResults} ->
- io:format("~nWarning: Query timed out. Returning partial results (~b tuples).~n",
- [length(PartialResults)]),
- io:format("Tip: For infinite relations, use 'take' before operations like 'sort'.~n~n"),
- PartialResults;
- {error, Reason, PartialResults} ->
- io:format("~nWarning: Query failed (~p). Returning partial results (~b tuples).~n~n",
- [Reason, length(PartialResults)]),
- PartialResults;
- _ ->
- Results
- end.
-
-%% @doc Explain a query plan in human-readable form.
-%%
-%% Converts the query plan to a readable string representation
-%% showing the operation tree.
-%%
-%% @param Plan The query plan to explain
-%% @returns String representation of the plan
--spec explain(query_plan()) -> string().
-explain(Plan) ->
- explain_plan(Plan, 0).
-
-%%% Internal Functions
-
-%% @doc Create an ephemeral relation for the top-level query plan.
-%%
-%% Uses pure relational operators from relational_operators module to construct
-%% the query result. Returns an ephemeral relation with a generator closure that
-%% can spawn fresh iterators, allowing reproducible query results.
-%%
-%% @param DB The database state (for looking up base relations only)
-%% @param Plan The query plan
-%% @returns Ephemeral relation record
--spec create_ephemeral_relation(term(), query_plan()) -> #relation{}.
-create_ephemeral_relation(DB, Plan) ->
- % Compile plan to ephemeral relation using pure operators
- compile_to_relation(DB, Plan).
-
-%% @doc Compute the schema for a query plan.
-%%
-%% Recursively analyzes the query plan structure to determine the output schema.
-%% The schema maps attribute names to their domain/type information.
-%%
-%% @param DB The database state
-%% @param Plan The query plan
-%% @returns Schema map
--spec compute_schema(term(), query_plan()) -> map().
-compute_schema(DB, RelationName) when is_atom(RelationName) ->
- case maps:get(RelationName, DB#database_state.relations, undefined) of
- undefined ->
- % Relation not found, return empty schema
- #{};
- RelationHash ->
- [Relation] = mnesia:dirty_read(relation, RelationHash),
- Relation#relation.schema
- end;
-
-compute_schema(DB, {select, SubPlan, _Predicate}) ->
- % Select doesn't change schema, just filters tuples
- compute_schema(DB, SubPlan);
-
-compute_schema(DB, {project, SubPlan, Attributes}) ->
- % Project filters the schema to only include selected attributes
- SubSchema = compute_schema(DB, SubPlan),
- maps:with(Attributes, SubSchema);
-
-compute_schema(DB, {join, LeftPlan, RightPlan, _Attribute}) ->
- % Join combines schemas from both sides, handling attribute conflicts
- LeftSchema = compute_schema(DB, LeftPlan),
- RightSchema = compute_schema(DB, RightPlan),
- merge_join_schemas(LeftSchema, RightSchema);
-
-compute_schema(DB, {theta_join, LeftPlan, RightPlan, _Predicate}) ->
- % Theta join also combines schemas from both sides, handling attribute conflicts
- LeftSchema = compute_schema(DB, LeftPlan),
- RightSchema = compute_schema(DB, RightPlan),
- merge_join_schemas(LeftSchema, RightSchema);
-
-compute_schema(DB, {sort, SubPlan, _Comparator}) ->
- % Sort doesn't change schema
- compute_schema(DB, SubPlan);
-
-compute_schema(DB, {take, SubPlan, _N}) ->
- % Take doesn't change schema, just limits tuples
- compute_schema(DB, SubPlan);
-
-compute_schema(DB, {rename, OldAttr, NewAttr, SubPlan}) ->
- % Rename changes one attribute name in the schema
- SubSchema = compute_schema(DB, SubPlan),
- case maps:is_key(OldAttr, SubSchema) of
- true ->
- AttrType = maps:get(OldAttr, SubSchema),
- SubSchema1 = maps:remove(OldAttr, SubSchema),
- maps:put(NewAttr, AttrType, SubSchema1);
- false ->
- % Attribute doesn't exist, return unchanged schema
- SubSchema
- end;
-
-compute_schema(DB, {materialize, SubPlan}) ->
- % Materialize doesn't change schema
- compute_schema(DB, SubPlan);
-
-compute_schema(DB, {materialize, SubPlan, _N}) ->
- % Materialize with limit doesn't change schema
- compute_schema(DB, SubPlan);
-
-compute_schema(_DB, _UnknownPlan) ->
- % Unknown plan type, return empty schema
- #{}.
-
-%% @doc Merge schemas for join operations, handling attribute conflicts elegantly.
-%%
-%% Instead of prefixing conflicting attribute names, this approach maintains
-%% both provenances in the schema value. For attributes that exist in both
-%% relations, the schema value becomes a list of the original values.
-%% This is especially important for natural joins where the join attribute
-%% logically represents the same entity from both sides.
-%%
-%% @param LeftSchema Schema from left relation
-%% @param RightSchema Schema from right relation
-%% @returns Merged schema map with provenance lists for conflicts
--spec merge_join_schemas(map(), map()) -> map().
-merge_join_schemas(LeftSchema, RightSchema) ->
- maps:fold(
- fun(Key, RightValue, Acc) ->
- case maps:get(Key, Acc, undefined) of
- undefined ->
- % No conflict, add the right attribute
- maps:put(Key, RightValue, Acc);
- LeftValue when LeftValue =:= RightValue ->
- % Same type/domain, keep single value (common in natural joins)
- maps:put(Key, LeftValue, Acc);
- LeftValue when is_list(LeftValue) ->
- % Left value is already a list, append if not already present
- case lists:member(RightValue, LeftValue) of
- true -> maps:put(Key, LeftValue, Acc);
- false -> maps:put(Key, LeftValue ++ [RightValue], Acc)
- end;
- LeftValue ->
- % Conflict with different types - create provenance list
- maps:put(Key, [LeftValue, RightValue], Acc)
- end
- end,
- LeftSchema,
- RightSchema
- ).
-
-%% @doc Compute provenance for a query plan.
-%%
-%% Recursively analyzes the query plan structure to determine the provenance,
-%% following the provenance types defined in operations.hrl.
-%%
-%% @param Plan The query plan
-%% @returns Provenance term
--spec compute_provenance(query_plan()) -> term().
-compute_provenance(RelationName) when is_atom(RelationName) ->
- {base, RelationName};
-
-compute_provenance({select, SubPlan, _Predicate}) ->
- % For select, we'd need to represent the predicate constraints
- % For now, simplified without constraint details
- SubProvenance = compute_provenance(SubPlan),
- {select, SubProvenance, #{}};
-
-compute_provenance({project, SubPlan, Attributes}) ->
- SubProvenance = compute_provenance(SubPlan),
- {project, SubProvenance, Attributes};
-
-compute_provenance({join, LeftPlan, RightPlan, _Attribute}) ->
- LeftProvenance = compute_provenance(LeftPlan),
- RightProvenance = compute_provenance(RightPlan),
- {join, LeftProvenance, RightProvenance};
-
-compute_provenance({theta_join, LeftPlan, RightPlan, _Predicate}) ->
- LeftProvenance = compute_provenance(LeftPlan),
- RightProvenance = compute_provenance(RightPlan),
- {join, LeftProvenance, RightProvenance};
-
-compute_provenance({sort, SubPlan, _Comparator}) ->
- % Sort doesn't change provenance
- compute_provenance(SubPlan);
-
-compute_provenance({take, SubPlan, N}) ->
- SubProvenance = compute_provenance(SubPlan),
- {take, SubProvenance, N};
-
-compute_provenance({rename, _OldAttr, _NewAttr, SubPlan}) ->
- % Rename preserves underlying provenance
- compute_provenance(SubPlan);
-
-compute_provenance({materialize, SubPlan}) ->
- % Materialize doesn't change provenance
- compute_provenance(SubPlan);
-
-compute_provenance({materialize, SubPlan, _N}) ->
- % Materialize with limit doesn't change provenance
- compute_provenance(SubPlan);
-
-compute_provenance(_UnknownPlan) ->
- % Unknown plan type
- undefined.
-
-%% Validate query plan structure
--spec validate_plan(query_plan()) -> ok | {error, term()}.
-validate_plan(Name) when is_atom(Name) ->
- ok;
-validate_plan({select, SubPlan, Pred}) when is_function(Pred, 1) ->
- validate_plan(SubPlan);
-validate_plan({project, SubPlan, Attrs}) when is_list(Attrs) ->
- case lists:all(fun is_atom/1, Attrs) of
- true -> validate_plan(SubPlan);
- false -> {error, {invalid_attributes, Attrs}}
- end;
-validate_plan({join, Left, Right, Attr}) when is_atom(Attr) ->
- case {validate_plan(Left), validate_plan(Right)} of
- {ok, ok} -> ok;
- {{error, _} = Error, _} -> Error;
- {_, {error, _} = Error} -> Error
- end;
-validate_plan({theta_join, Left, Right, Pred}) when is_function(Pred, 2) ->
- case {validate_plan(Left), validate_plan(Right)} of
- {ok, ok} -> ok;
- {{error, _} = Error, _} -> Error;
- {_, {error, _} = Error} -> Error
- end;
-validate_plan({sort, SubPlan, Comp}) when is_function(Comp, 2) ->
- validate_plan(SubPlan);
-validate_plan({take, SubPlan, N}) when is_integer(N), N > 0 ->
- validate_plan(SubPlan);
-validate_plan({rename, OldAttr, NewAttr, SubPlan}) when is_atom(OldAttr), is_atom(NewAttr) ->
- validate_plan(SubPlan);
-validate_plan({materialize, SubPlan}) ->
- validate_plan(SubPlan);
-validate_plan({materialize, SubPlan, N}) when is_integer(N), N > 0 ->
- validate_plan(SubPlan);
-validate_plan(Plan) ->
- {error, {unknown_operator, Plan}}.
-
-%% Compile query plan to iterator pipeline
--spec compile_to_iterator(term(), query_plan()) -> pid().
-compile_to_iterator(DB, Name) when is_atom(Name) ->
- operations:get_tuples_iterator(DB, Name);
-
-compile_to_iterator(DB, {select, SubPlan, Predicate}) ->
- SubIter = compile_to_iterator(DB, SubPlan),
- operations:select_iterator(SubIter, Predicate);
-
-compile_to_iterator(DB, {project, SubPlan, Attributes}) ->
- SubIter = compile_to_iterator(DB, SubPlan),
- operations:project_iterator(SubIter, Attributes);
-
-compile_to_iterator(DB, {join, LeftPlan, RightPlan, Attribute}) ->
- LeftIter = compile_to_iterator(DB, LeftPlan),
- RightIter = compile_to_iterator(DB, RightPlan),
- operations:equijoin_iterator(LeftIter, RightIter, Attribute);
-
-compile_to_iterator(DB, {theta_join, LeftPlan, RightPlan, Predicate}) ->
- LeftIter = compile_to_iterator(DB, LeftPlan),
- RightIter = compile_to_iterator(DB, RightPlan),
- operations:theta_join_iterator(LeftIter, RightIter, Predicate);
-
-compile_to_iterator(DB, {sort, SubPlan, Comparator}) ->
- SubIter = compile_to_iterator(DB, SubPlan),
- operations:sort_iterator(SubIter, Comparator);
-
-compile_to_iterator(DB, {take, SubPlan, N}) ->
- %% For iterator-based compilation, use take_iterator
- SubIter = compile_to_iterator(DB, SubPlan),
- operations:take_iterator(SubIter, N);
-
-compile_to_iterator(DB, {rename, OldAttr, NewAttr, SubPlan}) ->
- SubIter = compile_to_iterator(DB, SubPlan),
- %% Inline simple rename - no need for separate function
- spawn(fun() ->
- rename_loop_inline(SubIter, OldAttr, NewAttr)
- end);
-
-compile_to_iterator(DB, {materialize, SubPlan}) ->
- % Materialize returns all tuples as a "static" iterator
- SubIter = compile_to_iterator(DB, SubPlan),
- Results = operations:collect_all(SubIter),
- operations:close_iterator(SubIter),
- static_iterator(Results);
-
-compile_to_iterator(DB, {materialize, SubPlan, Limit}) ->
- % Materialize with limit
- SubIter = compile_to_iterator(DB, SubPlan),
- LimitedIter = operations:take_iterator(SubIter, Limit),
- Results = operations:collect_all(LimitedIter),
- operations:close_iterator(LimitedIter),
- static_iterator(Results).
-
-%% Static iterator - serves pre-materialized tuples
--spec static_iterator([map()]) -> pid().
-static_iterator(Tuples) ->
- spawn(fun() -> static_loop(Tuples) end).
-
-static_loop(Tuples) ->
- receive
- {next, Caller} ->
- case Tuples of
- [] ->
- Caller ! done,
- static_loop([]);
- [Tuple | Rest] ->
- Caller ! {tuple, Tuple},
- static_loop(Rest)
- end;
- {close, Caller} ->
- Caller ! ok
- end.
-
-%% @private
-%% Inline helper for compile_to_iterator rename case
-rename_loop_inline(SourceIter, OldAttr, NewAttr) ->
- receive
- {next, Caller} ->
- case operations:next_tuple(SourceIter) of
- done ->
- Caller ! done;
- {ok, Tuple} ->
- RenamedTuple = case maps:is_key(OldAttr, Tuple) of
- true ->
- Value = maps:get(OldAttr, Tuple),
- Tuple1 = maps:remove(OldAttr, Tuple),
- maps:put(NewAttr, Value, Tuple1);
- false ->
- Tuple
- end,
- Caller ! {tuple, RenamedTuple},
- rename_loop_inline(SourceIter, OldAttr, NewAttr)
- end;
- {close, Caller} ->
- operations:close_iterator(SourceIter),
- Caller ! ok
- end.
-
-%% Pretty-print query plan
--spec explain_plan(query_plan(), non_neg_integer()) -> string().
-explain_plan(Name, Indent) when is_atom(Name) ->
- indent(Indent) ++ "Relation: " ++ atom_to_list(Name) ++ "\n";
-
-explain_plan({select, SubPlan, _Pred}, Indent) ->
- indent(Indent) ++ "Select (filter)\n" ++
- explain_plan(SubPlan, Indent + 2);
-
-explain_plan({project, SubPlan, Attrs}, Indent) ->
- AttrStr = string:join([atom_to_list(A) || A <- Attrs], ", "),
- indent(Indent) ++ "Project [" ++ AttrStr ++ "]\n" ++
- explain_plan(SubPlan, Indent + 2);
-
-explain_plan({join, LeftPlan, RightPlan, Attr}, Indent) ->
- indent(Indent) ++ "Join on " ++ atom_to_list(Attr) ++ "\n" ++
- explain_plan(LeftPlan, Indent + 2) ++
- explain_plan(RightPlan, Indent + 2);
-
-explain_plan({theta_join, LeftPlan, RightPlan, _Pred}, Indent) ->
- indent(Indent) ++ "Theta Join\n" ++
- explain_plan(LeftPlan, Indent + 2) ++
- explain_plan(RightPlan, Indent + 2);
-
-explain_plan({sort, SubPlan, _Comp}, Indent) ->
- indent(Indent) ++ "Sort\n" ++
- explain_plan(SubPlan, Indent + 2);
-
-explain_plan({take, SubPlan, N}, Indent) ->
- indent(Indent) ++ "Take " ++ integer_to_list(N) ++ "\n" ++
- explain_plan(SubPlan, Indent + 2);
-
-explain_plan({rename, OldAttr, NewAttr, SubPlan}, Indent) ->
- indent(Indent) ++ "Rename " ++ atom_to_list(OldAttr) ++ " -> " ++ atom_to_list(NewAttr) ++ "\n" ++
- explain_plan(SubPlan, Indent + 2);
-
-explain_plan({materialize, SubPlan}, Indent) ->
- indent(Indent) ++ "Materialize (all)\n" ++
- explain_plan(SubPlan, Indent + 2);
-
-explain_plan({materialize, SubPlan, N}, Indent) ->
- indent(Indent) ++ "Materialize (" ++ integer_to_list(N) ++ " tuples)\n" ++
- explain_plan(SubPlan, Indent + 2).
-
-%% Helper: indent string
--spec indent(non_neg_integer()) -> string().
-indent(N) -> lists:duplicate(N, $\s).
-
-%% Helper: get relation from database
--spec get_relation_from_db(term(), atom()) -> #relation{}.
-get_relation_from_db(DB, RelationName) ->
- case maps:get(RelationName, DB#database_state.relations, undefined) of
- undefined ->
- erlang:error({relation_not_found, RelationName});
- RelationHash ->
- [Relation] = mnesia:dirty_read(relation, RelationHash),
- Relation
- end.
-
-%% @doc Compile query plan to ephemeral relation using pure relational operators.
-%%
-%% This is the new approach: build a relation tree using pure operators,
-%% then return the final ephemeral relation. No iterators are created until
-%% the generator is called.
--spec compile_to_relation(term(), query_plan()) -> #relation{}.
-compile_to_relation(DB, RelationName) when is_atom(RelationName) ->
- BaseRelation = get_relation_from_db(DB, RelationName),
- GeneratorFun = fun() ->
- operations:get_tuples_iterator(DB, RelationName)
- end,
- BaseRelation#relation{generator = GeneratorFun};
-
-compile_to_relation(DB, {select, SubPlan, Predicate}) ->
- SubRelation = compile_to_relation(DB, SubPlan),
- relational_operators:select(SubRelation, Predicate);
-
-compile_to_relation(DB, {project, SubPlan, Attributes}) ->
- SubRelation = compile_to_relation(DB, SubPlan),
- relational_operators:project(SubRelation, Attributes);
-
-compile_to_relation(DB, {join, LeftPlan, RightPlan, Attribute}) ->
- LeftRelation = compile_to_relation(DB, LeftPlan),
- RightRelation = compile_to_relation(DB, RightPlan),
- relational_operators:join(LeftRelation, RightRelation, Attribute);
-
-compile_to_relation(DB, {theta_join, LeftPlan, RightPlan, Predicate}) ->
- LeftRelation = compile_to_relation(DB, LeftPlan),
- RightRelation = compile_to_relation(DB, RightPlan),
- relational_operators:theta_join(LeftRelation, RightRelation, Predicate);
-
-compile_to_relation(DB, {sort, SubPlan, Comparator}) ->
- SubRelation = compile_to_relation(DB, SubPlan),
- relational_operators:sort(SubRelation, Comparator);
-
-compile_to_relation(DB, {take, SubPlan, N}) ->
- SubRelation = compile_to_relation(DB, SubPlan),
- relational_operators:take(SubRelation, N);
-
-compile_to_relation(DB, {rename, OldAttr, NewAttr, SubPlan}) ->
- SubRelation = compile_to_relation(DB, SubPlan),
- relational_operators:rename(SubRelation, #{OldAttr => NewAttr});
-
-compile_to_relation(DB, {materialize, SubPlan}) ->
- GeneratorFun = fun() ->
- compile_to_iterator(DB, SubPlan)
- end,
- EphemeralName = list_to_atom("materialized_" ++ integer_to_list(erlang:unique_integer([positive]))),
- #relation{
- hash = crypto:hash(sha256, term_to_binary({materialize, SubPlan})),
- name = EphemeralName,
- tree = undefined,
- schema = compute_schema(DB, SubPlan),
- constraints = constraint:empty_constraints(),
- cardinality = unknown,
- generator = GeneratorFun,
- membership_criteria = #{},
- provenance = compute_provenance(SubPlan)
- };
-
-compile_to_relation(DB, {materialize, SubPlan, N}) ->
- % Materialize with limit
- SubRelation = compile_to_relation(DB, SubPlan),
- TakeRelation = relational_operators:take(SubRelation, N),
- % Then wrap in a materialized form (for now just return take)
- TakeRelation.
diff --git a/src/relation.erl b/src/relation.erl
deleted file mode 100644
index 66930d2..0000000
--- a/src/relation.erl
+++ /dev/null
@@ -1,42 +0,0 @@
--module(relation).
--export([new_attribute/2, new_tuple/1, new_relation/1]).
-
-%% Attribute: A pair of (Name, Value), eg. ("Name", "Augusto")
-%% Tuple: A set of attributes, eg. (("Name", "Augusto"), ("Age", 20), ("Surname", "Rengel"))
-%% Relation: A set of tuples organized through some selector
-%% Example Relation: f(x: Nat) = x + 1
-%% |{(0, 1), (1, 2), (2, 3), ...}| = |N|
-%% RelationCustomer = [(Username, Text), (Password, Text)]
-%% RelationProduct = [(Name, Text), (Price, Real)]
-%% RelationOrder = [(OrderId, Natural), ((Username, Text),
-%% (Password, Text), RelationCustomer), ((Name, Text),
-%% (Price, Real), RelationProduct)]
-
-new_attribute(Name, Value) ->
- #{name => Name, value => Value}.
-
-new_tuple(Attributes) when is_list(Attributes) ->
- Fun = fun (#{name := Name, value := Value}, Acc) ->
- maps:put(Name, Value, Acc)
- end,
- lists:foldl(Fun, #{}, Attributes).
-
-new_relation(Tuples) when is_list(Tuples) ->
- sets:from_list(Tuples, [{version, 2}]).
-
-%% SoapName1 = relation:new_attribute(name, "ABC").
-%% SoapPrice1 = relation:new_attribute(price, 1.0).
-%% SoapName2 = relation:new_attribute(name, "XYZ").
-%% SoapPrice2 = relation:new_attribute(price, 1.25).
-%% BazookaName = relation:new_attribute(name, "Bazzum").
-%% BazookaPrice = relation:new_attribute(price, 413.0).
-%% SwordName = relation:new_attribute(name, "Sword").
-%% SwordPrice = relation:new_attribute(price, 100.0).
-%% relation:new_relation([relation:new_tuple([SoapName, SoapPrice]), relation:new_tuple([BazookaName, BazookaPrice]), relation:new_tuple([SwordName, SwordPrice])]).
-
-%% Jules = relation:new_attribute(customer_name, "Jules").
-%% Emily = relation:new_attribute(customer_name, "Emily").
-%% Customers = relation:new_relation([relation:new_tuple([Jules]),
-%% relation:new_tuple([Emily])]).
-
-%% operations:project(operations:select(operations:cartesian_product(relation:new_relation([relation:new_tuple([SoapName1, SoapPrice1]), relation:new_tuple([SoapName2, SoapPrice2]), relation:new_tuple([BazookaName, BazookaPrice]), relation:new_tuple([SwordName, SwordPrice])]), Customers), fun (Tuple) -> maps:get(price, Tuple) =< 1.0 end), [name, customer_name]).
diff --git a/src/relational_operators.erl b/src/relational_operators.erl
deleted file mode 100644
index 93bdf6d..0000000
--- a/src/relational_operators.erl
+++ /dev/null
@@ -1,490 +0,0 @@
-%%% @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.
-%%
-%% Constraints are propagated: source constraints are preserved, and new
-%% constraints may be inferred from declarative predicates.
-%%
-%% Predicate formats:
-%% - fun((map()) -> boolean()) - opaque function, no inference
-%% - {attr, AttrName, Op, Value} - declarative, infers constraints
-%% - {'and', [Pred1, ...]} - conjunction of declarative predicates
-%%
-%% @param SourceRelation Source #relation{} record
-%% @param Predicate Function or declarative term
-%% @returns Ephemeral #relation{} with generator and propagated constraints
--spec select(#relation{}, fun((map()) -> boolean()) | term()) -> #relation{}.
-select(SourceRelation, Predicate) when is_record(SourceRelation, relation) ->
- Name = generate_ephemeral_name(),
-
- %% Build actual filter function from predicate
- FilterFun = predicate_to_function(Predicate),
-
- SourceGen = SourceRelation#relation.generator,
- GeneratorFun = fun() ->
- SourceIter = SourceGen(),
- operations:select_iterator(SourceIter, FilterFun)
- end,
-
- %% Infer constraints from predicate and merge with source constraints
- Schema = SourceRelation#relation.schema,
- InferredConstraints = constraint:infer_constraints_from_pred(Predicate, Schema),
- MergedConstraints = constraint:merge_constraints(
- SourceRelation#relation.constraints,
- InferredConstraints
- ),
-
- #relation{
- hash = <<>>,
- name = Name,
- tree = undefined,
- schema = Schema,
- constraints = MergedConstraints,
- 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.
-%%
-%% Constraints are filtered to only include those for projected attributes.
-%% Constraints on attributes that are projected away are dropped.
-%%
-%% @param SourceRelation Source #relation{} record
-%% @param Attributes List of attribute names to keep
-%% @returns Ephemeral #relation{} with generator and filtered constraints
--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),
-
- %% Filter constraints to only projected attributes
- FilteredConstraints = constraint:filter_constraints(
- SourceRelation#relation.constraints,
- Attributes
- ),
-
- SourceGen = SourceRelation#relation.generator,
- GeneratorFun = fun() ->
- SourceIter = SourceGen(),
- operations:project_iterator(SourceIter, Attributes)
- end,
-
- #relation{
- hash = <<>>,
- name = Name,
- tree = undefined,
- schema = ProjectedSchema,
- constraints = FilteredConstraints,
- 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.
-%%
-%% Constraints from both relations are merged with AND semantics.
-%% If both relations have constraints on the same attribute, both are kept.
-%%
-%% @param LeftRelation Left #relation{} record
-%% @param RightRelation Right #relation{} record
-%% @param JoinAttribute Attribute name to join on
-%% @returns Ephemeral #relation{} with generator and merged constraints
--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),
-
- %% Merge constraints from both relations (AND semantics)
- MergedConstraints = constraint:merge_constraints(
- LeftRelation#relation.constraints,
- RightRelation#relation.constraints
- ),
-
- 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 = MergedConstraints,
- 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.
-%%
-%% Constraints are merged from both relations, and additional constraints may
-%% be inferred from declarative predicates.
-%%
-%% @param LeftRelation Left #relation{} record
-%% @param RightRelation Right #relation{} record
-%% @param Predicate Function or declarative term
-%% @returns Ephemeral #relation{} with generator and merged constraints
--spec theta_join(#relation{}, #relation{}, fun((map(), map()) -> boolean()) | term()) -> #relation{}.
-theta_join(LeftRelation, RightRelation, Predicate)
- when is_record(LeftRelation, relation),
- is_record(RightRelation, relation) ->
-
- 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),
-
- %% Merge constraints from both relations
- MergedConstraints = constraint:merge_constraints(
- LeftRelation#relation.constraints,
- RightRelation#relation.constraints
- ),
-
- %% Infer additional constraints from predicate
- InferredConstraints = constraint:infer_constraints_from_pred(Predicate, MergedSchema),
- FinalConstraints = constraint:merge_constraints(MergedConstraints, InferredConstraints),
-
- %% Build filter function from predicate (for theta join, needs 2-arity)
- %% The theta_join_iterator expects a 2-arity function
- FilterFun = theta_predicate_to_function(Predicate),
-
- LeftGen = LeftRelation#relation.generator,
- RightGen = RightRelation#relation.generator,
- GeneratorFun = fun() ->
- LeftIter = LeftGen(),
- RightIter = RightGen(),
- operations:theta_join_iterator(LeftIter, RightIter, FilterFun)
- end,
-
- #relation{
- hash = <<>>,
- name = Name,
- tree = undefined,
- schema = MergedSchema,
- constraints = FinalConstraints,
- 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.
-%%
-%% Constraints are updated: both the map keys and any {var, AttrName} references
-%% inside constraint terms are renamed according to the mapping.
-%%
-%% @param SourceRelation Source #relation{} record
-%% @param RenameMappings Map of old_name => new_name (atoms or strings)
-%% @returns Ephemeral #relation{} with generator and renamed constraints
--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
- ),
-
- %% Rename constraint attributes (both keys and internal variable references)
- RenamedConstraints = constraint:rename_constraint_attrs(
- SourceRelation#relation.constraints,
- RenameMappings
- ),
-
- SourceGen = SourceRelation#relation.generator,
- GeneratorFun = fun() ->
- SourceIter = SourceGen(),
- rename_iterator(SourceIter, RenameMappings)
- end,
-
- #relation{
- hash = <<>>,
- name = Name,
- tree = undefined,
- schema = RenamedSchema,
- constraints = RenamedConstraints,
- 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.
-
-%% @private
-%% @doc Convert a predicate (function or declarative term) to a filter function.
-%%
-%% Supports:
-%% - fun((map()) -> boolean()) - returns as-is
-%% - {attr, AttrName, Op, Value} - single attribute comparison
-%% - {'and', [Pred1, ...]} - conjunction
-%% - {'or', [Pred1, ...]} - disjunction
-predicate_to_function(Predicate) when is_function(Predicate, 1) ->
- Predicate;
-predicate_to_function({attr, AttrName, Op, Value}) ->
- fun(Tuple) ->
- AttrValue = maps:get(AttrName, Tuple, undefined),
- apply_comparison(Op, AttrValue, Value)
- end;
-predicate_to_function({'and', Predicates}) ->
- Funs = [predicate_to_function(P) || P <- Predicates],
- fun(Tuple) ->
- lists:all(fun(F) -> F(Tuple) end, Funs)
- end;
-predicate_to_function({'or', Predicates}) ->
- Funs = [predicate_to_function(P) || P <- Predicates],
- fun(Tuple) ->
- lists:any(fun(F) -> F(Tuple) end, Funs)
- end;
-predicate_to_function(_) ->
- %% Unknown format - accept all
- fun(_) -> true end.
-
-%% @private
-%% @doc Apply a comparison operation.
-apply_comparison(lt, A, B) -> A < B;
-apply_comparison(lte, A, B) -> A =< B;
-apply_comparison(gt, A, B) -> A > B;
-apply_comparison(gte, A, B) -> A >= B;
-apply_comparison(eq, A, B) -> A =:= B;
-apply_comparison(neq, A, B) -> A =/= B;
-apply_comparison(between, A, {Min, Max}) -> A >= Min andalso A < Max;
-apply_comparison(_, _, _) -> true.
-
-%% @private
-%% @doc Convert a theta predicate to a 2-arity filter function.
-%%
-%% Theta join predicates operate on merged tuples from both relations.
-%% For declarative predicates, we convert to a function that works on
-%% the merged tuple (left and right tuples combined).
-theta_predicate_to_function(Predicate) when is_function(Predicate, 2) ->
- Predicate;
-theta_predicate_to_function(Predicate) when is_function(Predicate, 1) ->
- %% Convert 1-arity function to 2-arity by merging tuples
- fun(LeftTuple, RightTuple) ->
- MergedTuple = maps:merge(LeftTuple, RightTuple),
- Predicate(MergedTuple)
- end;
-theta_predicate_to_function(DeclarativePred) ->
- %% Convert declarative predicate to 1-arity first, then wrap
- OneTupleFun = predicate_to_function(DeclarativePred),
- fun(LeftTuple, RightTuple) ->
- MergedTuple = maps:merge(LeftTuple, RightTuple),
- OneTupleFun(MergedTuple)
- end.
diff --git a/src/repl.erl b/src/repl.erl
deleted file mode 100644
index b26a418..0000000
--- a/src/repl.erl
+++ /dev/null
@@ -1,326 +0,0 @@
-%%% @doc Interactive REPL Helper for Sakura Query Planner
-%%%
-%%% Provides convenience functions for working with the query planner
-%%% in the Erlang shell (REPL). Includes utilities for pretty-printing
-%%% results and working with prepared statements.
-%%%
-%%% == Quick Start ==
-%%% <pre>
-%%% % Start the system
-%%% repl:start().
-%%%
-%%% % Create a database with sample data
-%%% DB = repl:example_db().
-%%%
-%%% % Execute a query
-%%% 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>
-%%% % Base relation (just use the name)
-%%% employees
-%%%
-%%% % Filter
-%%% {select, employees, fun(T) -&gt; maps:get(age, T) &gt; 30 end}
-%%%
-%%% % Project
-%%% {project, employees, [name, age]}
-%%%
-%%% % Join
-%%% {join, employees, departments, dept_id}
-%%%
-%%% % Rename
-%%% {rename, age, years, employees}
-%%%
-%%% % Complex pipeline
-%%% {take,
-%%% {sort,
-%%% {project,
-%%% {select,
-%%% {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},
-%%% 5}
-%%% </pre>
-%%%
-%%% @author Nekoma Team
-%%% @copyright 2025
-
--module(repl).
-
--export([start/0,
- example_db/0,
- q/2,
- q/3,
- query/2,
- show/1,
- show/2,
- explain/1,
- pretty/1,
- modes/0,
- help/0]).
-
--include("operations.hrl").
-
-%%% Public API
-
-%% @doc Start the Sakura system and initialize Mnesia.
-%%
-%% Call this first when starting a new REPL session.
-%%
-%% @returns ok
--spec start() -> ok.
-start() ->
- main:setup(),
- io:format("~n~s~n", [banner()]),
- io:format("Sakura REPL started. Type repl:help() for usage.~n~n"),
- ok.
-
-%% @doc Create an example database with sample data.
-%%
-%% Creates a database with employees and departments relations
-%% for testing and experimentation.
-%%
-%% @returns Database state
--spec example_db() -> term().
-example_db() ->
- DB = operations:create_database(example_db),
-
- % Create employees relation
- {DB1, _} = operations:create_relation(DB, employees, #{
- id => integer,
- name => string,
- dept_id => integer,
- age => integer,
- salary => integer
- }),
-
- {DB2, _} = operations:create_tuple(DB1, employees, #{id => 1, name => "Alice", dept_id => 10, age => 30, salary => 80000}),
- {DB3, _} = operations:create_tuple(DB2, employees, #{id => 2, name => "Bob", dept_id => 20, age => 35, salary => 90000}),
- {DB4, _} = operations:create_tuple(DB3, employees, #{id => 3, name => "Carol", dept_id => 10, age => 28, salary => 75000}),
- {DB5, _} = operations:create_tuple(DB4, employees, #{id => 4, name => "Dave", dept_id => 30, age => 42, salary => 95000}),
- {DB6, _} = operations:create_tuple(DB5, employees, #{id => 5, name => "Eve", dept_id => 20, age => 25, salary => 70000}),
-
- % Create departments relation
- {DB7, _} = operations:create_relation(DB6, departments, #{
- dept_id => integer,
- dept_name => string,
- budget => integer
- }),
-
- {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}),
-
- %% 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"),
- io:format(" - Built-in immutable relations: boolean, natural, integer, rational~n~n"),
-
- DB10.
-
-%% @doc Execute a query and pretty-print results (default table mode).
-%%
-%% Convenience function that prepares, executes, and displays
-%% query results in a readable format. Returns `ok' to avoid
-%% duplicate output in the Erlang shell.
-%%
-%% If you need the results for further processing, use `query/2'.
-%%
-%% @param DB Database state
-%% @param Plan Query plan tuple
-%% @returns ok
--spec q(term(), query_planner:query_plan()) -> ok.
-q(DB, Plan) ->
- q(DB, Plan, table).
-
-%% @doc Execute a query with specified visualization mode.
-%%
-%% Displays query results using the specified visualization mode.
-%% Returns `ok' to avoid duplicate output in the Erlang shell.
-%%
-%% If you need the results for further processing, use `query/2'.
-%%
-%% @param DB Database state
-%% @param Plan Query plan tuple
-%% @param Mode Visualization mode (table, tree, nested_table, etc.)
-%% @returns ok
--spec q(term(), query_planner:query_plan(), visualizer:render_mode()) -> ok.
-q(DB, Plan, Mode) ->
- PreparedPlan = query_planner:prepare(Plan),
- Results = query_planner:collect(DB, PreparedPlan),
- visualizer:render(Results, Mode).
-
-%% @doc Execute a query without printing results.
-%%
-%% This function executes a query and returns the raw results without
-%% any visualization. Use this when you want to work with the results
-%% programmatically or decide later how to display them.
-%%
-%% @param DB Database state
-%% @param Plan Query plan tuple
-%% @returns List of result tuples
-%%
-%% == Example ==
-%% <pre>
-%% Results = repl:query(DB, {scan, employees}).
-%% 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),
- query_planner:collect(DB, PreparedPlan).
-
-%% @doc Display results using default visualization mode (table).
-%%
-%% Takes a list of tuples (e.g., from query/2) and displays them
-%% using the table visualization mode.
-%%
-%% @param Results List of result tuples
-%% @returns ok
--spec show([map()]) -> ok.
-show(Results) ->
- show(Results, table).
-
-%% @doc Display results using specified visualization mode.
-%%
-%% Takes a list of tuples (e.g., from query/2) and displays them
-%% using the specified visualization mode.
-%%
-%% @param Results List of result tuples
-%% @param Mode Visualization mode (table, tree, nested_table, etc.)
-%% @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).
-
-%% @doc Explain a query plan.
-%%
-%% Displays the query execution plan in a readable tree format.
-%%
-%% @param Plan Query plan tuple
-%% @returns ok
--spec explain(query_planner:query_plan()) -> ok.
-explain(Plan) ->
- Explanation = query_planner:explain(Plan),
- io:format("~nQuery Plan:~n~s~n", [Explanation]),
- ok.
-
-%% @doc List available visualization modes.
-%%
-%% @returns List of available render modes
--spec modes() -> [visualizer:render_mode()].
-modes() ->
- visualizer:modes().
-
-%% @doc Pretty-print a list of tuples.
-%%
-%% Displays tuples using the visualizer module (default table mode).
-%%
-%% @param Results List of result tuples
-%% @returns ok
--spec pretty([map()]) -> ok.
-pretty(Results) ->
- visualizer:render(Results, table).
-
-%% @doc Display help message with query examples.
-%%
-%% @returns ok
--spec help() -> ok.
-help() ->
- io:format("~n~s~n~n", [banner()]),
- io:format("Sakura Query REPL - Lisp-inspired tuple-based query language~n~n"),
- 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, employees). %% Run a query~n~n"),
-
- io:format("Query Operators (all lazy by default):~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"),
- io:format(" {theta_join, Left, Right, Predicate} - Theta join~n"),
- io:format(" {sort, Plan, Comparator} - Sort tuples~n"),
- io:format(" {take, Plan, N} - Limit results~n"),
- io:format(" {rename, OldAttr, NewAttr, Plan} - Rename attribute~n"),
- io:format(" {materialize, Plan} - Force evaluation (all)~n"),
- io:format(" {materialize, Plan, N} - Force evaluation (N tuples)~n~n"),
-
- io:format("Lazy vs Eager:~n"),
- io:format(" All operators return lazy iterators (relations)~n"),
- io:format(" Use 'materialize' to force eager evaluation~n"),
- io:format(" Materialize can be nested anywhere in the query~n~n"),
-
- io:format("Query Execution:~n"),
- io:format(" repl:q(DB, Plan) - Execute and display (returns ok)~n"),
- io:format(" repl:q(DB, Plan, Mode) - Execute and display with mode (returns ok)~n"),
- io:format(" repl:query(DB, Plan) - Execute without display (returns results)~n"),
- io:format(" repl:show(Results) - Display results (returns ok)~n"),
- io:format(" repl:show(Results, Mode) - Display results with mode (returns ok)~n~n"),
-
- io:format("Visualization Modes:~n"),
- io:format(" Modes: table, tree, nested_table, linked_tables, outline~n"),
- 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, 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, employees, [name, age]}).~n~n"),
-
- io:format(" %% Join employees with departments~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, employees}).~n~n"),
-
- io:format(" %% Materialize first 10 tuples (eager evaluation)~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, 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, 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, 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, employees, ~n"),
- io:format(" fun(T) -> maps:get(age, T) > 30 end}).~n~n"),
-
- ok.
-
-%%% Internal Functions
-
-banner() ->
- "========================================\n"
- " Sakura - Extended Relational DB\n"
- " Tasmania / RM/T Implementation\n"
- "========================================".
diff --git a/src/visualizer.erl b/src/visualizer.erl
deleted file mode 100644
index 75f85d1..0000000
--- a/src/visualizer.erl
+++ /dev/null
@@ -1,348 +0,0 @@
-%%% @doc Relation Visualizer - Multiple Rendering Modes
-%%%
-%%% Provides various visual representations for nested relational data.
-%%% Supports different modes optimized for different use cases.
-%%%
-%%% == 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
-%%%
-%%% == Usage ==
-%%% <pre>
-%%% Results = query_planner:collect(DB, Plan),
-%%% visualizer:render(Results, tree).
-%%% visualizer:render(Results, nested_table).
-%%% </pre>
-%%%
-%%% @author Nekoma Team
-%%% @copyright 2025
-
--module(visualizer).
-
--export([render/1,
- render/2,
- modes/0]).
-
--type render_mode() :: table
- | nested_table
- | tree
- | linked_tables
- | outline.
-
--export_type([render_mode/0]).
-
-%%% Public API
-
-%% @doc Render tuples with default mode (table).
--spec render([map()]) -> ok.
-render(Tuples) ->
- render(Tuples, table).
-
-%% @doc Render tuples with specified visualization mode.
--spec render([map()], render_mode()) -> ok.
-render([], _Mode) ->
- io:format("(no results)~n"),
- ok;
-render(Tuples, table) ->
- render_table(Tuples);
-render(Tuples, nested_table) ->
- render_nested_table(Tuples);
-render(Tuples, tree) ->
- render_tree(Tuples);
-render(Tuples, linked_tables) ->
- render_linked_tables(Tuples);
-render(Tuples, outline) ->
- render_outline(Tuples).
-
-%% @doc List available rendering modes.
--spec modes() -> [render_mode()].
-modes() ->
- [table, nested_table, tree, linked_tables, outline].
-
-%%% Rendering Implementations
-
-%% Mode 1: Table with proper column width calculation
-render_table(Tuples) ->
- AllKeys = get_all_keys(Tuples),
-
- % Calculate column widths based on content
- Widths = calculate_column_widths(Tuples, AllKeys),
-
- io:format("~n"),
- print_separator_with_widths(Widths),
- print_row_with_widths([atom_to_list(K) || K <- AllKeys], Widths),
- print_separator_with_widths(Widths),
-
- lists:foreach(
- fun(Tuple) ->
- Values = [format_value_for_table(maps:get(K, Tuple, undefined)) || K <- AllKeys],
- print_row_with_widths(Values, Widths)
- end,
- Tuples
- ),
- print_separator_with_widths(Widths),
- io:format("~n~b tuple(s)~n~n", [length(Tuples)]),
- ok.
-
-calculate_column_widths(Tuples, Keys) ->
- lists:map(
- fun(Key) ->
- % Start with header width
- HeaderWidth = length(atom_to_list(Key)),
-
- % Find max value width in this column
- MaxValueWidth = lists:foldl(
- fun(Tuple, MaxW) ->
- Value = maps:get(Key, Tuple, undefined),
- ValueStr = format_value_for_table(Value),
- max(MaxW, length(ValueStr))
- end,
- HeaderWidth,
- Tuples
- ),
-
- max(12, MaxValueWidth) % Minimum width of 12
- end,
- Keys
- ).
-
-format_value_for_table(V) when is_map(V) ->
- % Format nested map as inline representation
- Keys = [K || K <- maps:keys(V), K =/= meta],
- Parts = [atom_to_list(K) ++ ":" ++ format_value(maps:get(K, V)) || K <- Keys],
- "[" ++ string:join(Parts, ", ") ++ "]";
-format_value_for_table(V) ->
- format_value(V).
-
-print_separator_with_widths(Widths) ->
- io:format("+"),
- lists:foreach(
- fun(W) ->
- io:format("~s+", [lists:duplicate(W + 2, $-)])
- end,
- Widths
- ),
- io:format("~n").
-
-print_row_with_widths(Values, Widths) ->
- io:format("| "),
- lists:foreach(
- fun({Val, Width}) ->
- io:format("~*s | ", [-Width, Val])
- end,
- lists:zip(Values, Widths)
- ),
- io:format("~n").
-
-%% Mode 2: Nested tables (tables within cells)
-render_nested_table(Tuples) ->
- io:format("~n=== Nested Table View ===~n~n"),
- lists:foreach(
- fun(Tuple) ->
- render_tuple_nested(Tuple, 0)
- end,
- Tuples
- ),
- io:format("~n~b tuple(s)~n~n", [length(Tuples)]),
- ok.
-
-render_tuple_nested(Tuple, Indent) ->
- Keys = [K || K <- maps:keys(Tuple), K =/= meta],
- IndentStr = lists:duplicate(Indent, $\s),
-
- io:format("~s+-- Tuple~n", [IndentStr]),
- lists:foreach(
- fun(Key) ->
- Value = maps:get(Key, Tuple),
- case is_map(Value) andalso (Key =/= meta) of
- true ->
- % Nested tuple - render as inner table
- io:format("~s| ~s:~n", [IndentStr, atom_to_list(Key)]),
- io:format("~s| +-------------------------+~n", [IndentStr]),
- NestedKeys = maps:keys(Value),
- lists:foreach(
- fun(NK) ->
- NV = format_value(maps:get(NK, Value)),
- io:format("~s| | ~-10s : ~-10s |~n",
- [IndentStr, atom_to_list(NK), NV])
- end,
- NestedKeys
- ),
- io:format("~s| +-------------------------+~n", [IndentStr]);
- false ->
- % Simple value
- io:format("~s| ~-15s : ~s~n",
- [IndentStr, atom_to_list(Key), format_value(Value)])
- end
- end,
- Keys
- ),
- io:format("~s+--~n", [IndentStr]).
-
-%% Mode 3: Tree view with hierarchy
-render_tree(Tuples) ->
- io:format("~n=== Tree View ===~n~n"),
- io:format("+-- Relation (~b tuples)~n", [length(Tuples)]),
- render_tuples_as_tree(Tuples, "| ", length(Tuples)),
- io:format("~n"),
- ok.
-
-render_tuples_as_tree([], _Prefix, _Total) ->
- ok;
-render_tuples_as_tree([Tuple], Prefix, Total) ->
- io:format("~s~n~s+-- Row ~b~n", [Prefix, Prefix, Total]),
- render_tuple_as_tree(Tuple, Prefix ++ " ");
-render_tuples_as_tree([Tuple | Rest], Prefix, Total) ->
- Index = Total - length(Rest),
- io:format("~s~n~s+-- Row ~b~n", [Prefix, Prefix, Index]),
- render_tuple_as_tree(Tuple, Prefix ++ "| "),
- render_tuples_as_tree(Rest, Prefix, Total).
-
-render_tuple_as_tree(Tuple, Prefix) ->
- Keys = [K || K <- maps:keys(Tuple), K =/= meta],
- render_attrs_as_tree(Keys, Tuple, Prefix).
-
-render_attrs_as_tree([], _Tuple, _Prefix) ->
- ok;
-render_attrs_as_tree([Key], Tuple, Prefix) ->
- Value = maps:get(Key, Tuple),
- case is_map(Value) andalso (Key =/= meta) of
- true ->
- io:format("~s+-- ~s:~n", [Prefix, atom_to_list(Key)]),
- render_tuple_as_tree(Value, Prefix ++ " ");
- false ->
- io:format("~s+-- ~s: ~s~n",
- [Prefix, atom_to_list(Key), format_value(Value)])
- end;
-render_attrs_as_tree([Key | Rest], Tuple, Prefix) ->
- Value = maps:get(Key, Tuple),
- case is_map(Value) andalso (Key =/= meta) of
- true ->
- io:format("~s+-- ~s:~n", [Prefix, atom_to_list(Key)]),
- render_tuple_as_tree(Value, Prefix ++ "| "),
- render_attrs_as_tree(Rest, Tuple, Prefix);
- false ->
- io:format("~s+-- ~s: ~s~n",
- [Prefix, atom_to_list(Key), format_value(Value)]),
- render_attrs_as_tree(Rest, Tuple, Prefix)
- end.
-
-%% Mode 4: Linked tables (parent + separate child tables)
-render_linked_tables(Tuples) ->
- io:format("~n=== Linked Tables View ===~n~n"),
-
- % Collect nested relations with parent references
- {FlatTuples, NestedRels} = extract_nested_relations_with_refs(Tuples),
-
- % Render main table
- io:format("Main Relation:~n"),
- render_table(FlatTuples),
-
- % Render nested tables with parent references
- case NestedRels of
- [] -> ok;
- _ ->
- io:format("~nNested Relations:~n"),
- lists:foreach(
- fun({RowIdx, AttrName, NestedTuple}) ->
- io:format("~n[~s for Row ~b]:~n", [atom_to_list(AttrName), RowIdx]),
- render_table([NestedTuple])
- end,
- NestedRels
- )
- end,
- ok.
-
-extract_nested_relations_with_refs(Tuples) ->
- % Process tuples with index tracking
- {FlatTuples, NestedRels} = lists:foldl(
- fun(Tuple, {FlatAcc, NestedAcc}) ->
- RowIdx = length(FlatAcc) + 1,
-
- % Flatten this tuple
- FlatTuple = maps:map(
- fun(K, V) when is_map(V), K =/= meta ->
- lists:flatten(io_lib:format("[~b]", [RowIdx]));
- (_K, V) -> V
- end,
- Tuple
- ),
-
- % Extract nested relations with parent reference
- Nested = lists:filtermap(
- fun(K) ->
- case maps:get(K, Tuple) of
- V when is_map(V), K =/= meta ->
- {true, {RowIdx, K, V}};
- _ ->
- false
- end
- end,
- maps:keys(Tuple)
- ),
-
- {[FlatTuple | FlatAcc], Nested ++ NestedAcc}
- end,
- {[], []},
- Tuples
- ),
-
- {lists:reverse(FlatTuples), lists:reverse(NestedRels)}.
-
-%% Mode 5: Outline format (indented text)
-render_outline(Tuples) ->
- io:format("~n=== Outline View ===~n~n"),
- lists:foreach(
- fun(Tuple) ->
- render_tuple_outline(Tuple, 0)
- end,
- Tuples
- ),
- io:format("~n~b tuple(s)~n~n", [length(Tuples)]),
- ok.
-
-render_tuple_outline(Tuple, Indent) ->
- Keys = [K || K <- maps:keys(Tuple), K =/= meta],
- IndentStr = lists:duplicate(Indent, $\s),
-
- io:format("~s+-- Tuple~n", [IndentStr]),
- lists:foreach(
- fun(Key) ->
- Value = maps:get(Key, Tuple),
- case is_map(Value) andalso (Key =/= meta) of
- true ->
- io:format("~s| ~s:~n", [IndentStr, atom_to_list(Key)]),
- render_tuple_outline(Value, Indent + 4);
- false ->
- io:format("~s| ~s: ~s~n",
- [IndentStr, atom_to_list(Key), format_value(Value)])
- end
- end,
- Keys
- ),
- io:format("~s+--~n", [IndentStr]).
-
-%%% Helper Functions
-
-get_all_keys(Tuples) ->
- lists:usort(
- lists:flatmap(
- fun(Tuple) ->
- [K || K <- maps:keys(Tuple), K =/= meta]
- end,
- Tuples
- )
- ).
-
-format_value(undefined) -> "(null)";
-format_value(V) when is_integer(V) -> integer_to_list(V);
-format_value(V) when is_float(V) -> float_to_list(V, [{decimals, 2}]);
-format_value(V) when is_atom(V) -> atom_to_list(V);
-format_value(V) when is_binary(V) -> binary_to_list(V);
-format_value(V) when is_list(V) -> V;
-format_value(V) when is_map(V) -> "(nested)";
-format_value(_) -> "(complex)".
diff --git a/test/dune b/test/dune
new file mode 100644
index 0000000..2f3e5eb
--- /dev/null
+++ b/test/dune
@@ -0,0 +1,6 @@
+(library
+ (name test_relational_engine)
+ (libraries relational_engine)
+ (inline_tests)
+ (preprocess
+ (pps ppx_inline_test)))
diff --git a/test/test_relational_engine.ml b/test/test_relational_engine.ml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/test_relational_engine.ml
diff --git a/treefmt.nix b/treefmt.nix
deleted file mode 100644
index 7a4812b..0000000
--- a/treefmt.nix
+++ /dev/null
@@ -1,10 +0,0 @@
-# treefmt.nix
-{ pkgs, ... }:
-{
- # Used to find the project root
- projectRootFile = "flake.nix";
- programs = {
- erlfmt.enable = true;
- nixfmt.enable = true;
- };
-}