aboutsummaryrefslogtreecommitdiffstats
path: root/docs/dcl-quantification.org
diff options
context:
space:
mode:
authorMarcos Magueta <maguetamarcos@gmail.com>2026-03-14 11:39:04 -0300
committerGitHub <noreply@github.com>2026-03-14 11:39:04 -0300
commite6661bfa6a3d7dc477d0b7717c80bd536eca2d68 (patch)
tree12510963feeed03221defa690e73edd4135b65e1 /docs/dcl-quantification.org
parent0ad04e638baa8f283150d3c2809cdb9bf21e2995 (diff)
downloadSakura-e6661bfa6a3d7dc477d0b7717c80bd536eca2d68.tar.gz
Sakura-e6661bfa6a3d7dc477d0b7717c80bd536eca2d68.zip
Branching and naïve merge strategies
* AGPL license * branching and merging * Delete LICENSE in favor of separate PR Signed-off-by: Marcos Magueta <maguetamarcos@gmail.com> * add unmonitored tests and docs --------- Signed-off-by: Marcos Magueta <maguetamarcos@gmail.com>
Diffstat (limited to 'docs/dcl-quantification.org')
-rw-r--r--docs/dcl-quantification.org480
1 files changed, 0 insertions, 480 deletions
diff --git a/docs/dcl-quantification.org b/docs/dcl-quantification.org
deleted file mode 100644
index 81e9cad..0000000
--- a/docs/dcl-quantification.org
+++ /dev/null
@@ -1,480 +0,0 @@
-#+TITLE: DCL Quantification: Runtime Model and Evaluation
-
-* Overview
-
-This document describes precisely how DCL constraints are represented, compiled,
-and evaluated at runtime. It covers the full stack from the s-expression syntax
-through AST compilation, context construction, and recursive evaluation — with
-particular attention to the cost model of quantifiers, which is the primary target
-for future performance work.
-
-* Constraint Type (~lib/constraint.ml~)
-
-The runtime constraint type is an algebraic tree:
-
-#+begin_src ocaml
-type t =
- | MemberOf of { target : relation_name; binding : binding }
- | Not of { body : t; universe : relation_name }
- | And of t list
- | Or of t list
- | Exists of { variable : attr_name; quantifier : relation_name; body : t }
- | Forall of { variable : attr_name; quantifier : relation_name; body : t }
-#+end_src
-
-A ~binding~ is a ~BindingMap~ (a ~Map.Make(String)~) from target attribute names
-to ~binding_expr~:
-
-#+begin_src ocaml
-type binding_expr =
- | Var of attr_name (* resolved from current tuple at eval time *)
- | Const of Conventions.AbstractValue.t (* literal, resolved at compile time *)
-#+end_src
-
-The ~variable~ field in ~Exists~ and ~Forall~ names the bound tuple for use in
-~Var~ expressions inside the body. Attributes from the quantifier row are merged
-into the evaluation tuple under the prefix ~variable ^ "."~, so ~(Var "d.dept_id")~
-unambiguously refers to the ~dept_id~ attribute of the tuple bound to variable ~"d"~.
-See [[*Tuple Extension: ~extend_tuple~][Tuple Extension: extend_tuple]] for the implementation and [[*The Variable Namespacing Problem][The Variable Namespacing Problem]]
-for the history of the bug this fixes.
-
-* Evaluation Context (~eval_context~)
-
-Evaluation does not access storage or the database directly. It works through an
-~eval_context~ record built once per insert operation and closed over a specific
-~(storage, db)~ snapshot:
-
-#+begin_src ocaml
-type eval_context = {
- check_membership :
- relation_name ->
- (attr_name * Conventions.AbstractValue.t) list ->
- bool;
- iterate_finite :
- relation_name ->
- (attr_name * Conventions.AbstractValue.t) list list option;
-}
-#+end_src
-
-Both fields are closures. This indirection decouples ~Constraint.evaluate~ from
-the storage backend and allows the same constraint tree to be evaluated against
-different snapshots.
-
-** ~check_membership~
-
-Built in ~Manipulation.Make.build_eval_context~. Given a relation name and a list
-of ~(attr_name, value)~ pairs, it returns ~true~ iff a tuple exists in the named
-relation whose attributes match every pair in the list.
-
-The implementation:
-1. Looks up the relation in the ~db~ snapshot.
-2. Constructs a synthetic ~Tuple.Materialized~ from the bound pairs and runs
- ~rel.membership_criteria~ against it. If it fails, returns ~false~ immediately.
-3. If the relation has no Merkle tree (ephemeral/generator relation),
- ~membership_criteria~ alone is trusted and the function returns ~true~.
-4. If the relation has a tree, it calls ~Merkle.keys tree~ to obtain *all* tuple
- hashes, then linearly scans them. For each hash it:
- a. Loads the raw bytes from storage (~Storage.load_raw~).
- b. Deserialises the ~Storable.Tuple.t~ to get the attribute hash list.
- c. For each bound pair ~(bk, bv)~, finds the matching attribute by name, loads
- its value bytes from storage (~Storage.load_raw~ again), unmarshals it, and
- compares with ~Stdlib.(=)~.
- d. Short-circuits to ~true~ on the first tuple that satisfies all pairs.
- e. Returns ~false~ if no tuple matches.
-
-The cost of a single ~check_membership~ call on a relation of cardinality /n/ with
-/a/ bound attributes and average /k/ attributes per stored tuple is:
-
- O(n · (1 + a · k)) storage round-trips in the worst case.
-
-No indexes exist. Every check is a full table scan at the storage level.
-
-** ~iterate_finite~
-
-Given a relation name, returns ~Some rows~ where ~rows~ is a fully materialised
-list of rows, each a ~(attr_name * AbstractValue.t) list~. Returns ~None~ if the
-relation has infinite or unconstrained cardinality
-(~AlephZero~ or ~Continuum~ in ~Conventions.Cardinality~).
-
-Only relations with cardinality ~Finite _~ or ~ConstrainedFinite~ are iterable.
-Generator-based relations with no tree return ~Some []~.
-
-The materialisation path:
-1. ~Merkle.keys tree~ — loads all tuple hashes.
-2. For each hash: ~Storage.load_raw~ → ~Storable.Tuple.of_bytes~ → attribute hash list.
-3. For each attribute hash: ~Storage.load_raw~ → ~Marshal.from_bytes~.
-
-This eagerly materialises the *entire* quantifier relation into memory before
-evaluation begins. For a relation of cardinality /n/ with /k/ attributes each,
-the cost is O(n · k) storage loads and the full result occupies O(n · k) memory.
-
-* Variable Resolution: ~bind~
-
-Before a ~MemberOf~ check is executed, the binding map is resolved against the
-current tuple via ~bind~:
-
-#+begin_src ocaml
-let bind (b : binding) (tuple : Tuple.materialized)
- : (attr_name * Conventions.AbstractValue.t) list =
- BindingMap.fold
- (fun target_attr expr acc ->
- let value = match expr with
- | Const v -> Some v
- | Var src_attr ->
- match Tuple.AttributeMap.find_opt src_attr tuple.attributes with
- | Some attr -> Some attr.Attribute.value
- | None -> None
- in
- match value with
- | Some v -> (target_attr, v) :: acc
- | None -> acc)
- b []
-#+end_src
-
-Key behaviour:
-- ~Const v~ is lifted directly; no attribute lookup.
-- ~Var src_attr~ looks up ~src_attr~ in ~tuple.attributes~. If the attribute is
- absent, the pair is *silently dropped* from the bound list. A ~MemberOf~ with
- incomplete bindings may match unintended tuples or always pass if the target
- relation contains tuples with fewer attributes.
-- The result is a plain assoc list, not a map; duplicate keys are possible if
- the binding contains both a ~Const~ and a ~Var~ targeting the same name.
-
-* Tuple Extension: ~extend_tuple~
-
-When a quantifier (~Exists~ or ~Forall~) iterates over a row, it extends the
-current evaluation tuple by merging that row's attributes in:
-
-#+begin_src ocaml
-and extend_tuple (tuple : Tuple.materialized)
- (variable : attr_name)
- (row : (attr_name * Conventions.AbstractValue.t) list)
- : Tuple.materialized =
- let extra_attrs =
- List.fold_left
- (fun acc (k, v) ->
- Tuple.AttributeMap.add (variable ^ "." ^ k) { Attribute.value = v } acc)
- tuple.attributes row
- in
- { tuple with attributes = extra_attrs }
-#+end_src
-
-Two important facts:
-
-1. *Quantifier attributes are namespaced by the variable name.* Each attribute ~k~
- from the quantifier row is inserted as ~variable ^ "." ^ k~. The incoming tuple's
- own attributes are never overwritten. ~(Var "dept_id")~ resolves to the base
- tuple's value; ~(Var "d.dept_id")~ resolves to the value contributed by the
- quantifier bound to ~"d"~. See [[*The Variable Namespacing Problem][The Variable Namespacing Problem]] for the
- earlier behaviour where ~variable~ was ignored and overwrites were silent.
-
-2. *A new ~AttributeMap~ is allocated on every extend call.* With /a/ existing
- attributes and /r/ new attributes from the row, ~fold_left~ performs /r/
- ~AttributeMap.add~ operations each building on the previous map. For /q/
- nested quantifiers of cardinalities /n₁, n₂, ..., nq/, ~extend_tuple~ is
- called /n₁ · n₂ · ... · nq/ times total — once per combination.
-
-* Recursive Evaluation: ~evaluate~
-
-#+begin_src ocaml
-let rec evaluate (ctx : eval_context) (tuple : Tuple.materialized) (c : t)
- : (bool, diagnostic) result =
-#+end_src
-
-The function is structurally recursive over the constraint tree. Each case:
-
-** ~MemberOf~
-
-#+begin_src ocaml
-| MemberOf { target; binding } ->
- let bound_values = bind binding tuple in
- if ctx.check_membership target bound_values then Ok true
- else Error (MembershipFailed { target; bound_values })
-#+end_src
-
-Resolves bindings from the current tuple (O(|binding|) attribute lookups), then
-delegates to ~check_membership~. Returns ~Ok true~ or ~Error~ (never ~Ok false~)
-— the ~Ok false~ branch of ~Exists~/~Forall~ is distinguished from ~Error~.
-
-** ~Not~
-
-#+begin_src ocaml
-| Not { body; universe = _ } ->
- match evaluate ctx tuple body with
- | Ok true -> Ok false
- | Ok false -> Ok true
- | Error (MembershipFailed _) -> Ok true
- | Error d -> Error d
-#+end_src
-
-~MembershipFailed~ from the body is treated as ~Ok true~ (negation of a failed
-membership = the tuple is not a member = negation succeeds). The ~universe~ field
-is a declarative annotation for closed-world reasoning and is not used at runtime.
-
-** ~And~ / ~Or~
-
-Both use short-circuit evaluation:
-- ~And~: stops at the first ~Ok false~ or ~Error~.
-- ~Or~: stops at the first ~Ok true~; swallows ~Error~ branches and continues.
-
-Neither produces diagnostics for the other branches once a decision is reached.
-Note that ~evaluate_named~ (which evaluates all named constraints on a tuple) does
-*not* short-circuit — it collects all failures before returning.
-
-** ~Exists~
-
-#+begin_src ocaml
-| Exists { variable; quantifier; body } ->
- match ctx.iterate_finite quantifier with
- | None -> Error (UnboundedQuantifier { variable; quantifier })
- | Some rows ->
- let rec check = function
- | [] -> Ok false
- | row :: rest ->
- let extended_tuple = extend_tuple tuple variable row in
- match evaluate ctx extended_tuple body with
- | Ok true -> Ok true
- | Ok false -> check rest
- | Error _ -> check rest
- in
- check rows
-#+end_src
-
-The execution model:
-1. ~iterate_finite~ is called, fully materialising the quantifier relation.
-2. Rows are iterated sequentially. For each row, ~extend_tuple~ merges it into
- the current tuple, and ~evaluate~ recurses into the body.
-3. The first ~Ok true~ short-circuits: remaining rows are not evaluated.
-4. Both ~Ok false~ and ~Error~ from the body are treated identically — the row is
- skipped and iteration continues. *Errors inside the body of an ~Exists~ are
- silently discarded.*
-5. If all rows are exhausted without ~Ok true~, returns ~Ok false~.
-
-An ~Exists~ over a relation of cardinality /n/ with a body whose evaluation cost
-is /C/ has worst-case cost O(n · C). Nesting two ~Exists~ quantifiers over
-relations of cardinalities /n/ and /m/ has worst-case cost O(n · m · C).
-In general, /q/ nested quantifiers produce O(∏ nᵢ · C) body evaluations — a
-Cartesian product.
-
-** ~Forall~
-
-#+begin_src ocaml
-| Forall { variable; quantifier; body } ->
- match ctx.iterate_finite quantifier with
- | None -> Error (UnboundedQuantifier { variable; quantifier })
- | Some rows ->
- let rec check = function
- | [] -> Ok true
- | row :: rest ->
- let extended_tuple = extend_tuple tuple variable row in
- match evaluate ctx extended_tuple body with
- | Ok true -> check rest
- | Ok false -> Ok false
- | Error (MembershipFailed _) -> Ok false
- | Error d -> Error d
- in
- check rows
-#+end_src
-
-Symmetric to ~Exists~ but short-circuits on ~Ok false~ or ~MembershipFailed~.
-Unlike ~Exists~, other ~Error~ variants (e.g., ~UnboundedQuantifier~) are
-propagated rather than silently consumed. The cost model is identical: O(∏ nᵢ · C).
-
-* The Variable Namespacing Problem
-
-This section documents the original bug and explains why the current implementation
-is correct. In the initial implementation, ~extend_tuple~ took ~variable~ as
-~_variable~ — a named-but-discarded argument — and merged quantifier row attributes
-flat into the tuple under their original names. This had two consequences.
-
-** Attribute Shadowing (fixed)
-
-If the incoming tuple and the quantifier row shared an attribute name, the
-quantifier's value won silently. Concretely: an Employee with ~dept_id=99~ being
-checked by an ~Exists~ over Department rows would have its ~dept_id~ overwritten
-by each Department row's ~dept_id~ in turn. ~(Var "dept_id")~ in the body then
-resolved to the Department's own id, not the Employee's. An FK constraint written
-as ~(MemberOf (target "Department") (binding ((dept_id (Var "dept_id")))))~ inside
-an ~Exists~ over Department would always pass — it was validating the Department
-against itself rather than checking whether the Employee's ~dept_id~ existed.
-
-** Multi-Quantifier Ambiguity (fixed)
-
-With two nested quantifiers over relations sharing attribute names, the inner
-quantifier's pass through ~extend_tuple~ overwrote what the outer one had written.
-There was no way to write a body that simultaneously referred to attributes from
-two distinct quantifier rows of the same name.
-
-** Resolution
-
-Both problems are resolved by namespacing: each attribute ~k~ from the quantifier
-row is stored as ~variable ^ "." ^ k~. The base tuple is never modified. The
-~variable~ argument, previously discarded as ~_variable~, is now the namespace
-prefix. The fix is in ~lib/constraint.ml~ with an inline TODO describing the
-deeper issue of tuple-level provenance as a structural solution.
-
-* N-Ary Quantification
-
-The current AST represents each quantifier as a single ~(variable, quantifier)~
-pair. N-ary constraints across three or more relations require nesting:
-
-#+begin_src lisp
-(Exists (variable "b") (quantifier "Building")
- (Exists (variable "r") (quantifier "Room")
- (Exists (variable "s") (quantifier "Suite")
- ...)))
-#+end_src
-
-This is semantically correct once namespacing is in place, but verbose and implies
-a strict left-to-right evaluation order with no opportunity for reordering.
-
-The proposed n-ary form makes the binding list first-class:
-
-#+begin_src lisp
-(Exists
- (bindings ((b "Building") (r "Room") (s "Suite")))
- (body ...))
-#+end_src
-
-The executor folds over the binding list left-to-right, extending the tuple at
-each step — equivalent to nested ~Exists~ but expressed as a single node:
-
-#+begin_src ocaml
-| Exists { bindings; body } ->
- let rec go bindings tuple =
- match bindings with
- | [] -> evaluate ctx tuple body
- | (variable, quantifier) :: rest ->
- match ctx.iterate_finite quantifier with
- | None -> Error (UnboundedQuantifier { variable; quantifier })
- | Some rows ->
- let rec check = function
- | [] -> Ok false
- | row :: rest' ->
- let extended = extend_tuple tuple variable row in
- match go rest extended with
- | Ok true -> Ok true
- | Ok false | Error _ -> check rest'
- in
- check rows
- in
- go bindings tuple
-#+end_src
-
-The cost remains O(∏ nᵢ · C) — the n-ary form does not change the complexity,
-but it makes the binding structure explicit and opens the door to reordering
-optimisations (e.g., evaluate the smallest relation first).
-
-* Performance Inventory
-
-The following is a precise account of known costs and inefficiencies, intended
-as a reference for future optimisation work.
-
-** Eager Full Materialisation of Quantifier Relations
-
-~iterate_finite~ loads every tuple in the quantifier relation from storage before
-the first body evaluation begins. For a ~Forall~ whose first body call returns
-~Ok false~, all subsequent tuples were loaded needlessly. A streaming/lazy
-iterator would allow early termination before full materialisation.
-
-** Linear Scan in ~check_membership~
-
-Every ~MemberOf~ check performs a full linear scan over all tuple hashes in the
-target relation's Merkle tree. There are no secondary indexes. For a relation of
-cardinality /n/:
-
-- /n/ calls to ~Storage.load_raw~ to fetch stored tuples.
-- For each tuple, up to /a/ additional ~Storage.load_raw~ calls to fetch
- attribute values.
-- Comparison via structural equality (~Stdlib.(=)~) on unmarshalled ~Obj.t~ values.
-
-An index mapping ~(relation_name, attr_name, value_hash)~ to tuple hashes would
-reduce this to O(log n) or O(1) per ~MemberOf~ check.
-
-** AttributeMap Allocation per ~extend_tuple~ Call
-
-~extend_tuple~ allocates a new ~AttributeMap~ on every invocation by folding over
-the row into the existing map. For nested quantifiers of cardinalities
-/n₁ × n₂ × ... × nq/, this allocates ∏ nᵢ maps during a single constraint check.
-A persistent/functional map amortises this somewhat (structural sharing), but
-each call still produces a new root node.
-
-A flat association list or array, indexed by a fixed schema, would be cheaper
-for extend and lookup at the cost of schema rigidity.
-
-** Redundant Storage Loads Across Constraints
-
-The ~eval_context~ is built once per ~create_tuple~ call, but it is not a cache —
-every ~check_membership~ and ~iterate_finite~ call re-reads from storage. If a
-relation is referenced in multiple constraints or multiple ~MemberOf~ nodes within
-one constraint tree, its tuples are loaded multiple times.
-
-A constraint-session cache (a ~Hashtbl~ from relation_name to materialised rows,
-populated lazily on first access) would eliminate redundant loads within a single
-tuple insertion.
-
-** No Short-Circuit Across Named Constraints
-
-~evaluate_named~ evaluates every constraint in the named list, collecting all
-failures before returning:
-
-#+begin_src ocaml
-let failures =
- List.filter_map
- (fun (name, c) ->
- match evaluate ctx tuple c with
- | Ok true -> None
- | Ok false -> Some (name, ...)
- | Error d -> Some (name, d))
- named
-in
-#+end_src
-
-For a tuple that violates the first constraint, all remaining constraints are
-still evaluated at full cost. Short-circuiting on first failure is correct when
-complete diagnostics are not required (e.g., interactive insertion). A lazy stream
-of failures, computed on demand, would support both use cases.
-
-** Cartesian Product Complexity of Nested Quantifiers
-
-For /q/ nested ~Exists~/~Forall~ quantifiers over relations of cardinalities
-/n₁, n₂, ..., nq/ with body cost /C/:
-
- Worst-case body evaluations = n₁ × n₂ × ... × nq
-
- Worst-case storage loads = (∑ nᵢ · kᵢ) + n₁ · n₂ · ... · nq · C_membership
-
-Where /kᵢ/ is the attribute count of relation /i/ and /C_membership/ is the cost
-of ~check_membership~ on the body's target relation.
-
-Mitigations:
-- *Evaluate smallest relation first* (selectivity-based reordering): reduces the
- Cartesian product by pruning earlier.
-- *Push ~MemberOf~ filters into the iteration*: instead of iterating all rows of
- the quantifier relation and then checking membership, filter the quantifier rows
- during iteration using the same index that would accelerate ~check_membership~.
-- *Semi-join reduction*: for FK-style constraints, a single hash lookup suffices
- and no Cartesian product is needed.
-
-* Summary of Call Chain for a Single Tuple Insertion
-
-#+begin_example
-create_tuple
- └── build_eval_context (storage, db) [once per insertion]
- └── evaluate_named ctx tuple named_constraints [once per insertion]
- └── evaluate ctx tuple c [once per named constraint]
- ├── Exists / Forall
- │ └── iterate_finite quantifier [materialises full relation]
- │ ├── Merkle.keys tree
- │ └── Storage.load_raw × (n × k)
- │ └── for each row:
- │ ├── extend_tuple [allocates new AttributeMap]
- │ └── evaluate ctx extended_tuple body
- │ └── MemberOf
- │ ├── bind [resolves Vars from extended tuple]
- │ └── check_membership target bound_values
- │ ├── Merkle.keys tree
- │ └── Storage.load_raw × (n' × (1 + a))
- └── And / Or / Not
- └── evaluate (recursive)
-#+end_example