aboutsummaryrefslogtreecommitdiffstats
path: root/docs/denial.org
blob: d79a39476060762c96073b1473b8b0e8c95e30bb (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
#+TITLE: Denials

* Purpose
A denial is a relation that must be empty. Each tuple it holds is one
breach of a rule, so a state where every denial is empty is a state
where every rule holds. RNT knows no rule language. It knows only that
denials must be empty, and it refuses to move a branch head to a state
where one is not. Constraints across relations, the assertions SQL
names but rarely implements, become relations like any other.

The idea is the denial form of integrity constraints, from Sadri and
Kowalski ("A theorem-proving approach to database integrity", 1988),
where a rule is written as a query for its own counterexamples.

* Where denials live
Each multigroup holds its denials beside its schemas.

#+begin_example
\system\branch\master\multigroup\library\schema\default\relation\book
\system\branch\master\multigroup\library\denial\no-books
#+end_example

A multigroup stores a denial the way a schema stores a relation. An
[[file:../lib/kernel/EphemeralRelation.ml][ephemeral relation]] that wraps a [[file:../lib/kernel/Program.ml][program]] becomes the program's
evaluator name and source, and any other relation becomes its tuples. A
program is the usual case, because a rule has to be computed against
the state it judges. When a branch reads the denial back, the
[[file:evaluators.org][evaluator]] registered under that name parses the source, and reading
the denial invokes the program =within= the branch state it was read
through. The same denial therefore reads one state through the head
and another through =previous=.

* Judging a state
[[file:../lib/protocols/Admission.mli][Admission]] is the protocol that judges a state.

#+begin_src ocaml
type violations = (string * Concepts.Tuple.t) list

class type implementation = object
  method check : (violations, Concepts.Condition.condition) result
end
#+end_src

=check= names every denial that holds a tuple, with one such tuple as
its witness. An empty list means the state is admitted. An error means
the state could not be judged, and an unjudged state is not admitted
either.

A multigroup judges each of its denials in two steps.

1. It asks the denial for its [[file:modes.org][modes]]. If the denial is not
   exhaustible with nothing bound, the multigroup fails with
   =unjudgeable-denial= and reads nothing.
2. Otherwise it reads the denial up to its first tuple. No tuple means
   the denial holds. A first tuple is the witness, and the multigroup
   reads no further.

For a denial that wraps a program, the multigroup invokes the program
once, within the branch state it judges, and asks both questions of the
relation the program returns.

A branch state asks each of its multigroups and names every violation
by its path, =\multigroup\[m]\denial\[d]=. Judging belongs to the
multigroup, the universe its relations describe. The branch only
versions states and collects the answers. A rule may still read
relations of any multigroup, because the branch asks all of them.

One witness is enough to refuse a state, and asking for more would cost
a read of the whole denial, which never ends for a =Countable= one.

* Trust
The guard rests on what a denial declares. A FOL denial over stored
relations derives its modes from theirs, so its claim to be finite
holds. A denial written in another language declares whatever its
evaluator returns. A program that declares =Finite= and then never ends
still hangs the check, and RNT cannot tell. The same holds when FOL
reads a relation from another evaluator, since FOL takes that
relation's modes as given. A cycle of programs across languages also
recurses without end.

* Moving a head
=BranchManager= asks the proposed state for its admission before it
moves a head. If the state is not admitted, it refuses the move with a
=refused= condition that names each denial and its witness, and the
head stays where it was. It asks before taking the head, since a denial
may run a program that reads storage.

#+begin_src plantuml :file ../images/denial-admission.svg
@startuml
participant "Scope.publish" as Publish
participant BranchManager
participant "proposed\nbranch state" as Branch
participant Multigroup
participant Denial

Publish -> BranchManager : move the head
BranchManager -> Branch : check
loop each multigroup
  Branch -> Multigroup : check
  loop each denial
    Multigroup -> Denial : modes
    alt not exhaustible with nothing bound
      Multigroup --> BranchManager : unjudgeable-denial
    else exhaustible
      Multigroup -> Denial : enumerate, read the first tuple
      Denial --> Multigroup : a witness, or nothing
    end
  end
  Multigroup --> Branch : violations
end
Branch --> BranchManager : violations, named by path
alt none
  BranchManager -> BranchManager : move the head
else some
  BranchManager --> Publish : refused, one witness per denial
end
@enduml
#+end_src

#+RESULTS:
[[file:../images/denial-admission.svg]]

The check judges the state itself, not a change to it. Every commit
lands only if the whole state it makes is admitted, whichever subsystem
made it, so a rule written in one language guards data written from
all of them.

* Publishing denials
[[file:../lib/kernel/Scope.mli][Scope.publish]] binds denials with its relations, in the same state.

#+begin_src ocaml
Scope.publish root ~branch:"master" ~home:library
  ~denials:["no-books", Some no_books; "audit:shelved-twice", Some twice]
  ["book", Some books]
#+end_src

A name =d= binds in the home multigroup, and =m:d= binds in multigroup
=m=. A commit can install a rule and the data it guards together, and
the rule is judged with the data. If the state it makes already breaks
the rule, the commit is refused. A denial that cannot be judged cannot
be installed either, since the state that holds it cannot be admitted.

* Judging a state without landing it
A subsystem that wants to know whether a change would be admitted builds
the state with [[file:../lib/kernel/Path.mli][Path.assoc_all]] and asks it.

#+begin_src ocaml
let* next = Path.assoc_all head [Scope.relations library, "book", Some books] in
let* admission = Protocols.Admission.require next in
Protocols.Admission.check admission
#+end_src

Nothing lands. A host can use this to preview a form, a move or an
import, and show which rule would refuse it.

* Open problems
** Incremental checking
Every commit judges every denial of every multigroup over the whole
state. Decker ("Translating Advanced Integrity Checking Technology to
SQL", 2002) shows how to check only what a change can break. Take the
denial of a book shelved twice.

#+begin_example
{(title, s1, s2) | shelved(title, s1) ∧ shelved(title, s2) ∧ s1 ≠ s2}
#+end_example

A commit that inserts =shelved("Codd", "A")= can only add violations
where =title = "Codd"= and one shelf is ="A"=. Asking the denial for
those tuples, with the other shelf free, is the whole incremental check.
It is the insertion-only case of [[file:maintenance.org][incremental maintenance]], which covers
the delta rules it reuses.

RNT cannot ask that today. The question binds some attributes of the
denial and leaves others free, and no operation produces the tuples of
a relation under a partial binding. This is the gap that [[file:modes.org::*Reachable bindings][reachable
bindings]] describes. Closing it takes three pieces.

1. A binding-aware read, which every relation implements and FOL passes
   through its plans.
2. Indexes, so that a substantial relation answers such a read by
   lookup rather than by filtering a scan.
3. Read sets, so that RNT knows which relations a denial reads and how
   their attributes line up with the denial's, and can turn an inserted
   tuple into bindings on each denial it touches.

** Denials do not protect themselves
A commit that removes a denial and breaks its rule is judged on the
state it makes, where the denial is gone, so it lands. Guarding
=\denial= needs the access checks the [[file:../README.org][README]] plans to keep apart
from lookup.

** Multigroups from before denials
A multigroup stored before denials existed has no =denials= field and
does not load.