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
|
#+title: Incremental Constraint Re-checking
#+date: 2026-03-22
* Background
The naive implementation of assertion-style constraints evaluates the full
constraint expression on every mutation of every relation it references. This
is correct but expensive. The state of the art (Oracle's implementation, and
the PostgreSQL community discussion that informed it) reduces this cost along
two orthogonal axes:
1. *When* to re-check — polarity analysis eliminates re-checks that cannot
possibly produce a violation.
2. *What* to execute — universal variable substitution replaces the full
constraint with a focused incremental expression.
Primary references:
- Hendrik Decker, /Translating Advanced Integrity Checking Technology to SQL Database/
- J. M. Nicolas, /Logic for Improving Integrity Checking/
- Ceri & Widom, /Deriving Production Rules for Constraint Maintenance/
- Lex de Hann & Toon Koppelaars, /Applied Mathematics for Database Professionals/,
ch. 11 (Oracle implementation)
- PostgreSQL hackers thread (2023):
=https://www.postgresql.org/message-id/flat/FD62F1DE-4227-4D67-A1A5-C17350B8CAA1%40elusive.cx=
* Current Sakura State
Constraints are only evaluated inside =create_tuple= against the tuple
being inserted. =retract_tuple= performs no constraint re-check. This
means:
- A DELETE from a relation referenced positively in a constraint (e.g. a
=Department= relation referenced by an Employee FK constraint) leaves
existing tuples in the constrained relation unchecked.
- Constraints degrade silently into insert-time assertions rather than
invariants maintained across all mutations.
Relevant code: =lib/manipulation.ml= (=create_tuple=, =retract_tuple=),
=lib/constraint.ml= (=evaluate=, =evaluate_named=).
The catalog already seeds =sakura:on= (insert/update/delete) and
=sakura:timing= (immediate/deferred) relations, indicating that support for
deferred and per-operation constraint scheduling was anticipated but not
implemented.
* Technique 1 — Polarity Analysis
** Concept
Every relation reference inside a constraint expression has a *polarity*: the
set of mutation types that could create a new violation.
| Polarity | Meaning |
|--------------+-----------------------------------------------------------|
| ⊕ (positive) | A DELETE from this relation could violate the constraint |
| ⊖ (negative) | An INSERT into this relation could violate the constraint |
| ⊕⊖ (both) | Either mutation type could violate the constraint |
Polarity is determined by a tree walk toggling sign at each negating node
(=NOT=, =EXCEPT=, =NOT EXISTS=). References under =OUTER JOIN= carry both
polarities.
** Mapping to Sakura's =Constraint.t= AST
#+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
| Constructor | Relation reference | Polarity | Re-check trigger |
|-------------------------+--------------------+----------+---------------------------------------------|
| =MemberOf { target }= | =target= | ⊕ | DELETE from =target= |
| =Not { body }= | refs in =body= | flipped | opposite of body |
| =Not { universe }= | =universe= | ⊕ | (closed-world annotation; no runtime check) |
| =Exists { quantifier }= | =quantifier= | ⊕ | DELETE from =quantifier= |
| =Exists { body }= | refs in =body= | as-is | as normal |
| =Forall { quantifier }= | =quantifier= | ⊖ | INSERT into =quantifier= |
| =Forall { body }= | refs in =body= | flipped | opposite of body |
| =And= / =Or= | refs in children | as-is | propagated |
Note: =Forall x in Q, P(x)= ≡ =NOT (EXISTS x in Q, NOT P(x))=, so =Q=
acquires negative polarity (the outer =NOT= flips the positive polarity of
=Exists=).
** Worked Example (Sakura FK constraint)
#+begin_src
-- Constraint on Employee: dept_id must exist in Department
Exists d in Department,
MemberOf Department (dept_id = Var "dept_id")
#+end_src
Polarity walk:
- =Exists { quantifier = "Department" }= -> =Department= is ⊕ (positive)
- =MemberOf { target = "Department" }= (inside body) -> also ⊕
Re-check schedule derived:
- INSERT into =Employee=: check immediately (current behavior).
- DELETE from =Department=: re-check all =Employee= tuples whose =dept_id=
equals the deleted department's =dept_id= (currently NOT done!!!!).
- INSERT into =Department=: no re-check needed.
** Implementation Sketch
A function =polarity : Constraint.t -> (relation_name * polarity_set) list=
walks the AST accumulating per-relation polarity sets. The result is stored
alongside each named constraint in the catalog (or derived on demand maybe?).
When =retract_tuple= removes a tuple from relation =R=:
- Look up all constraints (across all relations) that reference =R= with ⊕ polarity.
- For each such constraint =C= on relation =S=: schedule a re-check of =C=
against existing tuples in =S= that could be affected (see Technique 2).
When =create_tuple= inserts a tuple into =R=:
- Current behavior: check constraints on =R= itself.
- Additionally: look up all constraints referencing =R= with ⊖ polarity and
schedule re-checks of those (this covers =Forall= quantifiers).
* Technique 2 — Universal Variable Substitution
** Concept
Universally quantified variables in a constraint (those bound by =Forall= or
appearing under a =NOT EXISTS=) can be substituted with the concrete values of
the triggering tuple. This transforms the re-check from a full table scan into
a targeted query over the minimal set of affected rows.
The "transition tuple" (the inserted or deleted tuple) supplies the concrete
values (the NEW and OLD on PostgreSQL).
** Mapping to Sakura
In Sakura's =Forall { variable; quantifier; body }=:
- =variable= is universally quantified over =quantifier=.
- When a new row is inserted into =quantifier=, that specific row supplies the
binding for =variable=.
- Instead of re-checking all tuples in the constrained relation =S=, substitute
=variable= with the transition tuple's values and check only tuples in =S=
that share the constrained attributes.
For the Employee/Department FK example above, on DELETE of Department row
={dept_id = 99}=:
#+begin_src
-- Full re-check (naive):
For every employee E: Exists d in Department, dept_id = E.dept_id
-- Incremental re-check (substituted):
Exists E in Employee where E.dept_id = 99:
NOT (Exists d in Department where d.dept_id = E.dept_id)
-- i.e., find employees orphaned by this specific deletion
#+end_src
This is analogous to the PostgreSQL example in the background section, where
universally quantified =r.n= is substituted with the transition table value.
** Relation to the =extend_tuple= Namespacing Fix
The namespacing fix for =extend_tuple= (prefixing quantifier attributes as
=variable.attr=) is a prerequisite for universal variable substitution to be
correct: the substituted binding must be distinguishable from base tuple
attributes. Both fixes must land together.
* Technique 3 — Constant Discovery
Within a constraint expression, constants (=Const v= in =binding_expr=) can be
used to filter the re-check trigger:
- If =MemberOf { target; binding }= contains =Const v= bound to attribute =a=,
then a DELETE from =target= only needs to trigger a re-check if the deleted
row has =a = v=.
Sakura's =binding_expr= already distinguishes =Var= from =Const=, so constant
discovery is a straightforward fold over the binding map.
* Deferred Constraints
The =sakura:timing= catalog relation (seeded with =immediate= /
=deferred=) anticipates support for deferred constraint checking
(analogous to SQL's =DEFERRABLE INITIALLY DEFERRED=). Under deferred
mode:
- Violations discovered during a sequence of mutations are accumulated
rather than immediately raised.
- The full constraint is re-evaluated at commit time (end of the
mutation sequence) using the final database state.
In Sakura's functional/immutable model, a mutation sequence is a fold
over =Database.t= values. Deferred checking would mean accumulating
pending re-check obligations alongside the database value and
evaluating them at the end of the fold, rather than at each
=create_tuple= / =retract_tuple= call.
* Concurrency Considerations
Under =SERIALIZABLE= isolation, incremental re-checking (Technique 2)
is sound because the focused predicates cover exactly the rows that
could be newly violated. Under =READ COMMITTED= (weaker isolation),
additional care is needed; the Oracle paper (Toon Koppelaars)
describes how to handle this using the universal variable insight.
Sakura is currently single-threaded (=bin/server.ml= has a TODO on
this), so concurrency is deferred.
* Coq Proof Opportunities
See =doc/coq_roadmap.org= (Tier 2 — Certified Constraint Evaluator)
for related proof targets. The following specific lemmas are unlocked
by this technique:
1. *Polarity soundness*:
#+begin_src coq
Theorem polarity_sound_positive :
forall c r, polarity c r = Positive ->
forall db tuple_deleted,
eval c db = true ->
eval c (db_without r tuple_deleted) = true \/
(exists t, affected_by c r tuple_deleted t).
#+end_src
Meaning: if a relation has positive polarity in a constraint, only
deletions from it can create violations, and only for specifically
affected tuples.
2. *Universal substitution completeness*:
#+begin_src coq
Theorem substitution_complete :
forall c var q transition_tuple db,
universally_quantified c var q ->
eval (substitute c var transition_tuple) db = true ->
eval c (db_without q transition_tuple) = true.
#+end_src
Meaning: if the substituted re-check passes for the transition
tuple, the full constraint holds in the updated database.
3. *Constant filter soundness*:
#+begin_src coq
Theorem constant_filter_sound :
forall c r a v deleted_tuple,
binding_constant c r a v ->
deleted_tuple.(a) <> v ->
eval c db = eval c (db_without r deleted_tuple).
#+end_src
Meaning: if the deleted tuple doesn't match the constant, no
re-check is needed.
* Implementation Roadmap
| Step | Work item | Depends on | Status |
|------+-----------------------------------------------------------------+------------+----------|
| 1 | Implement =polarity= walker on =Constraint.t= | — | Done |
| 2 | =extend_tuple= namespacing (=variable.attr= convention) | — | Done |
| 3 | Store polarity metadata in catalog (=sakura:constraint=) | 1 | — |
| 4 | =retract_tuple= triggers polarity-driven re-checks | 1, 2 | Done |
| 5 | Universal variable substitution for focused re-checks | 2 | — |
| 6 | Constant discovery for trigger filtering | 1 | Done |
| 7 | Deferred constraint scheduling via =sakura:timing= | 4 | — |
| 8 | Coq proofs for polarity soundness and substitution completeness | 1–5 | — |
|