use std::borrow::Cow;
use std::sync::Arc;
use selene_core::{CharacterStringCoercionError, DbString, JsonValue, PropertyValueType, Value};
use crate::{
GqlType, SourceSpan,
runtime::{DataExceptionSubclass, EvalCtx, ExecutorError, value_type_match},
};
use super::uuid_fns::parse_uuid_string;
mod decimal;
mod float;
mod numeric_text;
mod record;
mod signed;
mod signed128;
mod temporal;
mod unsigned;
mod vector;
use float::{FloatTarget, cast_to_float};
use record::cast_to_record;
use signed::{SignedIntegerTarget, cast_to_signed_integer};
use signed128::cast_to_int128;
use temporal::cast_to_temporal;
use unsigned::{UnsignedIntegerTarget, cast_to_unsigned_integer};
use vector::cast_to_vector;
pub(super) fn eval_cast(
value: Value,
target_type: &GqlType,
span: SourceSpan,
ctx: &EvalCtx<'_, '_, '_, '_>,
) -> Result<Value, ExecutorError> {
if let GqlType::NotNull(inner) = target_type {
if matches!(value, Value::Null) {
return Err(ExecutorError::data_exception(
DataExceptionSubclass::NullValueNotAllowed,
"CAST to a NOT NULL value type cannot produce NULL",
span,
));
}
return eval_cast(value, inner, span, ctx);
}
if matches!(target_type, GqlType::ClosedDynamicUnion(_)) {
return cast_to_closed_dynamic_union(value, target_type, span);
}
if matches!(value, Value::Null) {
return Ok(Value::Null);
}
match target_type {
GqlType::Null => {
return Err(ExecutorError::FeatureNotSupportedYet {
feature: "CAST to NULL",
span,
});
}
GqlType::Nothing => {
return Err(ExecutorError::FeatureNotSupportedYet {
feature: "CAST to NOTHING",
span,
});
}
_ => {}
}
if matches!(target_type, GqlType::Any | GqlType::AnyProperty) {
return cast_to_dynamic_union(value, target_type, span);
}
if let GqlType::Record(record_type) = target_type {
return cast_to_record(value, record_type, span, ctx);
}
match &value {
Value::NodeRef(_) => {
return Err(ExecutorError::FeatureNotSupportedYet {
feature: "CAST from NODE",
span,
});
}
Value::EdgeRef(_) => {
return Err(ExecutorError::FeatureNotSupportedYet {
feature: "CAST from EDGE",
span,
});
}
Value::Path(_) => {
return Err(ExecutorError::FeatureNotSupportedYet {
feature: "CAST from PATH",
span,
});
}
Value::Record(_) | Value::RecordTyped(_) => {
return Err(ExecutorError::data_exception(
DataExceptionSubclass::InvalidValueType,
"CAST from RECORD to a non-record type is not a valid type combination",
span,
));
}
Value::Bytes(_) if !matches!(target_type, GqlType::Bytes | GqlType::ByteString(_)) => {
return Err(non_iso_combination(
"CAST from BYTES to a non-BYTES type is not a valid type combination",
span,
));
}
Value::Json(_)
if !matches!(
target_type,
GqlType::Json | GqlType::String | GqlType::CharacterString(_)
) =>
{
return Err(non_iso_combination(
"CAST from JSON to this target is not a valid type combination",
span,
));
}
_ => {}
}
match target_type {
GqlType::Integer | GqlType::Int64 | GqlType::BigInt => {
cast_to_signed_integer(value, SignedIntegerTarget::I64, span)
}
GqlType::Int8 => cast_to_signed_integer(value, SignedIntegerTarget::I8, span),
GqlType::Int16 | GqlType::SmallInt => {
cast_to_signed_integer(value, SignedIntegerTarget::I16, span)
}
GqlType::Int32 => cast_to_signed_integer(value, SignedIntegerTarget::I32, span),
GqlType::Int128 => cast_to_int128(value, span),
GqlType::Uint8 => cast_to_unsigned_integer(value, UnsignedIntegerTarget::U8, span),
GqlType::Uint16 | GqlType::USmallInt => {
cast_to_unsigned_integer(value, UnsignedIntegerTarget::U16, span)
}
GqlType::Uint32 | GqlType::Uint => {
cast_to_unsigned_integer(value, UnsignedIntegerTarget::U32, span)
}
GqlType::Uint64 | GqlType::UBigInt => {
cast_to_unsigned_integer(value, UnsignedIntegerTarget::U64, span)
}
GqlType::Uint128 => cast_to_unsigned_integer(value, UnsignedIntegerTarget::U128, span),
GqlType::Float | GqlType::Float64 | GqlType::Double => {
cast_to_float(value, FloatTarget::F64, span)
}
GqlType::Float32 | GqlType::Real => cast_to_float(value, FloatTarget::F32, span),
GqlType::Decimal => decimal::numeric_to_decimal(value, span),
GqlType::DecimalExact(decimal_type) => {
decimal::numeric_to_decimal_exact(value, *decimal_type, span)
}
GqlType::Boolean => cast_to_boolean(value, span),
GqlType::String => cast_to_string(value, None, span),
GqlType::CharacterString(character_type) => {
cast_to_string(value, Some(character_type), span)
}
GqlType::Bytes => cast_to_bytes(value, None, span),
GqlType::ByteString(byte_type) => cast_to_bytes(value, Some(byte_type), span),
GqlType::Uuid => cast_to_uuid(value, span),
GqlType::Json => cast_to_json(value, span),
GqlType::Vector => cast_to_vector(value, span),
GqlType::ZonedDateTime
| GqlType::LocalDateTime
| GqlType::Date
| GqlType::ZonedTime
| GqlType::LocalTime
| GqlType::Duration
| GqlType::DurationYearToMonth
| GqlType::DurationDayToSecond => cast_to_temporal(value, target_type, span, ctx),
GqlType::List(element_type) => cast_to_list(value, element_type, None, span, ctx),
GqlType::BoundedList {
element_type,
max_len,
} => cast_to_list(value, element_type, Some(*max_len), span, ctx),
other => Err(ExecutorError::FeatureNotSupportedYet {
feature: cast_to_type_feature(other),
span,
}),
}
}
fn cast_to_dynamic_union(
value: Value,
target_type: &GqlType,
span: SourceSpan,
) -> Result<Value, ExecutorError> {
match target_type {
GqlType::Any => Ok(value),
GqlType::AnyProperty if PropertyValueType::of(&value).is_some() => Ok(value),
GqlType::AnyProperty => Err(ExecutorError::data_exception(
DataExceptionSubclass::InvalidValueType,
"CAST source is not a supported property value",
span,
)),
_ => unreachable!("dynamic-union cast called for non-dynamic target"),
}
}
fn cast_to_closed_dynamic_union(
value: Value,
target_type: &GqlType,
span: SourceSpan,
) -> Result<Value, ExecutorError> {
if value_type_match::value_matches_gql_type(&value, target_type) {
return Ok(value);
}
Err(ExecutorError::data_exception(
DataExceptionSubclass::InvalidValueType,
"CAST source is not a member of the closed dynamic union type",
span,
))
}
fn cast_to_boolean(value: Value, span: SourceSpan) -> Result<Value, ExecutorError> {
match value {
Value::Bool(b) => Ok(Value::Bool(b)),
Value::String(s) => string_to_boolean(s.as_str(), span),
Value::Int(_)
| Value::Uint(_)
| Value::Int128(_)
| Value::Uint128(_)
| Value::Float(_)
| Value::Float32(_)
| Value::Decimal(_) => Err(non_iso_combination(
"CAST from a numeric type to BOOLEAN is not a valid type combination",
span,
)),
other => Err(
non_iso_static_source_for_target(&other, "BOOLEAN", span).unwrap_or(
ExecutorError::FeatureNotSupportedYet {
feature: "CAST source not supported for BOOLEAN target",
span,
},
),
),
}
}
fn cast_to_string(
value: Value,
target_type: Option<&crate::ast::CharacterStringType>,
span: SourceSpan,
) -> Result<Value, ExecutorError> {
let rendered: String = match value {
Value::Bool(b) => if b { "TRUE" } else { "FALSE" }.to_owned(),
Value::Int(v) => v.to_string(),
Value::Uint(v) => v.to_string(),
Value::Int128(v) => v.to_string(),
Value::Uint128(v) => v.to_string(),
Value::Float(f) => format_float(f),
Value::Float32(f) => format_float(f64::from(f)),
Value::Decimal(d) => decimal::decimal_to_string(&d),
Value::String(s) => s.as_str().to_owned(),
Value::Uuid(v) => v.to_string(),
Value::ZonedDateTime(v) => format!("{}{}", v.datetime(), v.offset()),
Value::LocalDateTime(v) => v.to_string(),
Value::Date(v) => v.to_string(),
Value::ZonedTime(v) => format!("{}{}", v.time(), v.offset()),
Value::LocalTime(v) => v.to_string(),
Value::Duration(v) => v.to_string(),
Value::Json(v) => v.to_canonical_string(),
other => {
if let Some(error) = non_iso_static_source_for_target(&other, "STRING", span) {
return Err(error);
}
return Err(ExecutorError::FeatureNotSupportedYet {
feature: "CAST source not supported for STRING target",
span,
});
}
};
let rendered = coerce_string_to_type(rendered, target_type, span)?;
match DbString::from_string(rendered) {
Ok(db_string) => Ok(Value::String(db_string)),
Err(_err) => Err(ExecutorError::data_exception(
DataExceptionSubclass::DataException,
"CAST result string exceeds the maximum byte length",
span,
)),
}
}
fn coerce_string_to_type(
mut value: String,
target_type: Option<&crate::ast::CharacterStringType>,
span: SourceSpan,
) -> Result<String, ExecutorError> {
let Some(target_type) = target_type else {
return Ok(value);
};
let target = selene_core::CharacterStringType {
min_len: target_type.min_len,
max_len: target_type.max_len,
};
let coerced = selene_core::coerce_character_string_to_type(&value, target).map_err(|err| {
let (subclass, detail) = match err {
CharacterStringCoercionError::SourceLengthOverflow => (
DataExceptionSubclass::NumericValueOutOfRange,
"character string source length exceeds supported range",
),
CharacterStringCoercionError::TargetMinOverflow => (
DataExceptionSubclass::NumericValueOutOfRange,
"character string target minimum length exceeds supported range",
),
CharacterStringCoercionError::TargetMaxOverflow => (
DataExceptionSubclass::NumericValueOutOfRange,
"character string target maximum length exceeds supported range",
),
CharacterStringCoercionError::NonSpaceTruncation => (
DataExceptionSubclass::StringDataRightTruncation,
"character string cast would truncate non-space trailing characters",
),
};
ExecutorError::data_exception(subclass, detail, span)
})?;
match coerced {
Cow::Owned(coerced) => Ok(coerced),
Cow::Borrowed(coerced) => {
let keep = coerced.len();
value.truncate(keep);
Ok(value)
}
}
}
fn cast_to_json(value: Value, span: SourceSpan) -> Result<Value, ExecutorError> {
match value {
Value::Json(value) => Ok(Value::Json(value)),
Value::String(value) => parse_json_value(value.as_str(), span),
_ => Err(non_iso_combination(
"CAST from this source to JSON is not a valid type combination",
span,
)),
}
}
pub(super) fn parse_json_value(text: &str, span: SourceSpan) -> Result<Value, ExecutorError> {
JsonValue::parse_str(text).map(Value::Json).map_err(|err| {
if err.gqlstatus() == "22018" {
ExecutorError::data_exception(
DataExceptionSubclass::InvalidCharacterValueForCast,
format!("STRING value is not valid JSON: {err}"),
span,
)
} else {
ExecutorError::data_exception(
DataExceptionSubclass::DataException,
format!("JSON value exceeds implementation-defined limits: {err}"),
span,
)
}
})
}
fn cast_to_uuid(value: Value, span: SourceSpan) -> Result<Value, ExecutorError> {
match value {
Value::Uuid(v) => Ok(Value::Uuid(v)),
Value::String(s) => parse_uuid_string(s.as_str(), span).map(Value::Uuid),
_ => Err(ExecutorError::FeatureNotSupportedYet {
feature: "CAST source not supported for UUID target",
span,
}),
}
}
fn cast_to_bytes(
value: Value,
target_type: Option<&crate::ast::ByteStringType>,
span: SourceSpan,
) -> Result<Value, ExecutorError> {
match value {
Value::Bytes(value) => coerce_bytes_to_type(value, target_type, span),
_ => Err(non_iso_combination(
"CAST from a non-BYTES type to BYTES is not a valid type combination",
span,
)),
}
}
fn coerce_bytes_to_type(
value: Arc<[u8]>,
target_type: Option<&crate::ast::ByteStringType>,
span: SourceSpan,
) -> Result<Value, ExecutorError> {
let Some(target_type) = target_type else {
return Ok(Value::Bytes(value));
};
let len = value.len() as u64;
if len >= target_type.min_len && len <= target_type.max_len {
return Ok(Value::Bytes(value));
}
if len < target_type.min_len {
let target_len = usize::try_from(target_type.min_len).map_err(|_| {
ExecutorError::data_exception(
DataExceptionSubclass::NumericValueOutOfRange,
"byte string target minimum length exceeds supported range",
span,
)
})?;
let mut padded = Vec::with_capacity(target_len);
padded.extend_from_slice(&value);
padded.resize(target_len, 0);
return Ok(Value::Bytes(Arc::<[u8]>::from(padded.into_boxed_slice())));
}
let max_len = usize::try_from(target_type.max_len).map_err(|_| {
ExecutorError::data_exception(
DataExceptionSubclass::NumericValueOutOfRange,
"byte string target maximum length exceeds supported range",
span,
)
})?;
if value[max_len..].iter().any(|byte| *byte != 0) {
return Err(ExecutorError::data_exception(
DataExceptionSubclass::StringDataRightTruncation,
"byte string cast would truncate non-zero trailing bytes",
span,
));
}
Ok(Value::Bytes(Arc::<[u8]>::from(&value[..max_len])))
}
fn cast_to_list(
value: Value,
element_type: &GqlType,
max_len: Option<u64>,
span: SourceSpan,
ctx: &EvalCtx<'_, '_, '_, '_>,
) -> Result<Value, ExecutorError> {
let items = match value {
Value::List(items) => items,
other => {
return Err(
non_iso_static_source_for_target(&other, "LIST", span).unwrap_or(
ExecutorError::FeatureNotSupportedYet {
feature: "CAST to LIST requires a LIST source",
span,
},
),
);
}
};
if let Some(max_len) = max_len
&& u64::try_from(items.len()).map_or(true, |len| len > max_len)
{
return Err(ExecutorError::data_exception(
DataExceptionSubclass::InvalidValueType,
"LIST cast result exceeds declared maximum cardinality",
span,
));
}
let mut out = Vec::with_capacity(items.len());
for item in items {
out.push(stacker::maybe_grow(64 * 1024, 1024 * 1024, || {
eval_cast(item, element_type, span, ctx)
})?);
}
Ok(Value::List(out))
}
fn non_iso_combination(message: impl Into<String>, span: SourceSpan) -> ExecutorError {
ExecutorError::data_exception(DataExceptionSubclass::InvalidValueType, message, span)
}
pub(super) fn non_iso_static_source_for_target(
value: &Value,
target: &'static str,
span: SourceSpan,
) -> Option<ExecutorError> {
let source = iso_static_source_name(value)?;
Some(non_iso_combination(
format!("CAST from {source} to {target} is not a valid type combination"),
span,
))
}
fn iso_static_source_name(value: &Value) -> Option<&'static str> {
Some(match value {
Value::Bool(_) => "BOOLEAN",
Value::Int(_) | Value::Int128(_) | Value::Decimal(_) => "signed exact numeric",
Value::Uint(_) | Value::Uint128(_) => "unsigned exact numeric",
Value::Float(_) | Value::Float32(_) => "approximate numeric",
Value::String(_) => "STRING",
Value::Bytes(_) => "BYTES",
Value::List(_) => "LIST",
Value::Record(_) | Value::RecordTyped(_) => "RECORD",
Value::Path(_) => "PATH",
Value::ZonedDateTime(_) | Value::LocalDateTime(_) | Value::Date(_) => "datetime",
Value::ZonedTime(_) | Value::LocalTime(_) => "time",
Value::Duration(_) => "DURATION",
Value::Null => "NULL",
Value::NodeRef(_) | Value::EdgeRef(_) | Value::GraphRef(_) | Value::TableRef(_) => {
return None;
}
Value::Extended { .. } | Value::Uuid(_) | Value::Vector(_) | Value::Json(_) => return None,
_ => return None,
})
}
fn string_to_boolean(text: &str, span: SourceSpan) -> Result<Value, ExecutorError> {
match text.trim().to_ascii_lowercase().as_str() {
"true" => Ok(Value::Bool(true)),
"false" => Ok(Value::Bool(false)),
_ => Err(invalid_character(text, "BOOLEAN", span)),
}
}
fn invalid_character(text: &str, target: &str, span: SourceSpan) -> ExecutorError {
ExecutorError::data_exception(
DataExceptionSubclass::InvalidCharacterValueForCast,
format!("STRING value `{text}` is not a valid {target}"),
span,
)
}
fn format_float(f: f64) -> String {
if f.is_nan() {
"NaN".to_owned()
} else if f.is_infinite() {
if f > 0.0 { "Infinity" } else { "-Infinity" }.to_owned()
} else {
format!("{f}")
}
}
fn cast_to_type_feature(target: &GqlType) -> &'static str {
match target {
GqlType::DecimalExact(_) => "CAST to DECIMAL",
GqlType::CharacterString(_) => "CAST to STRING",
GqlType::Bytes | GqlType::ByteString(_) => "CAST to BYTES",
GqlType::ZonedDateTime => "CAST to ZONED DATETIME",
GqlType::LocalDateTime => "CAST to LOCAL DATETIME",
GqlType::Date => "CAST to DATE",
GqlType::ZonedTime => "CAST to ZONED TIME",
GqlType::LocalTime => "CAST to LOCAL TIME",
GqlType::Duration | GqlType::DurationYearToMonth | GqlType::DurationDayToSecond => {
"CAST to DURATION"
}
GqlType::Vector => "CAST to VECTOR",
GqlType::Json => "CAST to JSON",
GqlType::Record(_) => "CAST to RECORD",
GqlType::ClosedDynamicUnion(_) => "CAST to closed dynamic union",
GqlType::NotNull(inner) => cast_to_type_feature(inner),
GqlType::Path => "CAST to PATH",
GqlType::GraphRef => "CAST to GRAPH",
GqlType::NodeRef => "CAST to NODE",
GqlType::EdgeRef => "CAST to EDGE",
GqlType::TableRef(_) => "CAST to TABLE",
_ => "CAST to unsupported target type",
}
}
#[cfg(test)]
mod tests;