Prevent overwriting of base game cells is_base_game value

This fixes a bug that was causing cell data to not get written since
November 9th, 2023. When batch inserting cells while processing plugins,
I allowed overwriting cells that had `is_base_game = true`. Since I
always set `is_base_game = false` for cell upserts from plugins, this
was causing the base game cells to revert to `is_base_game = false`. All
it took was one mod to bundle `Skyrim.esm` for this to happen.

This broke writing cell data since `get_cell_data` depends on the
`is_base_game` value to find edits to the Skyrim base game cells.

To prevent this in the future, batch inserts when processing plugins is
no longer allowed to update cells which have `is_base_game = true`. The
only time we allow upserting these rows is when running the
`is_base_game` backfill which initially seeds the database with the base
game cells.
This commit is contained in:
2025-03-02 14:15:05 -05:00
parent a5135b30f4
commit a677325c4d
4 changed files with 62 additions and 31 deletions

View File

@@ -60,7 +60,7 @@ pub async fn backfill_is_base_game(pool: &sqlx::Pool<sqlx::Postgres>) -> Result<
}
})
.collect();
let db_cells = cell::batched_insert(pool, &base_cells).await?;
let db_cells = cell::batched_insert(pool, &base_cells, true).await?;
info!("Upserted {} Skyrim.esm base cells", db_cells.len());
// This works for exterior cells, but there's a bug with the unique index on cells that
// creates duplicate interior cells. To fix that, I need to upgrade postgres to

View File

@@ -80,6 +80,7 @@ pub async fn insert(
pub async fn batched_insert<'a>(
pool: &sqlx::Pool<sqlx::Postgres>,
cells: &[UnsavedCell<'a>],
allow_upserting_base_game_cells: bool,
) -> Result<Vec<Cell>> {
let mut saved_cells = vec![];
for batch in cells.chunks(BATCH_SIZE) {
@@ -99,27 +100,58 @@ pub async fn batched_insert<'a>(
is_persistents.push(unsaved_cell.is_persistent);
is_base_games.push(unsaved_cell.is_base_game);
});
saved_cells.append(
// sqlx doesn't understand arrays of Options with the query_as! macro
&mut sqlx::query_as(
r#"INSERT INTO cells (form_id, master, x, y, world_id, is_persistent, is_base_game, created_at, updated_at)
SELECT *, now(), now() FROM UNNEST($1::int[], $2::text[], $3::int[], $4::int[], $5::int[], $6::bool[], $7::bool[])
ON CONFLICT (form_id, master, world_id) DO UPDATE
SET (x, y, is_persistent, is_base_game, updated_at) =
(EXCLUDED.x, EXCLUDED.y, EXCLUDED.is_persistent, EXCLUDED.is_base_game, now())
RETURNING *"#,
)
.bind(&form_ids)
.bind(&masters)
.bind(&xs)
.bind(&ys)
.bind(&world_ids)
.bind(&is_persistents)
.bind(&is_base_games)
.fetch_all(pool)
.await
.context("Failed to insert cells")?,
);
if allow_upserting_base_game_cells {
saved_cells.append(
// sqlx doesn't understand arrays of Options with the query_as! macro
// NOTE: allows overwriting base game cells. This should only be run in the
// `is_base_game` backfill in order to seed the database with base game cells.
&mut sqlx::query_as(
r#"INSERT INTO cells (form_id, master, x, y, world_id, is_persistent, is_base_game, created_at, updated_at)
SELECT *, now(), now() FROM UNNEST($1::int[], $2::text[], $3::int[], $4::int[], $5::int[], $6::bool[], $7::bool[])
ON CONFLICT (form_id, master, world_id) DO UPDATE
SET (x, y, is_persistent, is_base_game, updated_at) =
(EXCLUDED.x, EXCLUDED.y, EXCLUDED.is_persistent, EXCLUDED.is_base_game, now())
RETURNING *"#,
)
.bind(&form_ids)
.bind(&masters)
.bind(&xs)
.bind(&ys)
.bind(&world_ids)
.bind(&is_persistents)
.bind(&is_base_games)
.fetch_all(pool)
.await
.context("Failed to insert cells")?,
);
} else {
saved_cells.append(
// sqlx doesn't understand arrays of Options with the query_as! macro
// NOTE: excludes upserts on cells that have is_base_game = true since if we are trying
// to update base game cells that means a mod bundled the base game Skyrim.esm and we
// should ignore it. Additionally, overwriting `is_base_game` to false here will break dumping cell
// data since we rely on that field to find edits to Skyrim cells in `get_cell_data`.
&mut sqlx::query_as(
r#"INSERT INTO cells (form_id, master, x, y, world_id, is_persistent, is_base_game, created_at, updated_at)
SELECT *, now(), now() FROM UNNEST($1::int[], $2::text[], $3::int[], $4::int[], $5::int[], $6::bool[], $7::bool[])
ON CONFLICT (form_id, master, world_id) DO UPDATE
SET (x, y, is_persistent, is_base_game, updated_at) =
(EXCLUDED.x, EXCLUDED.y, EXCLUDED.is_persistent, EXCLUDED.is_base_game, now())
WHERE NOT cells.is_base_game
RETURNING *"#,
)
.bind(&form_ids)
.bind(&masters)
.bind(&xs)
.bind(&ys)
.bind(&world_ids)
.bind(&is_persistents)
.bind(&is_base_games)
.fetch_all(pool)
.await
.context("Failed to insert cells")?,
);
}
}
Ok(saved_cells)
}
@@ -251,6 +283,5 @@ pub async fn get_cell_data(
.fetch_one(pool)
.await
.context("Failed get cell data")
}
}

View File

@@ -128,7 +128,7 @@ pub async fn process_plugin(
}
})
.collect();
let db_cells = cell::batched_insert(&pool, &cells).await?;
let db_cells = cell::batched_insert(&pool, &cells, false).await?;
let plugin_cells: Vec<UnsavedPluginCell> = db_cells
.iter()
.zip(&plugin.cells)