vitaminc_ore/order.rs
1//! One order term per scalar, encoded as the index's declared type.
2//!
3//! An order index declares the type of what it holds: an `i32` column,
4//! say, as [`OrderTarget::I32`]. [`order`] turns a physical value and its
5//! optional logical type into the sealed [`OrderPlaintext`] a scheme
6//! encrypts, encoded as that target: bytes whose lexicographic order is
7//! the value's order, and the frozen domain label of the target. The
8//! target, not the value, decides the domain and the width, so values
9//! from every host compare in one space. A JavaScript `BigInt` arrives as
10//! the narrowest integer that holds it, and a Rust field at its own type;
11//! both widen to the target, and a value outside it is refused.
12//!
13//! Where an equality term folds every integer width into one encoding, an
14//! order term keeps the declared width, so block ORE over an `i32` column
15//! costs four blocks, not seventeen. A float, a date, a timestamp, a
16//! decimal and a uuid order by the same bytes they are matched by, so
17//! those come from `vitaminc_normalize` unchanged. Bytes are the equality
18//! encoding behind [`VARIABLE_WIDTH_MARKER`], and text folds further: an
19//! order term drops accents and case, which equality must not. See the
20//! crate docs for the table.
21
22use vitaminc_aead::{Encrypt, Int, Logical, Physical};
23use vitaminc_normalize::{terms_by, Normalize, NormalizeError, Term, TermsError};
24use vitaminc_protected::{Controlled, Protected};
25
26use crate::{fold, OrderPlaintext};
27
28/// The byte every text and bytes order encoding starts with, so that no
29/// order plaintext is ever empty.
30///
31/// An empty string and an empty byte string are values with an order,
32/// and without the marker they encoded to nothing. Both scheme crates
33/// then encrypted them into a term that never touched the key or the
34/// salt: the same term under every key and context, which anyone could
35/// write into any index. A constant leading byte keeps the order of the
36/// encodings, and a fixed-width encoding is never empty, so it goes in
37/// unchanged. The scheme crates added this byte themselves before it
38/// moved here; see the README's Encodings section for why the domain
39/// labels did not change with it.
40pub const VARIABLE_WIDTH_MARKER: u8 = 0x01;
41
42/// Frozen domain labels for order terms, one per [`OrderTarget`].
43pub mod domain {
44 // Retired in 0.7, never to be reused with another meaning:
45 // `vitaminc/ore/value/int8/v1` and `vitaminc/ore/value/uint8/v1`, the
46 // one-byte targets 0.6 published. See `OrderTarget` for why.
47
48 /// An `i16`, two bytes. The narrowest integer target: see
49 /// [`OrderTarget`](super::OrderTarget) for why there is no 8-bit one.
50 pub const INT16: &str = "vitaminc/ore/value/int16/v1";
51 /// A `u16`, two bytes.
52 pub const UINT16: &str = "vitaminc/ore/value/uint16/v1";
53 /// An `i32`, four bytes.
54 pub const INT32: &str = "vitaminc/ore/value/int32/v1";
55 /// A `u32`, four bytes.
56 pub const UINT32: &str = "vitaminc/ore/value/uint32/v1";
57 /// An `i64`, eight bytes.
58 pub const INT64: &str = "vitaminc/ore/value/int64/v1";
59 /// A `u64`, eight bytes.
60 pub const UINT64: &str = "vitaminc/ore/value/uint64/v1";
61 /// An `i128`, sixteen bytes.
62 pub const INT128: &str = "vitaminc/ore/value/int128/v1";
63 /// A `u128`, sixteen bytes.
64 pub const UINT128: &str = "vitaminc/ore/value/uint128/v1";
65 /// An `f64`, eight bytes, zero signs and NaN payloads folded.
66 pub const FLOAT64: &str = "vitaminc/ore/value/float64/v1";
67 /// A date, four bytes, over its day count.
68 pub const DATE: &str = "vitaminc/ore/value/date/v1";
69 /// A timestamp, twelve bytes, truncated to microseconds.
70 pub const TIMESTAMP: &str = "vitaminc/ore/value/timestamp-micros/v1";
71 /// A decimal, fourteen bytes, scale and signed zero normalized.
72 pub const DECIMAL: &str = "vitaminc/ore/value/decimal/v1";
73 /// Text: [`VARIABLE_WIDTH_MARKER`](super::VARIABLE_WIDTH_MARKER), then
74 /// Unicode 16 NFC with accents and case folded.
75 pub const TEXT: &str = "vitaminc/ore/value/text-folded/unicode-16/v1";
76 /// Bytes: [`VARIABLE_WIDTH_MARKER`](super::VARIABLE_WIDTH_MARKER), then
77 /// the bytes as they are.
78 pub const BYTES: &str = "vitaminc/ore/value/bytes/v1";
79 /// A uuid, over its sixteen raw bytes.
80 pub const UUID: &str = "vitaminc/ore/value/uuid/v1";
81
82 // A space that places nulls is a space of its own, with a label of its
83 // own: it admits other values than the plain space and gives a meaning
84 // to a code the plain space leaves unused. One pair per fixed-width
85 // target; text and bytes take no null policy.
86
87 /// An `i16` in a space that places nulls first: see
88 /// [`NullOrder::First`](super::NullOrder::First).
89 pub const INT16_NULLS_FIRST: &str = "vitaminc/ore/value/int16/nulls-first/v1";
90 /// An `i16` in a space that places nulls last: see
91 /// [`NullOrder::Last`](super::NullOrder::Last).
92 pub const INT16_NULLS_LAST: &str = "vitaminc/ore/value/int16/nulls-last/v1";
93 /// A `u16` in a space that places nulls first: see
94 /// [`NullOrder::First`](super::NullOrder::First).
95 pub const UINT16_NULLS_FIRST: &str = "vitaminc/ore/value/uint16/nulls-first/v1";
96 /// A `u16` in a space that places nulls last: see
97 /// [`NullOrder::Last`](super::NullOrder::Last).
98 pub const UINT16_NULLS_LAST: &str = "vitaminc/ore/value/uint16/nulls-last/v1";
99 /// An `i32` in a space that places nulls first: see
100 /// [`NullOrder::First`](super::NullOrder::First).
101 pub const INT32_NULLS_FIRST: &str = "vitaminc/ore/value/int32/nulls-first/v1";
102 /// An `i32` in a space that places nulls last: see
103 /// [`NullOrder::Last`](super::NullOrder::Last).
104 pub const INT32_NULLS_LAST: &str = "vitaminc/ore/value/int32/nulls-last/v1";
105 /// A `u32` in a space that places nulls first: see
106 /// [`NullOrder::First`](super::NullOrder::First).
107 pub const UINT32_NULLS_FIRST: &str = "vitaminc/ore/value/uint32/nulls-first/v1";
108 /// A `u32` in a space that places nulls last: see
109 /// [`NullOrder::Last`](super::NullOrder::Last).
110 pub const UINT32_NULLS_LAST: &str = "vitaminc/ore/value/uint32/nulls-last/v1";
111 /// An `i64` in a space that places nulls first: see
112 /// [`NullOrder::First`](super::NullOrder::First).
113 pub const INT64_NULLS_FIRST: &str = "vitaminc/ore/value/int64/nulls-first/v1";
114 /// An `i64` in a space that places nulls last: see
115 /// [`NullOrder::Last`](super::NullOrder::Last).
116 pub const INT64_NULLS_LAST: &str = "vitaminc/ore/value/int64/nulls-last/v1";
117 /// A `u64` in a space that places nulls first: see
118 /// [`NullOrder::First`](super::NullOrder::First).
119 pub const UINT64_NULLS_FIRST: &str = "vitaminc/ore/value/uint64/nulls-first/v1";
120 /// A `u64` in a space that places nulls last: see
121 /// [`NullOrder::Last`](super::NullOrder::Last).
122 pub const UINT64_NULLS_LAST: &str = "vitaminc/ore/value/uint64/nulls-last/v1";
123 /// An `i128` in a space that places nulls first: see
124 /// [`NullOrder::First`](super::NullOrder::First).
125 pub const INT128_NULLS_FIRST: &str = "vitaminc/ore/value/int128/nulls-first/v1";
126 /// An `i128` in a space that places nulls last: see
127 /// [`NullOrder::Last`](super::NullOrder::Last).
128 pub const INT128_NULLS_LAST: &str = "vitaminc/ore/value/int128/nulls-last/v1";
129 /// A `u128` in a space that places nulls first: see
130 /// [`NullOrder::First`](super::NullOrder::First).
131 pub const UINT128_NULLS_FIRST: &str = "vitaminc/ore/value/uint128/nulls-first/v1";
132 /// A `u128` in a space that places nulls last: see
133 /// [`NullOrder::Last`](super::NullOrder::Last).
134 pub const UINT128_NULLS_LAST: &str = "vitaminc/ore/value/uint128/nulls-last/v1";
135 /// An `f64` in a space that places nulls first: see
136 /// [`NullOrder::First`](super::NullOrder::First).
137 pub const FLOAT64_NULLS_FIRST: &str = "vitaminc/ore/value/float64/nulls-first/v1";
138 /// An `f64` in a space that places nulls last: see
139 /// [`NullOrder::Last`](super::NullOrder::Last).
140 pub const FLOAT64_NULLS_LAST: &str = "vitaminc/ore/value/float64/nulls-last/v1";
141 /// A date in a space that places nulls first: see
142 /// [`NullOrder::First`](super::NullOrder::First).
143 pub const DATE_NULLS_FIRST: &str = "vitaminc/ore/value/date/nulls-first/v1";
144 /// A date in a space that places nulls last: see
145 /// [`NullOrder::Last`](super::NullOrder::Last).
146 pub const DATE_NULLS_LAST: &str = "vitaminc/ore/value/date/nulls-last/v1";
147 /// A timestamp in a space that places nulls first: see
148 /// [`NullOrder::First`](super::NullOrder::First).
149 pub const TIMESTAMP_NULLS_FIRST: &str = "vitaminc/ore/value/timestamp-micros/nulls-first/v1";
150 /// A timestamp in a space that places nulls last: see
151 /// [`NullOrder::Last`](super::NullOrder::Last).
152 pub const TIMESTAMP_NULLS_LAST: &str = "vitaminc/ore/value/timestamp-micros/nulls-last/v1";
153 /// A decimal in a space that places nulls first: see
154 /// [`NullOrder::First`](super::NullOrder::First).
155 pub const DECIMAL_NULLS_FIRST: &str = "vitaminc/ore/value/decimal/nulls-first/v1";
156 /// A decimal in a space that places nulls last: see
157 /// [`NullOrder::Last`](super::NullOrder::Last).
158 pub const DECIMAL_NULLS_LAST: &str = "vitaminc/ore/value/decimal/nulls-last/v1";
159 /// A uuid in a space that places nulls first: see
160 /// [`NullOrder::First`](super::NullOrder::First).
161 pub const UUID_NULLS_FIRST: &str = "vitaminc/ore/value/uuid/nulls-first/v1";
162 /// A uuid in a space that places nulls last: see
163 /// [`NullOrder::Last`](super::NullOrder::Last).
164 pub const UUID_NULLS_LAST: &str = "vitaminc/ore/value/uuid/nulls-last/v1";
165
166 /// Every domain, for a test or an index layer that enumerates them: the
167 /// fifteen plain labels in [`OrderTarget::ALL`](super::OrderTarget::ALL)
168 /// order, then the thirteen nulls-first labels and the thirteen
169 /// nulls-last labels, each in the same order. A slice, so a domain
170 /// added later does not change its type.
171 pub const ALL: &[&str] = &[
172 INT16,
173 UINT16,
174 INT32,
175 UINT32,
176 INT64,
177 UINT64,
178 INT128,
179 UINT128,
180 FLOAT64,
181 DATE,
182 TIMESTAMP,
183 DECIMAL,
184 TEXT,
185 BYTES,
186 UUID,
187 INT16_NULLS_FIRST,
188 UINT16_NULLS_FIRST,
189 INT32_NULLS_FIRST,
190 UINT32_NULLS_FIRST,
191 INT64_NULLS_FIRST,
192 UINT64_NULLS_FIRST,
193 INT128_NULLS_FIRST,
194 UINT128_NULLS_FIRST,
195 FLOAT64_NULLS_FIRST,
196 DATE_NULLS_FIRST,
197 TIMESTAMP_NULLS_FIRST,
198 DECIMAL_NULLS_FIRST,
199 UUID_NULLS_FIRST,
200 INT16_NULLS_LAST,
201 UINT16_NULLS_LAST,
202 INT32_NULLS_LAST,
203 UINT32_NULLS_LAST,
204 INT64_NULLS_LAST,
205 UINT64_NULLS_LAST,
206 INT128_NULLS_LAST,
207 UINT128_NULLS_LAST,
208 FLOAT64_NULLS_LAST,
209 DATE_NULLS_LAST,
210 TIMESTAMP_NULLS_LAST,
211 DECIMAL_NULLS_LAST,
212 UUID_NULLS_LAST,
213 ];
214}
215
216/// Where the nulls of an order space sort: below every value or above
217/// every value, as SQL's `NULLS FIRST` and `NULLS LAST`.
218///
219/// A space takes a policy when it is made, like its target and context,
220/// and every term of it agrees. This is the policy as a value, for a
221/// target chosen while the program runs:
222/// `scheme.target(OrderTarget::I32).nulls(NullOrder::Last)`. On a marker
223/// target the policy is a marker too, [`First`](crate::target::First) or
224/// [`Last`](crate::target::Last), each standing for one of these through
225/// [`NullPolicy`](crate::target::NullPolicy):
226/// `scheme.target(I32).nulls(Last)`. Without one, a null has no order term, as before. With one, every null
227/// in the space, typed or plain, encodes to the space's sentinel: the
228/// lowest code the target's width holds (all `0x00`) for
229/// [`First`](Self::First), the highest (all `0xFF`) for
230/// [`Last`](Self::Last). Only a fixed-width target takes a policy.
231///
232/// For an integer or uuid target the policy costs one value: the target's
233/// maximum, whose code the nulls-last sentinel already is, is refused with
234/// [`NormalizeError::Reserved`]. Under nulls-first every code moves up by
235/// one, so zero, the minimum and the nil uuid stay encodable and the
236/// maximum is again the one refused. A column that places nulls cannot
237/// hold its type's maximum. Float, date, timestamp and decimal codes never
238/// reach either extreme, so those targets shift nothing and refuse
239/// nothing.
240///
241/// Each policy is its own set of domain labels, and the salt binds the
242/// label, so a term of a space that places nulls never compares with a
243/// term of a plain space of the same target.
244///
245/// # What a placed null reveals
246///
247/// In any order index whose stored terms compare with each other, the
248/// nulls are visible as the tied group at one end, and in a sparse domain
249/// as the group that differs from every value at the top of the encoding.
250/// That is never more than the SQL `NULL` it replaces, which needs no
251/// inference to read.
252///
253/// # Examples
254///
255/// ```
256/// use vitaminc_ore::{domain, order_with_nulls, Int, NormalizeError, NullOrder, OrderTarget, Physical};
257/// use vitaminc_protected::Controlled;
258///
259/// let null = order_with_nulls(OrderTarget::U16, NullOrder::First, None, Physical::Null)?;
260/// assert_eq!(null.domain(), domain::UINT16_NULLS_FIRST);
261/// assert_eq!(null.bytes().risky_ref(), &[0x00, 0x00]);
262///
263/// // Zero moves up by one, so it stays above the null.
264/// let zero = order_with_nulls(OrderTarget::U16, NullOrder::First, None, Physical::Int(Int::U16(0)))?;
265/// assert_eq!(zero.bytes().risky_ref(), &[0x00, 0x01]);
266///
267/// // The maximum is the one value refused.
268/// let max = Physical::Int(Int::U16(u16::MAX));
269/// assert_eq!(
270/// order_with_nulls(OrderTarget::U16, NullOrder::First, None, max).err(),
271/// Some(NormalizeError::Reserved)
272/// );
273/// # Ok::<(), NormalizeError>(())
274/// ```
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
276#[non_exhaustive]
277pub enum NullOrder {
278 /// Nulls sort below every value, as `NULLS FIRST`.
279 First,
280 /// Nulls sort above every value, as `NULLS LAST`.
281 Last,
282}
283
284/// The type an order index declares, which every value it holds is
285/// encoded as. A layer above maps its own column types onto these: an
286/// EQL `int` column is [`OrderTarget::I32`], whatever language the value
287/// comes from.
288///
289/// An integer of any width is accepted by an integer target it fits,
290/// and refused with [`NormalizeError::OutOfRange`] otherwise. An `f32` is
291/// widened exactly to [`OrderTarget::F64`]. Every other target takes only
292/// its own kind; anything else is [`NormalizeError::Mismatch`]. An
293/// integer is not a float, a date or a decimal, so none of those take
294/// one.
295///
296/// # What has no target
297///
298/// An order term reveals the order of the values it encrypts. That is
299/// the point of the scheme, and it is safe only when the set of possible
300/// values is large enough that knowing the order of the rows does not
301/// give the values away. A boolean has two values, so an order term over
302/// a boolean column says which rows are `true`: [`order`] refuses one
303/// with [`NormalizeError::NoDomain`]. An 8-bit integer has 256, so an
304/// order index over such a column is a frequency table of the column with
305/// the order on top, and anyone who knows a handful of rows' values, or
306/// the rough distribution of the data (ages, quantities, ratings), reads
307/// the rest off. So the narrowest integer target is sixteen bits. An
308/// `i8` or `u8` **value** still goes into any wider target it fits; what
309/// there is not is a one-byte space.
310///
311/// The floor removes only the case where the *type* guarantees few
312/// values. An `I16` or `I32` column that holds ages or ratings leaks the
313/// same frequency table, since the leak is a property of how many
314/// distinct values the column holds, not of the width. Whether a column
315/// should have an order index at all is a question about its data; a
316/// column of few distinct values wants an equality index.
317#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
318#[non_exhaustive]
319pub enum OrderTarget {
320 /// An `i16`. The narrowest integer target; see above for why.
321 I16,
322 /// A `u16`.
323 U16,
324 /// An `i32`.
325 I32,
326 /// A `u32`.
327 U32,
328 /// An `i64`.
329 I64,
330 /// A `u64`.
331 U64,
332 /// An `i128`.
333 I128,
334 /// A `u128`.
335 U128,
336 /// An `f64`; an `f32` widens to it exactly.
337 F64,
338 /// A [`Logical::Date`].
339 Date,
340 /// A [`Logical::Timestamp`] in any unit, truncated to microseconds.
341 Timestamp,
342 /// A [`Logical::Decimal`] at any scale.
343 Decimal,
344 /// Text.
345 Text,
346 /// Bytes.
347 Bytes,
348 /// A [`Logical::Uuid`].
349 Uuid,
350}
351
352impl OrderTarget {
353 /// Every target, for a test or an index layer that enumerates them.
354 pub const ALL: &[OrderTarget] = &[
355 Self::I16,
356 Self::U16,
357 Self::I32,
358 Self::U32,
359 Self::I64,
360 Self::U64,
361 Self::I128,
362 Self::U128,
363 Self::F64,
364 Self::Date,
365 Self::Timestamp,
366 Self::Decimal,
367 Self::Text,
368 Self::Bytes,
369 Self::Uuid,
370 ];
371
372 /// The frozen domain label of every term of this target.
373 pub fn domain(self) -> &'static str {
374 match self {
375 Self::I16 => domain::INT16,
376 Self::U16 => domain::UINT16,
377 Self::I32 => domain::INT32,
378 Self::U32 => domain::UINT32,
379 Self::I64 => domain::INT64,
380 Self::U64 => domain::UINT64,
381 Self::I128 => domain::INT128,
382 Self::U128 => domain::UINT128,
383 Self::F64 => domain::FLOAT64,
384 Self::Date => domain::DATE,
385 Self::Timestamp => domain::TIMESTAMP,
386 Self::Decimal => domain::DECIMAL,
387 Self::Text => domain::TEXT,
388 Self::Bytes => domain::BYTES,
389 Self::Uuid => domain::UUID,
390 }
391 }
392
393 /// The frozen domain label of every term of this target in a space
394 /// that places nulls as `nulls` says, or `None` for text and bytes,
395 /// which take no null policy.
396 ///
397 /// ```
398 /// use vitaminc_ore::{domain, NullOrder, OrderTarget};
399 ///
400 /// assert_eq!(
401 /// OrderTarget::I32.domain_with_nulls(NullOrder::Last),
402 /// Some(domain::INT32_NULLS_LAST)
403 /// );
404 /// assert_eq!(OrderTarget::Text.domain_with_nulls(NullOrder::Last), None);
405 /// ```
406 pub fn domain_with_nulls(self, nulls: NullOrder) -> Option<&'static str> {
407 Some(match (self, nulls) {
408 (Self::I16, NullOrder::First) => domain::INT16_NULLS_FIRST,
409 (Self::I16, NullOrder::Last) => domain::INT16_NULLS_LAST,
410 (Self::U16, NullOrder::First) => domain::UINT16_NULLS_FIRST,
411 (Self::U16, NullOrder::Last) => domain::UINT16_NULLS_LAST,
412 (Self::I32, NullOrder::First) => domain::INT32_NULLS_FIRST,
413 (Self::I32, NullOrder::Last) => domain::INT32_NULLS_LAST,
414 (Self::U32, NullOrder::First) => domain::UINT32_NULLS_FIRST,
415 (Self::U32, NullOrder::Last) => domain::UINT32_NULLS_LAST,
416 (Self::I64, NullOrder::First) => domain::INT64_NULLS_FIRST,
417 (Self::I64, NullOrder::Last) => domain::INT64_NULLS_LAST,
418 (Self::U64, NullOrder::First) => domain::UINT64_NULLS_FIRST,
419 (Self::U64, NullOrder::Last) => domain::UINT64_NULLS_LAST,
420 (Self::I128, NullOrder::First) => domain::INT128_NULLS_FIRST,
421 (Self::I128, NullOrder::Last) => domain::INT128_NULLS_LAST,
422 (Self::U128, NullOrder::First) => domain::UINT128_NULLS_FIRST,
423 (Self::U128, NullOrder::Last) => domain::UINT128_NULLS_LAST,
424 (Self::F64, NullOrder::First) => domain::FLOAT64_NULLS_FIRST,
425 (Self::F64, NullOrder::Last) => domain::FLOAT64_NULLS_LAST,
426 (Self::Date, NullOrder::First) => domain::DATE_NULLS_FIRST,
427 (Self::Date, NullOrder::Last) => domain::DATE_NULLS_LAST,
428 (Self::Timestamp, NullOrder::First) => domain::TIMESTAMP_NULLS_FIRST,
429 (Self::Timestamp, NullOrder::Last) => domain::TIMESTAMP_NULLS_LAST,
430 (Self::Decimal, NullOrder::First) => domain::DECIMAL_NULLS_FIRST,
431 (Self::Decimal, NullOrder::Last) => domain::DECIMAL_NULLS_LAST,
432 (Self::Uuid, NullOrder::First) => domain::UUID_NULLS_FIRST,
433 (Self::Uuid, NullOrder::Last) => domain::UUID_NULLS_LAST,
434 (Self::Text | Self::Bytes, _) => return None,
435 })
436 }
437
438 /// The width of every term of this target, or `None` for text and
439 /// bytes, whose terms vary in length. Never one byte: see the type's
440 /// docs.
441 pub fn width(self) -> Option<usize> {
442 match self {
443 Self::I16 | Self::U16 => Some(2),
444 Self::I32 | Self::U32 | Self::Date => Some(4),
445 Self::I64 | Self::U64 | Self::F64 => Some(8),
446 Self::Timestamp => Some(12),
447 Self::Decimal => Some(14),
448 Self::I128 | Self::U128 | Self::Uuid => Some(16),
449 Self::Text | Self::Bytes => None,
450 }
451 }
452}
453
454/// Encode one scalar as `target`, sealed with the target's domain. The one
455/// place a value meets the data model.
456///
457/// An integer is converted to the target's width, then encoded
458/// big-endian, with the sign bit flipped when signed, so the bytes sort
459/// as the numbers do. A float, a date, a timestamp, a decimal and a uuid
460/// encode exactly as [`vitaminc_normalize::normalize`] encodes them for
461/// equality. Bytes are the equality encoding behind
462/// [`VARIABLE_WIDTH_MARKER`], and text is the marker, then Unicode 16 NFC
463/// with accents and case folded, so `É` and `e` tie. The marker keeps a
464/// variable-width encoding from ever being empty.
465///
466/// # Examples
467///
468/// ```
469/// use vitaminc_ore::{domain, order, Int, NormalizeError, OrderTarget, Physical};
470///
471/// // Every integer width widens to the target.
472/// let small = order(OrderTarget::I32, None, Physical::Int(Int::I8(100)))?;
473/// let large = order(OrderTarget::I32, None, Physical::Int(Int::U64(300)))?;
474/// assert_eq!(small.domain(), domain::INT32);
475/// assert_eq!(large.domain(), domain::INT32);
476///
477/// // A value the target cannot hold is refused, not truncated.
478/// let big = Physical::Int(Int::I64(1 << 40));
479/// assert_eq!(order(OrderTarget::I32, None, big).err(), Some(NormalizeError::OutOfRange));
480/// assert_eq!(
481/// order(OrderTarget::I32, None, Physical::Text("1")).err(),
482/// Some(NormalizeError::Mismatch)
483/// );
484/// # Ok::<(), NormalizeError>(())
485/// ```
486///
487/// This is the plain space, which places no nulls: a null has no term
488/// here. [`order_with_nulls`] encodes for a space that places them.
489///
490/// # Errors
491///
492/// [`NormalizeError::NoDomain`] for a null, a boolean (see [`OrderTarget`]
493/// for why), an `undefined` or a logical type this build does not know;
494/// [`NormalizeError::Invalid`] for a value that does not fit its own
495/// logical type;
496/// [`NormalizeError::Mismatch`] for a value of another kind than
497/// `target`; [`NormalizeError::OutOfRange`] for an integer outside
498/// `target`'s width; [`NormalizeError::UnassignedCodePoint`] for text
499/// holding a code point Unicode 16 does not assign.
500pub fn order(
501 target: OrderTarget,
502 ty: Option<Logical>,
503 v: Physical<'_>,
504) -> Result<OrderPlaintext, NormalizeError> {
505 encode(target, None, ty, v)
506}
507
508/// Encode one scalar as `target` in a space that places nulls as `nulls`
509/// says, sealed with that space's domain: [`order`] for a space with a
510/// [`NullOrder`].
511///
512/// A null, typed or plain, is the space's sentinel: all `0x00` for
513/// [`NullOrder::First`], all `0xFF` for [`NullOrder::Last`], at the
514/// target's width. A value encodes as in [`order`], except that under
515/// nulls-first an integer's or a uuid's code is one higher, as a
516/// big-endian number over the target's width, so the lowest code is free
517/// for the sentinel. See [`NullOrder`] for the whole rule.
518///
519/// # Examples
520///
521/// ```
522/// use vitaminc_ore::{domain, order_with_nulls, Int, NormalizeError, NullOrder, OrderTarget, Physical};
523/// use vitaminc_protected::Controlled;
524///
525/// let null = order_with_nulls(OrderTarget::I16, NullOrder::Last, None, Physical::Null)?;
526/// assert_eq!(null.domain(), domain::INT16_NULLS_LAST);
527/// assert_eq!(null.bytes().risky_ref(), &[0xFF, 0xFF]);
528/// assert_eq!(
529/// order_with_nulls(OrderTarget::I16, NullOrder::Last, None, Physical::Int(Int::I16(i16::MAX))).err(),
530/// Some(NormalizeError::Reserved)
531/// );
532/// // Text takes no null policy.
533/// assert_eq!(
534/// order_with_nulls(OrderTarget::Text, NullOrder::Last, None, Physical::Text("a")).err(),
535/// Some(NormalizeError::NoDomain)
536/// );
537/// # Ok::<(), NormalizeError>(())
538/// ```
539///
540/// # Errors
541///
542/// As [`order`], except that a null is not refused, and:
543/// [`NormalizeError::NoDomain`] for every value when `target` is text or
544/// bytes, which take no null policy; [`NormalizeError::Reserved`] for the
545/// maximum of an integer or uuid target, whose code is the nulls-last
546/// sentinel or would overflow after the nulls-first shift. An `undefined`
547/// is still [`NormalizeError::NoDomain`]: it is not a value and not a
548/// null.
549pub fn order_with_nulls(
550 target: OrderTarget,
551 nulls: NullOrder,
552 ty: Option<Logical>,
553 v: Physical<'_>,
554) -> Result<OrderPlaintext, NormalizeError> {
555 encode(target, Some(nulls), ty, v)
556}
557
558/// The domain label of a space: the target's own, or the one its null
559/// policy names. `None` for a null policy on text or bytes.
560pub(crate) fn label(target: OrderTarget, nulls: Option<NullOrder>) -> Option<&'static str> {
561 match nulls {
562 None => Some(target.domain()),
563 Some(nulls) => target.domain_with_nulls(nulls),
564 }
565}
566
567/// The sealed sentinel of a space that places nulls: every null of it.
568/// `None` for text and bytes, which take no policy.
569pub(crate) fn null_plaintext(target: OrderTarget, nulls: NullOrder) -> Option<OrderPlaintext> {
570 let domain = target.domain_with_nulls(nulls)?;
571 let width = target.width()?;
572 Some(OrderPlaintext::new(
573 target,
574 Some(nulls),
575 domain,
576 sentinel(width, nulls),
577 ))
578}
579
580/// [`order`] and [`order_with_nulls`]: one scalar as `target`, in the plain
581/// space when `nulls` is `None`.
582pub(crate) fn encode(
583 target: OrderTarget,
584 nulls: Option<NullOrder>,
585 ty: Option<Logical>,
586 v: Physical<'_>,
587) -> Result<OrderPlaintext, NormalizeError> {
588 use OrderTarget as T;
589 // As `normalize`: a value that does not fit its logical type is refused
590 // before the target is looked at.
591 if ty.is_some_and(|ty| !ty.admits(&v)) {
592 return Err(NormalizeError::Invalid);
593 }
594 // A null policy on text or bytes has no label, so no value has a term
595 // there.
596 let domain = label(target, nulls).ok_or(NormalizeError::NoDomain)?;
597 let bytes = match (target, ty, v) {
598 // A null is the sentinel of a space that places nulls, and has no
599 // order in one that does not, where a walk skips it.
600 (_, None, Physical::Null) => {
601 return match nulls.and_then(|nulls| null_plaintext(target, nulls)) {
602 Some(sentinel) => Ok(sentinel),
603 None => Err(NormalizeError::NoDomain),
604 }
605 }
606 // A boolean or an `undefined` has no order, whatever the target or
607 // the null policy, and a walk skips it.
608 (_, None, Physical::Bool(_)) | (_, Some(Logical::Undefined), _) => {
609 return Err(NormalizeError::NoDomain)
610 }
611 (_, Some(Logical::Unknown(_)), _) => return Err(NormalizeError::NoDomain),
612 (target, None, Physical::Int(i)) if target.is_int() => {
613 int(convert(target, i).ok_or(NormalizeError::OutOfRange)?)
614 }
615 (T::F64, None, Physical::Float(f)) => vitaminc_normalize::float(f),
616 (T::Text, None, Physical::Text(s)) => text(s)?,
617 (T::Bytes, None, Physical::Bytes(b)) => variable_width(b),
618 (T::Date, Some(Logical::Date), Physical::Int(days)) => {
619 vitaminc_normalize::date(days).ok_or(NormalizeError::Invalid)?
620 }
621 (T::Timestamp, Some(Logical::Timestamp(unit)), Physical::Int(count)) => {
622 vitaminc_normalize::timestamp(unit, count).ok_or(NormalizeError::Invalid)?
623 }
624 (T::Decimal, Some(Logical::Decimal { scale }), Physical::Int(mantissa)) => {
625 vitaminc_normalize::decimal(scale, mantissa).ok_or(NormalizeError::Invalid)?
626 }
627 (T::Uuid, Some(Logical::Uuid), Physical::Bytes(b)) => {
628 vitaminc_normalize::uuid(b).ok_or(NormalizeError::Invalid)?
629 }
630 // Another kind than the target's, including a logical type added
631 // to the data model later: it has no term here until it is given
632 // a target.
633 _ => return Err(NormalizeError::Mismatch),
634 };
635 let bytes = match nulls {
636 None => bytes,
637 Some(nulls) => make_room(target, nulls, bytes)?,
638 };
639 Ok(OrderPlaintext::new(target, nulls, domain, bytes))
640}
641
642/// The sentinel of a space that places nulls: the extreme code of the
643/// target's width, all `0x00` for nulls-first and all `0xFF` for
644/// nulls-last, in one protected buffer allocated at its final size.
645fn sentinel(width: usize, nulls: NullOrder) -> Protected<Vec<u8>> {
646 let fill = match nulls {
647 NullOrder::First => 0x00,
648 NullOrder::Last => 0xFF,
649 };
650 Protected::new(vec![fill; width])
651}
652
653/// A value's code in a space that places nulls, so that no value is the
654/// sentinel.
655///
656/// An integer or a uuid owns every code of its width, so the maximum,
657/// whose code is all `0xFF`, is refused under both policies, and under
658/// nulls-first every other code moves up by one to free all `0x00`. A
659/// float, a date, a timestamp and a decimal never reach either extreme
660/// (the tests walk each encoder's range), so their codes stay as they
661/// are; a code that did reach the sentinel is refused rather than tied
662/// with the nulls.
663fn make_room(
664 target: OrderTarget,
665 nulls: NullOrder,
666 mut code: Protected<Vec<u8>>,
667) -> Result<Protected<Vec<u8>>, NormalizeError> {
668 if target.owns_every_code() {
669 if every_byte_is(code.risky_ref(), 0xFF) {
670 return Err(NormalizeError::Reserved);
671 }
672 if nulls == NullOrder::First {
673 code.update(|code| increment(code));
674 }
675 return Ok(code);
676 }
677 let reserved = match nulls {
678 NullOrder::First => 0x00,
679 NullOrder::Last => 0xFF,
680 };
681 if every_byte_is(code.risky_ref(), reserved) {
682 return Err(NormalizeError::Reserved);
683 }
684 Ok(code)
685}
686
687/// Whether every byte of `code` is `byte`. Reads every byte whatever it
688/// finds, so the time taken does not depend on how many leading bytes of
689/// a value's code match, as the fixed-width encoders take the same time
690/// for every value. Only the answer, which the caller reports, says
691/// anything about the value.
692fn every_byte_is(code: &[u8], byte: u8) -> bool {
693 code.iter().fold(0u8, |differ, b| differ | (b ^ byte)) == 0
694}
695
696/// Add one to `code` as a big-endian unsigned number, in place. The caller
697/// has refused all `0xFF`, so it never overflows.
698fn increment(code: &mut [u8]) {
699 let mut carry = 1u8;
700 for byte in code.iter_mut().rev() {
701 let (sum, over) = byte.overflowing_add(carry);
702 *byte = sum;
703 carry = u8::from(over);
704 }
705}
706
707impl OrderTarget {
708 /// Whether every code of the target's width is some value's: an
709 /// integer or a uuid. Such a target gives up its maximum to a null
710 /// policy.
711 fn owns_every_code(self) -> bool {
712 self.is_int() || self == Self::Uuid
713 }
714
715 /// Whether this is an integer target.
716 fn is_int(self) -> bool {
717 matches!(
718 self,
719 Self::I16
720 | Self::U16
721 | Self::I32
722 | Self::U32
723 | Self::I64
724 | Self::U64
725 | Self::I128
726 | Self::U128
727 )
728 }
729}
730
731/// `value` at an integer target's width, or `None` if it does not fit.
732/// Every width converts through `i128`, except a `u128` above
733/// `i128::MAX`, which only a `u128` target holds.
734fn convert(target: OrderTarget, value: Int) -> Option<Int> {
735 let Some(v) = value.to_i128() else {
736 return match (target, value) {
737 (OrderTarget::U128, Int::U128(v)) => Some(Int::U128(v)),
738 _ => None,
739 };
740 };
741 match target {
742 OrderTarget::I16 => i16::try_from(v).ok().map(Int::I16),
743 OrderTarget::U16 => u16::try_from(v).ok().map(Int::U16),
744 OrderTarget::I32 => i32::try_from(v).ok().map(Int::I32),
745 OrderTarget::U32 => u32::try_from(v).ok().map(Int::U32),
746 OrderTarget::I64 => i64::try_from(v).ok().map(Int::I64),
747 OrderTarget::U64 => u64::try_from(v).ok().map(Int::U64),
748 OrderTarget::I128 => Some(Int::I128(v)),
749 OrderTarget::U128 => u128::try_from(v).ok().map(Int::U128),
750 _ => None,
751 }
752}
753
754/// An integer at its own width: big-endian, with the sign bit flipped for
755/// a signed width, so two's complement sorts as the numbers do. The same
756/// bytes `orderable-bytes` produces, checked against it in the tests.
757fn int(value: Int) -> Protected<Vec<u8>> {
758 match value {
759 // `convert` widens every 8-bit value to its target, so no
760 // one-byte encoding is ever made: there is no 8-bit target.
761 Int::I8(_) | Int::U8(_) => unreachable!("convert widens every 8-bit value to its target"),
762 Int::I16(v) => fixed(|b| *b = ((v as u16) ^ (1 << 15)).to_be_bytes()),
763 Int::U16(v) => fixed(|b| *b = v.to_be_bytes()),
764 Int::I32(v) => fixed(|b| *b = ((v as u32) ^ (1 << 31)).to_be_bytes()),
765 Int::U32(v) => fixed(|b| *b = v.to_be_bytes()),
766 Int::I64(v) => fixed(|b| *b = ((v as u64) ^ (1 << 63)).to_be_bytes()),
767 Int::U64(v) => fixed(|b| *b = v.to_be_bytes()),
768 Int::I128(v) => fixed(|b| *b = ((v as u128) ^ (1 << 127)).to_be_bytes()),
769 Int::U128(v) => fixed(|b| *b = v.to_be_bytes()),
770 }
771}
772
773/// The text order encoding: [`VARIABLE_WIDTH_MARKER`], then `text` as
774/// Unicode 16 NFC with accents and case folded. See the crate docs for
775/// the exact steps.
776///
777/// # Errors
778///
779/// [`NormalizeError::UnassignedCodePoint`] if `text` holds a code point
780/// Unicode 16 does not assign: its folding is not known.
781pub fn text(text: &str) -> Result<Protected<Vec<u8>>, NormalizeError> {
782 let folded = fold::folded(text)?;
783 Ok(variable_width(folded.risky_ref()))
784 // `folded` drops here and is wiped.
785}
786
787/// A variable-width encoding: the marker, then `bytes`, in one protected
788/// buffer sized up front so it never reallocates and frees an unwiped
789/// partial copy. Never empty.
790fn variable_width(bytes: &[u8]) -> Protected<Vec<u8>> {
791 let mut out = Protected::new(Vec::with_capacity(bytes.len() + 1));
792 out.risky_inner_mut().push(VARIABLE_WIDTH_MARKER);
793 out.risky_inner_mut().extend_from_slice(bytes);
794 out
795}
796
797/// A fixed-width encoding, written by `write` straight into the protected
798/// vector that is returned.
799///
800/// The vector is allocated once at its final size, so it never
801/// reallocates, and `write` fills it in place. No separate array of the
802/// encoding is made and copied, so there is no extra copy to wipe. The
803/// compiler can still make short-lived temporaries while `write` runs, for
804/// example the array that `to_be_bytes` returns, and Rust cannot promise
805/// to wipe those.
806fn fixed<const N: usize>(mut write: impl FnMut(&mut [u8; N])) -> Protected<Vec<u8>> {
807 let mut bytes = Protected::new(vec![0u8; N]);
808 bytes.update(|bytes| {
809 let array = <&mut [u8; N]>::try_from(bytes.as_mut_slice())
810 .expect("the vector was made with exactly N bytes");
811 write(array);
812 });
813 bytes
814}
815
816/// The order normalizer, for [`vitaminc_normalize::Terms::by`]: [`order`]
817/// at one target, whose terms are sealed [`OrderPlaintext`]s. Every scalar
818/// of the walk is encoded as that target, so it suits a value of one kind
819/// throughout, such as an array column; a scalar of another kind stops
820/// the walk with [`NormalizeError::Mismatch`].
821///
822/// Made with [`Order::new`], which places no nulls, so a null has no term
823/// and the walk skips it. [`Order::nulls`] places them, as a space does:
824/// every null of the walk is then the space's sentinel, at its path.
825///
826/// ```
827/// use vitaminc_normalize::terms_by;
828/// use vitaminc_ore::{domain, NullOrder, Order, OrderTarget};
829///
830/// let ages = vec![Some(36i32), None];
831/// let plain = terms_by(Order::new(OrderTarget::I32), &ages)?;
832/// assert_eq!(plain.len(), 1);
833///
834/// let last = Order::new(OrderTarget::I32).nulls(NullOrder::Last).expect("fixed width");
835/// let placed = terms_by(last, &ages)?;
836/// assert_eq!(placed.len(), 2);
837/// assert_eq!(placed[1].normalized.domain(), domain::INT32_NULLS_LAST);
838/// # Ok::<(), vitaminc_ore::TermsError>(())
839/// ```
840#[derive(Debug, Clone, Copy, PartialEq, Eq)]
841pub struct Order {
842 target: OrderTarget,
843 nulls: Option<NullOrder>,
844}
845
846impl Order {
847 /// The normalizer for `target`, placing no nulls.
848 pub const fn new(target: OrderTarget) -> Self {
849 Self {
850 target,
851 nulls: None,
852 }
853 }
854
855 /// This normalizer, placing nulls as `nulls` says.
856 ///
857 /// # Errors
858 ///
859 /// [`NormalizeError::NoDomain`] for a text or bytes target, which takes
860 /// no null policy, rather than a normalizer that refuses every value.
861 pub fn nulls(self, nulls: NullOrder) -> Result<Self, NormalizeError> {
862 self.target
863 .domain_with_nulls(nulls)
864 .map(|_| Self {
865 nulls: Some(nulls),
866 ..self
867 })
868 .ok_or(NormalizeError::NoDomain)
869 }
870
871 /// The target every scalar is encoded as.
872 pub fn target(self) -> OrderTarget {
873 self.target
874 }
875
876 /// Where the normalizer places nulls, or `None` if it gives them no
877 /// term.
878 pub fn null_order(self) -> Option<NullOrder> {
879 self.nulls
880 }
881}
882
883impl Normalize for Order {
884 type Term = OrderPlaintext;
885
886 const INDEX: &'static str = "order";
887
888 fn normalize(
889 &self,
890 ty: Option<Logical>,
891 value: Physical<'_>,
892 ) -> Result<OrderPlaintext, NormalizeError> {
893 encode(self.target, self.nulls, ty, value)
894 }
895}
896
897/// Every order term of `value` as `target`, in walk order, without
898/// encrypting it: [`vitaminc_normalize::terms`] under the [`Order`]
899/// normalizer.
900///
901/// A null, a boolean or an `undefined` produces no term and does not stop
902/// the walk (for nulls placed first or last, walk with [`Order::nulls`]
903/// through [`vitaminc_normalize::terms_by`]); a scalar of another kind
904/// than `target`, or one outside it, stops it, and so does a value
905/// encryption would refuse, for the same reason at the same path.
906/// [`vitaminc_normalize::Terms`] has the rules.
907///
908/// # Examples
909///
910/// ```
911/// use vitaminc_normalize::Step;
912/// use vitaminc_ore::{domain, order_terms, OrderTarget};
913///
914/// let scores: Vec<u16> = vec![7, 9];
915/// let found = order_terms(OrderTarget::I32, &scores)?;
916/// assert_eq!(found[1].path.steps(), [Step::Index(1)]);
917/// assert_eq!(found[1].normalized.domain(), domain::INT32);
918/// # Ok::<(), vitaminc_ore::TermsError>(())
919/// ```
920///
921/// # Errors
922///
923/// [`TermsError`]: what the walk refuses, a value encryption would refuse
924/// included, or the source's own failure.
925pub fn order_terms<P: Encrypt>(
926 target: OrderTarget,
927 value: P,
928) -> Result<Vec<Term<OrderPlaintext>>, TermsError> {
929 terms_by(Order::new(target), value)
930}
931
932#[cfg(test)]
933#[path = "order_tests.rs"]
934mod tests;