use crate::core::QueryError;
#[derive(Debug, thiserror::Error)]
pub enum SqlError {
#[error("`Op::In` requires `SqlValue::List`")]
InRequiresList,
#[error("`Op::IsNull` requires `SqlValue::Bool` (true = IS NULL, false = IS NOT NULL)")]
IsNullRequiresBool,
#[error("`Op::Between` requires `SqlValue::List([lo, hi])` with exactly two elements")]
BetweenRequiresTwoElementList,
#[error("`Op::JsonHasKey` requires `SqlValue::String`")]
JsonKeyRequiresString,
#[error("`Op::JsonHasAnyKey` / `Op::JsonHasAllKeys` require `SqlValue::List` of strings")]
JsonKeysRequiresList,
#[error("`Op::JsonContains` / `Op::JsonContainedBy` require `SqlValue::Json`")]
JsonOpRequiresJson,
#[error(
"`Op::ArrayContains` / `Op::ArrayContainedBy` / `Op::ArrayOverlap` require `SqlValue::Array`"
)]
ArrayOpRequiresArray,
#[error("bulk UPDATE requires a primary key on the model")]
MissingPrimaryKey,
#[error("relation aggregate `{kind}` requires a target column")]
RelAggregateMissingColumn { kind: &'static str },
#[error("empty `IN` list is not supported")]
EmptyInList,
#[error("INSERT requires at least one column")]
EmptyInsert,
#[error("INSERT columns ({columns}) and values ({values}) length mismatch")]
InsertShapeMismatch { columns: usize, values: usize },
#[error("UPDATE requires at least one assignment in `set`")]
EmptyUpdateSet,
#[error("bulk INSERT requires at least one row")]
EmptyBulkInsert,
#[error("bulk INSERT requires every row's `Auto<T>` PKs to agree on Set vs Unset; mixed Set/Unset is not supported")]
BulkAutoMixed,
#[error("bulk INSERT RETURNING returned {actual} rows but {expected} were inserted")]
BulkInsertReturningMismatch { expected: usize, actual: usize },
#[error("`WhereExpr::Or` with an empty branch list matches no rows; was that intentional?")]
EmptyOrBranch,
#[error("`WhereExpr::Xor` with an empty branch list matches no rows; was that intentional?")]
EmptyXorBranch,
#[error(
"{dialect} dialect query compilation is not implemented yet — \
lands in a future rustango v0.23.0 batch."
)]
DialectQueryCompilationNotImplemented { dialect: &'static str },
#[error("operator `{op}` is not supported by the `{dialect}` dialect")]
OperatorNotSupportedInDialect {
op: &'static str,
dialect: &'static str,
},
#[error("ON CONFLICT shape `{shape}` is not supported by the `{dialect}` dialect")]
ConflictNotSupportedInDialect {
shape: &'static str,
dialect: &'static str,
},
#[error("operator `{op}` is not supported by the `{dialect}` dialect")]
OpNotSupportedInDialect {
op: &'static str,
dialect: &'static str,
},
#[error("aggregate `{aggregate}` is not supported by the `{dialect}` dialect")]
AggregateNotSupportedInDialect {
aggregate: &'static str,
dialect: &'static str,
},
#[error("function `{func}` expects {expected} arg(s), got {got}")]
FunctionArityMismatch {
func: &'static str,
expected: &'static str,
got: usize,
},
#[error("CASE expression must have at least one WHEN branch")]
EmptyCaseBranches,
#[error("CASE WHEN branch condition must not be empty")]
EmptyCaseWhenCondition,
#[error(
"`OuterRef(\"{column}\")` used outside of a subquery — \
it can only appear inside Exists / NotExists / InSubquery / \
Subquery wrappers that know the enclosing query's table"
)]
OuterRefOutsideSubquery { column: &'static str },
#[error(
"`Expr::Aggregate(...)` used outside of an aggregate-accepting \
SQL slot — aggregates may only appear in SELECT projection, \
HAVING predicate, or ORDER BY of an aggregating query"
)]
AggregateOutsideAggregateContext,
#[error("JOIN `on` predicate must not be empty")]
EmptyJoinOnCondition,
#[error("aggregate `{aggregate}` is not supported by the `{dialect}` dialect")]
AggregateNotSupported {
aggregate: &'static str,
dialect: &'static str,
},
#[error("nested aggregate wrapper `{wrapper}` is not supported")]
NestedAggregateWrapper { wrapper: &'static str },
#[error("`{kind} JOIN` is not supported by the `{dialect}` dialect")]
JoinKindNotSupported {
kind: &'static str,
dialect: &'static str,
},
#[error(
"`JOIN LATERAL` is not supported by the `{dialect}` dialect (PostgreSQL / MySQL only)"
)]
LateralJoinNotSupported { dialect: &'static str },
}
#[derive(Debug, thiserror::Error)]
pub enum ExecError {
#[error(transparent)]
Query(#[from] QueryError),
#[error(transparent)]
Sql(#[from] SqlError),
#[error(transparent)]
Driver(#[from] sqlx::Error),
#[error("`insert_returning` requires `query.returning` to be non-empty; use `insert` instead")]
EmptyReturning,
#[error("foreign-key target `{table}` has no row with primary key {pk}")]
ForeignKeyTargetMissing {
table: &'static str,
pk: String,
},
#[error("model `{table}` has no `#[rustango(primary_key)]` field — required for FK lookup")]
MissingPrimaryKey { table: &'static str },
#[error("no content type registered for model `{table}` — seed `rustango_content_types` (run migrate)")]
ContentTypeNotRegistered { table: &'static str },
#[error("`{op}` filter matched {count} rows on `{table}`; expected at most 1")]
MultipleRowsReturned {
op: &'static str,
table: &'static str,
count: usize,
},
}
#[cfg(feature = "mysql")]
#[must_use]
pub fn is_mysql_dup_index_error(e: &crate::sql::sqlx::Error) -> bool {
if let crate::sql::sqlx::Error::Database(db) = e {
return db.code().as_deref() == Some("42000")
|| db.message().contains("Duplicate key name");
}
false
}
#[cfg(not(feature = "mysql"))]
#[must_use]
pub fn is_mysql_dup_index_error(_e: &crate::sql::sqlx::Error) -> bool {
false
}
#[cfg(feature = "postgres")]
#[must_use]
pub fn is_pg_dup_object_error(e: &crate::sql::sqlx::Error) -> bool {
if let crate::sql::sqlx::Error::Database(db) = e {
return pg_dup_object_decision(db.code().as_deref(), &db.message());
}
false
}
#[cfg_attr(not(feature = "postgres"), allow(dead_code))]
pub(crate) fn pg_dup_object_decision(code: Option<&str>, message: &str) -> bool {
match code {
Some("42P07" | "42710") => true,
Some("23505") => [
"pg_class_relname_nsp_index",
"pg_type_typname_nsp_index",
"pg_namespace_nspname_index",
]
.iter()
.any(|idx| message.contains(idx)),
_ => false,
}
}
#[cfg(all(test, feature = "postgres"))]
mod pg_dup_object_tests {
use super::pg_dup_object_decision as decide;
#[test]
fn the_race_shapes_are_swallowed() {
assert!(decide(
Some("42P07"),
"relation \"rustango_jobs\" already exists"
));
assert!(decide(Some("42710"), "object already exists"));
assert!(decide(
Some("23505"),
"duplicate key value violates unique constraint \
\"pg_class_relname_nsp_index\""
));
assert!(
decide(
Some("23505"),
"duplicate key value violates unique constraint \
\"pg_type_typname_nsp_index\""
),
"a racing CREATE TABLE can lose on the composite-type row rather than \
the relation row; missing it leaves half of #1458 unfixed"
);
assert!(decide(
Some("23505"),
"duplicate key value violates unique constraint \
\"pg_namespace_nspname_index\""
));
}
#[test]
fn an_ordinary_unique_violation_is_not_swallowed() {
assert!(
!decide(
Some("23505"),
"duplicate key value violates unique constraint \"users_email_key\""
),
"a unique violation on application data must stay an error — swallowing \
it would hide real bugs, which is worse than the race being fixed"
);
assert!(!decide(Some("23503"), "foreign key violation"));
assert!(!decide(Some("42P01"), "relation does not exist"));
assert!(!decide(None, "pg_class_relname_nsp_index"));
}
}
#[cfg(not(feature = "postgres"))]
#[must_use]
pub fn is_pg_dup_object_error(_e: &crate::sql::sqlx::Error) -> bool {
false
}