use crate::catalog::{IndexSpec, ValType};
use crate::value::{IndexValue, order_key};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompositeCol {
pub name: Vec<u8>,
pub ty: ValType,
pub desc: bool,
}
pub const MAX_COMPOSITE_COLS: usize = 8;
pub const MAX_STR_COMPONENT: usize = 255;
pub const WHERE_NOT_COMPOSITE: &str =
"WHERE requires a composite index (an ORDERPATH-compiled one) — this index is not one";
fn encode_component(col: &CompositeCol, raw: &[u8]) -> Option<Vec<u8>> {
let mut framed = match col.ty {
ValType::I64 | ValType::F64 => order_key(col.ty, raw)?,
ValType::Str => {
if raw.len() > MAX_STR_COMPONENT {
return None;
}
let mut out = Vec::with_capacity(raw.len() + 2);
for &b in raw {
out.push(b);
if b == 0x00 {
out.push(0xFF);
}
}
out.extend_from_slice(&[0x00, 0x00]);
out
}
ValType::Vector => return None,
};
if col.desc {
for b in &mut framed {
*b = !*b;
}
}
Some(framed)
}
pub fn composite_encode(cols: &[CompositeCol], vals: &[Option<&[u8]>]) -> Option<Vec<u8>> {
composite_classify(cols, vals).into_value()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RowDerivation {
Indexed(Vec<u8>),
Absent,
CoerceFailed,
Oversize,
}
impl RowDerivation {
fn into_value(self) -> Option<Vec<u8>> {
match self {
Self::Indexed(v) => Some(v),
_ => None,
}
}
}
pub fn composite_classify(cols: &[CompositeCol], vals: &[Option<&[u8]>]) -> RowDerivation {
let mut out = Vec::new();
for (col, raw) in cols.iter().zip(vals) {
let Some(raw) = raw else { return RowDerivation::Absent };
match classify_component(col, raw) {
RowDerivation::Indexed(bytes) => out.extend_from_slice(&bytes),
other => return other,
}
}
RowDerivation::Indexed(out)
}
fn classify_component(col: &CompositeCol, raw: &[u8]) -> RowDerivation {
if col.ty == ValType::Str && raw.len() > MAX_STR_COMPONENT {
return RowDerivation::Oversize;
}
match encode_component(col, raw) {
Some(bytes) => RowDerivation::Indexed(bytes),
None => RowDerivation::CoerceFailed,
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct WhereClause {
pub eqs: Vec<(Vec<u8>, Vec<u8>)>,
pub range: Option<(Vec<u8>, Vec<u8>, Vec<u8>)>,
}
pub fn parse_where(
argv: &[Vec<u8>],
at: usize,
stop: impl Fn(&[u8]) -> bool,
) -> Option<(WhereClause, usize)> {
let mut w = WhereClause::default();
let mut i = at;
while i < argv.len() && !stop(&argv[i]) {
if argv[i].eq_ignore_ascii_case(b"RANGE") {
let col = argv.get(i + 1)?.clone();
let min = argv.get(i + 2)?.clone();
let max = argv.get(i + 3)?.clone();
w.range = Some((col, min, max));
i += 4;
break;
}
if !argv.get(i + 1)?.eq_ignore_ascii_case(b"EQ") {
return None;
}
w.eqs.push((argv[i].clone(), argv.get(i + 2)?.clone()));
i += 3;
}
if w.eqs.is_empty() && w.range.is_none() {
return None;
}
Some((w, i))
}
fn declared_list(cols: &[CompositeCol]) -> String {
cols.iter()
.map(|c| String::from_utf8_lossy(&c.name).into_owned())
.collect::<Vec<_>>()
.join(", ")
}
fn bound_component(col: &CompositeCol, raw: &[u8]) -> Result<Vec<u8>, String> {
encode_component(col, raw).ok_or_else(|| {
format!(
"WHERE bound '{}' is not a valid {}, which is how this composite declares '{}'",
String::from_utf8_lossy(raw),
col.ty.tag(),
String::from_utf8_lossy(&col.name),
)
})
}
fn component_max(col: &CompositeCol) -> Vec<u8> {
match (col.ty, col.desc) {
(ValType::I64 | ValType::F64, _) => vec![0xFF; 8],
(ValType::Str, true) => vec![0xFF, 0xFF],
(ValType::Str, false) => vec![0xFF; MAX_STR_COMPONENT * 2 + 3],
(ValType::Vector, _) => Vec::new(),
}
}
pub fn composite_bounds(
cols: &[CompositeCol],
w: &WhereClause,
) -> Result<(Vec<u8>, Vec<u8>), String> {
let mut lo = Vec::new();
let mut hi = Vec::new();
let mut at = 0usize;
for (name, value) in &w.eqs {
let col = resolve_col(cols, at, name)?;
let enc = bound_component(col, value)?;
lo.extend_from_slice(&enc);
hi.extend_from_slice(&enc);
at += 1;
}
if let Some((name, min, max)) = &w.range {
let col = resolve_col(cols, at, name)?;
let a = bound_component(col, min)?;
let b = bound_component(col, max)?;
let (emin, emax) = if a <= b { (a, b) } else { (b, a) };
lo.extend_from_slice(&emin);
hi.extend_from_slice(&emax);
at += 1;
}
for col in &cols[at..] {
let m = component_max(col);
let dominates = col.ty == ValType::Str && !col.desc;
hi.extend_from_slice(&m);
if dominates {
break;
}
}
Ok((lo, hi))
}
fn resolve_col<'c>(
cols: &'c [CompositeCol],
at: usize,
name: &[u8],
) -> Result<&'c CompositeCol, String> {
if !cols.iter().any(|c| c.name == name) {
return Err(format!(
"WHERE names column '{}', which this composite does not declare — it declares: {}",
String::from_utf8_lossy(name),
declared_list(cols),
));
}
match cols.get(at) {
Some(c) if c.name == name => Ok(c),
_ => Err(format!(
"WHERE columns must be a leading prefix of the composite's declared order ({})",
declared_list(cols),
)),
}
}
pub(crate) fn composite_guard(spec: &IndexSpec) -> Result<(), &'static str> {
let Some(cols) = &spec.composite else { return Ok(()) };
if spec.kind != crate::IndexKind::Range {
return Err("ERR COMPOSITE requires KIND range");
}
if spec.ty != ValType::Str {
return Err("ERR COMPOSITE requires TYPE str");
}
if !spec.values.is_empty() {
return Err("ERR COMPOSITE cannot combine with VALUES");
}
if spec.fields.len() != 1 {
return Err("ERR COMPOSITE declares exactly one FIELD");
}
if cols.is_empty() {
return Err("ERR COMPOSITE needs at least one column");
}
if cols.len() > MAX_COMPOSITE_COLS {
return Err("ERR COMPOSITE supports at most 8 columns");
}
if cols.iter().any(|c| matches!(c.ty, ValType::Vector)) {
return Err("ERR COMPOSITE columns must be i64|f64|str");
}
Ok(())
}
impl IndexSpec {
pub fn scalar_read_names(&self) -> Vec<&[u8]> {
let mut names: Vec<&[u8]> = match &self.composite {
Some(cols) => cols.iter().map(|c| c.name.as_slice()).collect(),
None => vec![self.field()],
};
names.extend(self.values.iter().map(|v| v.name.as_slice()));
names
}
pub fn primary_width(&self) -> usize {
self.composite.as_ref().map_or(1, Vec::len)
}
pub fn derive_scalar(&self, prim: &[Option<Vec<u8>>]) -> Option<IndexValue> {
match &self.composite {
Some(_) => match self.classify_scalar(prim) {
RowDerivation::Indexed(v) => Some(IndexValue::Str(v)),
_ => None,
},
None => IndexValue::coerce(self.ty, prim.first()?.as_deref()?),
}
}
pub fn classify_scalar(&self, prim: &[Option<Vec<u8>>]) -> RowDerivation {
match &self.composite {
Some(cols) => {
let refs: Vec<Option<&[u8]>> = prim.iter().map(|o| o.as_deref()).collect();
composite_classify(cols, &refs)
}
None => match prim.first().and_then(|o| o.as_deref()) {
None => RowDerivation::Absent,
Some(raw) => match IndexValue::coerce(self.ty, raw) {
Some(_) => RowDerivation::Indexed(raw.to_vec()),
None => RowDerivation::CoerceFailed,
},
},
}
}
}
#[cfg(test)]
#[path = "composite_tests.rs"]
mod tests;