aboutsummaryrefslogtreecommitdiffstats
path: root/docs/constraints.org
blob: 9f866fa7ad2247ade87bc5dbe66d209eecf35d0f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
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"