Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions packages/stack-encrypt/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
copies of the text. An exact term has no `verify`, and none of this
crate's indexes can answer `op::Matches` but with a probabilistic term
(`op::TermFor`).
- **The ordering operators, and `Between` as one term.** `op::Lt`, `Lte`,
`Gt` and `Gte` take one value and `op::Between` a pair of bounds,
`cipher.query(op::Between, &(18, 65)).on(&age_plan)`, inclusive at both
ends. They are answered over the integer types (`op::Ordered`) by the
`Ore` index by default and by `Ope` through `.via(Ope)`; an ordering on
text does not compile. A comparison's query term is the index's own term
for the value (`OreTerm`, `OpeTerm`); a range's is one `sem::RangeQuery`
carrying both bounds' terms, read with `lo()` and `hi()`, so no statement
can pair the low bound of one query with the high bound of another. A
range whose low bound is above its high bound is refused when the query
is awaited, before either term is derived (`TermError::InvertedRange`).
What a query takes is the operator's `op::Arity` (`op::One` or
`op::Pair`), and `op::Input` relates the value given to the plaintext the
plan is checked against (ADR-0009, decision 7).
- **`target_limit` says how many rows a match query should fetch.**
`query.target_limit(want, &stats)` is `want` plus the false positives
expected at the confidence `stats` names (99% by default), as an
Expand Down Expand Up @@ -118,15 +132,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
value is empty, separator-only or shorter than the n-gram length
(ADR-0009).

- **`StackCipher::query_index` is gone**, with `QueryIndexBuilder` and
`QueryIndex`. Every query names an operator now that the orderings have
arrived:
- `query_index(Ore, &v)` over an integer is `query(op::Lt, &v)` (or any
ordering; the term is the same `OreTerm`).
- `query_index(Match, text)` is `query(op::Matches, text)`, which is not
the same call. It returns a `MatchQuery`, not the stored `MatchTerms`;
it refuses text that cuts no token (`TermError::EmptyTermText`); and it
compiles only for a configuration that implements `op::MatchCase`. A
configuration without one needs that one-line impl to be queried.
- `query_index(Ore, text)` and `query_index(Ope, text)` have no
replacement: an ordering over text does not compile until the order of
text is settled (ADR-0009). A text column can still store ORE and OPE
terms.
- **`op::Supports`, `op::Answers` and `op::DefaultIndex` are keyed by the
query's input**, `Supports<Op, In>` with `In: Input<Op::Arity>`, in
place of the plaintext; for every operator but `Between` the input is
the plaintext, so `Supports<Eq, S>` reads as before. `Supports::check`
takes the input and returns `Error`, and `Supports::query` takes the
input's terms (`In::Terms<Self::Term>`: one term, or a pair for a
range). `plan::Resolves::resolve` takes the index and the call's context
and returns the index and the label.
- **The query chain starts with the operator.** `cipher.query(op, v)`
replaces `cipher.query(v)`: `cipher.query(op::Eq, v).on(&plan)` replaces
`cipher.query(v).using(&plan).equality()`, and `on` replaces `using` for
queries only (a write still runs `using` a plan). An index's own term, for
an index no operator answers yet (`Ore`, `Ope`), is
`cipher.query_index(Ore, v).on(&plan)`, which goes when the ordering
operators arrive. `query_index` does not compile for `Equality` or `Match`
(`plan::Unanswered`), since an operator answers both. `QueryUsing` is
gone; the chain's types are `QueryBuilder`, `QueryVia`,
operators arrive. `QueryUsing` is gone; the chain's types are `QueryBuilder`, `QueryVia`,
`QueryIndexBuilder`, `QueryIndex` and `QueryOp`, and the plan shapes
implement `plan::Chooses` and `plan::Resolves`.
- **`Match::default()` is `Match`.** The match index under the default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,30 +141,46 @@ query against a case-folding index is a type error, not an empty result.
### 2. An index declares the operators it supports, and the query term for each

```rust
pub trait Operator: Copy + 'static {}

pub trait Supports<Op: Operator, S>: Index<S> {
type Query: TermFor<Op>; // what this index sends for `Op`
type Verifier; // what the query keeps to check a row
fn check(&self) -> Result<(), PlanError>;
fn verifier(&self, value: &S) -> Self::Verifier;
fn query(verifier: Self::Verifier, term: Self::Term, token: QueryToken) -> Self::Query;
pub trait Operator: Copy + 'static { type Arity: Arity; } // `One` plaintext, or a `Pair`

pub trait Supports<Op: Operator, In: Input<Op::Arity>>: Index<In::Plaintext> {
type Query: TermFor<Op>; // what this index sends for `Op`
type Verifier; // what the query keeps to check a row
fn check(&self, input: &In) -> Result<(), Error>; // a refusal, before any term exists
fn verifier(&self, input: &In) -> Self::Verifier;
fn query(verifier: Self::Verifier, terms: In::Terms<Self::Term>, token: QueryToken) -> Self::Query;
}
```

