use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::dbms::table::{CandidDataTypeKind, JoinColumnDef};
use crate::dbms::transaction::TransactionId;
use crate::dbms::value::Value;
use crate::error::DbmsError;
pub type SqlRow = Vec<(JoinColumnDef, Value)>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "candid", derive(candid::CandidType))]
pub enum SqlResult {
Rows(Vec<SqlRow>),
RowsAffected(u64),
TxBegin(TransactionId),
TxCommit,
TxRollback,
}
#[derive(Debug, Error, Serialize, Deserialize)]
#[cfg_attr(feature = "candid", derive(candid::CandidType))]
pub enum SqlError {
#[error("SQL parse error at line {line}, column {col}: {msg}")]
Parse {
line: usize,
col: usize,
msg: String,
},
#[error("Unknown table: {0}")]
UnknownTable(String),
#[error("Unknown column '{column}' on table '{table}'")]
UnknownColumn { table: String, column: String },
#[error("Ambiguous column: {0}")]
AmbiguousColumn(String),
#[error("Type mismatch on column '{column}': expected {expected:?}, got {got}")]
TypeMismatch {
column: String,
expected: CandidDataTypeKind,
got: String,
},
#[error("Invalid value for column '{column}' of type {expected:?}: {reason}")]
InvalidLiteral {
column: String,
expected: CandidDataTypeKind,
reason: String,
},
#[error("UPDATE and DELETE require a WHERE clause")]
MissingWhereClause,
#[error("Statement expects {expected} parameters, got {got}")]
ParameterCountMismatch { expected: usize, got: usize },
#[error("BEGIN was issued inside an active transaction")]
TransactionAlreadyActive,
#[error("COMMIT or ROLLBACK was issued without an active transaction")]
NoActiveTransaction,
#[error("Unsupported SQL: {0}")]
Unsupported(String),
#[error("Runtime error: {0}")]
Runtime(#[from] DbmsError),
}
#[cfg(test)]
mod test {
use super::*;
use crate::dbms::query::QueryError;
use crate::dbms::table::{CandidDataTypeKind, JoinColumnDef};
use crate::dbms::transaction::TransactionError;
use crate::dbms::value::Value;
use crate::error::DbmsError;
fn sample_rows() -> SqlResult {
SqlResult::Rows(vec![vec![(
JoinColumnDef {
table: None,
name: "id".to_string(),
data_type: CandidDataTypeKind::Uint32,
nullable: false,
primary_key: true,
foreign_key: None,
},
Value::from(1u32),
)]])
}
#[test]
fn test_should_display_parse_error_with_position() {
let error = SqlError::Parse {
line: 2,
col: 7,
msg: "expected `FROM`, found end of input".to_string(),
};
assert_eq!(
error.to_string(),
"SQL parse error at line 2, column 7: expected `FROM`, found end of input"
);
}
#[test]
fn test_should_display_name_resolution_errors() {
assert_eq!(
SqlError::UnknownTable("ghosts".to_string()).to_string(),
"Unknown table: ghosts"
);
assert_eq!(
SqlError::UnknownColumn {
table: "users".to_string(),
column: "age".to_string(),
}
.to_string(),
"Unknown column 'age' on table 'users'"
);
assert_eq!(
SqlError::AmbiguousColumn("id".to_string()).to_string(),
"Ambiguous column: id"
);
}
#[test]
fn test_should_display_value_errors() {
assert_eq!(
SqlError::TypeMismatch {
column: "age".to_string(),
expected: CandidDataTypeKind::Uint32,
got: "String".to_string(),
}
.to_string(),
"Type mismatch on column 'age': expected Uint32, got String"
);
assert_eq!(
SqlError::InvalidLiteral {
column: "born".to_string(),
expected: CandidDataTypeKind::Date,
reason: "`2026-02-30` is not a valid date".to_string(),
}
.to_string(),
"Invalid value for column 'born' of type Date: `2026-02-30` is not a valid date"
);
assert_eq!(
SqlError::ParameterCountMismatch {
expected: 2,
got: 1,
}
.to_string(),
"Statement expects 2 parameters, got 1"
);
}
#[test]
fn test_should_display_statement_errors() {
assert_eq!(
SqlError::MissingWhereClause.to_string(),
"UPDATE and DELETE require a WHERE clause"
);
assert_eq!(
SqlError::TransactionAlreadyActive.to_string(),
"BEGIN was issued inside an active transaction"
);
assert_eq!(
SqlError::NoActiveTransaction.to_string(),
"COMMIT or ROLLBACK was issued without an active transaction"
);
assert_eq!(
SqlError::Unsupported("DISTINCT is not supported with JOIN".to_string()).to_string(),
"Unsupported SQL: DISTINCT is not supported with JOIN"
);
}
#[test]
fn test_should_wrap_dbms_error_as_runtime() {
let error: SqlError = DbmsError::Query(QueryError::PrimaryKeyConflict).into();
assert!(matches!(
error,
SqlError::Runtime(DbmsError::Query(QueryError::PrimaryKeyConflict))
));
assert_eq!(
error.to_string(),
"Runtime error: Query error: Primary key conflict: record with the same primary key \
already exists"
);
let error: SqlError = DbmsError::Transaction(TransactionError::NoActiveTransaction).into();
assert_eq!(
error.to_string(),
"Runtime error: Transaction error: No active transaction"
);
}
#[test]
fn test_should_round_trip_sql_result_through_serde() {
for result in [
sample_rows(),
SqlResult::RowsAffected(3),
SqlResult::TxBegin(7),
SqlResult::TxCommit,
SqlResult::TxRollback,
] {
let encoded = serde_json::to_string(&result).expect("failed to encode");
let decoded: SqlResult = serde_json::from_str(&encoded).expect("failed to decode");
assert_eq!(result, decoded);
}
}
#[test]
fn test_should_round_trip_sql_error_through_serde() {
let error = SqlError::Parse {
line: 1,
col: 8,
msg: "expected `FROM`".to_string(),
};
let encoded = serde_json::to_string(&error).expect("failed to encode");
let decoded: SqlError = serde_json::from_str(&encoded).expect("failed to decode");
assert!(matches!(
decoded,
SqlError::Parse {
line: 1,
col: 8,
..
}
));
}
#[cfg(feature = "candid")]
#[test]
fn test_should_candid_encode_decode_sql_types() {
let result = sample_rows();
let encoded = candid::encode_one(&result).expect("failed to encode");
let decoded: SqlResult = candid::decode_one(&encoded).expect("failed to decode");
assert_eq!(result, decoded);
let error = SqlError::TypeMismatch {
column: "age".to_string(),
expected: CandidDataTypeKind::Uint32,
got: "String".to_string(),
};
let encoded = candid::encode_one(&error).expect("failed to encode");
let decoded: SqlError = candid::decode_one(&encoded).expect("failed to decode");
assert!(matches!(decoded, SqlError::TypeMismatch { .. }));
}
}