use crate::sql::Pool;
#[cfg_attr(not(feature = "mysql"), allow(dead_code))]
const MYSQL_DUPLICATES: &[u16] = &[1050, 1061, 1826];
#[cfg_attr(not(feature = "mysql"), allow(dead_code))]
fn is_mysql_duplicate(number: u16) -> bool {
MYSQL_DUPLICATES.contains(&number)
}
fn is_already_exists(e: &crate::sql::ExecError, dialect: &str) -> bool {
let crate::sql::ExecError::Driver(err) = e else {
return false;
};
match dialect {
"postgres" => crate::sql::is_pg_dup_object_error(err),
#[cfg(feature = "mysql")]
"mysql" => err
.as_database_error()
.and_then(|db| db.try_downcast_ref::<sqlx::mysql::MySqlDatabaseError>())
.is_some_and(|my| is_mysql_duplicate(my.number())),
_ => {
let msg = format!("{e}").to_lowercase();
msg.contains("already exists") || msg.contains("duplicate")
}
}
}
fn idempotent<'a>(stmt: &'a str, dialect: &str) -> std::borrow::Cow<'a, str> {
if dialect != "postgres" {
return std::borrow::Cow::Borrowed(stmt);
}
match stmt.strip_prefix("CREATE TABLE ") {
Some(rest) if !rest.starts_with("IF NOT EXISTS") => {
std::borrow::Cow::Owned(format!("CREATE TABLE IF NOT EXISTS {rest}"))
}
_ => std::borrow::Cow::Borrowed(stmt),
}
}
pub(crate) async fn apply_idempotent(
pool: &Pool,
batch: &super::RenderedBatch,
) -> Result<(), sqlx::Error> {
let dialect = pool.dialect().name();
for stmt in batch.immediate.iter().chain(batch.deferred_fks.iter()) {
let stmt = idempotent(stmt, dialect);
if let Err(e) = crate::sql::raw_execute_pool(pool, &stmt, Vec::new()).await {
if is_already_exists(&e, dialect) {
continue;
}
return Err(match e {
crate::sql::ExecError::Driver(err) => err,
other => sqlx::Error::Protocol(format!("{other}")),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{idempotent, is_mysql_duplicate};
#[test]
fn create_table_gains_if_not_exists_on_postgres() {
assert_eq!(
idempotent(r#"CREATE TABLE "t" ("id" BIGSERIAL)"#, "postgres"),
r#"CREATE TABLE IF NOT EXISTS "t" ("id" BIGSERIAL)"#
);
}
#[test]
fn other_dialects_are_left_alone() {
let s = r#"CREATE TABLE "t" ("id" BIGSERIAL)"#;
assert_eq!(idempotent(s, "mysql"), s);
assert_eq!(idempotent(s, "sqlite"), s);
}
#[test]
fn already_guarded_create_is_left_alone() {
let s = r#"CREATE TABLE IF NOT EXISTS "t" ("id" BIGSERIAL)"#;
assert_eq!(idempotent(s, "postgres"), s, "must not double up the guard");
}
#[test]
fn non_create_table_statements_are_untouched() {
let s =
r#"ALTER TABLE "a" ADD CONSTRAINT "a_b_fkey" FOREIGN KEY ("b") REFERENCES "b" ("id")"#;
assert_eq!(idempotent(s, "postgres"), s);
let idx = r#"CREATE UNIQUE INDEX "i" ON "t" ("a")"#;
assert_eq!(idempotent(idx, "postgres"), idx);
}
#[test]
fn the_renderer_output_is_actually_rewritten() {
use crate::core::Model as _;
let snapshot = crate::migrate::SchemaSnapshot::from_models(&[
crate::contenttypes::ContentType::SCHEMA,
]);
let changes =
crate::migrate::detect_changes(&crate::migrate::SchemaSnapshot::default(), &snapshot);
let batch = crate::migrate::render_changes_split_with_dialect(
&changes,
&snapshot,
&crate::sql::Postgres,
)
.expect("render");
let creates: Vec<_> = batch
.immediate
.iter()
.filter(|s| s.contains("CREATE TABLE"))
.collect();
assert!(!creates.is_empty(), "expected at least one CREATE TABLE");
for stmt in creates {
assert!(
idempotent(stmt, "postgres").starts_with("CREATE TABLE IF NOT EXISTS"),
"renderer emits a shape `idempotent` does not rewrite, so the \
ensure paths are back to erroring per call: {stmt}"
);
}
}
#[cfg(feature = "sqlite")]
#[tokio::test]
async fn apply_idempotent_runs_both_lists_and_still_propagates() {
let pool = crate::sql::Pool::connect("sqlite::memory:")
.await
.expect("sqlite");
let batch = super::super::RenderedBatch {
immediate: vec!["CREATE TABLE a (id INTEGER PRIMARY KEY)".to_owned()],
deferred_fks: vec!["CREATE TABLE b (id INTEGER PRIMARY KEY)".to_owned()],
..Default::default()
};
super::apply_idempotent(&pool, &batch)
.await
.expect("first run creates both");
for t in ["a", "b"] {
crate::sql::raw_execute_pool(&pool, &format!("SELECT 1 FROM {t}"), Vec::new())
.await
.unwrap_or_else(|e| panic!("{t} was never created — deferred_fks skipped? {e}"));
}
super::apply_idempotent(&pool, &batch)
.await
.expect("re-running an ensure must be idempotent");
let bad = super::super::RenderedBatch {
immediate: vec!["CREATE TABLE c (id INTEGER PRIMARY KEY".to_owned()],
..Default::default()
};
assert!(
super::apply_idempotent(&pool, &bad).await.is_err(),
"a syntax error must propagate; swallowing everything would \
make every ensure silently succeed"
);
}
#[test]
fn mysql_duplicates_are_numbers() {
assert!(is_mysql_duplicate(1050), "ER_TABLE_EXISTS_ERROR");
assert!(is_mysql_duplicate(1061), "ER_DUP_KEYNAME");
assert!(is_mysql_duplicate(1826), "ER_FK_DUP_NAME");
assert!(!is_mysql_duplicate(1062), "ER_DUP_ENTRY must not swallow");
assert!(!is_mysql_duplicate(1064), "syntax error must not swallow");
assert!(!is_mysql_duplicate(1170), "TEXT-in-index must not swallow");
}
}