`S` is the plaintext type the index is over, and `Index<S>::Term` is the
stored term it derives. `Query` is the query term, and `query` makes it from
the term the index derived for the query's value. `QueryToken` can be made
only inside the crate, so only a query chain calls `query`, and no caller can
turn a stored term into a query term by hand. `check` refuses a configuration
that cannot answer `Op` although its type says it can: a match index whose
declared case disagrees with its `downcase` option.
`In::Plaintext` is the plaintext type the index is over, and its
`Index::Term` is the stored term it derives. `Query` is the query term, and
`query` makes it from the verifier and the terms the index derived for the
query's input. `QueryToken` can be made only inside the crate, so only a
query chain calls `query`, and no caller can turn a stored term into a query
term by hand.

`QueryTerm` and the two traits that refine it are defined in decision 4.
`Query` is not `Term`. An ORE index's `Supports<Lt, S>::Query` is the left
ciphertext; its `Term` is left and right. A Bloom index's
`Supports<Matches, S>::Query` is a filter over the needle (`MatchQuery`); its
`Term` is a filter over the value (`MatchTerms`).
ciphertext; its `Term` is left and right. (This crate's ORE is CLLW today,
whose ciphertext has no left half to send alone, so until block ORE lands,
#1140, the query term is the ciphertext itself, as an HMAC's is; `Supports`
is where the left-only term goes when it arrives.) A Bloom index's
`Supports<Matches, S>::Query` is a filter over the query's value
(`MatchQuery`); its `Term` is a filter over the value (`MatchTerms`). There is
no other path from a derived term to the server:
`cipher.query_index(index, v).on(&plan)`, which returned an index's own term,
went with the ordering operators (#1186).

An operator says what a query takes through its arity. `Eq`, the orderings
and `Matches` take the plaintext the index is declared over
(`Supports<Lt, S>`); `Between` takes a pair of them
(`Supports<Between, (S, S)>`); `Input` relates the input to that plaintext,
so the plan is checked against `S` either way, and the chain derives one
term per plaintext under the one label. `check` is where a query is refused
before any term exists: a match index whose declared case disagrees with its
`downcase` option, a `Between` whose bounds are the wrong way round, a match
query with no tokens. `AtPath<Op>` takes a path and `Op`'s input, and gains
its shape with the JSON index.

The bound tightens per operator, through `TermFor<Op>`: any `QueryTerm`
answers `Eq`, but only a `Probabilistic` one answers `Matches`, and only a
Expand Down Expand Up @@ -554,10 +570,14 @@ the ranked loop is worth packaging; it is deferred, not refused.

### 7. `Between` is one term and one operand

`cipher.query(op::Between, (lo, hi))` produces one term carrying both ORE left
ciphertexts, inclusive at both ends, and the client refuses `lo > hi`. One
call derives both bounds, under one context, so a caller cannot pair a bound
from one column with a bound from another.
`cipher.query(op::Between, &(lo, hi)).on(&plan)` produces one term,
`sem::RangeQuery`, carrying the index's term for each bound (the ORE left
ciphertexts, once block ORE lands), inclusive at both ends, and the client
refuses `lo > hi` (`TermError::InvertedRange`) before either term is
derived; `(v, v)` is one value. One call derives both bounds, under one
context, so a caller cannot pair a bound from one column with a bound from
another. The ordering operators are answered over the integer types
(`op::Ordered`), by the ORE index by default and by OPE through `.via(Ope)`.

In SQL the term stays one operand. EQL v4 gains a function that takes the
column and the range, and a range query domain beside each ordered query
Expand Down Expand Up @@ -685,9 +705,13 @@ EQL v4 gains `eql_v4.between(column, range)` and a range query domain per
ordered query domain, ORE and OPE, under the inlining requirement of decision 7. That is
EQL-side work this ADR asks for, not a deferral.

It is a breaking change to the query chain. `QueryUsing::index(..)` goes;
`QueryUsing::equality()` becomes `query(op::Eq, ..)`. The write side
(`encrypt`, targets, plans) does not change.
It is a breaking change to the query chain. `cipher.query(v).using(&plan)`
with `.equality()` becomes `cipher.query(op::Eq, v).on(&plan)`: the operator
first, and `on` for a query where a write says `using`.
`cipher.query_index(index, v).on(&plan)`, the index's own term, is gone:
every index a plan can declare is reachable through an operator, ORE and OPE
through the ordering operators (#1186). The write side (`encrypt`, targets,
plans) does not change.

## Deferred

Expand Down
2 changes: 2 additions & 0 deletions packages/stack-encrypt/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ mod tests {
TermError::Ore(_) => TermError::Ore(cllw_ore::Error),
TermError::InvalidOptions(_) => TermError::InvalidOptions("k out of range"),
TermError::EmptyTermText => TermError::EmptyTermText,
TermError::InvertedRange => TermError::InvertedRange,
TermError::Bytes(_) => TermError::Bytes(TermBytesError::OddMatchTermsLength(3)),
]));
errors.extend(boxed(variants![
Expand Down Expand Up @@ -385,6 +386,7 @@ mod tests {
json!({ "len": 3 }),
),
(Box::new(TermError::EmptyTermText), json!({})),
(Box::new(TermError::InvertedRange), json!({})),
(
Box::new(TermBytesError::MalformedCllwCiphertext(5)),
json!({ "len": 5 }),
Expand Down
Loading
Loading