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)]
43pub struct TableSpec {
44 pub name: Vec<u8>,
46 pub prefix: Vec<u8>,
48 pub pk: Vec<u8>,
51 pub columns: Vec<(Vec<u8>, ValType)>,
53 pub indexes: Vec<TableIndex>,
55 pub orderpaths: Vec<OrderPath>,
57}
58
59pub const MAX_TABLES: usize = 64;
61
62impl TableSpec {
63 pub fn column_type(&self, col: &[u8]) -> Option<ValType> {
65 self.columns.iter().find(|(n, _)| n == col).map(|(_, t)| *t)
66 }
67
68 pub fn validate(&self) -> Result<(), String> {
71 if self.name.is_empty() {
72 return Err("ERR table name must be non-empty".into());
73 }
74 if self.prefix.is_empty() {
75 return Err("ERR PREFIX must be non-empty".into());
76 }
77 if self.columns.is_empty() {
78 return Err("ERR a table needs at least one COLUMN".into());
79 }
80 self.validate_columns_and_pk()?;
81 self.validate_indexes()?;
82 self.validate_orderpaths()
83 }
84
85 fn validate_columns_and_pk(&self) -> Result<(), String> {
86 for (i, (name, ty)) in self.columns.iter().enumerate() {
87 if !matches!(ty, ValType::I64 | ValType::F64 | ValType::Str) {
88 return Err("ERR COLUMN type must be i64|f64|str".into());
89 }
90 if self.columns[..i].iter().any(|(n, _)| n == name) {
91 return Err(format!("ERR duplicate COLUMN '{}'", show(name)));
92 }
93 }
94 if self.column_type(&self.pk).is_none() {
95 return Err(format!(
96 "ERR PK column '{}' is not declared (add COLUMN {} ...)",
97 show(&self.pk),
98 show(&self.pk)
99 ));
100 }
101 Ok(())
102 }
103
104 fn validate_indexes(&self) -> Result<(), String> {
105 for (i, ix) in self.indexes.iter().enumerate() {
106 if !matches!(ix.kind, IndexKind::Range | IndexKind::Unique) {
107 return Err("ERR INDEX kind must be range|unique".into());
108 }
109 if self.column_type(&ix.column).is_none() {
110 return Err(format!("ERR INDEX names unknown column '{}'", show(&ix.column)));
111 }
112 if self.indexes[..i].iter().any(|p| p.column == ix.column) {
113 return Err(format!("ERR duplicate INDEX on column '{}'", show(&ix.column)));
114 }
115 for v in &ix.values {
116 if self.column_type(v).is_none() {
117 return Err(format!("ERR VALUES names unknown column '{}'", show(v)));
118 }
119 }
120 }
121 Ok(())
122 }
123
124 fn validate_orderpaths(&self) -> Result<(), String> {
125 for (i, op) in self.orderpaths.iter().enumerate() {
126 if op.on.is_empty() {
127 return Err("ERR ORDERPATH needs ON <col>".into());
128 }
129 if op.on.len() > MAX_COMPOSITE_COLS {
130 return Err("ERR ORDERPATH supports at most 8 columns".into());
131 }
132 if self.orderpaths[..i].iter().any(|p| p.name == op.name) {
133 return Err(format!("ERR duplicate ORDERPATH '{}'", show(&op.name)));
134 }
135 if self.indexes.iter().any(|ix| ix.column == op.name) {
139 return Err(format!(
140 "ERR ORDERPATH '{}' collides with INDEX '{}'",
141 show(&op.name),
142 show(&op.name)
143 ));
144 }
145 for (col, _) in &op.on {
146 if self.column_type(col).is_none() {
147 return Err(format!(
148 "ERR ORDERPATH '{}' names unknown column '{}'",
149 show(&op.name),
150 show(col)
151 ));
152 }
153 }
154 }
155 Ok(())
156 }
157}
158
159fn show(b: &[u8]) -> String {
160 String::from_utf8_lossy(b).into_owned()
161}
162
163fn dotted(table: &[u8], suffix: &[u8]) -> Vec<u8> {
165 let mut n = table.to_vec();
166 n.push(b'.');
167 n.extend_from_slice(suffix);
168 n
169}
170
171pub fn compile_table(t: &TableSpec) -> Result<Vec<IndexSpec>, String> {
187 t.validate()?;
188 let col_ty = |col: &[u8]| {
189 t.column_type(col)
193 .ok_or_else(|| format!("ERR column '{}' is not declared", show(col)))
194 };
195 let mut out = Vec::with_capacity(t.indexes.len() + t.orderpaths.len());
196 for ix in &t.indexes {
197 let ty = col_ty(&ix.column)?;
198 let mut spec = IndexSpec::single_field(
199 dotted(&t.name, &ix.column),
200 t.prefix.clone(),
201 ix.column.clone(),
202 ty,
203 ix.kind,
204 );
205 spec.values = ix
206 .values
207 .iter()
208 .map(|c| Ok(ValueSpec { name: c.clone(), ty: col_ty(c)? }))
209 .collect::<Result<_, String>>()?;
210 out.push(spec);
211 }
212 for op in &t.orderpaths {
213 let mut spec = IndexSpec::single_field(
214 dotted(&t.name, &op.name),
215 t.prefix.clone(),
216 op.on[0].0.clone(),
217 ValType::Str,
218 IndexKind::Range,
219 );
220 spec.composite = Some(
221 op.on
222 .iter()
223 .map(|(col, desc)| {
224 Ok(CompositeCol { name: col.clone(), ty: col_ty(col)?, desc: *desc })
225 })
226 .collect::<Result<_, String>>()?,
227 );
228 out.push(spec);
229 }
230 Ok(out)
231}
232
233#[derive(Debug, Clone, Default)]
236pub struct TableCatalog {
237 specs: Vec<TableSpec>,
238}
239
240impl TableCatalog {
241 pub fn new() -> Self {
243 Self::default()
244 }
245
246 pub fn create(&mut self, spec: TableSpec) -> Result<(), String> {
248 spec.validate()?;
249 if self.specs.len() >= MAX_TABLES {
250 return Err("ERR table limit reached (64)".into());
251 }
252 if self.specs.iter().any(|s| s.name == spec.name) {
253 return Err("ERR table already exists".into());
254 }
255 self.specs.push(spec);
256 Ok(())
257 }
258
259 pub fn drop_table(&mut self, name: &[u8]) -> bool {
261 let n = self.specs.len();
262 self.specs.retain(|s| s.name != name);
263 self.specs.len() != n
264 }
265
266 pub fn get(&self, name: &[u8]) -> Option<&TableSpec> {
268 self.specs.iter().find(|s| s.name == name)
269 }
270
271 pub fn iter(&self) -> impl Iterator<Item = &TableSpec> {
273 self.specs.iter()
274 }
275
276 pub fn len(&self) -> usize {
278 self.specs.len()
279 }
280
281 pub fn is_empty(&self) -> bool {
283 self.specs.is_empty()
284 }
285
286 pub fn to_sidecar(&self) -> String {
289 let mut out = String::from("kevy-table-catalog v1\n");
290 for s in &self.specs {
291 out.push_str(&spec_to_line(s));
292 out.push('\n');
293 }
294 out
295 }
296
297 pub fn from_sidecar(text: &str) -> Option<TableCatalog> {
301 let mut lines = text.lines();
302 if lines.next()? != "kevy-table-catalog v1" {
303 return None;
304 }
305 let mut c = TableCatalog::new();
306 for line in lines {
307 if line.is_empty() {
308 continue;
309 }
310 c.create(spec_from_line(line)?).ok()?;
311 }
312 Some(c)
313 }
314}
315
316fn tesc(b: &[u8]) -> String {
325 use core::fmt::Write as _;
326 let mut out = String::with_capacity(b.len());
327 for &c in b {
328 if c == b'\t' || c == b'\n' || c == b'%' || c == b',' || c == b':' || !(32..127).contains(&c)
329 {
330 let _ = write!(out, "%{c:02X}");
331 } else {
332 out.push(c as char);
333 }
334 }
335 if out.is_empty() { "%".into() } else { out }
336}
337
338fn tunesc(s: &str) -> Option<Vec<u8>> {
339 if s == "%" {
340 return Some(Vec::new());
341 }
342 let mut out = Vec::with_capacity(s.len());
343 let b = s.as_bytes();
344 let mut i = 0;
345 while i < b.len() {
346 if b[i] == b'%' {
347 out.push(u8::from_str_radix(s.get(i + 1..i + 3)?, 16).ok()?);
348 i += 3;
349 } else {
350 out.push(b[i]);
351 i += 1;
352 }
353 }
354 Some(out)
355}
356
357fn spec_to_line(s: &TableSpec) -> String {
358 let cols = s
359 .columns
360 .iter()
361 .map(|(n, t)| format!("{}:{}", tesc(n), t.tag()))
362 .collect::<Vec<_>>()
363 .join(",");
364 let idxs = if s.indexes.is_empty() {
365 "-".into()
366 } else {
367 s.indexes
368 .iter()
369 .map(|ix| {
370 let mut e = format!("{}:{}", tesc(&ix.column), ix.kind.tag());
371 for v in &ix.values {
372 e.push(':');
373 e.push_str(&tesc(v));
374 }
375 e
376 })
377 .collect::<Vec<_>>()
378 .join(",")
379 };
380 let ops = if s.orderpaths.is_empty() {
381 "-".into()
382 } else {
383 s.orderpaths
384 .iter()
385 .map(|op| {
386 let mut e = tesc(&op.name);
387 for (col, desc) in &op.on {
388 e.push(':');
389 e.push_str(&tesc(col));
390 e.push(':');
391 e.push(if *desc { 'd' } else { 'a' });
392 }
393 e
394 })
395 .collect::<Vec<_>>()
396 .join(",")
397 };
398 format!("{}\t{}\t{}\t{}\t{}\t{}", tesc(&s.name), tesc(&s.prefix), tesc(&s.pk), cols, idxs, ops)
399}
400
401fn spec_from_line(line: &str) -> Option<TableSpec> {
402 let parts: Vec<&str> = line.split('\t').collect();
403 if parts.len() != 6 {
404 return None;
405 }
406 let columns = parts[3]
407 .split(',')
408 .map(|e| {
409 let (n, t) = e.rsplit_once(':')?;
410 Some((tunesc(n)?, ValType::parse(t.as_bytes())?))
411 })
412 .collect::<Option<Vec<_>>>()?;
413 let indexes = if parts[4] == "-" {
414 Vec::new()
415 } else {
416 parts[4].split(',').map(index_from_entry).collect::<Option<Vec<_>>>()?
417 };
418 let orderpaths = if parts[5] == "-" {
419 Vec::new()
420 } else {
421 parts[5].split(',').map(orderpath_from_entry).collect::<Option<Vec<_>>>()?
422 };
423 Some(TableSpec {
424 name: tunesc(parts[0])?,
425 prefix: tunesc(parts[1])?,
426 pk: tunesc(parts[2])?,
427 columns,
428 indexes,
429 orderpaths,
430 })
431}
432
433fn index_from_entry(e: &str) -> Option<TableIndex> {
434 let mut segs = e.split(':');
435 let column = tunesc(segs.next()?)?;
436 let kind = IndexKind::parse(segs.next()?.as_bytes())?;
437 let values = segs.map(tunesc).collect::<Option<Vec<_>>>()?;
438 Some(TableIndex { column, kind, values })
439}
440
441fn orderpath_from_entry(e: &str) -> Option<OrderPath> {
442 let segs: Vec<&str> = e.split(':').collect();
443 if segs.len() < 3 || !(segs.len() - 1).is_multiple_of(2) {
444 return None;
445 }
446 let name = tunesc(segs[0])?;
447 let on = segs[1..]
448 .chunks(2)
449 .map(|pair| {
450 let col = tunesc(pair[0])?;
451 let desc = match pair[1] {
452 "a" => false,
453 "d" => true,
454 _ => return None,
455 };
456 Some((col, desc))
457 })
458 .collect::<Option<Vec<_>>>()?;
459 Some(OrderPath { name, on })
460}
461
462#[cfg(test)]
463#[path = "table_tests.rs"]
464mod tests;