Skip to main content

kevy_index/
table.rs

1//! The `TABLE.*` declaration layer.
2//!
3//! A table is a named, verifiable, catalog-managed DECLARATION that
4//! compiles AT DECLARE TIME into the existing IDX primitives — the
5//! engine gains no query language, no planner, and enforces no schema
6//! at query time (Law 3): a row with a missing column is a row with an
7//! absent field, exactly today's NULL semantics. Queries still name
8//! their access path explicitly (`IDX.QUERY <table>.<col> …`).
9//!
10//! [`TableSpec::compile`] is the SINGLE implementation both the server and
11//! the embedded store call — the IDX.CREATE parity lesson: a
12//! hand-mirrored compiler is the shape that drifts, and the dispatch
13//! oracle is the net that catches it.
14
15use crate::catalog::{IndexKind, ValType};
16use crate::spec::IndexSpec;
17use crate::table_error::TableError;
18use kevy_text::SortOrder;
19
20/// One declared secondary index: a column and a scalar kind, plus the
21/// stored `VALUES` columns residual FILTER/SORT read.
22///
23/// ```
24/// use kevy_index::{IndexKind, TableIndex};
25/// let mut ix = TableIndex::new("at", IndexKind::Range);
26/// ix.values.push(b"city".to_vec());
27/// assert_eq!((ix.column.as_slice(), ix.values.len()), (&b"at"[..], 1));
28/// ```
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30#[non_exhaustive]
31pub struct TableIndex {
32    /// Declared column the index reads.
33    ///
34    /// ```
35    /// # use kevy_index::TableError;
36    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
37    /// let t = declare("TABLE.DECLARE t PREFIX t: PK id COLUMN id i64 COLUMN at i64 INDEX at range")?;
38    /// assert_eq!(t.indexes[0].column, b"at");
39    /// assert_eq!(t.compile()?[0].name(), b"t.at", "the column names the compiled index");
40    /// # Ok::<(), TableError>(())
41    /// ```
42    pub column: Vec<u8>,
43    /// `Range` or `Unique` — nothing else compiles from a table
44    /// (aggregates stay a direct `IDX.CREATE KIND agg` declaration).
45    ///
46    /// ```
47    /// # use kevy_index::{IndexKind, TableError};
48    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
49    /// let t = declare("TABLE.DECLARE t PREFIX t: PK id COLUMN id i64 INDEX id unique")?;
50    /// assert_eq!(t.indexes[0].kind, IndexKind::Unique);
51    /// # Ok::<(), TableError>(())
52    /// ```
53    pub kind: IndexKind,
54    /// Declared columns stored per row (typed from the column decls).
55    ///
56    /// ```
57    /// # use kevy_index::TableError;
58    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
59    /// let t = declare("TABLE.DECLARE t PREFIX t: PK id COLUMN id i64 COLUMN city str INDEX id range VALUES city")?;
60    /// assert_eq!(t.indexes[0].values, [b"city".to_vec()]);
61    /// assert_eq!(t.compile()?[0].values().len(), 1, "stored alongside each entry");
62    /// # Ok::<(), TableError>(())
63    /// ```
64    pub values: Vec<Vec<u8>>,
65}
66
67impl TableIndex {
68    /// An index of `kind` on `column`, storing no `VALUES`.
69    ///
70    /// ```
71    /// use kevy_index::{IndexKind, TableIndex};
72    /// assert!(TableIndex::new("id", IndexKind::Unique).values.is_empty());
73    /// ```
74    pub fn new(column: impl Into<Vec<u8>>, kind: IndexKind) -> TableIndex {
75        TableIndex { column: column.into(), kind, values: Vec::new() }
76    }
77}
78
79/// One composite-sort path (`ORDERPATH` — cookbook §8 mechanized):
80/// compiles to a composite Range index named `<table>.<name>`.
81///
82/// ```
83/// use kevy_index::{OrderPath, SortOrder};
84/// let p = OrderPath::new("recent", vec![(b"at".to_vec(), SortOrder::Desc)]);
85/// assert_eq!(p.on[0].1, SortOrder::Desc);
86/// ```
87#[derive(Debug, Clone, PartialEq, Eq, Hash)]
88#[non_exhaustive]
89pub struct OrderPath {
90    /// Path name (the compiled index's suffix).
91    ///
92    /// ```
93    /// # use kevy_index::TableError;
94    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
95    /// let t = declare("TABLE.DECLARE t PREFIX t: PK id COLUMN id i64 ORDERPATH newest ON id DESC")?;
96    /// assert_eq!(t.orderpaths[0].name, b"newest");
97    /// assert_eq!(t.compile()?[0].name(), b"t.newest");
98    /// # Ok::<(), TableError>(())
99    /// ```
100    pub name: Vec<u8>,
101    /// `(column, direction)` in sort-significance order.
102    ///
103    /// ```
104    /// # use kevy_index::{SortOrder, TableError};
105    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
106    /// let t = declare("TABLE.DECLARE t PREFIX t: PK id COLUMN id i64 COLUMN c str ORDERPATH p ON c THEN id DESC")?;
107    /// assert_eq!(t.orderpaths[0].on, [(b"c".to_vec(), SortOrder::Asc), (b"id".to_vec(), SortOrder::Desc)]);
108    /// # Ok::<(), TableError>(())
109    /// ```
110    pub on: Vec<(Vec<u8>, SortOrder)>,
111}
112
113impl OrderPath {
114    /// Path `name` over `on`, most significant column first.
115    ///
116    /// ```
117    /// use kevy_index::{OrderPath, SortOrder};
118    /// let p = OrderPath::new("by_city", vec![(b"city".to_vec(), SortOrder::Asc)]);
119    /// assert_eq!(p.name, b"by_city");
120    /// ```
121    pub fn new(name: impl Into<Vec<u8>>, on: Vec<(Vec<u8>, SortOrder)>) -> OrderPath {
122        OrderPath { name: name.into(), on }
123    }
124
125    /// Whether this path's first column is `column`, ascending — the
126    /// shape whose tree prefix below a boundary is the out-of-window batch.
127    pub(crate) fn led_ascending_by(&self, column: &[u8]) -> bool {
128        self.on.first().is_some_and(|(c, o)| c == column && *o == SortOrder::Asc)
129    }
130}
131
132/// The sliding value-domain window: rows whose window-column value
133/// falls behind the moving boundary become eviction candidates for the
134/// cold segment tier. Units belong to the caller — the engine never
135/// interprets the column's i64 beyond ordering, so a window column can
136/// be epoch seconds, epoch millis, a sequence number, anything
137/// monotone with data age.
138///
139/// ```
140/// use kevy_index::WindowSpec;
141/// let w = WindowSpec::new("at", 86_400, 3_600);
142/// assert_eq!((w.span, w.bucket), (86_400, 3_600));
143/// ```
144#[derive(Debug, Clone, PartialEq, Eq, Hash)]
145#[non_exhaustive]
146pub struct WindowSpec {
147    /// Declared i64 column the window slides over.
148    ///
149    /// ```
150    /// # use kevy_index::TableError;
151    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
152    /// let t = declare("TABLE.DECLARE t PREFIX t: PK at COLUMN at i64 INDEX at range WINDOW at SPAN 60 BUCKET 10")?;
153    /// assert_eq!(t.window.map(|w| w.column), Some(b"at".to_vec()));
154    /// # Ok::<(), TableError>(())
155    /// ```
156    pub column: Vec<u8>,
157    /// Window length, in the column's own units.
158    ///
159    /// ```
160    /// # use kevy_index::TableError;
161    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
162    /// let t = declare("TABLE.DECLARE t PREFIX t: PK at COLUMN at i64 INDEX at range WINDOW at SPAN 60 BUCKET 10")?;
163    /// assert_eq!(t.window.map(|w| w.span), Some(60));
164    /// # Ok::<(), TableError>(())
165    /// ```
166    pub span: i64,
167    /// Slide granularity, same units: the boundary advances in whole
168    /// buckets, and an evicted bucket is a segment.
169    ///
170    /// ```
171    /// # use kevy_index::TableError;
172    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
173    /// let t = declare("TABLE.DECLARE t PREFIX t: PK at COLUMN at i64 INDEX at range WINDOW at SPAN 60 BUCKET 10")?;
174    /// assert_eq!(t.window.map(|w| w.bucket), Some(10));
175    /// # Ok::<(), TableError>(())
176    /// ```
177    pub bucket: i64,
178}
179
180impl WindowSpec {
181    /// A window of `span` over i64 column `column`, sliding a `bucket`
182    /// at a time (same units as the column).
183    ///
184    /// ```
185    /// assert_eq!(kevy_index::WindowSpec::new("seq", 100, 10).column, b"seq");
186    /// ```
187    pub fn new(column: impl Into<Vec<u8>>, span: i64, bucket: i64) -> WindowSpec {
188        WindowSpec { column: column.into(), span, bucket }
189    }
190}
191
192/// One declared table.
193///
194/// A declaration callers assemble (or [`parse_table_declare`](crate::parse_table_declare)
195/// parses) and the auto loop edits: its rules span fields — an index
196/// must name a declared column, a window needs an ascending access path —
197/// so they are checked where a table is admitted ([`TableSpec::validate`],
198/// run by [`TableCatalog::create`] and [`TableSpec::compile`]), not per field.
199///
200/// ```
201/// use kevy_index::{IndexKind, TableIndex, TableSpec, ValType};
202/// let mut t = TableSpec::default();
203/// t.name = b"t".to_vec();
204/// t.prefix = b"t:".to_vec();
205/// t.pk = b"id".to_vec();
206/// t.columns = vec![(b"id".to_vec(), ValType::I64), (b"at".to_vec(), ValType::I64)];
207/// t.indexes.push(TableIndex::new("at", IndexKind::Range));
208/// assert_eq!(t.compile()?[0].name(), b"t.at");
209/// # Ok::<(), Box<dyn std::error::Error>>(())
210/// ```
211#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
212#[non_exhaustive]
213pub struct TableSpec {
214    /// Unique catalog name.
215    ///
216    /// ```
217    /// # use kevy_index::TableError;
218    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
219    /// let t = declare("TABLE.DECLARE users PREFIX user: PK id COLUMN id i64 COLUMN age i64 INDEX age range")?;
220    /// assert_eq!(t.name, b"users");
221    /// assert_eq!(t.compile()?[0].name(), b"users.age", "compiled paths are named under the table");
222    /// # Ok::<(), TableError>(())
223    /// ```
224    pub name: Vec<u8>,
225    /// Key-prefix domain the table's rows live under.
226    ///
227    /// ```
228    /// # use kevy_index::TableError;
229    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
230    /// let t = declare("TABLE.DECLARE users PREFIX user: PK id COLUMN id i64 COLUMN age i64 INDEX age range")?;
231    /// assert_eq!(t.prefix, b"user:");
232    /// assert_eq!(t.compile()?[0].prefix(), b"user:", "every path indexes the rows under it");
233    /// # Ok::<(), TableError>(())
234    /// ```
235    pub prefix: Vec<u8>,
236    /// Primary-key column (documentation + VERIFY surface; rows are
237    /// addressed by their key, exactly as today).
238    ///
239    /// ```
240    /// # use kevy_index::TableError;
241    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
242    /// let t = declare("TABLE.DECLARE users PREFIX user: PK id COLUMN id i64 COLUMN age i64 INDEX age range")?;
243    /// assert_eq!(t.pk, b"id");
244    /// # Ok::<(), TableError>(())
245    /// ```
246    pub pk: Vec<u8>,
247    /// Declared columns with their scalar types, declaration order.
248    ///
249    /// ```
250    /// # use kevy_index::{TableError, ValType};
251    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
252    /// let t = declare("TABLE.DECLARE users PREFIX user: PK id COLUMN id i64 COLUMN age i64 INDEX age range")?;
253    /// assert_eq!(t.columns, [(b"id".to_vec(), ValType::I64), (b"age".to_vec(), ValType::I64)]);
254    /// assert_eq!(t.column_type(b"age"), Some(ValType::I64));
255    /// assert_eq!(t.column_type(b"city"), None);
256    /// # Ok::<(), TableError>(())
257    /// ```
258    pub columns: Vec<(Vec<u8>, ValType)>,
259    /// Declared secondary indexes.
260    ///
261    /// ```
262    /// # use kevy_index::{IndexKind, TableError, TableIndex};
263    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
264    /// let t = declare("TABLE.DECLARE users PREFIX user: PK id COLUMN id i64 COLUMN age i64 INDEX age range")?;
265    /// assert_eq!(t.indexes, [TableIndex::new("age", IndexKind::Range)]);
266    /// # Ok::<(), TableError>(())
267    /// ```
268    pub indexes: Vec<TableIndex>,
269    /// Declared composite-sort paths.
270    ///
271    /// ```
272    /// # use kevy_index::TableError;
273    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
274    /// let t = declare("TABLE.DECLARE users PREFIX user: PK id COLUMN id i64 COLUMN age i64 INDEX age range ORDERPATH oldest ON age DESC THEN id")?;
275    /// assert_eq!(t.orderpaths.len(), 1);
276    /// assert_eq!(t.compile()?.len(), 2, "one index and one order path");
277    /// # Ok::<(), TableError>(())
278    /// ```
279    pub orderpaths: Vec<OrderPath>,
280    /// Optional sliding hot window (`WINDOW <col> SPAN <n> BUCKET <n>`).
281    ///
282    /// ```
283    /// # use kevy_index::{TableError, WindowSpec};
284    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
285    /// assert_eq!(declare("TABLE.DECLARE users PREFIX user: PK id COLUMN id i64 COLUMN age i64 INDEX age range")?.window, None);
286    /// let t = declare("TABLE.DECLARE ev PREFIX ev: PK at COLUMN at i64 INDEX at range WINDOW at SPAN 3600 BUCKET 60")?;
287    /// assert_eq!(t.window, Some(WindowSpec::new("at", 3600, 60)));
288    /// # Ok::<(), TableError>(())
289    /// ```
290    pub window: Option<WindowSpec>,
291    /// `AUTODECLARE <n>`: how many paths the engine may declare for
292    /// this table from observed refusals (0 = the loop is off, the
293    /// default). Building is addition-safe — the worst case is
294    /// bounded wasted memory; dropping stays a human act.
295    ///
296    /// ```
297    /// # use kevy_index::TableError;
298    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
299    /// assert_eq!(declare("TABLE.DECLARE users PREFIX user: PK id COLUMN id i64 COLUMN age i64 INDEX age range")?.autodeclare, 0, "off unless declared");
300    /// assert_eq!(declare("TABLE.DECLARE users PREFIX user: PK id COLUMN id i64 COLUMN age i64 INDEX age range AUTODECLARE 3")?.autodeclare, 3);
301    /// # Ok::<(), TableError>(())
302    /// ```
303    pub autodeclare: usize,
304    /// The paths the auto loop has declared, in declaration order —
305    /// its spent budget, and the `auto` marker IDX.LIST shows.
306    /// Runtime provenance, not declaration intent: equality checks
307    /// that answer "is this the same declaration?" must ignore it
308    /// (see [`Self::sans_auto`]).
309    ///
310    /// ```
311    /// # use kevy_index::{IndexKind, TableError, TableIndex};
312    /// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
313    /// let declared = declare("TABLE.DECLARE users PREFIX user: PK id COLUMN id i64 COLUMN age i64 INDEX age range AUTODECLARE 2")?;
314    /// // the engine added an index on id, and records that it did
315    /// let mut grown = declared.clone();
316    /// grown.indexes.push(TableIndex::new("id", IndexKind::Unique));
317    /// grown.auto_added.push(b"users.id".to_vec());
318    /// assert_eq!(grown.sans_auto(), declared, "what the human declared is unchanged");
319    /// # Ok::<(), TableError>(())
320    /// ```
321    pub auto_added: Vec<Vec<u8>>,
322}
323
324/// Hard cap on declared tables.
325///
326/// ```
327/// use kevy_index::{CatalogError, Declared, MAX_TABLES, TableCatalog};
328/// # let declare = |s: &str| kevy_index::parse_table_declare(&s.split(' ').map(str::as_bytes).collect::<Vec<_>>());
329/// let mut tables = TableCatalog::new();
330/// for i in 0..MAX_TABLES {
331///     tables.create(declare(&format!("TABLE.DECLARE t{i} PREFIX t{i}: PK id COLUMN id i64"))?)?;
332/// }
333/// let one_more = declare("TABLE.DECLARE extra PREFIX x: PK id COLUMN id i64")?;
334/// assert_eq!(tables.create(one_more), Err(CatalogError::Full(Declared::Table)));
335/// # Ok::<(), Box<dyn std::error::Error>>(())
336/// ```
337pub const MAX_TABLES: usize = 64;
338
339impl TableSpec {
340    /// Compile a table into its access paths: each `INDEX col KIND` becomes
341    /// an IndexSpec named `<table>.<col>` on the table's prefix (FIELD col,
342    /// TYPE from the column decl, VALUES typed from the column decls); each
343    /// `ORDERPATH` becomes a composite Range IndexSpec named
344    /// `<table>.<orderpath>`. Pure — the SINGLE compilation both the server
345    /// and the embedded store install.
346    ///
347    /// **Validates first, itself.** The 4.0 shape took "a validated table"
348    /// on trust and cashed that trust as `expect("validated")` — and the
349    /// typed embedded face never called `validate()` at all, so a spec
350    /// whose ORDERPATH named an undeclared column panicked in here, on a
351    /// consumer's boot path, and restart-looped their container (dogfood
352    /// F9). An invariant a function needs is one it establishes: admission
353    /// has exactly one authority now, and it is this signature. The wire
354    /// path's second validation costs microseconds.
355    ///
356    /// ```
357    /// use kevy_index::parse_table_declare;
358    /// let t = parse_table_declare(&[
359    ///     b"TABLE.DECLARE", b"t", b"PREFIX", b"t:", b"PK", b"id", b"COLUMN", b"id", b"i64",
360    ///     b"COLUMN", b"at", b"i64", b"INDEX", b"at", b"range",
361    /// ])?;
362    /// let compiled = t.compile()?;
363    /// assert_eq!(compiled[0].name(), b"t.at");
364    /// # Ok::<(), kevy_index::TableError>(())
365    /// ```
366    pub fn compile(&self) -> Result<Vec<IndexSpec>, TableError> {
367        compile_table(self)
368    }
369
370    /// The declared type of `col`, if declared.
371    pub fn column_type(&self, col: &[u8]) -> Option<ValType> {
372        self.columns.iter().find(|(n, _)| n == col).map(|(_, t)| *t)
373    }
374
375    /// This declaration with the auto loop's runtime additions
376    /// removed — what the human actually declared. `ENSURE`-style
377    /// "is this the same declaration?" comparisons go through here,
378    /// so paths the engine added never read as drift. Entries are
379    /// path names (a whole auto index/orderpath) or `path#field` (an
380    /// auto VALUES column on a human-declared index).
381    #[must_use]
382    pub fn sans_auto(&self) -> TableSpec {
383        let mut s = self.clone();
384        let auto = std::mem::take(&mut s.auto_added);
385        let suffix_of = |entry: &[u8]| -> Option<Vec<u8>> {
386            let e = entry.split(|&b| b == b'#').next()?;
387            let dot = e.iter().position(|&b| b == b'.')?;
388            Some(e[dot + 1..].to_vec())
389        };
390        for entry in &auto {
391            if let Some(pos) = entry.iter().position(|&b| b == b'#') {
392                let field = &entry[pos + 1..];
393                if let Some(sfx) = suffix_of(entry)
394                    && let Some(ix) = s.indexes.iter_mut().find(|ix| ix.column == sfx)
395                {
396                    ix.values.retain(|v| v != field);
397                }
398            } else if let Some(sfx) = suffix_of(entry) {
399                s.indexes.retain(|ix| ix.column != sfx);
400                s.orderpaths.retain(|op| op.name != sfx);
401            }
402        }
403        s
404    }
405
406    /// Structural validation — every refusal named. Runs at parse time
407    /// AND at catalog admission (a sidecar line re-validates on load).
408    pub fn validate(&self) -> Result<(), TableError> {
409        if self.name.is_empty() {
410            return Err(TableError::EmptyName);
411        }
412        if self.prefix.is_empty() {
413            return Err(TableError::EmptyPrefix);
414        }
415        if self.columns.is_empty() {
416            return Err(TableError::NoColumns);
417        }
418        self.validate_columns_and_pk()?;
419        self.validate_indexes()?;
420        self.validate_orderpaths()?;
421        self.validate_window()
422    }
423}
424
425pub use crate::table_catalog::TableCatalog;
426
427/// `<table>.<suffix>` — the compiled access-path name.
428pub(crate) fn dotted(table: &[u8], suffix: &[u8]) -> Vec<u8> {
429    let mut n = table.to_vec();
430    n.push(b'.');
431    n.extend_from_slice(suffix);
432    n
433}
434
435#[path = "table_compile.rs"]
436mod compile;
437use compile::compile_table;
438
439#[cfg(test)]
440#[path = "table_tests.rs"]
441mod tests;