diff options
| author | Marcos Magueta <maguetamarcos@gmail.com> | 2026-03-08 01:12:02 -0300 |
|---|---|---|
| committer | Marcos Magueta <maguetamarcos@gmail.com> | 2026-03-08 01:12:02 -0300 |
| commit | 99ac1e7d9b77d69f2a85a108468d4433953d8269 (patch) | |
| tree | fe549934d1eff7ad3b4384f07ef5c9355a1fee64 /docs | |
| parent | 54015bfbe98cfd850e1a6c142d8f514a9f48ddd9 (diff) | |
| download | Sakura-99ac1e7d9b77d69f2a85a108468d4433953d8269.tar.gz Sakura-99ac1e7d9b77d69f2a85a108468d4433953d8269.zip | |
constraints
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/constraints.org | 403 |
1 files changed, 403 insertions, 0 deletions
diff --git a/docs/constraints.org b/docs/constraints.org new file mode 100644 index 0000000..9f866fa --- /dev/null +++ b/docs/constraints.org @@ -0,0 +1,403 @@ +#+TITLE: Constraints and Membership Criteria + +* Conceptual Foundation + +A relation's *intension* is its meaning: what the relation represents +in the conceptual model. Where the extension is the set of tuples +currently admitted, the intension declares which tuples /could/ be +admitted. Constraints are the formal mechanism by which a relation's +intension narrows the infinite space of possible tuples to only those +consistent with the conceptual model. + +Because Sakura models everything as a relation, constraints are +themselves relations. Checking a constraint is checking membership in +a constraint-relation. When a DML operation (insert/delete) +manipulates a relation, it verifies that the manipulated data +satisfies membership in all attached constraint-relations before +committing the change. + +** Membership Criteria vs. Constraints + +Sakura distinguishes two levels of admission control: + +- *Membership criteria* (=membership_criteria : Tuple.t -> bool=) is + the opaque predicate baked into every relation. For stored relations + it validates schema/domain conformance. For ephemeral relations + (less_than, plus, etc.) it /is/ the intensional definition --- the + rule that characterizes the relation rather than an enumeration of + members. + +- *Constraints* (=Constraint.t=) are declarative, inspectable + structures attached to a relation via its =constraints= field. They + are evaluated at DML time against the current database snapshot and + produce structured diagnostics on failure. + +Membership criteria are checked first. If the tuple passes, the +constraint list is evaluated. + +** Negation and Closed-World Reasoning + +The =Not= constructor requires an explicit universe relation name. +This forces closed-world reasoning: negation is only meaningful +relative to a known domain. Without a declared universe, =NOT P= +would vacuously hold for anything outside the domain of discourse. + +At runtime the universe annotation is declarative --- it is not +checked as a tuple-existence predicate, because the tuple being +validated has not been stored yet. The evaluator simply negates the +body and treats any =MembershipFailed= diagnostic from the body as +successful negation. + +** Cardinality Awareness + +Constraints can narrow cardinality. A =Forall= over a finite +quantifier relation, or a range restriction via comparison relations, +constrains an otherwise infinite domain to a finite subset. The +=ConstrainedFinite= variant of =Conventions.Cardinality.t= tags +relations known to be finite due to constraints but whose exact count +is not computed. + +* Constraint Language + +The constraint language is a small first-order logic over relation +membership: + +#+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 + +** MemberOf + +The atomic constraint. It checks whether the tuple, after binding +resolution, is a member of the target relation. + +#+begin_src ocaml +MemberOf { target = "orders"; binding = BindingMap.singleton "order_id" (Var "order_id") } +#+end_src + +This says: "the value of =order_id= in the current tuple must appear +as =order_id= in some tuple of the =orders= relation." This is a +foreign key constraint. + +Everything is a relation --- domains, comparison relations, user +relations. =MemberOf= resolves them uniformly from the database. + +** Bindings + +Bindings map target attribute names to expressions: + +#+begin_src ocaml +type binding_expr = + | Var of attr_name (* resolve from current tuple *) + | Const of AbstractValue.t (* literal value *) +#+end_src + +A binding like ={ "left" -> Var "age"; "right" -> Const 18 }= applied +to a tuple with =age=25= produces the pairs +=[("left", 25); ("right", 18)]=, which are then checked for +membership in the target relation (e.g., =greater_than_or_equal=). + +** Logical Connectives + +- =And cs=: All sub-constraints must hold. Short-circuits on first failure. +- =Or cs=: At least one must hold. Short-circuits on first success. +- =Not { body; universe }=: The body must /not/ hold. The universe + declares the domain of negation for logical soundness. + +** Quantifiers + +- =Exists { variable; quantifier; body }=: There exists a tuple in + the quantifier relation such that, when bound to =variable=, the + body holds. The quantifier must be a finite relation. +- =Forall { variable; quantifier; body }=: For all tuples in the + quantifier relation, the body holds. Also requires finiteness. + +Quantifiers bind values from the quantifier relation into the tuple's +attribute map, extending the scope for the body evaluation. + +* Comparison Shorthands + +Because comparisons are relations, comparison constraints are +=MemberOf= with the appropriate relation name and binding. Shorthand +constructors eliminate boilerplate: + +#+begin_src ocaml +Constraint.lt ~left:(Var "age") ~right:(Const (Obj.repr 65)) +(* equivalent to: + MemberOf { target = "less_than"; + binding = { "left" -> Var "age"; "right" -> Const 65 } } *) + +Constraint.between ~value:(Var "score") ~low:(Const 0) ~high:(Const 100) +(* And [ gte score 0; lte score 100 ] *) +#+end_src + +Available shorthands: =lt=, =lte=, =gt=, =gte=, =eq=, =neq=, =between=. + +* Comparison and Arithmetic Relations + +The prelude registers domain-parameterized comparison relations. Each +takes a domain's =compare= function and a predicate over the compare +result: + +| Relation name | Schema | Predicate | +|-----------------------+----------------+-----------------| +| =less_than= | (left, right) | =compare < 0= | +| =less_than_or_equal= | (left, right) | =compare <= 0= | +| =greater_than= | (left, right) | =compare > 0= | +| =greater_than_or_equal= | (left, right) | =compare >= 0= | +| =equal= | (left, right) | =compare = 0= | +| =not_equal= | (left, right) | =compare <> 0= | + +Arithmetic relations follow the same pattern with richer schemas: + +| Relation | Schema | Invariant | +|--------------+---------------------------------+-----------------------| +| =plus= | (a, b, sum) | =a + b = sum= | +| =times= | (a, b, product) | =a * b = product= | +| =minus= | (a, b, difference) | =a - b = difference= | +| =divide= | (a, b, quotient, remainder) | =a = b*q + r, 0<=r<b= | + +All are ephemeral (generator-based, infinite). They serve as the +vocabulary for constraint expressions. + +* Domain Compare + +Each =Domain.t= now carries a =compare= field: + +#+begin_src ocaml +type t = { + name : Name.t; + generator : Generator.t; + membership_criteria : Tuple.t -> bool; + cardinality : Cardinality.t; + compare : AbstractValue.t -> AbstractValue.t -> int; +} +#+end_src + +The =compare= function dispatches comparison for values of that +domain. The comparison relations use domain comparators rather than +spreading =Obj.magic= casts at each call site. The four prelude +domains define their comparators: + +- =integer=, =natural=: integer comparison +- =rational=: float comparison +- =string=: lexicographic comparison + +* Evaluation Engine + +** Eval Context + +The evaluator cannot depend on =Relation.t=, =Database.t=, or +=Manipulation= (they compile later). Instead it receives a callback +record: + +#+begin_src ocaml +type eval_context = { + check_membership : + relation_name -> + (attr_name * AbstractValue.t) list -> + bool; + iterate_finite : + relation_name -> + (attr_name * AbstractValue.t) list list option; +} +#+end_src + +Both callbacks are closed over a specific =(storage, db)= pair at +construction time. The =db= is a =Management.Database.t= representing +one exact version (one hash). All relation lookups resolve against +that snapshot only. This is critical: constraint evaluation sees a +consistent view of the database, never a partially-mutated state. + +** How =check_membership= Works + +For a stored relation, =check_membership= loads every tuple from the +Merkle tree and checks whether any tuple's attributes match the bound +pairs. For an ephemeral relation (no tree), it delegates to +=membership_criteria=. + +This distinction matters for foreign key constraints. A FK like +"order_items.order_id must exist in orders" requires checking actual +tuple existence in the =orders= relation, not just schema +conformance. + +** Evaluation Rules + +| Constructor | Rule | +|-------------+-------------------------------------------------------------| +| =MemberOf= | Bind variables from tuple, call =check_membership= | +| =Not= | Negate body. =MembershipFailed= from body counts as success | +| =And= | All must hold (short-circuit on first failure) | +| =Or= | Any must hold (short-circuit on first success) | +| =Exists= | Iterate quantifier, bind each row, true if any body holds | +| =Forall= | Iterate quantifier, bind each row, true only if all hold | + +Quantifiers require finite quantifier relations (=iterate_finite= +returns =Some rows=). An infinite quantifier produces +=UnboundedQuantifier= diagnostic. + +** Diagnostics + +Failures produce structured diagnostics rather than opaque booleans: + +#+begin_src ocaml +type diagnostic = + | MembershipFailed of { target : string; bound_values : ... } + | RelationNotFound of string + | UnboundedQuantifier of { variable; quantifier } + | ConstraintFailures of (string * diagnostic) list +#+end_src + +Named constraint evaluation collects all failures into +=ConstraintFailures=, giving the caller a complete picture of which +constraints were violated and why. + +* Enforcement in Manipulation + +Constraints are enforced in =Manipulation.create_tuple=. The sequence is: + +1. Check =membership_criteria= (schema/domain validation) +2. If the relation has constraints, build an =eval_context= from the + current =(storage, db)= pair +3. Evaluate each named constraint against the tuple +4. On any failure, return =ConstraintViolation= with diagnostic details +5. On success, proceed to hash, store, and persist + +The =eval_context= is built fresh for each =create_tuple= call with +the /current/ database state. This ensures FK lookups see recently +inserted tuples, not a stale snapshot from constraint registration +time. + +** Registering Constraints + +=register_constraint= performs two actions: + +1. Attaches the constraint body to the relation via + =update_relation_constraints=, which merges new constraints with + any existing ones +2. Records the constraint name and relation in the =sakura:constraint= + catalog relation + +The constraint body is stored in the =Relation.t= object's +=constraints= field, not separately. This means the constraint +travels with the relation through algebraic operations. + +* Propagation Through Algebra + +When algebraic operations derive new relations, constraints propagate +according to rules that preserve soundness: + +| Operation | Propagation rule | +|-----------+-----------------------------------------------------| +| Select | Preserve all source constraints | +| Project | Filter: keep only constraints whose variables are | +| | all within the projected attributes | +| Rename | Apply variable renaming to constraint references | +| Equijoin | Merge constraints from both input relations | +| Union | Drop all constraints (conservative: constraints | +| | only hold if both inputs agree) | +| Diff | Preserve left relation's constraints | +| Take | Preserve source constraints | + +The =filter_by_attrs=, =rename_vars=, and =merge= utility functions +on =Constraint.t= implement these transformations. + +** Example: Project Filtering + +Given a relation =R(x, y)= with constraint "=x < 100=", projecting +to =[y]= drops the constraint because =x= is no longer in scope. +Projecting to =[x]= or =[x, y]= preserves it. + +#+begin_src ocaml +Constraint.filter_by_attrs ["y"] (Constraint.lt ~left:(Var "x") ~right:(Const 100)) +(* => None — dropped because "x" not in ["y"] *) + +Constraint.filter_by_attrs ["x"] (Constraint.lt ~left:(Var "x") ~right:(Const 100)) +(* => Some (lt ...) — preserved *) +#+end_src + +* Utility Functions + +=Constraint.t= provides introspection and transformation utilities: + +- =vars_in : t -> attr_name list= --- collect all =Var= names + referenced in a constraint, recursively through all sub-expressions. + +- =rename_vars : (attr_name * attr_name) list -> t -> t= --- rename + variable references throughout a constraint tree. Used by the rename + algebra operator. + +- =filter_by_attrs : attr_name list -> t -> t option= --- returns + =None= if the constraint references any variable not in the given + set. Used by projection to drop inapplicable constraints. For =Or=, + all branches must survive (dropping a branch changes semantics). + +- =merge : (string * t) list -> (string * t) list -> (string * t) list= + --- AND-merge two named constraint lists. Constraints sharing a name + are combined with =And=. Used by equijoin. + +* Examples + +** Foreign Key + +#+begin_src ocaml +(* order_items.order_id must reference an existing order *) +let fk = + Constraint.MemberOf { + target = "orders"; + binding = Constraint.BindingMap.singleton "order_id" (Var "order_id") + } +in +register_constraint storage db + ~constraint_name:"fk_order" + ~relation_name:"order_items" + ~body:fk +#+end_src + +Inserting an order_item with =order_id=999= when no such order exists +produces =ConstraintViolation "constraint fk_order violated"=. + +** Mutual Exclusion + +#+begin_src ocaml +(* A ticket cannot be both open and closed *) +let not_closed = + Constraint.Not { + body = Constraint.MemberOf { + target = "closed_ticket"; + binding = Constraint.BindingMap.singleton "ticket_id" (Var "ticket_id") + }; + universe = "open_ticket" + } +in +register_constraint storage db + ~constraint_name:"not_closed" + ~relation_name:"open_ticket" + ~body:not_closed +#+end_src + +** Self-Reference Inequality + +#+begin_src ocaml +(* An employee cannot manage themselves *) +let neq = Constraint.neq ~left:(Var "emp_id") ~right:(Var "mgr_id") in +register_constraint storage db + ~constraint_name:"no_self_manage" + ~relation_name:"reports_to" + ~body:neq +#+end_src + +This uses the =not_equal= comparison relation: it checks membership +in ={(left, right) | left <> right}=. + +* References + +- Codd, E. F. (1979), "Extending the Database Relational Model to Capture More Meaning" +- Date, C. J. and Darwen, H. (2006), "The Third Manifesto" +- McGoveran, D. (1994), "The Semantic Relational Data Model" |
