#![cfg(test)]
use crate::{
ConflictTarget, ModelColumn, ModelDescriptor, SqlColumnValue, SqlValue, SqlxRuntime,
UpsertRecord, UpsertRecordDoNothing,
};
struct Marker {
_id: i64,
}
struct UpsertMarkerInput {
k: String,
}
impl cratestack_sql::CreateModelInput<Marker> for UpsertMarkerInput {
fn sql_values(&self) -> Vec<SqlColumnValue> {
vec![SqlColumnValue {
column: "k",
value: SqlValue::String(self.k.clone()),
}]
}
}
impl cratestack_sql::UpsertModelInput<Marker> for UpsertMarkerInput {
fn sql_values(&self) -> Vec<SqlColumnValue> {
<Self as cratestack_sql::CreateModelInput<Marker>>::sql_values(self)
}
fn primary_key_value(&self) -> SqlValue {
SqlValue::Int(0)
}
}
const COLUMNS: &[ModelColumn] = &[
ModelColumn {
rust_name: "id",
sql_name: "id",
},
ModelColumn {
rust_name: "k",
sql_name: "k",
},
];
static UPSERT_UPDATE_COLUMNS: &[&str] = &["k"];
static MARKER_DESCRIPTOR: ModelDescriptor<Marker, i64> = ModelDescriptor::new(
"Marker",
"markers",
COLUMNS,
"id",
&[],
&[],
&[],
&[],
&[],
&[],
&[],
&[],
&[],
&[],
&[],
&[],
&[],
&[],
&[],
None,
false,
&[],
&[],
None,
None,
UPSERT_UPDATE_COLUMNS,
);
fn runtime() -> SqlxRuntime {
let pool = crate::sqlx::pool::PoolOptions::<crate::sqlx::Postgres>::new()
.connect_lazy("postgres://cratestack:cratestack@localhost/cratestack")
.expect("lazy pool should parse");
SqlxRuntime::new(pool)
}
#[tokio::test]
async fn do_update_preview_sql_is_byte_identical_when_unpredicated() {
let runtime = runtime();
let record = UpsertRecord {
runtime: &runtime,
descriptor: &MARKER_DESCRIPTOR,
input: UpsertMarkerInput { k: "x".into() },
conflict_target: ConflictTarget::columns(&["k"]),
};
let sql = record.preview_sql();
assert!(
sql.contains("ON CONFLICT (k) DO UPDATE SET"),
"unpredicated ON CONFLICT must render with no WHERE clause, got: {sql}",
);
assert!(!sql.contains("WHERE"), "got: {sql}");
}
#[tokio::test]
async fn do_update_preview_sql_renders_predicate() {
let runtime = runtime();
let record = UpsertRecord {
runtime: &runtime,
descriptor: &MARKER_DESCRIPTOR,
input: UpsertMarkerInput { k: "x".into() },
conflict_target: ConflictTarget::columns(&["k"]).where_index("status = 'active'"),
};
let sql = record.preview_sql();
assert!(
sql.contains("ON CONFLICT (k) WHERE status = 'active' DO UPDATE SET"),
"got: {sql}",
);
}
#[tokio::test]
async fn do_nothing_preview_sql_is_byte_identical_when_unpredicated() {
let runtime = runtime();
let record = UpsertRecordDoNothing {
runtime: &runtime,
descriptor: &MARKER_DESCRIPTOR,
input: UpsertMarkerInput { k: "x".into() },
conflict_target: ConflictTarget::columns(&["k"]),
};
let sql = record.preview_sql();
assert!(
sql.contains("ON CONFLICT (k) DO NOTHING"),
"unpredicated ON CONFLICT must render with no WHERE clause, got: {sql}",
);
assert!(!sql.contains("WHERE"), "got: {sql}");
}
#[tokio::test]
async fn do_nothing_preview_sql_renders_predicate() {
let runtime = runtime();
let record = UpsertRecordDoNothing {
runtime: &runtime,
descriptor: &MARKER_DESCRIPTOR,
input: UpsertMarkerInput { k: "x".into() },
conflict_target: ConflictTarget::columns(&["k"]).where_index("k IS NOT NULL"),
};
let sql = record.preview_sql();
assert!(
sql.contains("ON CONFLICT (k) WHERE k IS NOT NULL DO NOTHING"),
"got: {sql}",
);
}
#[test]
fn predicate_on_primary_key_is_rejected() {
let target = ConflictTarget::PRIMARY_KEY.where_index("status = 'active'");
let err = target.validate().expect_err("PK + predicate must error");
let message = err.to_string();
assert!(
message.contains("primary key"),
"error should explain why, got: {message}",
);
}
#[test]
fn unpredicated_targets_validate_cleanly() {
ConflictTarget::PRIMARY_KEY.validate().unwrap();
ConflictTarget::columns(&["k"]).validate().unwrap();
ConflictTarget::columns(&["k"])
.where_index("k IS NOT NULL")
.validate()
.unwrap();
}
#[test]
fn non_exhaustive_does_not_block_construction_of_any_variant() {
let pk = ConflictTarget::PrimaryKey;
let cols = ConflictTarget::Columns(&["k"]);
assert!(pk.is_primary_key());
assert_eq!(cols.as_columns(), Some(["k"].as_slice()));
let pk_with_predicate = ConflictTarget::PrimaryKey.where_index("status = 'active'");
let cols_with_predicate = ConflictTarget::Columns(&["k"]).where_index("status = 'active'");
assert_eq!(pk_with_predicate.predicate(), Some("status = 'active'"));
assert_eq!(cols_with_predicate.predicate(), Some("status = 'active'"));
assert_eq!(cols_with_predicate.as_columns(), Some(["k"].as_slice()));
assert_eq!(ConflictTarget::default(), ConflictTarget::PrimaryKey);
}
#[tokio::test]
async fn do_update_preview_sql_does_not_validate_pk_plus_predicate() {
let runtime = runtime();
let bad_target = ConflictTarget::PRIMARY_KEY.where_index("status = 'active'");
assert!(bad_target.validate().is_err(), "sanity: must be invalid");
let record = UpsertRecord {
runtime: &runtime,
descriptor: &MARKER_DESCRIPTOR,
input: UpsertMarkerInput { k: "x".into() },
conflict_target: bad_target,
};
let sql = record.preview_sql();
assert!(
sql.contains("ON CONFLICT (id) WHERE status = 'active' DO UPDATE SET"),
"preview must still render even though the target is invalid, got: {sql}",
);
}