use crate::catalog::{IndexKind, IndexSpec, ValType, ValueSpec};
use crate::composite::{CompositeCol, MAX_COMPOSITE_COLS};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableIndex {
pub column: Vec<u8>,
pub kind: IndexKind,
pub values: Vec<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrderPath {
pub name: Vec<u8>,
pub on: Vec<(Vec<u8>, bool)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WindowSpec {
pub column: Vec<u8>,
pub span: i64,
pub bucket: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TableSpec {
pub name: Vec<u8>,
pub prefix: Vec<u8>,
pub pk: Vec<u8>,
pub columns: Vec<(Vec<u8>, ValType)>,
pub indexes: Vec<TableIndex>,
pub orderpaths: Vec<OrderPath>,
pub window: Option<WindowSpec>,
pub autodeclare: usize,
pub auto_added: Vec<Vec<u8>>,
}
pub const MAX_TABLES: usize = 64;
impl TableSpec {
pub fn column_type(&self, col: &[u8]) -> Option<ValType> {
self.columns.iter().find(|(n, _)| n == col).map(|(_, t)| *t)
}
#[must_use]
pub fn sans_auto(&self) -> TableSpec {
let mut s = self.clone();
let auto = std::mem::take(&mut s.auto_added);
let suffix_of = |entry: &[u8]| -> Option<Vec<u8>> {
let e = entry.split(|&b| b == b'#').next()?;
let dot = e.iter().position(|&b| b == b'.')?;
Some(e[dot + 1..].to_vec())
};
for entry in &auto {
if let Some(pos) = entry.iter().position(|&b| b == b'#') {
let field = &entry[pos + 1..];
if let Some(sfx) = suffix_of(entry)
&& let Some(ix) = s.indexes.iter_mut().find(|ix| ix.column == sfx)
{
ix.values.retain(|v| v != field);
}
} else if let Some(sfx) = suffix_of(entry) {
s.indexes.retain(|ix| ix.column != sfx);
s.orderpaths.retain(|op| op.name != sfx);
}
}
s
}
pub fn validate(&self) -> Result<(), String> {
if self.name.is_empty() {
return Err("ERR table name must be non-empty".into());
}
if self.prefix.is_empty() {
return Err("ERR PREFIX must be non-empty".into());
}
if self.columns.is_empty() {
return Err("ERR a table needs at least one COLUMN".into());
}
self.validate_columns_and_pk()?;
self.validate_indexes()?;
self.validate_orderpaths()?;
self.validate_window()
}
fn validate_window(&self) -> Result<(), String> {
let Some(w) = &self.window else { return Ok(()) };
match self.column_type(&w.column) {
None => {
return Err(format!("ERR WINDOW names unknown column '{}'", show(&w.column)));
}
Some(ValType::I64) => {}
Some(_) => return Err("ERR WINDOW column must be i64".into()),
}
if w.span <= 0 || w.bucket <= 0 {
return Err("ERR WINDOW SPAN and BUCKET must be positive".into());
}
if w.bucket > w.span {
return Err("ERR WINDOW BUCKET must not exceed SPAN".into());
}
let indexed = self.indexes.iter().any(|ix| ix.column == w.column);
let leads_path = self
.orderpaths
.iter()
.any(|op| op.on.first().is_some_and(|(c, desc)| c == &w.column && !desc));
if !indexed && !leads_path {
return Err(format!(
"ERR WINDOW needs an access path on '{}' (add INDEX {} range, or lead an ORDERPATH with it ascending)",
show(&w.column),
show(&w.column)
));
}
Ok(())
}
fn validate_columns_and_pk(&self) -> Result<(), String> {
for (i, (name, ty)) in self.columns.iter().enumerate() {
if !matches!(ty, ValType::I64 | ValType::F64 | ValType::Str) {
return Err("ERR COLUMN type must be i64|f64|str".into());
}
if self.columns[..i].iter().any(|(n, _)| n == name) {
return Err(format!("ERR duplicate COLUMN '{}'", show(name)));
}
}
if self.column_type(&self.pk).is_none() {
return Err(format!(
"ERR PK column '{}' is not declared (add COLUMN {} ...)",
show(&self.pk),
show(&self.pk)
));
}
Ok(())
}
fn validate_indexes(&self) -> Result<(), String> {
for (i, ix) in self.indexes.iter().enumerate() {
if !matches!(ix.kind, IndexKind::Range | IndexKind::Unique) {
return Err("ERR INDEX kind must be range|unique".into());
}
if self.column_type(&ix.column).is_none() {
return Err(format!("ERR INDEX names unknown column '{}'", show(&ix.column)));
}
if self.indexes[..i].iter().any(|p| p.column == ix.column) {
return Err(format!("ERR duplicate INDEX on column '{}'", show(&ix.column)));
}
for v in &ix.values {
if self.column_type(v).is_none() {
return Err(format!("ERR VALUES names unknown column '{}'", show(v)));
}
}
}
Ok(())
}
fn validate_orderpaths(&self) -> Result<(), String> {
for (i, op) in self.orderpaths.iter().enumerate() {
if op.on.is_empty() {
return Err("ERR ORDERPATH needs ON <col>".into());
}
if op.on.len() > MAX_COMPOSITE_COLS {
return Err("ERR ORDERPATH supports at most 8 columns".into());
}
if self.orderpaths[..i].iter().any(|p| p.name == op.name) {
return Err(format!("ERR duplicate ORDERPATH '{}'", show(&op.name)));
}
if self.indexes.iter().any(|ix| ix.column == op.name) {
return Err(format!(
"ERR ORDERPATH '{}' collides with INDEX '{}'",
show(&op.name),
show(&op.name)
));
}
for (col, _) in &op.on {
if self.column_type(col).is_none() {
return Err(format!(
"ERR ORDERPATH '{}' names unknown column '{}'",
show(&op.name),
show(col)
));
}
}
}
Ok(())
}
}
pub(crate) use crate::table_sidecar::{spec_from_line, spec_to_line};
pub fn window_for(
cat: &TableCatalog,
index_name: &[u8],
) -> Option<(WindowSpec, crate::WindowShape)> {
let dot = index_name.iter().position(|&b| b == b'.')?;
let (tname, suffix) = (&index_name[..dot], &index_name[dot + 1..]);
let t = cat.get(tname)?;
let w = t.window.clone()?;
if suffix == w.column {
return Some((w, crate::WindowShape::PlainI64));
}
let leads = t.orderpaths.iter().any(|op| {
op.name == suffix && op.on.first().is_some_and(|(c, desc)| c == &w.column && !desc)
});
leads.then_some((w, crate::WindowShape::CompositeLed))
}
pub fn window_text_for(cat: &TableCatalog, spec: &IndexSpec) -> bool {
if spec.kind != crate::IndexKind::Text {
return false;
}
let Some(dot) = spec.name.iter().position(|&b| b == b'.') else { return false };
cat.get(&spec.name[..dot]).is_some_and(|t| t.window.is_some())
}
pub fn window_driver(cat: &TableCatalog, index_name: &[u8]) -> bool {
let Some(dot) = index_name.iter().position(|&b| b == b'.') else { return false };
let (tname, suffix) = (&index_name[..dot], &index_name[dot + 1..]);
let Some(t) = cat.get(tname) else { return false };
let Some(w) = &t.window else { return false };
if t.indexes.iter().any(|ix| ix.column == w.column) {
return suffix == w.column;
}
t.orderpaths
.iter()
.find(|op| op.on.first().is_some_and(|(c, desc)| c == &w.column && !desc))
.is_some_and(|op| op.name == suffix)
}
fn show(b: &[u8]) -> String {
String::from_utf8_lossy(b).into_owned()
}
fn dotted(table: &[u8], suffix: &[u8]) -> Vec<u8> {
let mut n = table.to_vec();
n.push(b'.');
n.extend_from_slice(suffix);
n
}
pub fn compile_table(t: &TableSpec) -> Result<Vec<IndexSpec>, String> {
t.validate()?;
let col_ty = |col: &[u8]| {
t.column_type(col)
.ok_or_else(|| format!("ERR column '{}' is not declared", show(col)))
};
let mut out = Vec::with_capacity(t.indexes.len() + t.orderpaths.len());
for ix in &t.indexes {
let ty = col_ty(&ix.column)?;
let mut spec = IndexSpec::single_field(
dotted(&t.name, &ix.column),
t.prefix.clone(),
ix.column.clone(),
ty,
ix.kind,
);
spec.values = ix
.values
.iter()
.map(|c| Ok(ValueSpec { name: c.clone(), ty: col_ty(c)? }))
.collect::<Result<_, String>>()?;
out.push(spec);
}
for op in &t.orderpaths {
let mut spec = IndexSpec::single_field(
dotted(&t.name, &op.name),
t.prefix.clone(),
op.on[0].0.clone(),
ValType::Str,
IndexKind::Range,
);
spec.composite = Some(
op.on
.iter()
.map(|(col, desc)| {
Ok(CompositeCol { name: col.clone(), ty: col_ty(col)?, desc: *desc })
})
.collect::<Result<_, String>>()?,
);
out.push(spec);
}
Ok(out)
}
#[derive(Debug, Clone, Default)]
pub struct TableCatalog {
specs: Vec<TableSpec>,
}
impl TableCatalog {
pub fn new() -> Self {
Self::default()
}
pub fn create(&mut self, spec: TableSpec) -> Result<(), String> {
spec.validate()?;
if self.specs.len() >= MAX_TABLES {
return Err("ERR table limit reached (64)".into());
}
if self.specs.iter().any(|s| s.name == spec.name) {
return Err("ERR table already exists".into());
}
self.specs.push(spec);
Ok(())
}
pub fn drop_table(&mut self, name: &[u8]) -> bool {
let n = self.specs.len();
self.specs.retain(|s| s.name != name);
self.specs.len() != n
}
pub fn get(&self, name: &[u8]) -> Option<&TableSpec> {
self.specs.iter().find(|s| s.name == name)
}
pub fn iter(&self) -> impl Iterator<Item = &TableSpec> {
self.specs.iter()
}
pub fn len(&self) -> usize {
self.specs.len()
}
pub fn is_empty(&self) -> bool {
self.specs.is_empty()
}
pub fn to_sidecar(&self) -> String {
let mut out = String::from("kevy-table-catalog v1\n");
for s in &self.specs {
out.push_str(&spec_to_line(s));
out.push('\n');
}
out
}
pub fn from_sidecar(text: &str) -> Option<TableCatalog> {
let mut lines = text.lines();
if lines.next()? != "kevy-table-catalog v1" {
return None;
}
let mut c = TableCatalog::new();
for line in lines {
if line.is_empty() {
continue;
}
c.create(spec_from_line(line)?).ok()?;
}
Some(c)
}
}
#[cfg(test)]
#[path = "table_tests.rs"]
mod tests;