Skip to content

Commit 49511df

Browse files
committed
Drop automatic migration relocation for a documented manual step
Remove the auto relocation + checksum re-stamp from setup() and the public relocate_legacy_migrations_table / reconcile_migration_checksums helpers. setup() now just runs migrations. Docs now cover the two failure modes (function-already-exists on a blind upgrade, and the empty apalis._sqlx_migrations that then blocks the ALTER TABLE).
1 parent fcfd12b commit 49511df

3 files changed

Lines changed: 15 additions & 93 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
- `generate_ulid` is now `apalis.generate_ulid` and no longer depends on `pgcrypto` — its random bytes come from core `gen_random_uuid()`. The sole caller (`apalis.push_job`) is repointed and the `public.generate_ulid` copy is dropped (via a new forward migration; existing migrations are not rewritten).
77
- The sqlx migrations table is tracked in `apalis._sqlx_migrations` instead of `public._sqlx_migrations` (configured in a new `sqlx.toml`). This also isolates apalis's migration history from a user's own sqlx migrations on the same database, which previously collided over the shared default table name.
88
- bump: upgrade `sqlx` 0.8 → 0.9 (required for `sqlx.toml`); remap the runtime/TLS cargo features since 0.9 removed the combined `runtime-*-tls` flags.
9-
- **Upgrade is automatic when you use `PostgresStorage::setup()`.** On first run it relocates an existing `public._sqlx_migrations` into the `apalis` schema and re-stamps checksums before running migrations, so existing deployments migrate with no manual steps and nothing is re-run. (The only edited migration is the first one — `CREATE SCHEMA``CREATE SCHEMA IF NOT EXISTS`, needed so the `apalis` schema can be created before the tracking table on fresh installs — whose checksum is healed automatically.)
10-
- **If you apply migrations yourself**, `setup()` doesn't run, so on an existing database you perform the one-time transition before upgrading (no-op on fresh databases). The relocation and checksum re-stamp are exposed as `PostgresStorage::relocate_legacy_migrations_table` and `PostgresStorage::reconcile_migration_checksums`:
11-
- sqlx CLI, or you copied the migration files into your own project: run the relocation + re-stamp SQL documented under "Upgrading to 1.0" in the README.
12-
- you merge `PostgresStorage::migrations()` into your own `Migrator`: your migrator keeps its own tracking table, so don't relocate — just call `reconcile_migration_checksums(&pool, "<your table>")` before running it to heal the one edited migration's checksum.
9+
- **Existing databases need a one-time manual step before upgrading** (fresh databases need nothing — `sqlx.toml` creates the `apalis` schema and tracking table). Move the migration history into the `apalis` schema and re-stamp the one edited migration's checksum — the first migration changed `CREATE SCHEMA``CREATE SCHEMA IF NOT EXISTS` so the `apalis` schema can be created before the tracking table on fresh installs. See "Upgrading to 1.0" in the README for the SQL; it applies to every apply path (`setup()`, sqlx-cli, copied migrations, or a merged `Migrator`).
1310
- note: `pgcrypto` is no longer used by apalis but is left where an earlier version installed it (usually `public`). If nothing else needs it, you can `DROP EXTENSION pgcrypto;`.
1411

1512
## [1.0.0-rc.8] - 2026-05-08

