use std::collections::HashSet;
use std::ops::Bound;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::document::Value;
pub type IndexBounds = Option<(Bound<Vec<u8>>, Bound<Vec<u8>>)>;
pub(crate) const TAG_INT: u8 = 0x20;
pub(crate) const TAG_FLOAT: u8 = 0x30;
pub fn encode_index_key(value: &Value, id: Ulid) -> Option<Vec<u8>> {
let mut buf = Vec::new();
encode_value_prefix(value, &mut buf)?;
buf.extend_from_slice(&id.to_bytes());
Some(buf)
}
pub fn encode_index_keys(value: &Value, id: Ulid) -> Vec<Vec<u8>> {
match value {
Value::Array(items) => {
let mut keys: Vec<Vec<u8>> = Vec::with_capacity(items.len());
let mut seen: HashSet<Vec<u8>> = HashSet::with_capacity(items.len());
for item in items {
if let Some(key) = encode_index_key(item, id)
&& seen.insert(key.clone())
{
keys.push(key);
}
}
keys
}
other => encode_index_key(other, id).into_iter().collect(),
}
}
fn encode_value_prefix(value: &Value, buf: &mut Vec<u8>) -> Option<()> {
match value {
Value::Null => {
buf.push(0x00);
}
Value::Bool(false) => {
buf.push(0x10);
}
Value::Bool(true) => {
buf.push(0x11);
}
Value::Int(n) => {
buf.push(TAG_INT);
let sortable = (*n as u64) ^ 0x8000_0000_0000_0000u64;
buf.extend_from_slice(&sortable.to_be_bytes());
}
Value::Float(f) => {
buf.push(TAG_FLOAT);
let bits = f.to_bits();
let sortable = if bits >> 63 == 1 {
!bits
} else {
bits ^ 0x8000_0000_0000_0000
};
buf.extend_from_slice(&sortable.to_be_bytes());
}
Value::Str(s) => {
buf.push(0x40);
for &b in s.as_bytes() {
if b == 0x00 {
buf.extend_from_slice(&[0x00, 0xFF]);
} else {
buf.push(b);
}
}
buf.push(0x00); }
Value::Bytes(b) => {
buf.push(0x50);
for &byte in b {
if byte == 0x00 {
buf.extend_from_slice(&[0x00, 0xFF]);
} else {
buf.push(byte);
}
}
buf.push(0x00); }
Value::Array(_) | Value::Object(_) => return None, }
Some(())
}
pub fn index_range_eq(value: &Value) -> Option<(Vec<u8>, Vec<u8>)> {
let mut start = Vec::new();
encode_value_prefix(value, &mut start)?;
let mut end = start.clone();
start.extend_from_slice(&[0x00u8; 16]);
end.extend_from_slice(&[0xFFu8; 16]);
Some((start, end))
}
pub fn index_range_cmp(
lower: Option<(&Value, bool)>, upper: Option<(&Value, bool)>,
) -> IndexBounds {
let start = match lower {
None => Bound::Unbounded,
Some((v, inclusive)) => {
let mut key = Vec::new();
encode_value_prefix(v, &mut key)?;
if inclusive {
key.extend_from_slice(&[0x00u8; 16]);
Bound::Included(key)
} else {
key.extend_from_slice(&[0xFFu8; 16]);
Bound::Excluded(key)
}
}
};
let end = match upper {
None => Bound::Unbounded,
Some((v, inclusive)) => {
let mut key = Vec::new();
encode_value_prefix(v, &mut key)?;
if inclusive {
key.extend_from_slice(&[0xFFu8; 16]);
Bound::Included(key)
} else {
key.extend_from_slice(&[0x00u8; 16]);
Bound::Excluded(key)
}
}
};
Some((start, end))
}
pub fn ulid_from_index_key(key: &[u8]) -> Option<Ulid> {
if key.len() < 16 {
return None;
}
let bytes: [u8; 16] = key[key.len() - 16..].try_into().ok()?;
Some(Ulid::from_bytes(bytes))
}
pub(crate) fn decode_value_prefix(bytes: &[u8]) -> Option<(Value, &[u8])> {
let (&tag, rest) = bytes.split_first()?;
match tag {
0x00 => Some((Value::Null, rest)),
0x10 | 0x11 => Some((Value::Bool(tag == 0x11), rest)),
TAG_INT | TAG_FLOAT => {
let word = u64::from_be_bytes(rest.get(..8)?.try_into().ok()?);
let value = if tag == TAG_INT {
Value::Int((word ^ 0x8000_0000_0000_0000) as i64)
} else {
let bits = if word >> 63 == 1 {
word ^ 0x8000_0000_0000_0000
} else {
!word
};
Value::Float(f64::from_bits(bits))
};
Some((value, &rest[8..]))
}
0x40 | 0x50 => {
let mut value = Vec::new();
let mut rest = rest;
loop {
let (&byte, tail) = rest.split_first()?;
rest = tail;
if byte == 0 {
if rest.first() == Some(&0xff) {
value.push(0);
rest = &rest[1..];
} else {
break;
}
} else {
value.push(byte);
}
}
Some((
if tag == 0x40 {
Value::Str(String::from_utf8(value).ok()?)
} else {
Value::Bytes(value)
},
rest,
))
}
_ => None,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexDef {
pub collection: String,
pub field: String,
}
pub fn meta_key(collection: &str, field: &str) -> String {
format!("{collection}::{field}")
}
pub fn index_table_name(collection: &str, field: &str) -> String {
format!("idx::{collection}::{field}")
}
pub fn docs_table_name(collection: &str) -> String {
format!("docs::{collection}")
}
pub(crate) const REMOVED_TABLE_PREFIXES: [&str; 2] = ["tomb::", "quarantine::"];
pub const META_INDEXES_TABLE: &str = "meta::indexes";
pub(crate) const META_INDEX_ARRAYS_TABLE: &str = "meta::index_arrays";
pub(crate) fn index_array_count(bytes: &[u8]) -> Option<u64> {
Some(u64::from_le_bytes(bytes.try_into().ok()?))
}
pub(crate) fn adjust_index_arrays(
txn: &mut dyn crate::engine::WriteTxn,
collection: &str,
field: &str,
removed: u64,
added: u64,
) -> Result<(), crate::error::TalaDbError> {
if removed == added {
return Ok(());
}
let key = meta_key(collection, field);
let Some(bytes) = txn.get(META_INDEX_ARRAYS_TABLE, key.as_bytes())? else {
return Ok(()); };
let count = index_array_count(&bytes)
.and_then(|n| n.checked_sub(removed))
.and_then(|n| n.checked_add(added));
if let Some(count) = count {
txn.put(
META_INDEX_ARRAYS_TABLE,
key.as_bytes(),
&count.to_le_bytes(),
)?;
} else {
txn.delete(META_INDEX_ARRAYS_TABLE, key.as_bytes())?;
}
Ok(())
}
pub const META_COMPOUND_TABLE: &str = "meta::compound_indexes";
pub const META_VERSION_TABLE: &str = "meta::db_version";
pub const META_VERSION_KEY: &[u8] = b"version";
pub const META_USER_VERSION_TABLE: &str = "meta::user_version";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompoundIndexDef {
pub collection: String,
pub fields: Vec<String>,
}
pub fn compound_meta_key(collection: &str, fields: &[&str]) -> String {
format!("{}::{}", collection, fields.join("::"))
}
pub fn compound_table_name(collection: &str, fields: &[&str]) -> String {
format!("cidx::{}::{}", collection, fields.join("::"))
}
pub fn encode_compound_key(values: &[&Value], id: Ulid) -> Option<Vec<u8>> {
let mut buf = Vec::new();
for v in values {
encode_value_prefix(v, &mut buf)?;
}
buf.extend_from_slice(&id.to_bytes());
Some(buf)
}
pub fn encode_compound_keys(
values: &[&Value],
id: Ulid,
) -> Result<Vec<Vec<u8>>, crate::TalaDbError> {
let array_members = values
.iter()
.filter(|v| matches!(v, Value::Array(_)))
.count();
if array_members > 1 {
return Err(crate::TalaDbError::CompoundIndexMultipleArrays);
}
if array_members == 0 {
return Ok(encode_compound_key(values, id).into_iter().collect());
}
let mut keys: Vec<Vec<u8>> = Vec::new();
let mut seen: HashSet<Vec<u8>> = HashSet::new();
let position = values
.iter()
.position(|v| matches!(v, Value::Array(_)))
.expect("array_members == 1");
let Value::Array(items) = values[position] else {
unreachable!("position found an array")
};
for item in items {
if matches!(item, Value::Array(_) | Value::Object(_)) {
continue;
}
let mut expanded: Vec<&Value> = values.to_vec();
expanded[position] = item;
if let Some(key) = encode_compound_key(&expanded, id)
&& seen.insert(key.clone())
{
keys.push(key);
}
}
Ok(keys)
}
pub fn compound_range_eq(values: &[&Value]) -> Option<(Vec<u8>, Vec<u8>)> {
let mut prefix = Vec::new();
for v in values {
encode_value_prefix(v, &mut prefix)?;
}
let mut start = prefix.clone();
start.extend_from_slice(&[0x00u8; 16]);
let mut end = prefix;
end.extend_from_slice(&[0xFFu8; 16]);
Some((start, end))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn int_sort_order() {
let values = [
Value::Int(-100),
Value::Int(-1),
Value::Int(0),
Value::Int(1),
Value::Int(100),
];
let id = Ulid::nil();
let keys: Vec<Vec<u8>> = values
.iter()
.map(|v| encode_index_key(v, id).unwrap())
.collect();
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(keys, sorted, "int index keys must sort in numeric order");
}
#[test]
fn float_sort_order() {
let values = [
Value::Float(-1.0),
Value::Float(0.0),
Value::Float(0.5),
Value::Float(1.0),
];
let id = Ulid::nil();
let keys: Vec<Vec<u8>> = values
.iter()
.map(|v| encode_index_key(v, id).unwrap())
.collect();
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(keys, sorted, "float index keys must sort in numeric order");
}
#[test]
fn string_sort_order() {
let values = [
Value::Str("alpha".into()),
Value::Str("beta".into()),
Value::Str("gamma".into()),
];
let id = Ulid::nil();
let keys: Vec<Vec<u8>> = values
.iter()
.map(|v| encode_index_key(v, id).unwrap())
.collect();
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(
keys, sorted,
"string index keys must sort lexicographically"
);
}
#[test]
fn non_indexable_values_return_none() {
let arr = Value::Array(vec![]);
let obj = Value::Object(vec![]);
assert!(encode_index_key(&arr, Ulid::nil()).is_none());
assert!(encode_index_key(&obj, Ulid::nil()).is_none());
}
#[test]
fn eq_range_does_not_match_longer_prefixes() {
let (start, end) = index_range_eq(&Value::Str("alpha".into())).unwrap();
let longer_key =
encode_index_key(&Value::Str("alphabet".into()), Ulid::from_bytes([0xAA; 16])).unwrap();
assert!(
longer_key < start || longer_key > end,
"longer prefix 'alphabet' must fall outside the 'alpha' range"
);
}
#[test]
fn eq_range_matches_exact_value() {
let id = Ulid::from_bytes([0x55; 16]);
let (start, end) = index_range_eq(&Value::Str("alpha".into())).unwrap();
let exact = encode_index_key(&Value::Str("alpha".into()), id).unwrap();
assert!(
exact >= start && exact <= end,
"exact value must lie inside its own range"
);
}
}