use std::cmp::Ordering;
use selene_core::{DbString, Value};
use crate::{
GqlType, IndexKey, IndexKind, Literal, SourceSpan, TypedIndexBounds,
ast::format::format_gql_type,
runtime::{EvalCtx, ExecutorError, parameter_type, value_compare},
};
pub(super) enum IndexKeyOutcome {
Value(Value),
EmptyResult,
}
pub(super) fn resolve_bitmap_union_key_values(
key: &IndexKey,
expected_kind: IndexKind,
ctx: &EvalCtx<'_, '_, '_, '_>,
) -> Result<Vec<Value>, ExecutorError> {
match key {
IndexKey::ParameterList {
name,
declared_type,
span,
} => resolve_parameter_list_key(name, declared_type, *span, expected_kind, ctx),
_ => match resolve_index_key(key, expected_kind, ctx)? {
IndexKeyOutcome::Value(value) => Ok(vec![value]),
IndexKeyOutcome::EmptyResult => Ok(Vec::new()),
},
}
}
pub(super) enum ResolvedBounds {
Equality(Value),
GreaterThan(Value),
GreaterEqual(Value),
LessThan(Value),
LessEqual(Value),
Range {
lo: Value,
lo_inclusive: bool,
hi: Value,
hi_inclusive: bool,
},
}
pub(super) fn range_satisfiable_runtime(resolved: &ResolvedBounds) -> bool {
let ResolvedBounds::Range {
lo,
lo_inclusive,
hi,
hi_inclusive,
} = resolved
else {
return true;
};
let Some(ordering) = value_compare::compare_non_null(lo, hi) else {
return false;
};
match ordering {
Ordering::Less => true,
Ordering::Greater => false,
Ordering::Equal => *lo_inclusive && *hi_inclusive,
}
}
pub(super) fn resolve_index_key(
key: &IndexKey,
expected_kind: IndexKind,
ctx: &EvalCtx<'_, '_, '_, '_>,
) -> Result<IndexKeyOutcome, ExecutorError> {
match key {
IndexKey::Literal(literal) => Ok(IndexKeyOutcome::Value(literal_to_value(literal))),
IndexKey::Parameter {
name,
declared_type,
span,
} => {
let raw =
ctx.tx
.parameters()
.get(name)
.cloned()
.ok_or(ExecutorError::UnboundParameter {
name: name.clone(),
span: *span,
})?;
if matches!(raw, Value::Null) {
return Ok(IndexKeyOutcome::EmptyResult);
}
if let Some(declared) = declared_type {
parameter_type::validate_declared_type(name.clone(), &raw, declared, *span)?;
}
check_value_index_kind(&raw, expected_kind, name.clone(), *span)?;
Ok(IndexKeyOutcome::Value(raw))
}
IndexKey::ParameterList { name, span, .. } => Err(ExecutorError::InvalidParameterType {
name: name.clone(),
expected: "scalar index key".into(),
actual: "LIST",
span: *span,
}),
}
}
fn resolve_parameter_list_key(
name: &DbString,
declared_type: &GqlType,
span: SourceSpan,
expected_kind: IndexKind,
ctx: &EvalCtx<'_, '_, '_, '_>,
) -> Result<Vec<Value>, ExecutorError> {
let raw = ctx
.tx
.parameters()
.get(name)
.cloned()
.ok_or(ExecutorError::UnboundParameter {
name: name.clone(),
span,
})?;
if matches!(raw, Value::Null) {
return Ok(Vec::new());
}
parameter_type::validate_declared_type(name.clone(), &raw, declared_type, span)?;
let Value::List(items) = raw else {
return Err(ExecutorError::InvalidParameterType {
name: name.clone(),
expected: format_gql_type(declared_type).into(),
actual: value_kind_label(&raw),
span,
});
};
let mut values = Vec::with_capacity(items.len());
for item in items {
if matches!(item, Value::Null) {
continue;
}
check_value_index_kind(&item, expected_kind, name.clone(), span)?;
values.push(item);
}
Ok(values)
}
pub(super) fn resolve_bounds(
bounds: &TypedIndexBounds,
expected_kind: IndexKind,
ctx: &EvalCtx<'_, '_, '_, '_>,
) -> Result<Option<ResolvedBounds>, ExecutorError> {
let resolve_one = |key: &IndexKey| -> Result<Option<Value>, ExecutorError> {
match resolve_index_key(key, expected_kind, ctx)? {
IndexKeyOutcome::Value(value) => Ok(Some(value)),
IndexKeyOutcome::EmptyResult => Ok(None),
}
};
Ok(Some(match bounds {
TypedIndexBounds::Equality(key) => {
let Some(value) = resolve_one(key)? else {
return Ok(None);
};
ResolvedBounds::Equality(value)
}
TypedIndexBounds::GreaterThan(key) => {
let Some(value) = resolve_one(key)? else {
return Ok(None);
};
ResolvedBounds::GreaterThan(value)
}
TypedIndexBounds::GreaterEqual(key) => {
let Some(value) = resolve_one(key)? else {
return Ok(None);
};
ResolvedBounds::GreaterEqual(value)
}
TypedIndexBounds::LessThan(key) => {
let Some(value) = resolve_one(key)? else {
return Ok(None);
};
ResolvedBounds::LessThan(value)
}
TypedIndexBounds::LessEqual(key) => {
let Some(value) = resolve_one(key)? else {
return Ok(None);
};
ResolvedBounds::LessEqual(value)
}
TypedIndexBounds::Range {
lo,
lo_inclusive,
hi,
hi_inclusive,
} => {
let Some(lo_value) = resolve_one(lo)? else {
return Ok(None);
};
let Some(hi_value) = resolve_one(hi)? else {
return Ok(None);
};
ResolvedBounds::Range {
lo: lo_value,
lo_inclusive: *lo_inclusive,
hi: hi_value,
hi_inclusive: *hi_inclusive,
}
}
}))
}
pub(super) fn literal_to_value(literal: &Literal) -> Value {
match literal {
Literal::Bool(value, _) => Value::Bool(*value),
Literal::Integer(value, _) | Literal::RadixInteger(value, _, _) => Value::Int(*value),
Literal::Decimal(value, _, _) => Value::Decimal(*value),
Literal::Float(value, _, _) => Value::Float(*value),
Literal::String(value, _, _) => Value::String(value.clone()),
Literal::Bytes(value, _) => Value::Bytes(value.clone()),
Literal::Uuid(value, _, _) => Value::Uuid(*value),
Literal::ZonedDateTime(value, _, _) => Value::ZonedDateTime(value.clone()),
Literal::LocalDateTime(value, _, _) => Value::LocalDateTime(*value),
Literal::Date(value, _, _) => Value::Date(*value),
Literal::ZonedTime(value, _, _) => Value::ZonedTime(value.clone()),
Literal::LocalTime(value, _, _) => Value::LocalTime(*value),
Literal::Duration(value, _, _) => Value::Duration(value.clone()),
Literal::Null(_) => Value::Null,
}
}
fn check_value_index_kind(
value: &Value,
expected: IndexKind,
name: DbString,
span: SourceSpan,
) -> Result<(), ExecutorError> {
let matches = match expected {
IndexKind::Boolean => matches!(value, Value::Bool(_)),
IndexKind::Integer => matches!(value, Value::Int(_)),
IndexKind::UnsignedInteger => matches!(value, Value::Uint(_)),
IndexKind::Integer128 => matches!(value, Value::Int128(_)),
IndexKind::UnsignedInteger128 => matches!(value, Value::Uint128(_)),
IndexKind::Decimal => matches!(value, Value::Decimal(_)),
IndexKind::Float32 => matches!(value, Value::Float32(_)),
IndexKind::Float => matches!(value, Value::Float(_)),
IndexKind::String => matches!(value, Value::String(_)),
IndexKind::Date => matches!(value, Value::Date(_)),
IndexKind::LocalDateTime => matches!(value, Value::LocalDateTime(_)),
IndexKind::ZonedDateTime => matches!(value, Value::ZonedDateTime(_)),
IndexKind::LocalTime => matches!(value, Value::LocalTime(_)),
IndexKind::ZonedTime => matches!(value, Value::ZonedTime(_)),
IndexKind::Duration => matches!(value, Value::Duration(_)),
IndexKind::Uuid => matches!(value, Value::Uuid(_)),
};
if matches {
return Ok(());
}
Err(ExecutorError::InvalidParameterType {
name,
expected: index_kind_label(expected).into(),
actual: value_kind_label(value),
span,
})
}
fn index_kind_label(kind: IndexKind) -> &'static str {
match kind {
IndexKind::Boolean => "BOOLEAN",
IndexKind::Integer => "INTEGER",
IndexKind::UnsignedInteger => "UINT64",
IndexKind::Integer128 => "INT128",
IndexKind::UnsignedInteger128 => "UINT128",
IndexKind::Decimal => "DECIMAL",
IndexKind::Float32 => "FLOAT32",
IndexKind::Float => "FLOAT",
IndexKind::String => "STRING",
IndexKind::Date => "DATE",
IndexKind::LocalDateTime => "LOCAL DATETIME",
IndexKind::ZonedDateTime => "ZONED DATETIME",
IndexKind::LocalTime => "LOCAL TIME",
IndexKind::ZonedTime => "ZONED TIME",
IndexKind::Duration => "DURATION",
IndexKind::Uuid => "UUID",
}
}
fn value_kind_label(value: &Value) -> &'static str {
match value {
Value::Bool(_) => "BOOLEAN",
Value::Int(_) => "INTEGER",
Value::Uint(_) => "UINT64",
Value::Int128(_) => "INT128",
Value::Uint128(_) => "UINT128",
Value::Float(_) => "FLOAT64",
Value::Float32(_) => "FLOAT32",
Value::Decimal(_) => "DECIMAL",
Value::String(_) => "STRING",
Value::Bytes(_) => "BYTES",
Value::List(_) => "LIST",
Value::Record(_) | Value::RecordTyped(_) => "RECORD",
Value::Path(_) => "PATH",
Value::NodeRef(_) => "NODE",
Value::EdgeRef(_) => "EDGE",
Value::GraphRef(_) => "GRAPH",
Value::TableRef(_) => "TABLE",
Value::ZonedDateTime(_) => "ZONED DATETIME",
Value::LocalDateTime(_) => "LOCAL DATETIME",
Value::Date(_) => "DATE",
Value::ZonedTime(_) => "ZONED TIME",
Value::LocalTime(_) => "LOCAL TIME",
Value::Duration(_) => "DURATION",
Value::Uuid(_) => "UUID",
Value::Vector(_) => "VECTOR",
Value::Json(_) => "JSON",
Value::Extended { .. } => "EXTENDED",
Value::Null => "NULL",
_ => "UNKNOWN",
}
}