README.md

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -227,39 +227,33 @@ Track your jobs using [apalis-board](https://github.com/apalis-dev/apalis-board)
227227
- The sqlx migrations table is now tracked in `apalis._sqlx_migrations` (configured in `sqlx.toml`) instead of `public._sqlx_migrations`. This also keeps apalis's migration history from colliding with your own sqlx migrations on the same database.
228228
- `generate_ulid()` is now `apalis.generate_ulid()` and no longer depends on the `pgcrypto` extension — its random bytes come from core `gen_random_uuid()`. The `public.generate_ulid()` copy is dropped.
229229

230-
### If you use `PostgresStorage::setup()`
230+
### Existing databases: one-time manual step
231231

232-
Nothing to do. On the next start, `setup()` relocates an existing `public._sqlx_migrations` into the `apalis` schema and re-stamps checksums before running migrations, so the upgrade is automatic and nothing is re-run.
233-
234-
### If you apply migrations yourself (sqlx-cli, or you copied the migration files into your own project)
235-
236-
`setup()` is what performs the relocation, so paths that bypass it need one manual, one-time step **before** running the 1.0 migrations against an existing database:
232+
This applies to **every** way of applying migrations — `PostgresStorage::setup()`, sqlx-cli, copied migration files, or a merged `Migrator`. Run this **once per database, before upgrading**:
237233

238234
```sql
239235
-- Move apalis's existing migration history into the apalis schema.
240236
ALTER TABLE public._sqlx_migrations SET SCHEMA apalis;
241237

242238
-- The first migration gained `IF NOT EXISTS` (so the apalis schema can be
243239
-- created before the tracking table on fresh installs). Re-stamp its checksum
244-
-- so sqlx doesn't reject it as modified:
240+
-- so the migrator doesn't reject it as modified:
245241
UPDATE apalis._sqlx_migrations
246242
SET checksum = decode('d0839c6f57a379769dc27ccd581feb3d2709239c8f138e05271c9e3c760c4517a78a4d8912ab3d63b074b28d15ec74e9', 'hex')
247243
WHERE version = 20220530084123;
248244
```
249245

250-
Fresh databases need none of this — `sqlx.toml` creates the `apalis` schema and tracking table for you.
246+
Run it **before** upgrading. If you upgrade first without it, the migrator re-runs the first migration against your existing objects and fails with e.g. `function "notify_new_jobs" already exists`. If you've already hit that failure, an empty `apalis._sqlx_migrations` may have been created, which makes the `ALTER TABLE` above fail because the name is taken — drop it first:
251247

252-
### If you merge `PostgresStorage::migrations()` into your own `Migrator`
248+
```sql
249+
DROP TABLE apalis._sqlx_migrations;
250+
```
253251

254-
Your migrator owns its own tracking table, so it doesn't move to `apalis` and you don't run the relocation above. You only need to heal the one edited migration's checksum so your migrator doesn't reject it as modified. On an existing database, before running your migrator:
252+
then run the two statements above.
255253

256-
```rust
257-
// `_sqlx_migrations` (or whatever table your Migrator uses)
258-
PostgresStorage::reconcile_migration_checksums(&pool, "_sqlx_migrations").await?;
259-
merged_migrator.run(&pool).await?;
260-
```
254+
If you maintain your **own** `Migrator` (merging in `PostgresStorage::migrations()`), your tracking table stays where it is — skip the `ALTER TABLE` and run only the `UPDATE`, targeting your table name.
261255

262-
It is a no-op on fresh databases.
256+
Fresh databases need none of this — `sqlx.toml` creates the `apalis` schema and tracking table for you.
263257

264258
### `pgcrypto`
265259

src/lib.rs

Lines changed: 4 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -113,85 +113,16 @@ impl<Args, Compact, Codec, Fetcher: Clone> Clone
113113
impl PostgresStorage<(), (), ()> {
114114
/// Perform migrations for storage.
115115
///
116-
/// On an existing database this also performs the one-time `1.0` migration
117-
/// history transition before running migrations, so upgrades are automatic.
118-
/// If you instead merge [`migrations()`](Self::migrations) into your own
119-
/// [`Migrator`](sqlx::migrate::Migrator), you bypass `setup()` — call
120-
/// [`reconcile_migration_checksums`](Self::reconcile_migration_checksums)
121-
/// against that migrator's table before running it.
116+
/// On an existing (pre-`1.0`) database, run the one-time transition documented
117+
/// under "Upgrading to 1.0" in the README before calling this — `setup()` no
118+
/// longer relocates the migration history automatically. Fresh databases need
119+
/// no manual steps.
122120
#[cfg(feature = "migrate")]
123121
pub async fn setup(pool: &PgPool) -> Result<(), sqlx::Error> {
124-
Self::relocate_legacy_migrations_table(pool).await?;
125-
Self::reconcile_migration_checksums(pool, "apalis._sqlx_migrations").await?;
126122
Self::migrations().run(pool).await?;
127123
Ok(())
128124
}
129125

130-
/// Move apalis's pre-`1.0` migration history from `public._sqlx_migrations`
131-
/// into `apalis._sqlx_migrations`, where `1.0` tracks it. No-op once the
132-
/// `apalis` table exists or on fresh databases. Does not re-stamp checksums.
133-
#[cfg(feature = "migrate")]
134-
pub async fn relocate_legacy_migrations_table(pool: &PgPool) -> Result<(), sqlx::Error> {
135-
let needs_relocation: bool = sqlx::query_scalar(
136-
"SELECT to_regclass('public._sqlx_migrations') IS NOT NULL \
137-
AND to_regclass('apalis._sqlx_migrations') IS NULL",
138-
)
139-
.fetch_one(pool)
140-
.await?;
141-
if !needs_relocation {
142-
return Ok(());
143-
}
144-
145-
// Only adopt a table that is apalis's: a user's own won't have this row.
146-
let is_apalis_table: bool = sqlx::query_scalar(
147-
"SELECT EXISTS (SELECT 1 FROM public._sqlx_migrations WHERE version = 20220530084123)",
148-
)
149-
.fetch_one(pool)
150-
.await?;
151-
if !is_apalis_table {
152-
return Ok(());
153-
}
154-
155-
sqlx::query("CREATE SCHEMA IF NOT EXISTS apalis")
156-
.execute(pool)
157-
.await?;
158-
sqlx::query("ALTER TABLE public._sqlx_migrations SET SCHEMA apalis")
159-
.execute(pool)
160-
.await?;
161-
Ok(())
162-
}
163-
164-
/// Re-stamp the checksum of any migration whose content changed in `1.0` (only
165-
/// the first, which gained `IF NOT EXISTS`) so an existing table isn't rejected
166-
/// as modified. `setup()` calls this; call it yourself only when merging
167-
/// [`migrations()`](Self::migrations) into your own
168-
/// [`Migrator`](sqlx::migrate::Migrator), passing its table (e.g.
169-
/// `"_sqlx_migrations"`), before you run it. No-op if `table` is fresh or absent.
170-
#[cfg(feature = "migrate")]
171-
pub async fn reconcile_migration_checksums(
172-
pool: &PgPool,
173-
table: &str,
174-
) -> Result<(), sqlx::Error> {
175-
let table_exists: bool = sqlx::query_scalar("SELECT to_regclass($1) IS NOT NULL")
176-
.bind(table)
177-
.fetch_one(pool)
178-
.await?;
179-
if !table_exists {
180-
return Ok(());
181-
}
182-
183-
let update =
184-
format!("UPDATE {table} SET checksum = $1 WHERE version = $2 AND checksum <> $1");
185-
for migration in Self::migrations().iter() {
186-
sqlx::query(sqlx::AssertSqlSafe(update.clone()))
187-
.bind(migration.checksum.as_ref())
188-
.bind(migration.version)
189-
.execute(pool)
190-
.await?;
191-
}
192-
Ok(())
193-
}
194-
195126
/// Get postgres migrations without running them
196127
#[cfg(feature = "migrate")]
197128
pub fn migrations() -> sqlx::migrate::Migrator {

0 commit comments

Comments
 (0)