1use crate::catalog::{IndexKind, IndexSpec, ValType, ValueSpec};
16use crate::composite::{CompositeCol, MAX_COMPOSITE_COLS};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct TableIndex {
22 pub column: Vec<u8>,
24 pub kind: IndexKind,
27 pub values: Vec<Vec<u8>>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct OrderPath {
35 pub name: Vec<u8>,
37 pub on: Vec<(Vec<u8>, bool)>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct WindowSpec {
49 pub column: Vec<u8>,
51 pub span: i64,
53 pub bucket: i64,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Default)]
60pub struct TableSpec {
61 pub name: Vec<u8>,
63 pub prefix: Vec<u8>,
65 pub pk: Vec<u8>,
68 pub columns: Vec<(Vec<u8>, ValType)>,
70 pub indexes: Vec<TableIndex>,
72 pub orderpaths: Vec<OrderPath>,
74 pub window: Option<WindowSpec>,
76 pub autodeclare: usize,
81 pub auto_added: Vec<Vec<u8>>,
87}
88
89pub const MAX_TABLES: usize = 64;
91
92impl TableSpec {
93 pub fn column_type(&self, col: &[u8]) -> Option<ValType> {
95 self.columns.iter().find(|(n, _)| n == col).map(|(_, t)| *t)
96 }
97
98 #[must_use]
105 pub fn sans_auto(&self) -> TableSpec {
106 let mut s = self.clone();
107 let auto = std::mem::take(&mut s.auto_added);
108 let suffix_of = |entry: &[u8]| -> Option<Vec<u8>> {
109 let e = entry.split(|&b| b == b'#').next()?;
110 let dot = e.iter().position(|&b| b == b'.')?;
111 Some(e[dot + 1..].to_vec())
112 };
113 for entry in &auto {
114 if let Some(pos) = entry.iter().position(|&b| b == b'#') {
115 let field = &entry[pos + 1..];
116 if let Some(sfx) = suffix_of(entry)
117 && let Some(ix) = s.indexes.iter_mut().find(|ix| ix.column == sfx)
118 {
119 ix.values.retain(|v| v != field);
120 }
121 } else if let Some(sfx) = suffix_of(entry) {
122 s.indexes.retain(|ix| ix.column != sfx);
123 s.orderpaths.retain(|op| op.name != sfx);
124 }
125 }
126 s
127 }
128
129 pub fn validate(&self) -> Result<(), String> {
132 if self.name.is_empty() {
133 return Err("ERR table name must be non-empty".into());
134 }
135 if self.prefix.is_empty() {
136 return Err("ERR PREFIX must be non-empty".into());
137 }
138 if self.columns.is_empty() {
139 return Err("ERR a table needs at least one COLUMN".into());
140 }
141 self.validate_columns_and_pk()?;
142 self.validate_indexes()?;
143 self.validate_orderpaths()?;
144 self.validate_window()
145 }
146
147 fn validate_window(&self) -> Result<(), String> {
152 let Some(w) = &self.window else { return Ok(()) };
153 match self.column_type(&w.column) {
154 None => {
155 return Err(format!("ERR WINDOW names unknown column '{}'", show(&w.column)));
156 }
157 Some(ValType::I64) => {}
158 Some(_) => return Err("ERR WINDOW column must be i64".into()),
159 }
160 if w.span <= 0 || w.bucket <= 0 {
161 return Err("ERR WINDOW SPAN and BUCKET must be positive".into());
162 }
163 if w.bucket > w.span {
164 return Err("ERR WINDOW BUCKET must not exceed SPAN".into());
165 }
166 let indexed = self.indexes.iter().any(|ix| ix.column == w.column);
167 let leads_path = self
168 .orderpaths
169 .iter()
170 .any(|op| op.on.first().is_some_and(|(c, desc)| c == &w.column && !desc));
171 if !indexed && !leads_path {
172 return Err(format!(
173 "ERR WINDOW needs an access path on '{}' (add INDEX {} range, or lead an ORDERPATH with it ascending)",
174 show(&w.column),
175 show(&w.column)
176 ));
177 }
178 Ok(())
179 }
180
181 fn validate_columns_and_pk(&self) -> Result<(), String> {
182 for (i, (name, ty)) in self.columns.iter().enumerate() {
183 if !matches!(ty, ValType::I64 | ValType::F64 | ValType::Str) {
184 return Err("ERR COLUMN type must be i64|f64|str".into());
185 }
186 if self.columns[..i].iter().any(|(n, _)| n == name) {
187 return Err(format!("ERR duplicate COLUMN '{}'", show(name)));
188 }
189 }
190 if self.column_type(&self.pk).is_none() {
191 return Err(format!(
192 "ERR PK column '{}' is not declared (add COLUMN {} ...)",
193 show(&self.pk),
194 show(&self.pk)
195 ));
196 }
197 Ok(())
198 }
199
200 fn validate_indexes(&self) -> Result<(), String> {
201 for (i, ix) in self.indexes.iter().enumerate() {
202 if !matches!(ix.kind, IndexKind::Range | IndexKind::Unique) {
203 return Err("ERR INDEX kind must be range|unique".into());
204 }
205 if self.column_type(&ix.column).is_none() {
206 return Err(format!("ERR INDEX names unknown column '{}'", show(&ix.column)));
207 }
208 if self.indexes[..i].iter().any(|p| p.column == ix.column) {
209 return Err(format!("ERR duplicate INDEX on column '{}'", show(&ix.column)));
210 }
211 for v in &ix.values {
212 if self.column_type(v).is_none() {
213 return Err(format!("ERR VALUES names unknown column '{}'", show(v)));
214 }
215 }
216 }
217 Ok(())
218 }
219
220 fn validate_orderpaths(&self) -> Result<(), String> {
221 for (i, op) in self.orderpaths.iter().enumerate() {
222 if op.on.is_empty() {
223 return Err("ERR ORDERPATH needs ON <col>".into());
224 }
225 if op.on.len() > MAX_COMPOSITE_COLS {
226 return Err("ERR ORDERPATH supports at most 8 columns".into());
227 }
228 if self.orderpaths[..i].iter().any(|p| p.name == op.name) {
229 return Err(format!("ERR duplicate ORDERPATH '{}'", show(&op.name)));
230 }
231 if self.indexes.iter().any(|ix| ix.column == op.name) {
235 return Err(format!(
236 "ERR ORDERPATH '{}' collides with INDEX '{}'",
237 show(&op.name),
238 show(&op.name)
239 ));
240 }
241 for (col, _) in &op.on {
242 if self.column_type(col).is_none() {
243 return Err(format!(
244 "ERR ORDERPATH '{}' names unknown column '{}'",
245 show(&op.name),
246 show(col)
247 ));
248 }
249 }
250 }
251 Ok(())
252 }
253}
254
255pub(crate) use crate::table_sidecar::{spec_from_line, spec_to_line};
256
257pub fn window_for(
264 cat: &TableCatalog,
265 index_name: &[u8],
266) -> Option<(WindowSpec, crate::WindowShape)> {
267 let dot = index_name.iter().position(|&b| b == b'.')?;
268 let (tname, suffix) = (&index_name[..dot], &index_name[dot + 1..]);
269 let t = cat.get(tname)?;
270 let w = t.window.clone()?;
271 if suffix == w.column {
272 return Some((w, crate::WindowShape::PlainI64));
273 }
274 let leads = t.orderpaths.iter().any(|op| {
275 op.name == suffix && op.on.first().is_some_and(|(c, desc)| c == &w.column && !desc)
276 });
277 leads.then_some((w, crate::WindowShape::CompositeLed))
278}
279
280pub fn window_text_for(cat: &TableCatalog, spec: &IndexSpec) -> bool {
285 if spec.kind != crate::IndexKind::Text {
286 return false;
287 }
288 let Some(dot) = spec.name.iter().position(|&b| b == b'.') else { return false };
289 cat.get(&spec.name[..dot]).is_some_and(|t| t.window.is_some())
290}
291
292pub fn window_driver(cat: &TableCatalog, index_name: &[u8]) -> bool {
299 let Some(dot) = index_name.iter().position(|&b| b == b'.') else { return false };
300 let (tname, suffix) = (&index_name[..dot], &index_name[dot + 1..]);
301 let Some(t) = cat.get(tname) else { return false };
302 let Some(w) = &t.window else { return false };
303 if t.indexes.iter().any(|ix| ix.column == w.column) {
304 return suffix == w.column;
305 }
306 t.orderpaths
307 .iter()
308 .find(|op| op.on.first().is_some_and(|(c, desc)| c == &w.column && !desc))
309 .is_some_and(|op| op.name == suffix)
310}
311
312fn show(b: &[u8]) -> String {
313 String::from_utf8_lossy(b).into_owned()
314}
315
316fn dotted(table: &[u8], suffix: &[u8]) -> Vec<u8> {
318 let mut n = table.to_vec();
319 n.push(b'.');
320 n.extend_from_slice(suffix);
321 n
322}
323
324pub fn compile_table(t: &TableSpec) -> Result<Vec<IndexSpec>, String> {
340 t.validate()?;
341 let col_ty = |col: &[u8]| {
342 t.column_type(col)
346 .ok_or_else(|| format!("ERR column '{}' is not declared", show(col)))
347 };
348 let mut out = Vec::with_capacity(t.indexes.len() + t.orderpaths.len());
349 for ix in &t.indexes {
350 let ty = col_ty(&ix.column)?;
351 let mut spec = IndexSpec::single_field(
352 dotted(&t.name, &ix.column),
353 t.prefix.clone(),
354 ix.column.clone(),
355 ty,
356 ix.kind,
357 );
358 spec.values = ix
359 .values
360 .iter()
361 .map(|c| Ok(ValueSpec { name: c.clone(), ty: col_ty(c)? }))
362 .collect::<Result<_, String>>()?;
363 out.push(spec);
364 }
365 for op in &t.orderpaths {
366 let mut spec = IndexSpec::single_field(
367 dotted(&t.name, &op.name),
368 t.prefix.clone(),
369 op.on[0].0.clone(),
370 ValType::Str,
371 IndexKind::Range,
372 );
373 spec.composite = Some(
374 op.on
375 .iter()
376 .map(|(col, desc)| {
377 Ok(CompositeCol { name: col.clone(), ty: col_ty(col)?, desc: *desc })
378 })
379 .collect::<Result<_, String>>()?,
380 );
381 out.push(spec);
382 }
383 Ok(out)
384}
385
386#[derive(Debug, Clone, Default)]
389pub struct TableCatalog {
390 specs: Vec<TableSpec>,
391}
392
393impl TableCatalog {
394 pub fn new() -> Self {
396 Self::default()
397 }
398
399 pub fn create(&mut self, spec: TableSpec) -> Result<(), String> {
401 spec.validate()?;
402 if self.specs.len() >= MAX_TABLES {
403 return Err("ERR table limit reached (64)".into());
404 }
405 if self.specs.iter().any(|s| s.name == spec.name) {
406 return Err("ERR table already exists".into());
407 }
408 self.specs.push(spec);
409 Ok(())
410 }
411
412 pub fn drop_table(&mut self, name: &[u8]) -> bool {
414 let n = self.specs.len();
415 self.specs.retain(|s| s.name != name);
416 self.specs.len() != n
417 }
418
419 pub fn get(&self, name: &[u8]) -> Option<&TableSpec> {
421 self.specs.iter().find(|s| s.name == name)
422 }
423
424 pub fn iter(&self) -> impl Iterator<Item = &TableSpec> {
426 self.specs.iter()
427 }
428
429 pub fn len(&self) -> usize {
431 self.specs.len()
432 }
433
434 pub fn is_empty(&self) -> bool {
436 self.specs.is_empty()
437 }
438
439 pub fn to_sidecar(&self) -> String {
442 let mut out = String::from("kevy-table-catalog v1\n");
443 for s in &self.specs {
444 out.push_str(&spec_to_line(s));
445 out.push('\n');
446 }
447 out
448 }
449
450 pub fn from_sidecar(text: &str) -> Option<TableCatalog> {
454 let mut lines = text.lines();
455 if lines.next()? != "kevy-table-catalog v1" {
456 return None;
457 }
458 let mut c = TableCatalog::new();
459 for line in lines {
460 if line.is_empty() {
461 continue;
462 }
463 c.create(spec_from_line(line)?).ok()?;
464 }
465 Some(c)
466 }
467}
468
469#[cfg(test)]
470#[path = "table_tests.rs"]
471mod tests;