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;