diff options
Diffstat (limited to 'docs/partial-bindings.org')
| -rw-r--r-- | docs/partial-bindings.org | 331 |
1 files changed, 331 insertions, 0 deletions
diff --git a/docs/partial-bindings.org b/docs/partial-bindings.org new file mode 100644 index 0000000..cf9cf32 --- /dev/null +++ b/docs/partial-bindings.org @@ -0,0 +1,331 @@ +#+TITLE: Partial bindings + +* Purpose +A partial binding gives values to some attributes of a relation and +leaves the rest free. Asking =shelved= for the tuples where +=title = "Codd"=, with the shelf free, is one. Both [[file:denial.org::*Incremental checking][incremental denial +checking]] and [[file:maintenance.org][incremental maintenance]] are built from such questions. + +This document says why answering a partial binding well takes indexes, +where the binding itself belongs, how RNT stores and reads its indexes, +and what is still missing, including builds that do not fit in memory. + +* Why a partial binding needs an index +A [[file:../lib/kernel/SubstantialRelation.ml][substantial relation]] keeps its tuples in a tree keyed by the hash of +each whole tuple. + +- With every attribute bound, the hash can be computed, and the answer + is one path down the tree. That is =contains=, and it costs the depth + of the tree. +- With some attribute free, there is no hash to compute. The hash + scatters tuples uniformly, so the tuples that agree with the binding + sit in arbitrary leaves, and every leaf has to be read. + +Correctness does not need an index. Filtering a scan answers any +binding, at the cost of reading the whole relation, and two questions +show what that costs. + +- An incremental check on the denial of a book shelved twice turns the + insertion of =shelved("Codd", "A")= into =shelved(title="Codd", shelf=_)=. + Without an index, every commit that shelves a book scans all of + =shelved=. +- Asking whether ={title: "Codd"}= belongs to =book= projected on =title= + reads all of =book=, as [[file:modes.org::*Modes of a FOL plan][the modes of a FOL plan]] describe. + +An index holds the same tuples ordered by the value of one attribute. +All the tuples with =a = v= are then contiguous, and answering the +binding means seeking to the first of them and walking until the value +changes. That costs the depth of the tree plus the number of matches. + +* Where a binding belongs +A binding is an equality condition on some attributes, and a condition +on tuples belongs to a query language. RNT does not ask every relation +to answer one. [[file:../lib/evaluators/FOL.ml][FOL]] states it with a term of its own, and a stored +relation offers its indexes to whoever reads it. + +An earlier design had every relation answer a binding-aware read, so +that the kernel could ask a denial in any language for its tuples under +a binding. That buys nothing. The rules that turn a change into +bindings are written operator by operator for each language, as +[[file:maintenance.org::*Deltas][maintenance]] shows, and the kernel cannot derive them for a program it +cannot read. Incremental checking is therefore something an evaluator +offers, and an evaluator that knows its own plans passes a binding +through them without the kernel's help. + +* Indexing every attribute +Every attribute of a substantial relation gets an index. Three reasons +make that the right default rather than a choice per relation. + +- The indexes of a relation are then a function of its tuples and its + heading alone. Their roots sit in the relation's record, and so in + its address, without changing what the address means, since two + relations with the same tuples have the same indexes. That holds + only while the shape of a tree depends on its keys alone, which + [[file:maintenance.org::*Checking it][Merkle does not promise yet]]. +- Each index adds the mode ={a; Finite}=, and a FOL projection keeps it + whenever it keeps =a=. +- A binding over several attributes reads the index of one of them and + filters on the rest, so indexes on combinations are not needed yet. + +RNT has no NULLs, so every tuple appears in every index, and no index +is sparse. + +** Equality only +A mode binds attributes to values. It cannot say that an attribute lies +in a range, and [[file:../lib/concepts/Value.mli][Value]] has only =equal=, with no order, least of all +between strings and integers. An index therefore answers equality +alone. Its key keeps the encoded value rather than a hash of it, so that +ranges stay possible later, given an encoding that sorts in value order +and modes that can state a range. + +** Inside the relation +An index is not an object of its own in the kernel. It is derived from +the tuples, and an index that could be published or removed at a path +could disagree with them, which only access checks could prevent. The +relation keeps its indexes and reads them on request. + +** Selectivity +Modes carry no cost. An index on a boolean attribute declares the same +={a; Finite}= as one on a key, and nothing can prefer the selective one. +A restriction on several attributes reads the index of whichever bound +attribute the relation lists first. Counts of entries kept in the nodes +of each tree would give estimates later. + +* What RNT has +** Walking a range of a tree +[[file:../lib/kernel/Merkle.ml][Merkle]]'s =fold_left= takes an optional start and stop. + +#+begin_src ocaml +val fold_left : + ?from:K.t -> + ?until:(K.t -> bool) -> + S.transaction -> + ('c -> K.t -> address -> 'c) -> + 'c -> + node -> + ('c, Concepts.Condition.condition) result +#+end_src + +The walk starts at the first key not below =from= and stops before the +first key =until= accepts. In a trunk it descends only into the child +that holds =from=, using the same rule as =lookup=, and once it stops it +fetches no further children. Without either, it is the whole walk it +always was. + +** Index trees +The index on an attribute =a= is a tree whose key is the encoded value +of =a= followed by the raw hash of the tuple, and whose value is that +hash. + +#+begin_example +book(title, author), index on title + +ts4:Codd ++ hash(t₁) → hash(t₁) +ts4:Codd ++ hash(t₂) → hash(t₂) +ts4:Date ++ hash(t₃) → hash(t₃) +#+end_example + +- The value is encoded as a tuple encodes its values, with + =Tuple.bencode_of_value=. Bencode is injective and no encoding is a + prefix of another, so the keys of one value are contiguous, and the + index is a =Merkle.Make= over =StringKey=, which compares strings byte + by byte. +- The hash of a tuple is the address under which the tuple set stored + it, so an index entry points at the stored tuple, and no tuple is + stored twice. +- The record of a substantial relation holds one root per attribute + beside the tuple set. + + #+begin_src ocaml + type t = + { schematics: Concepts.Hash.hash; + tuples: TupleSet.address; + indexes: Index.address BatMap.String.t } + #+end_src + +- =store= hashes each tuple once, builds the tuple set, and then one + index per attribute of the heading. A tuple with no value for some + attribute is refused with =missing-attribute=. +- =instantiate= stores no tuples under the heading its schema + describes, so an empty relation has empty indexes. +- =modes= adds ={a; Finite}= for each index the record holds. + +A relation stored before indexes has no =indexes= field and does not +load, as [[file:denial.org::*Multigroups from before denials][multigroups from before denials]] do not. + +** Reading an index +A substantial relation offers the [[file:../lib/protocols/Indexed.mli][Indexed]] protocol. + +#+begin_src ocaml +class type implementation = object + method indexes : string list + method lookup : string -> Concepts.Value.value -> (Handle.t, Concepts.Condition.condition) result +end +#+end_src + +=lookup a v= returns a cursor over the tuples whose =a= is =v=. It walks +the index on =a= from the encoded =v= until a key no longer starts with +it, and reads each tuple at the address its entry holds. An attribute +with no index is refused with =unindexed-attribute=. =indexes= names the +attributes that have one, so that a reader can choose without asking. + +** Restriction in FOL +FOL has a term that restricts a plan to the tuples agreeing with a +binding, named after the =WHERE= of Tutorial D. + +#+begin_src ocaml +type binding = Concepts.Value.value BatMap.String.t + +type 'r term = + | Base of 'r + | Project of 'r term * string BatFingerTree.t + | Where of 'r term * binding +#+end_src + +- A binding may name only attributes of the heading it restricts, and + =describe= refuses any other with =unknown-attribute=. +- =instantiate= pushes each =Where= below a =Project= that keeps every + attribute it binds, once stored programs are expanded. A restriction + asked of a stored program therefore reaches the stored relations the + program reads. A =Where= that binds an attribute the projection drops + stays where it is and is refused. +- A =Where= over a =Base= reads the base relation itself. If it offers an + index on a bound attribute, the restriction looks that value up. + Otherwise it enumerates. Either way it keeps only the tuples that + agree with the whole binding. +- Its membership test checks the binding, then asks its input. +- Through an index, its modes are those of the base with the bound + attributes taken out of each =bound=, by =Mode.given=, since the binding + supplies their values and the index produces the tuples. ={a; Finite}= + becomes ={∅; Finite}=. Otherwise it keeps the modes of its input, + since it can only filter what that input enumerates. + +A denial written as =Where (Base book, {title: "Codd"})= is therefore +exhaustible with nothing bound, and judging it reads only the index +entries for ="Codd"=. + +* Memory +** Reading +An index is a tree in storage like the tuple set, and LMDB maps it in +as it is read. A lookup holds one path and the leaf it is in, a few +nodes of some kilobytes each, whatever the size of the index. A value +that matches half the relation streams through the cursor rather than +being collected. Its cost is a matter of [[*Selectivity][selectivity]], not of memory. + +** Building today +=store= receives its tuples as a list, so the relation is in memory +before any index is built. =TupleSet.of_list= and =Index.of_list= then +insert one entry at a time inside =with_batch=. Each insert persists the +whole path to its leaf, and the batch keeps every version of every node +until it flushes, so a build holds about =N × depth= nodes of some 64 +entries each rather than =N= entries. The indexes are built one after +another, so the peak is the tuple list, the entries of one index, and +one batch. + +** Steady state +With [[file:roadmap.org::*2. Tuple deltas][tuple deltas]], a commit would insert and remove its changed tuples +in the tuple set and in each index, touching about the size of the +change times the depth of the tree in each. Nothing would be rebuilt. + +** Builds that do not fit +Some builds start from nothing: an import, a relation materialized for +the first time, a relation stored before indexes, or a new kind of index +added later. Three things in them outgrow memory, and RNT handles none of +them yet. + +| What grows | Why | What to do | +|-------------------------------+-------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------| +| The input to sort | entries arrive in the order of the tuple hash, and each index needs them in the order of its own key | sort externally: write sorted runs to scratch storage and merge them into the builder; a scratch LMDB database can do the sort at first | +| The builder | building by repeated =insert= copies a path on each insert and keeps every version | build bottom-up from the sorted stream, holding one node per level | +| The batch and the transaction | =with_batch= keeps every node it makes until the end, and one transaction holds every page it writes | write nodes directly, in short transactions of a bounded number of entries | + +Writing outside the commit's own transaction is safe because nodes are +content addressed. Nothing reaches a node until the head moves to a +state that holds it, and the head moves atomically. A build that fails +leaves nodes nothing reaches, which a garbage collector has to reclaim. +A publish that fails already leaves such nodes today, since each level +of a commit writes in its own transaction. + +A bottom-up build must make the same tree that inserting the same keys +would, or the address of a relation would depend on how it was built. + +** What storing a relation reads +=Multigroup= stores a relation by reading it whole, and that read is +easy to misjudge. + +- A relation with an address never reaches the read. Its admission + stores it as that address. +- A program is stored as its source, and is not read either. +- What is read is a relation with neither: a literal, whose tuples are + already in memory, or a lazy relation, such as the result of a query, + read for the first time. + +The large read is the caller's. [[file:../lib/kernel/Scope.mli][Scope.publish]] binds whole relations and +FOL has no union, so adding one tuple to a stored relation means +reading all of it into a literal first. + +** Delta relations +A delta relation would be a stored relation, its base, together with +the tuples to insert, =I=, and the tuples to delete, =D=. + +#+begin_example +t ∈ delta ⇔ t ∈ I ∨ (t ∈ base ∧ t ∉ D) +#+end_example + +Its membership is decided by lookups, without reading the extension. +Admission would store it by applying =I= and =D= to the tree of the base +and to each of its indexes, reading only the change, and =publish= would +take one in place of a whole relation. + +* Reuse for maintenance and checking +Views in RNT are [[file:../lib/kernel/EphemeralRelation.ml][ephemeral relations]], and [[file:maintenance.org][incremental maintenance]] +would materialize them. Each piece above is one that maintenance and +incremental checking need. + +- Delta rules are restrictions. =ΔR⁺ ⋈ S= asks =S= for the tuples that + agree with each inserted tuple of =R= on the join attributes, which is + a =Where= on =S=, answered from its index. +- A denial check for an inserted tuple is the denial restricted by the + binding that tuple implies, stopped at its first tuple. For a FOL + denial, the restriction passes through its plan to the stored + relations it reads. +- Applying a change to a materialized view would be admitting a delta + relation, with =ΔV⁺= and =ΔV⁻= as =I= and =D=. +- A materialized view stored as a substantial relation has indexes of + its own, maintained by the same change, so a view over a view stays + incremental. +- Deleting through a projection asks how many tuples of the input + produce a tuple of the view. That binds every attribute of the view, + so it is a lookup by hash. +- With the index roots in the address, comparing a maintained view + with the same view recomputed from scratch, as [[file:maintenance.org::*Checking it][checking it]] proposes, + tests the maintenance of its indexes too. + +A view that is not materialized needs none of this. A restriction +passes through its plan, and it pays for the computation on each read. + +* What is missing +1. Read sets, so that RNT knows which denials a commit can break and + how an inserted tuple binds each of them. +2. Tuple deltas, and delta relations to carry them in a commit. +3. A check offered by the evaluator: given the tuples a commit inserted + into the relations a denial reads, restrict the denial by each and + read its first tuple. +4. Joins in FOL. Most rules worth checking incrementally join two + relations, and a restriction through a join binds both sides on the + join attributes. +5. Builds that do not fit in memory, once there is a large or lazy + source to build from. + +* Open questions +- Where a materialized view keeps its counts. If a count lives in the + record a tuple set points to, a change of count changes that address. + Index entries should point to the tuple itself, so that a count can + change without touching any index. +- When indexes on combinations of attributes earn their place. +- What collects the nodes that failed builds and failed publishes leave + behind. +- A tuple's =type_= is part of its hash and not of its heading. Two + tuples that differ only in =type_= agree with every binding, so + ={all; Bounded 1}= holds only while the tuples of a relation share + one =type_=. |
