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
|
#+TITLE: Evaluators
* Position
RNT should supply objects, storage, transactions, and execution
lifecycle, but not directly a sublanguage. That duty is then on the
clients to implement one and register it, using the kernel's
capabilities.
In this context, an evaluator is an RNT object, registered and
selected through the object interface, with a special "evaluator"
protocol. Unlike SQL where the boundaries between execution contexts
of the language is confusing and sometmes non-trivial, a sublanguage
=A= may serve retrieval, =B= may serve DML and DDL while =C= may serve
DML alongside =B=. Nothing arbitrates between B and C; making the
client free to opt for one.
The FOL evaluator is therefore, one of such evaluators that is shipped
alongside RNT for specifically Data Retrieval.
* Capability tags
An evaluator object declares a set of opaque labels — =drl=, =dml=,
=ddl=, =vcs=, or anything a client invents. The registry stores and
indexes them so a client can ask which evaluators declare =dml= and
get back B and C.
* Protocols
An evaluator is an object like any other, that can be used to execute
program with a context, exactly as an interpreter takes an expression
and an environment. The plan tree should already bear every value the
program needs, so there is nothing left to bind. Each invocation
carries independent state based atop of a database view of the world
(being therefore hash dependent and isolated).
#+begin_src ocaml
module Make (P : PROGRAM) : sig
class type implementation = object
method invoke :
program:P.t -> Context.t -> (Handle.t, condition) result
end
end
#+end_src
The protocol is parameterized by the evaluator's own plan type, so a
plan is never encoded into values only to be decoded again. Each
application of the functor yields its own protocol constructor, which
is what keeps one evaluator from answering another's programs: a
client obtains the interface through the module of the evaluator it
intends to speak to. The FOL evaluator, which lives under
=lib/evaluators= alongside the evaluators shipped with RNT, currently
understands a single node:
#+begin_src ocaml
type plan = Base of string
#+end_src
=Base= names a relation, which is resolved through the context and
enumerated to exhaustion. The name is resolved rather than an address
being followed, which keeps the evaluator free of storage and makes
every resolution happen under one snapshot.
The returned handle carries a result protocol with =Protocols.Cursor=,
giving it incremental consumption. Since executing a plan node yields
a cursor, operators stack as cursors over cursors, in the manner of a
volcano iterator, and no intermediate result is materialized. A
successor state and a command completion are further result protocols,
added for DML and DDL later. It's important to notice that the kernel
enumerates result protocols; it thererfore does not enumerate
sublanguages. That is not to say however that we cannot have a builtin
catalog relation installed and configured by the client that
enumerates them (that's a concern for the future).
* Context
=Protocols.Context.t= supplies the selected database state, the root
handle through which names are resolved (which assumes authorization,
since we are a capability based system), and the status of the
evaluation. Evaluators reach kernel resources *only* through it.
#+begin_src ocaml
type t =
{ snapshot : Concepts.Hash.hash
; root : Handle.t
; status : unit -> [`Live | `Cancelled | `Exhausted] }
#+end_src
The snapshot is the hash of a commit. Since every object is content
addressed and prolly trees do not depend on insertion order, equal
state yields an equal hash, so a commit hash is a frozen world rather
than a pointer into a changing one. A cursor therefore pins a hash
instead of an open transaction, and every node of a plan reads the
same state, which gives repeatable reads across a whole plan for free
and makes concurrent commits simply invisible to a running
evaluation.
=status= collapses cancellation and resource exhaustion into the
single question an operator asks between tuples, namely whether it may
produce another tuple. The runtime owns how =Exhausted= is decided. A
nested invocation shares the snapshot, so nesting does not need to
resolve a branch tip again, and a successful evaluation and cursor
exhaustion never commit. Commit should be an explicit host operation.
Budgets, authority narrowing, and resource accounting are not
implemented, and adding a fourth status case would break exhaustive
matches in consumers, which is the price of keeping this closed.
* Composition
Evaluators compose through invocation and result protocols alone. A
DML program written in B may invoke a retrieval program written in A,
consume its cursor, and build a successor state from the tuples. A
retrieval program in A may invoke a procedural relation implemented by
C. Neither evaluator however knows the other's representation. The
caller must establish that the callee's contract meets its needs,
obviously.
* Registration
Registration is made by the client, and in the case of builtins,
automatically at initialization, just as =Relation= and =Schematics=,
for example. A foreign registration path is a later concern and is not
designed for here.
Only =Protocols.Cursor= exists as a result protocol (at least for now),
so a successor state, completion, plan persistence, and cross-evaluator
optimizations are absent by design.
|