use std::collections::BTreeMap;
use uqa_core::Value;
use uqa_execution::ScalarExpr;
use uqa_graph::agtype;
use uqa_sql::SQLError;
use crate::Engine;
pub(super) fn build_rows(
engine: &Engine,
args: &[ScalarExpr],
evaluated: &[Value],
_qualifier: Option<&str>,
column_aliases: &[String],
column_types: &[String],
) -> Result<Vec<Vec<Value>>, SQLError> {
if !(2..=3).contains(&evaluated.len()) {
return Err(SQLError::TypeMismatch(
"cypher requires 2-3 args (graph_name, query_string[, parameters])".into(),
));
}
if column_aliases.is_empty() {
return Err(SQLError::TypeMismatch(
"cypher requires a record definition: AS (column agtype, ...)".into(),
));
}
if args.len() == 3 && !is_valid_parameter_expr(&args[2]) {
return Err(SQLError::TypeMismatch(
"cypher parameters must be supplied through an SQL parameter".into(),
));
}
let graph = match &evaluated[0] {
Value::Str(s) => s.clone(),
_ => {
return Err(SQLError::TypeMismatch(
"cypher.graph_name must be string".into(),
))
}
};
let query = match &evaluated[1] {
Value::Str(s) => s.clone(),
_ => {
return Err(SQLError::TypeMismatch(
"cypher.query_string must be string".into(),
))
}
};
if engine.current_transaction_is_read_only() && query_is_mutating(&query)? {
return Err(SQLError::Routine {
sqlstate: "25006".into(),
message: "cannot execute SELECT in a read-only transaction".into(),
});
}
if !engine
.has_graph(&graph)
.map_err(|err| SQLError::Internal(format!("read graph catalog: {err}")))?
{
return Err(SQLError::Unsupported(format!(
"graph \"{graph}\" does not exist"
)));
}
let params = match evaluated.get(2) {
Some(value) => parameter_map(value)?,
None => BTreeMap::new(),
};
let (cypher_columns, cypher_rows) =
engine
.run_cypher(&graph, &query, params)
.map_err(|error| match error {
uqa_graph::cypher::CypherError::MissingLabelRelation(relation) => {
SQLError::UnknownTable(relation)
}
other => SQLError::Unsupported(format!("cypher: {other}")),
})?;
if !cypher_columns.is_empty() && cypher_columns.len() != column_aliases.len() {
return Err(SQLError::TypeMismatch(
"return row and column definition list do not match".into(),
));
}
let mut out = Vec::with_capacity(cypher_rows.len());
for src in cypher_rows {
let mut row = Vec::with_capacity(column_aliases.len());
for (idx, target_col) in column_aliases.iter().enumerate() {
let value = cypher_columns
.get(idx)
.and_then(|source_col| src.get(source_col))
.cloned()
.unwrap_or(Value::Null);
let declared = column_types.get(idx).map_or("agtype", String::as_str);
let value = coerce_to_column_type(value, declared, target_col)?;
row.push(value);
}
out.push(row);
}
Ok(out)
}
pub(super) fn query_is_mutating(query: &str) -> Result<bool, SQLError> {
uqa_graph::cypher::parse_cypher(query)
.map(|query| query.mutates_graph())
.map_err(|error| SQLError::Unsupported(format!("cypher: {error}")))
}
fn coerce_to_column_type(value: Value, declared: &str, column: &str) -> Result<Value, SQLError> {
match declared {
"agtype" | "" => Ok(match value {
Value::Null => Value::Null,
other => Value::Str(agtype::render(&other)),
}),
"int2" | "smallint" => coerce_int(value, i64::from(i16::MIN), i64::from(i16::MAX)),
"int4" | "int" | "integer" => coerce_int(value, i64::from(i32::MIN), i64::from(i32::MAX)),
"int8" | "bigint" => coerce_int(value, i64::MIN, i64::MAX),
"float4" | "float8" | "float" | "real" | "double precision" => coerce_float(value),
"text" | "varchar" | "bpchar" | "char" | "name" => coerce_text(value),
"bool" | "boolean" => coerce_bool(value),
other => Err(SQLError::TypeMismatch(format!(
"cannot cast type agtype to {other} for column \"{column}\""
))),
}
}
fn parse_agtype_scalar(s: &str) -> Result<Value, SQLError> {
let trimmed = s.trim();
if let Ok(n) = trimmed.parse::<i64>() {
return Ok(Value::Int(n));
}
if let Ok(f) = trimmed.parse::<f64>() {
return Ok(Value::Float(f));
}
match trimmed {
"true" => Ok(Value::Bool(true)),
"false" => Ok(Value::Bool(false)),
"null" => Ok(Value::Null),
_ => Err(SQLError::TypeMismatch(format!(
"invalid input syntax for type agtype: expected agtype value, but found \"{s}\""
))),
}
}
fn coerce_int(value: Value, min: i64, max: i64) -> Result<Value, SQLError> {
let out = match value {
Value::Null => return Ok(Value::Null),
Value::Int(n) => n,
Value::Float(f) => {
let rounded = f.round_ties_even();
let above_max = if max == i64::MAX {
rounded >= 9_223_372_036_854_775_808.0
} else {
rounded > max as f64
};
if !rounded.is_finite() || rounded < min as f64 || above_max {
return Err(SQLError::TypeMismatch("integer out of range".into()));
}
rounded as i64
}
Value::Bool(b) => i64::from(b),
Value::Str(s) => {
return coerce_int(parse_agtype_scalar(&s)?, min, max);
}
other => {
return Err(SQLError::TypeMismatch(format!(
"cannot cast agtype {} to type integer",
agtype::agtype_type_name(&other)
)));
}
};
if out < min || out > max {
return Err(SQLError::TypeMismatch("integer out of range".into()));
}
Ok(Value::Int(out))
}
fn coerce_float(value: Value) -> Result<Value, SQLError> {
match value {
Value::Null => Ok(Value::Null),
Value::Int(n) => Ok(Value::Float(n as f64)),
Value::Float(f) => Ok(Value::Float(f)),
Value::Str(s) => coerce_float(parse_agtype_scalar(&s)?),
other => Err(SQLError::TypeMismatch(format!(
"cannot cast agtype {} to type float",
agtype::agtype_type_name(&other)
))),
}
}
fn coerce_text(value: Value) -> Result<Value, SQLError> {
match value {
Value::Null => Ok(Value::Null),
Value::Str(s) => Ok(Value::Str(s)),
other => {
if agtype::entity_kind(&other).is_some() {
return Err(SQLError::TypeMismatch(format!(
"agtype_value_to_text: unsupported argument agtype {}",
agtype::agtype_type_ordinal(&other)
)));
}
Ok(Value::Str(agtype::render(&other)))
}
}
}
fn coerce_bool(value: Value) -> Result<Value, SQLError> {
match value {
Value::Null => Ok(Value::Null),
Value::Bool(b) => Ok(Value::Bool(b)),
other => Err(SQLError::TypeMismatch(format!(
"cannot cast agtype {} to type boolean",
agtype::agtype_type_name(&other)
))),
}
}
fn is_valid_parameter_expr(expr: &ScalarExpr) -> bool {
matches!(
expr,
ScalarExpr::Param(_) | ScalarExpr::Literal(Value::Null)
)
}
fn parameter_map(value: &Value) -> Result<BTreeMap<String, Value>, SQLError> {
match value {
Value::Null => Ok(BTreeMap::new()),
Value::Map(map) => Ok(map.clone()),
Value::Str(s) => {
let parsed = serde_json::from_str::<serde_json::Value>(s)
.map_err(|e| SQLError::TypeMismatch(format!("invalid cypher parameters: {e}")))?;
match super::json_to_core_value(parsed) {
Value::Map(map) => Ok(map),
_ => Err(SQLError::TypeMismatch(
"cypher parameters must be a map".into(),
)),
}
}
_ => Err(SQLError::TypeMismatch(
"cypher parameters must be a map".into(),
)),
}
}