use reddb_types::Value;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ExecutionError {
pub message: String,
}
impl ExecutionError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl std::fmt::Display for ExecutionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Execution error: {}", self.message)
}
}
impl std::error::Error for ExecutionError {}
#[derive(Debug, Clone, Default)]
pub struct UnifiedResult {
pub columns: Vec<String>,
pub records: Vec<UnifiedRecord>,
pub stats: QueryStats,
pub pre_serialized_json: Option<String>,
}
impl UnifiedResult {
pub fn empty() -> Self {
Self::default()
}
pub fn with_columns(columns: Vec<String>) -> Self {
Self {
columns,
records: Vec::new(),
stats: QueryStats::default(),
pre_serialized_json: None,
}
}
pub fn push(&mut self, record: UnifiedRecord) {
self.records.push(record);
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct UnifiedRecord {
schema: Arc<Vec<Arc<str>>>,
values: Vec<Value>,
overflow: Option<HashMap<Arc<str>, Value>>,
pub nodes: HashMap<String, MatchedNode>,
pub edges: HashMap<String, MatchedEdge>,
pub paths: Vec<GraphPath>,
pub vector_results: Vec<VectorSearchResult>,
}
fn empty_schema() -> Arc<Vec<Arc<str>>> {
static EMPTY: std::sync::OnceLock<Arc<Vec<Arc<str>>>> = std::sync::OnceLock::new();
Arc::clone(EMPTY.get_or_init(|| Arc::new(Vec::new())))
}
impl Default for UnifiedRecord {
fn default() -> Self {
Self {
schema: empty_schema(),
values: Vec::new(),
overflow: None,
nodes: HashMap::new(),
edges: HashMap::new(),
paths: Vec::new(),
vector_results: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ColumnarRow<'a> {
pub schema: &'a Arc<Vec<Arc<str>>>,
pub values: &'a [Value],
}
pub fn sys_key_rid() -> Arc<str> {
static KEY: std::sync::OnceLock<Arc<str>> = std::sync::OnceLock::new();
Arc::clone(KEY.get_or_init(|| Arc::from("rid")))
}
pub fn sys_key_collection() -> Arc<str> {
static KEY: std::sync::OnceLock<Arc<str>> = std::sync::OnceLock::new();
Arc::clone(KEY.get_or_init(|| Arc::from("collection")))
}
pub fn sys_key_kind() -> Arc<str> {
static KEY: std::sync::OnceLock<Arc<str>> = std::sync::OnceLock::new();
Arc::clone(KEY.get_or_init(|| Arc::from("kind")))
}
pub fn sys_key_tenant() -> Arc<str> {
static KEY: std::sync::OnceLock<Arc<str>> = std::sync::OnceLock::new();
Arc::clone(KEY.get_or_init(|| Arc::from("tenant")))
}
pub fn sys_key_created_at() -> Arc<str> {
static KEY: std::sync::OnceLock<Arc<str>> = std::sync::OnceLock::new();
Arc::clone(KEY.get_or_init(|| Arc::from("created_at")))
}
pub fn sys_key_updated_at() -> Arc<str> {
static KEY: std::sync::OnceLock<Arc<str>> = std::sync::OnceLock::new();
Arc::clone(KEY.get_or_init(|| Arc::from("updated_at")))
}
pub fn sys_key_row_id() -> Arc<str> {
static KEY: std::sync::OnceLock<Arc<str>> = std::sync::OnceLock::new();
Arc::clone(KEY.get_or_init(|| Arc::from("row_id")))
}
pub fn sys_key_red_collection() -> Arc<str> {
static KEY: std::sync::OnceLock<Arc<str>> = std::sync::OnceLock::new();
Arc::clone(KEY.get_or_init(|| Arc::from("red_collection")))
}
pub fn sys_key_red_kind() -> Arc<str> {
static KEY: std::sync::OnceLock<Arc<str>> = std::sync::OnceLock::new();
Arc::clone(KEY.get_or_init(|| Arc::from("red_kind")))
}
pub fn sys_key_red_sequence_id() -> Arc<str> {
static KEY: std::sync::OnceLock<Arc<str>> = std::sync::OnceLock::new();
Arc::clone(KEY.get_or_init(|| Arc::from("red_sequence_id")))
}
pub fn sys_key_red_entity_type() -> Arc<str> {
static KEY: std::sync::OnceLock<Arc<str>> = std::sync::OnceLock::new();
Arc::clone(KEY.get_or_init(|| Arc::from("red_entity_type")))
}
pub fn sys_key_red_capabilities() -> Arc<str> {
static KEY: std::sync::OnceLock<Arc<str>> = std::sync::OnceLock::new();
Arc::clone(KEY.get_or_init(|| Arc::from("red_capabilities")))
}
impl UnifiedRecord {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(capacity: usize) -> Self {
let mut rec = Self::default();
if capacity > 0 {
rec.overflow = Some(HashMap::with_capacity(capacity));
}
rec
}
pub fn with_schema(schema: Arc<Vec<Arc<str>>>, values: Vec<Value>) -> Self {
Self {
schema,
values,
overflow: None,
nodes: HashMap::new(),
edges: HashMap::new(),
paths: Vec::new(),
vector_results: Vec::new(),
}
}
#[inline]
pub fn from_columnar(schema: Arc<Vec<Arc<str>>>, values: Vec<Value>) -> Self {
Self::with_schema(schema, values)
}
pub fn schemaless() -> Self {
Self::default()
}
#[inline]
pub fn columns(&self) -> &[Arc<str>] {
self.schema.as_ref()
}
#[inline]
pub fn schema_values(&self) -> &[Value] {
&self.values
}
#[inline]
pub fn overflow(&self) -> Option<&HashMap<Arc<str>, Value>> {
self.overflow.as_ref()
}
#[inline]
fn schema_index(&self, column: &str) -> Option<usize> {
self.schema.iter().rposition(|k| &**k == column)
}
pub fn set(&mut self, column: &str, value: Value) {
if let Some(idx) = self.schema_index(column) {
if idx < self.values.len() {
self.values[idx] = value;
return;
}
}
self.overflow_mut().insert(Arc::from(column), value);
}
#[inline]
pub fn set_owned(&mut self, column: String, value: Value) {
if let Some(idx) = self.schema_index(&column) {
if idx < self.values.len() {
self.values[idx] = value;
return;
}
}
self.overflow_mut()
.insert(Arc::from(column.into_boxed_str()), value);
}
#[inline]
pub fn set_arc(&mut self, column: Arc<str>, value: Value) {
if let Some(idx) = self.schema_index(&column) {
if idx < self.values.len() {
self.values[idx] = value;
return;
}
}
self.overflow_mut().insert(column, value);
}
#[inline]
pub fn set_arc_if_absent(&mut self, column: Arc<str>, value: Value) {
if self.get(&column).is_none() {
self.set_arc(column, value);
}
}
pub fn get(&self, column: &str) -> Option<&Value> {
if let Some(idx) = self.schema_index(column) {
if let Some(v) = self.values.get(idx) {
return Some(v);
}
}
self.overflow.as_ref().and_then(|m| m.get(column))
}
pub fn field_count(&self) -> usize {
self.values.len() + self.overflow.as_ref().map(|m| m.len()).unwrap_or(0)
}
pub fn contains_column(&self, column: &str) -> bool {
self.get(column).is_some()
}
pub fn iter_fields(&self) -> Box<dyn Iterator<Item = (&Arc<str>, &Value)> + '_> {
let schema_iter = self.schema.iter().zip(self.values.iter());
let overflow_iter = self.overflow.iter().flat_map(|m| m.iter());
Box::new(schema_iter.chain(overflow_iter))
}
pub fn values_mut(&mut self) -> Box<dyn Iterator<Item = &mut Value> + '_> {
let schema_iter = self.values.iter_mut();
let overflow_iter = self.overflow.iter_mut().flat_map(|m| m.values_mut());
Box::new(schema_iter.chain(overflow_iter))
}
pub fn column_names(&self) -> Vec<Arc<str>> {
let mut out: Vec<Arc<str>> = Vec::with_capacity(self.field_count());
out.extend(self.schema.iter().cloned());
if let Some(m) = &self.overflow {
out.extend(m.keys().cloned());
}
out
}
#[inline]
pub fn columnar(&self) -> Option<ColumnarRow<'_>> {
if self.schema.is_empty() {
None
} else {
Some(ColumnarRow {
schema: &self.schema,
values: &self.values,
})
}
}
#[inline]
pub fn columnar_schema(&self) -> Option<&Arc<Vec<Arc<str>>>> {
if self.schema.is_empty() {
None
} else {
Some(&self.schema)
}
}
pub fn overflow_mut(&mut self) -> &mut HashMap<Arc<str>, Value> {
self.overflow.get_or_insert_with(HashMap::new)
}
pub fn overflow_entry_or_insert(&mut self, column: Arc<str>, value: Value) {
self.overflow_mut().entry(column).or_insert(value);
}
pub fn set_node(&mut self, alias: &str, node: MatchedNode) {
self.nodes.insert(alias.to_string(), node);
}
pub fn get_node(&self, alias: &str) -> Option<&MatchedNode> {
self.nodes.get(alias)
}
pub fn set_edge(&mut self, alias: &str, edge: MatchedEdge) {
self.edges.insert(alias.to_string(), edge);
}
}
#[derive(Debug, Clone)]
pub struct MatchedNode {
pub id: String,
pub label: String,
pub node_label: String,
pub properties: HashMap<String, Value>,
}
impl MatchedNode {
pub fn from_stored(node: impl Into<Self>) -> Self {
node.into()
}
}
#[derive(Debug, Clone)]
pub struct MatchedEdge {
pub from: String,
pub to: String,
pub edge_label: String,
pub weight: f32,
pub properties: HashMap<String, Value>,
}
impl MatchedEdge {
pub fn from_tuple(
source: &str,
edge_label: impl Into<String>,
target: &str,
weight: f32,
) -> Self {
Self {
from: source.to_string(),
to: target.to_string(),
edge_label: edge_label.into(),
weight,
properties: HashMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct VectorSearchResult {
pub id: u64,
pub collection: String,
pub distance: f32,
pub vector: Option<Vec<f32>>,
pub metadata: Option<HashMap<String, Value>>,
pub linked_node: Option<String>,
pub linked_row: Option<(String, u64)>,
}
impl VectorSearchResult {
pub fn new(id: u64, collection: impl Into<String>, distance: f32) -> Self {
Self {
id,
collection: collection.into(),
distance,
vector: None,
metadata: None,
linked_node: None,
linked_row: None,
}
}
pub fn with_vector(mut self, vector: Vec<f32>) -> Self {
self.vector = Some(vector);
self
}
pub fn with_metadata(mut self, metadata: HashMap<String, Value>) -> Self {
self.metadata = Some(metadata);
self
}
pub fn with_linked_node(mut self, node_id: impl Into<String>) -> Self {
self.linked_node = Some(node_id.into());
self
}
pub fn with_linked_row(mut self, table: impl Into<String>, row_id: u64) -> Self {
self.linked_row = Some((table.into(), row_id));
self
}
}
impl Default for VectorSearchResult {
fn default() -> Self {
Self::new(0, String::new(), 0.0)
}
}
#[derive(Debug, Clone)]
pub struct GraphPath {
pub nodes: Vec<String>,
pub edges: Vec<MatchedEdge>,
pub total_weight: f32,
}
impl GraphPath {
pub fn start(node_id: &str) -> Self {
Self {
nodes: vec![node_id.to_string()],
edges: Vec::new(),
total_weight: 0.0,
}
}
pub fn extend(&self, edge: MatchedEdge, node_id: &str) -> Self {
let mut new_path = self.clone();
new_path.total_weight += edge.weight;
new_path.edges.push(edge);
new_path.nodes.push(node_id.to_string());
new_path
}
pub fn len(&self) -> usize {
self.edges.len()
}
pub fn is_empty(&self) -> bool {
self.edges.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct QueryStats {
pub nodes_scanned: u64,
pub edges_scanned: u64,
pub rows_scanned: u64,
pub segments_total: u64,
pub segments_scanned: u64,
pub segments_pruned: u64,
pub exec_time_us: u64,
pub vector: Option<VectorQueryStats>,
}
impl QueryStats {
pub fn merge(a: &QueryStats, b: &QueryStats) -> QueryStats {
QueryStats {
nodes_scanned: a.nodes_scanned + b.nodes_scanned,
edges_scanned: a.edges_scanned + b.edges_scanned,
rows_scanned: a.rows_scanned + b.rows_scanned,
segments_total: a.segments_total + b.segments_total,
segments_scanned: a.segments_scanned + b.segments_scanned,
segments_pruned: a.segments_pruned + b.segments_pruned,
exec_time_us: a.exec_time_us + b.exec_time_us,
vector: None, }
}
}
#[derive(Debug, Clone, Default)]
pub struct VectorOperatorStats {
pub operator: String,
pub input_rows: u64,
pub output_rows: u64,
pub inclusive_time_us: u64,
}
#[derive(Debug, Clone, Default)]
pub struct VectorQueryStats {
pub operators: Vec<VectorOperatorStats>,
pub mode_requested: String,
pub mode_executed: String,
pub fallback_reason: Option<String>,
pub approximate_distance_evaluations: u64,
pub peak_topk_entries: u64,
pub cache_hit: bool,
pub access_path: String,
pub index_used: bool,
pub candidates_examined: u64,
pub metadata_rejected: u64,
pub rls_rejected: u64,
pub visibility_rejected: u64,
pub exact_distance_evaluations: u64,
pub rows_returned: u64,
}
impl VectorQueryStats {
pub fn operators_json(&self) -> crate::json::Value {
use crate::json::{Map, Value as JsonValue};
JsonValue::Array(
self.operators
.iter()
.map(|operator| {
let mut fields = Map::new();
fields.insert(
"operator".into(),
JsonValue::String(operator.operator.clone()),
);
for (name, value) in [
("input_rows", operator.input_rows),
("output_rows", operator.output_rows),
("inclusive_time_us", operator.inclusive_time_us),
] {
fields.insert(name.into(), Value::UnsignedInteger(value).to_json());
}
JsonValue::Object(fields)
})
.collect(),
)
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod record_layout_tests {
use super::*;
use proptest::collection::vec;
use proptest::prelude::*;
use reddb_types::Value;
fn arb_value() -> impl Strategy<Value = Value> {
prop_oneof![
Just(Value::Null),
any::<bool>().prop_map(Value::Boolean),
any::<i64>().prop_map(Value::Integer),
any::<f64>()
.prop_filter("nan-free", |v| !v.is_nan())
.prop_map(Value::Float),
"[a-zA-Z0-9 ]{0,16}".prop_map(Value::text),
]
}
fn arb_column_name() -> impl Strategy<Value = String> {
"[a-d]{1,3}".prop_map(|s| s.to_string())
}
fn arb_schema() -> impl Strategy<Value = Vec<String>> {
vec(arb_column_name(), 0..6).prop_map(|mut v| {
v.sort();
v.dedup();
v
})
}
fn arb_writes() -> impl Strategy<Value = Vec<(String, Value)>> {
vec((arb_column_name(), arb_value()), 0..16)
}
fn record_with_schema(schema: &[String], writes: &[(String, Value)]) -> UnifiedRecord {
let arc_schema: Arc<Vec<Arc<str>>> =
Arc::new(schema.iter().map(|s| Arc::from(s.as_str())).collect());
let mut rec =
UnifiedRecord::with_schema(Arc::clone(&arc_schema), vec![Value::Null; schema.len()]);
for (k, v) in writes {
rec.set_owned(k.clone(), v.clone());
}
rec
}
fn hashmap_reference(
schema: &[String],
writes: &[(String, Value)],
) -> std::collections::HashMap<String, Value> {
let mut map: std::collections::HashMap<String, Value> =
schema.iter().map(|c| (c.clone(), Value::Null)).collect();
for (k, v) in writes {
map.insert(k.clone(), v.clone());
}
map
}
proptest! {
#[test]
fn get_set_round_trip(schema in arb_schema(), writes in arb_writes()) {
let rec = record_with_schema(&schema, &writes);
let reference = hashmap_reference(&schema, &writes);
for (k, expected) in &reference {
prop_assert_eq!(rec.get(k), Some(expected), "key {}", k);
}
}
#[test]
fn overflow_promotion_only_for_missing_keys(
schema in arb_schema(),
writes in arb_writes(),
) {
let rec = record_with_schema(&schema, &writes);
for col in &schema {
if let Some(over) = rec.overflow() {
prop_assert!(
!over.contains_key(col.as_str()),
"schema column {} leaked into overflow",
col
);
}
}
for (k, _) in &writes {
if !schema.iter().any(|c| c == k) {
let over = rec.overflow().expect("overflow allocated");
prop_assert!(
over.contains_key(k.as_str()),
"out-of-schema key {} missing from overflow",
k
);
}
}
}
#[test]
fn iter_fields_matches_hashmap(
schema in arb_schema(),
writes in arb_writes(),
) {
let rec = record_with_schema(&schema, &writes);
let reference = hashmap_reference(&schema, &writes);
let mut got: Vec<(String, Value)> = rec
.iter_fields()
.map(|(k, v)| (k.as_ref().to_string(), v.clone()))
.collect();
let mut want: Vec<(String, Value)> = reference
.into_iter()
.collect();
got.sort_by(|a, b| a.0.cmp(&b.0));
want.sort_by(|a, b| a.0.cmp(&b.0));
prop_assert_eq!(got, want);
}
#[test]
fn shared_schema_scan_parity(
schema in arb_schema().prop_filter("non-empty schema", |s| !s.is_empty()),
rows in vec(arb_writes(), 0..8),
) {
let arc_schema: Arc<Vec<Arc<str>>> =
Arc::new(schema.iter().map(|s| Arc::from(s.as_str())).collect());
let shared: Vec<UnifiedRecord> = rows
.iter()
.map(|row_writes| {
let mut r = UnifiedRecord::with_schema(
Arc::clone(&arc_schema),
vec![Value::Null; schema.len()],
);
for (k, v) in row_writes {
r.set_owned(k.clone(), v.clone());
}
r
})
.collect();
let schemaless: Vec<UnifiedRecord> = rows
.iter()
.map(|row_writes| {
let mut r = UnifiedRecord::schemaless();
for col in &schema {
r.set_owned(col.clone(), Value::Null);
}
for (k, v) in row_writes {
r.set_owned(k.clone(), v.clone());
}
r
})
.collect();
for (s_row, h_row) in shared.iter().zip(schemaless.iter()) {
for col in &schema {
prop_assert_eq!(
s_row.get(col),
h_row.get(col),
"column {} diverged on shared vs schemaless build",
col
);
}
}
for s_row in &shared {
prop_assert!(s_row.columnar().is_some() || schema.is_empty());
}
}
#[test]
fn duplicate_schema_last_write_wins(
col in arb_column_name(),
v1 in arb_value(),
v2 in arb_value(),
) {
let arc_schema: Arc<Vec<Arc<str>>> = Arc::new(vec![
Arc::from(col.as_str()),
Arc::from(col.as_str()),
]);
let rec = UnifiedRecord::with_schema(arc_schema, vec![v1.clone(), v2.clone()]);
prop_assert_eq!(rec.get(&col), Some(&v2));
}
#[test]
fn schemaless_path_uses_overflow_only(writes in arb_writes()) {
let mut rec = UnifiedRecord::schemaless();
for (k, v) in &writes {
rec.set_owned(k.clone(), v.clone());
}
prop_assert!(rec.columns().is_empty());
prop_assert!(rec.schema_values().is_empty());
let mut reference: std::collections::HashMap<String, Value> =
std::collections::HashMap::new();
for (k, v) in &writes {
reference.insert(k.clone(), v.clone());
}
for (k, v) in reference {
prop_assert_eq!(rec.get(&k), Some(&v));
}
}
}
}