use super::is_universal_entity_source;
use crate::query::unified::UnifiedRecord;
use reddb_rql::ast::FieldRef;
use reddb_types::{json::Value as JsonValue, Value};
pub fn parse_canonical_field_ref(value: &str) -> Result<FieldRef, String> {
if let Some(rest) = value.strip_prefix("table:") {
let (table, column) = rest
.rsplit_once('.')
.ok_or_else(|| format!("invalid canonical table field ref {value}"))?;
return Ok(FieldRef::TableColumn {
table: table.to_string(),
column: column.to_string(),
});
}
if let Some(rest) = value.strip_prefix("node:") {
let (alias, property) = rest
.rsplit_once('.')
.ok_or_else(|| format!("invalid canonical node field ref {value}"))?;
return Ok(FieldRef::NodeProperty {
alias: alias.to_string(),
property: property.to_string(),
});
}
if let Some(rest) = value.strip_prefix("edge:") {
let (alias, property) = rest
.rsplit_once('.')
.ok_or_else(|| format!("invalid canonical edge field ref {value}"))?;
return Ok(FieldRef::EdgeProperty {
alias: alias.to_string(),
property: property.to_string(),
});
}
if let Some(alias) = value.strip_prefix("node_id:") {
return Ok(FieldRef::NodeId {
alias: alias.to_string(),
});
}
Err(format!("unsupported canonical field ref {value}"))
}
pub fn legacy_runtime_system_alias(column: &str) -> Option<&'static str> {
match column {
"entity_id" => Some("rid"),
"red_collection" => Some("collection"),
"red_kind" => Some("kind"),
_ => None,
}
}
pub fn resolve_runtime_field(
record: &UnifiedRecord,
field: &FieldRef,
table_name: Option<&str>,
table_alias: Option<&str>,
) -> Option<Value> {
match field {
FieldRef::TableColumn { table, column } => {
if !table.is_empty() {
if let Some(value) = record.get(format!("{table}.{column}").as_str()) {
return Some(value.clone());
}
let matches_context =
runtime_table_context_matches(table.as_str(), table_name, table_alias);
if !matches_context {
if table_name.is_none() && table_alias.is_none() {
if let Some(value) = record.get(column.as_str()) {
return Some(value.clone());
}
}
return resolve_runtime_document_path(record, &format!("{table}.{column}"));
}
}
record
.get(column.as_str())
.cloned()
.or_else(|| {
legacy_runtime_system_alias(column)
.and_then(|canonical| record.get(canonical).cloned())
})
.or_else(|| resolve_runtime_document_path(record, column))
}
FieldRef::NodeProperty { alias, property } => {
if let Some(value) = record.get(format!("{alias}.{property}").as_str()) {
return Some(value.clone());
}
let node = record.nodes.get(alias)?;
match property.as_str() {
"id" => Some(Value::NodeRef(node.id.clone())),
"label" => Some(Value::text(node.label.clone())),
"type" | "node_type" => Some(Value::text(node.node_label.clone())),
_ => node.properties.get(property).cloned(),
}
}
FieldRef::EdgeProperty { alias, property } => {
if let Some(value) = record.get(format!("{alias}.{property}").as_str()) {
return Some(value.clone());
}
let edge = record.edges.get(alias)?;
match property.as_str() {
"from" | "source" => Some(Value::NodeRef(edge.from.clone())),
"to" | "target" => Some(Value::NodeRef(edge.to.clone())),
"type" | "edge_type" | "label" => Some(Value::text(edge.edge_label.clone())),
"weight" => Some(Value::Float(edge.weight as f64)),
_ => None,
}
}
FieldRef::NodeId { alias } => record
.nodes
.get(alias)
.map(|node| Value::NodeRef(node.id.clone()))
.or_else(|| record.get(format!("{alias}.id").as_str()).cloned()),
}
}
pub fn runtime_table_context_matches(
field_table: &str,
table_name: Option<&str>,
table_alias: Option<&str>,
) -> bool {
if Some(field_table) == table_name || Some(field_table) == table_alias {
return true;
}
if !is_universal_entity_source(field_table) {
return false;
}
table_name.is_some_and(is_universal_entity_source)
|| table_alias.is_some_and(is_universal_entity_source)
}
pub fn resolve_runtime_document_path(record: &UnifiedRecord, path: &str) -> Option<Value> {
let segments = parse_runtime_document_path(path);
let (root, tail) = segments.split_first()?;
if let Some(root_value) = record.get(root.as_str()) {
return resolve_runtime_document_path_from_value(root_value, tail);
}
let (flattened_root, flattened_tail) = tail.split_first()?;
if let Some(value) = record.get(&tail.join(".")) {
return Some(value.clone());
}
let flattened_value = record.get(flattened_root.as_str())?;
resolve_runtime_document_path_from_value(flattened_value, flattened_tail)
}
pub fn resolve_runtime_document_path_from_value(value: &Value, path: &[String]) -> Option<Value> {
if path.is_empty() {
return Some(value.clone());
}
match value {
Value::Json(bytes) | Value::Blob(bytes) => {
let json = reddb_types::document_body_codec::decode_container_to_json(bytes)
.or_else(|| crate::json::from_slice::<JsonValue>(bytes).ok())?;
resolve_runtime_document_json_path(&json, path)
}
Value::Text(raw) => {
let trimmed = raw.trim_start();
if trimmed.starts_with('{') || trimmed.starts_with('[') {
let json = crate::json::from_str::<JsonValue>(raw).ok()?;
resolve_runtime_document_json_path(&json, path)
} else {
None
}
}
_ => None,
}
}
pub fn resolve_runtime_document_json_path(value: &JsonValue, path: &[String]) -> Option<Value> {
let mut current = value;
for segment in path {
current = match current {
JsonValue::Object(entries) => entries
.iter()
.find_map(|(key, value)| (key == segment).then_some(value))
.or_else(|| {
entries
.iter()
.find_map(|(key, value)| key.eq_ignore_ascii_case(segment).then_some(value))
})?,
JsonValue::Array(items) => {
let index = segment.parse::<usize>().ok()?;
items.get(index)?
}
_ => return None,
};
}
runtime_json_value_to_runtime_value(current)
}
pub fn runtime_json_value_to_runtime_value(value: &JsonValue) -> Option<Value> {
match value {
JsonValue::Null => Some(Value::Null),
JsonValue::Bool(value) => Some(Value::Boolean(*value)),
JsonValue::Integer(value) => Some(Value::Integer(*value)),
JsonValue::Number(value) => Some(Value::Float(*value)),
JsonValue::Decimal(value) => Some(Value::DecimalText(value.clone())),
JsonValue::String(value) => Some(Value::text(value.clone())),
JsonValue::Array(values) => Some(Value::Array(
values
.iter()
.map(|value| runtime_json_value_to_runtime_value(value).unwrap_or(Value::Null))
.collect(),
)),
JsonValue::Object(_) => Some(Value::Json(value.to_string_compact().into_bytes())),
}
}
pub fn parse_runtime_document_path(path: &str) -> Vec<String> {
let mut segments = Vec::new();
let mut current = String::new();
let mut chars = path.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'.' => {
if !current.is_empty() {
segments.push(std::mem::take(&mut current));
}
}
'[' => {
if !current.is_empty() {
segments.push(std::mem::take(&mut current));
}
let mut index = String::new();
for next in chars.by_ref() {
if next == ']' {
break;
}
index.push(next);
}
if !index.is_empty() {
segments.push(index);
}
}
_ => current.push(ch),
}
}
if !current.is_empty() {
segments.push(current);
}
segments
}
pub fn runtime_record_has_document_capability(record: &UnifiedRecord) -> bool {
let has_capability = record
.get("red_capabilities")
.and_then(|value| match value {
reddb_types::Value::Text(value) => Some(value),
_ => None,
})
.map(|capabilities| {
capabilities
.split(',')
.any(|capability| capability.trim() == "document")
})
.unwrap_or(false);
if has_capability {
return true;
}
record
.get("kind")
.and_then(|value| match value {
reddb_types::Value::Text(value) => Some(value),
_ => None,
})
.map(|kind| kind.as_ref() == "document")
.unwrap_or(false)
}