Skip to content

Commit 84eb3ac

Browse files
committed
docs(issues): add current-code-analysis findings to 1525-06 spec
Add 11 findings (F1–F11) before the Tasks section reflecting the actual develop-branch state after subissue 1525-05, and rewrite spec passages and code blocks to match. Highlights: - Remove obsolete references to an ensure_schema() latch (1525-05 explicitly chose not to use one). - Correct the false claim that drop_database_tables() omits torrent_aggregate_metrics; only _sqlx_migrations is the new drop. - Align the proposed error API with the existing tuple-From pattern: introduce an Error::MigrationError variant and impl From<(MigrateError, Driver)> for Error instead of a free-standing Error::migration_error() constructor. - Add 'notnull' to the cspell dictionary for the SQLite PRAGMA table_info reference.
1 parent 0864ded commit 84eb3ac

2 files changed

Lines changed: 161 additions & 25 deletions

File tree

docs/issues/1719-1525-06-introduce-schema-migrations.md

Lines changed: 160 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ be added (subissue `1525-08`).
2323
### Starting point
2424

2525
By the time this subissue is implemented, subissue `1525-05` will have delivered async SQLite
26-
and MySQL drivers backed by `sqlx`. Each driver has an `ensure_schema()` latch that calls
27-
`create_database_tables()` lazily. That method currently issues raw `sqlx::query()` DDL. This
28-
subissue replaces that raw DDL path with `sqlx::migrate!()`.
26+
and MySQL drivers backed by `sqlx`. `SchemaMigrator::create_database_tables()` is invoked
27+
once from `databases::setup::initialize_database()` after the driver is built; subissue
28+
`1525-05` explicitly chose **not** to use a per-method lazy `ensure_schema()` latch. The
29+
current `create_database_tables()` issues raw `sqlx::query()` DDL. This subissue replaces
30+
that raw DDL path with `sqlx::migrate!()`.
2931

3032
There are already 3 migration files under `packages/tracker-core/migrations/` (both `sqlite/`
3133
and `mysql/` subdirectories) that capture the schema history:
@@ -44,8 +46,10 @@ automatically. This subissue is the first time they are wired into the applicati
4446
The current `create_database_tables()` method issues `CREATE TABLE IF NOT EXISTS` for all four
4547
tables (`whitelist`, `torrents`, `torrent_aggregate_metrics`, `keys`) using hardcoded DDL that
4648
already reflects the final schema state (nullable `valid_until`, all four tables present). The
47-
current `drop_database_tables()` drops `whitelist`, `torrents`, and `keys` but **not**
48-
`torrent_aggregate_metrics`, which leaks across test drop/create cycles.
49+
current `drop_database_tables()` already drops all four tables (`whitelist`, `torrents`,
50+
`keys`, **and** `torrent_aggregate_metrics`) — there is no pre-existing omission. What is
51+
missing is `_sqlx_migrations`, which does not exist today and will be introduced by this
52+
subissue. All current drops use bare `DROP TABLE` (no `IF EXISTS`).
4953

5054
This gives two distinct behaviors today:
5155

@@ -154,9 +158,125 @@ This logic lives in a helper function called before `MIGRATOR.run(&pool)` inside
154158

155159
After this subissue, `SchemaMigrator::create_database_tables()` calls the legacy-bootstrap
156160
helper and then `MIGRATOR.run(&pool)` instead of issuing raw DDL. `drop_database_tables()`
157-
(used only in tests) must also drop the `_sqlx_migrations` and `torrent_aggregate_metrics`
158-
tables (fixing the pre-existing omission) so that the drop/create cycle used in the test suite
159-
works correctly.
161+
(used in tests and in the `axum-rest-tracker-api-server` `force_database_error` helper) must
162+
also drop `_sqlx_migrations` (newly introduced by this subissue) and switch every drop to
163+
`DROP TABLE IF EXISTS` so the drop/create cycle used by `databases::driver::tests::run_tests`
164+
(create → drop → create) leaves a clean slate that `MIGRATOR.run()` can re-bootstrap as a
165+
fresh database.
166+
167+
## Findings from current-code analysis (2026-04-30)
168+
169+
Review of `develop` (post-`1525-05`) before starting implementation. These items refine or
170+
correct statements elsewhere in this spec; tasks below should be read with these in mind.
171+
172+
### F1. No `ensure_schema()` latch exists — and none is planned
173+
174+
Subissue `1525-05` explicitly decided not to introduce a per-method lazy schema latch (see
175+
`docs/issues/1717-1525-05-migrate-sqlite-and-mysql-to-sqlx.md`: _"Do **not** use per-method
176+
lazy schema checks (`ensure_schema()`)"_). `create_database_tables()` is called exactly once
177+
from `databases::setup::initialize_database()`. Any references to an `ensure_schema()` latch
178+
in earlier drafts of this spec are obsolete. Replace mentions of "the `ensure_schema()` latch
179+
remains in place" with "`create_database_tables()` continues to be invoked once from
180+
`initialize_database()`".
181+
182+
### F2. `drop_database_tables()` already drops `torrent_aggregate_metrics`
183+
184+
Both the SQLite and MySQL drivers in current code already drop all four tables. The spec's
185+
claim that this is a "pre-existing omission" is incorrect. The only **new** drop required by
186+
this subissue is `_sqlx_migrations`. Acceptance criteria below are reworded accordingly. The
187+
`DROP TABLE IF EXISTS` switch (covering all five drops) remains a real change — current code
188+
uses bare `DROP TABLE`.
189+
190+
### F3. Error construction follows a tuple-`From` pattern, not a constructor
191+
192+
All existing `sqlx`-error sites use `.map_err(|e| (e, DRIVER))?` and rely on
193+
`impl From<(SqlxError, Driver)> for Error`. The proposed `Error::migration_error(driver,
194+
source)` constructor breaks that convention. Preferred shape:
195+
196+
- Add a new `Error::MigrationError { source, driver }` variant.
197+
- Add `impl From<(sqlx::migrate::MigrateError, Driver)> for Error`.
198+
- Call sites then write `.map_err(|e| (e, DRIVER))?`, identical to every other driver call.
199+
200+
Update Task 2 and the bootstrap helper code in Task 3 to use this shape. The acceptance
201+
criterion "`Error::migration_error()` wraps `MigrateError`" should be reworded as "a new
202+
`Error::MigrationError` variant + `From<(MigrateError, Driver)>` impl wraps `MigrateError`".
203+
204+
### F4. `sqlx`'s `migrate` feature is already enabled transitively; only `macros` is missing
205+
206+
`cargo tree` confirms `sqlx-core` is built with the `migrate` feature already (so the
207+
`sqlx::migrate::Migrator` and `MigrateError` types are reachable today). The required
208+
addition in `packages/tracker-core/Cargo.toml` is the **`macros`** feature on `sqlx`, which
209+
gates the compile-time `sqlx::migrate!()` macro. No other feature additions are needed.
210+
211+
### F5. SQLite migration 1 contains an invalid `#` comment
212+
213+
`packages/tracker-core/migrations/sqlite/20240730183000_torrust_tracker_create_all_tables.sql`
214+
contains a Bash-style comment line (`# todo: rename to torrent_metrics`). SQLite's lexer does not
215+
accept `#` as a comment introducer (only `--` and `/* … */`); only MySQL does. When
216+
`MIGRATOR.run()` executes this file against SQLite, the statement parser is expected to
217+
fail with a syntax error. **Action in Task 1**: replace `#` with `--` in the SQLite file
218+
(and in the MySQL file as well, for consistency, since `--` is portable). Verify by running
219+
the SQLite driver tests after the change.
220+
221+
### F6. MySQL migration 1 still uses `INT(10)` display-width syntax
222+
223+
MySQL 8.0 deprecated integer display-width attributes. `INT(10)` still parses but emits a
224+
warning and is dropped from `SHOW CREATE TABLE` output, which can cause schema-comparison
225+
noise. Not blocking for this subissue; flag as an optional cleanup or defer to subissue
226+
`1525-07` (Rust ↔ SQL type alignment) where integer widths are revisited.
227+
228+
### F7. `keys.key` width is `VARCHAR(32)`, matches `AUTH_KEY_LENGTH`
229+
230+
Verified: `AUTH_KEY_LENGTH = 32` in `packages/tracker-core/src/authentication/key/mod.rs`.
231+
MySQL migration 1 uses `VARCHAR(32)`, so the migration file matches the `format!`-built DDL
232+
in the current driver. No discrepancy. Once migrations own the schema, the `format!` /
233+
`AUTH_KEY_LENGTH` coupling in `mysql/schema_migrator.rs` disappears (the column width is
234+
frozen in the migration file).
235+
236+
### F8. Other consumers of `drop_database_tables()` outside the test harness
237+
238+
`packages/axum-rest-tracker-api-server/tests/server/mod.rs::force_database_error` calls
239+
`drop_database_tables()` to provoke query failures. After this subissue it will additionally
240+
drop `_sqlx_migrations`. Behaviour is unchanged for the test (subsequent queries still
241+
fail), but worth a sentence in the PR description.
242+
243+
### F9. `bootstrap_legacy_schema()` precondition queries — concrete forms
244+
245+
The spec describes the checks abstractly. Concrete queries to use:
246+
247+
- **`_sqlx_migrations` exists**
248+
- SQLite: `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_sqlx_migrations'`
249+
- MySQL: `SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND
250+
table_name = '_sqlx_migrations'`
251+
- **Legacy sentinel (`whitelist` exists)** — same shape as above with `name='whitelist'`.
252+
- **Migration 2 applied (`keys.valid_until` is nullable)**
253+
- SQLite: `PRAGMA table_info(keys)` → row where `name='valid_until'` has `notnull = 0`.
254+
- MySQL: `SELECT is_nullable FROM information_schema.columns WHERE table_schema =
255+
DATABASE() AND table_name = 'keys' AND column_name = 'valid_until'``'YES'`.
256+
- **Migration 3 applied (`torrent_aggregate_metrics` exists)** — sentinel-table check, same
257+
shape as the first two.
258+
259+
Important ordering: check `_sqlx_migrations` existence with a raw query **before** calling
260+
`MIGRATOR.ensure_migrations_table(pool)`, because the latter creates the table if absent and
261+
would defeat the detection.
262+
263+
### F10. `apply_fake` SQL — confirm column types and key types in sqlx 0.8
264+
265+
`Migration::version` is `i64`, `Migration::description` is `Cow<'static, str>`, and
266+
`Migration::checksum` is `Cow<'static, [u8]>`. Binding `&[u8]` for the checksum column works
267+
in both backends. The `_sqlx_migrations` schema has columns
268+
`(version BIGINT PK, description TEXT, installed_on TIMESTAMP, success BOOL, checksum BLOB,
269+
execution_time BIGINT)` — verify this once during implementation by inspecting the table sqlx
270+
creates against a fresh DB; if column types differ across backends, adjust the INSERT bind
271+
types accordingly.
272+
273+
### F11. `database_setup` test cycle is the natural drop/create test
274+
275+
`packages/tracker-core/src/databases/driver/mod.rs::database_setup` already does
276+
`create → drop → create`. After this subissue, the second `create` runs `MIGRATOR.run()` on
277+
a database where everything (including `_sqlx_migrations`) was just dropped. No additional
278+
test is needed for the drop/create cycle scenario beyond verifying that this existing test
279+
still passes.
160280

161281
## Tasks
162282

@@ -167,7 +287,15 @@ their SQL content is correct and consistent with the current schema produced by
167287
DDL in `1525-05`. Do not change existing file timestamps or names. Fix content only if a
168288
discrepancy is found.
169289

170-
**Outcome**: all three migration files are verified correct; nothing else changes yet.
290+
Known issue to fix as part of this task (see finding F5): the SQLite (and MySQL) migration
291+
`20240730183000_torrust_tracker_create_all_tables.sql` contains a Bash-style line
292+
(`# todo: rename to ...torrent_metrics`). SQLite does not accept `#` line comments — replace `#` with `--` in
293+
both backend files. This is the only content change expected; verify by running
294+
`cargo test -p bittorrent-tracker-core run_sqlite_driver_tests` after Task 3 wires the
295+
migrator in.
296+
297+
**Outcome**: all three migration files compile under `sqlx::migrate!()` for both backends;
298+
the `#`-comment incompatibility is fixed.
171299

172300
### Task 2 — Enable `sqlx` `macros` feature and add `MIGRATOR` statics
173301

@@ -190,7 +318,9 @@ static MIGRATOR: Migrator = sqlx::migrate!("migrations/sqlite");
190318
static MIGRATOR: Migrator = sqlx::migrate!("migrations/mysql");
191319
```
192320

193-
Add `Error::migration_error()` to `databases/error.rs` to wrap `sqlx::migrate::MigrateError`.
321+
Add a new `Error::MigrationError { source, driver }` variant to `databases/error.rs` and an
322+
`impl From<(sqlx::migrate::MigrateError, Driver)> for Error` so the new code can keep the
323+
established `.map_err(|e| (e, DRIVER))?` call pattern (see finding F3).
194324

195325
**Outcome**: project compiles with migration statics defined but not yet called.
196326

@@ -225,14 +355,17 @@ async fn bootstrap_legacy_schema(pool: &Pool) -> Result<(), Error> {
225355
let migration_2_applied: bool = /* check keys.valid_until is nullable */;
226356
let migration_3_applied: bool = /* check torrent_aggregate_metrics table exists */;
227357
if !migration_2_applied || !migration_3_applied {
228-
return Err(Error::migration_error(
229-
DRIVER,
230-
std::io::Error::new(
231-
std::io::ErrorKind::InvalidData,
358+
// Build a `MigrateError` directly so the conversion goes through the
359+
// standard `From<(MigrateError, Driver)> for Error` impl introduced in Task 2.
360+
return Err((
361+
sqlx::migrate::MigrateError::Source(
232362
"Legacy database is not fully migrated. Apply all three manual migrations \
233-
listed in packages/tracker-core/migrations/README.md before upgrading to v4.",
363+
listed in packages/tracker-core/migrations/README.md before upgrading to v4."
364+
.into(),
234365
),
235-
));
366+
DRIVER,
367+
)
368+
.into());
236369
}
237370

238371
// PRECONDITION: all three manual migrations have been verified as applied:
@@ -244,7 +377,7 @@ async fn bootstrap_legacy_schema(pool: &Pool) -> Result<(), Error> {
244377
MIGRATOR
245378
.ensure_migrations_table(pool)
246379
.await
247-
.map_err(|e| Error::migration_error(DRIVER, e))?;
380+
.map_err(|e| (e, DRIVER))?;
248381
for migration in MIGRATOR.iter() {
249382
if migration.version <= 20_250_527_093_000 {
250383
// sqlx 0.8 does not expose a public `apply_fake()` API on `Migrator`.
@@ -265,7 +398,7 @@ async fn bootstrap_legacy_schema(pool: &Pool) -> Result<(), Error> {
265398
.bind(migration.checksum.as_ref())
266399
.execute(pool)
267400
.await
268-
.map_err(|e| Error::migration_error(DRIVER, e))?;
401+
.map_err(|e| (e, DRIVER))?;
269402
}
270403
}
271404
Ok(())
@@ -277,7 +410,7 @@ async fn bootstrap_legacy_schema(pool: &Pool) -> Result<(), Error> {
277410
```rust
278411
async fn create_database_tables(&self) -> Result<(), Error> {
279412
bootstrap_legacy_schema(&self.pool).await?;
280-
MIGRATOR.run(&self.pool).await.map_err(|e| Error::migration_error(DRIVER, e))?;
413+
MIGRATOR.run(&self.pool).await.map_err(|e| (e, DRIVER))?;
281414
Ok(())
282415
}
283416
```
@@ -332,8 +465,8 @@ Update `packages/tracker-core/migrations/README.md` to replace the stale content
332465
migration file causes a checksum-mismatch error on the next startup for any database that has
333466
already applied that migration.
334467

335-
The `ensure_schema()` latch remains in place — it now guards the
336-
`bootstrap_legacy_schema()` + `MIGRATOR.run()` sequence.
468+
`create_database_tables()` continues to be invoked once from
469+
`databases::setup::initialize_database()` (no `ensure_schema()` latch — see finding F1).
337470

338471
**Outcome**: `cargo test --workspace --all-targets` passes. Schema is owned by migration files.
339472
The README accurately reflects the new automatic migration behavior.
@@ -383,12 +516,14 @@ modules.
383516
confirmed correct and match the final schema produced by the hardcoded DDL in `1525-05`.
384517
- [ ] `sqlx::migrate!()` (`macros` feature) is used in both drivers; no raw DDL remains in
385518
`create_database_tables()`.
386-
- [ ] `drop_database_tables()` drops `_sqlx_migrations` **and** `torrent_aggregate_metrics`
387-
(fixing the pre-existing omission) so the test cycle works. All five drops use
388-
`DROP TABLE IF EXISTS`.
519+
- [ ] `drop_database_tables()` adds a drop for `_sqlx_migrations` (the only newly required
520+
drop — `torrent_aggregate_metrics` is already dropped today; see finding F2) and every
521+
drop is converted to `DROP TABLE IF EXISTS`.
389522
- [ ] `bootstrap_legacy_schema()` verifies that migrations 2 and 3 were applied before
390523
fake-applying, and returns a descriptive error if the precondition is not met.
391-
- [ ] `Error::migration_error()` wraps `sqlx::migrate::MigrateError`.
524+
- [ ] A new `Error::MigrationError` variant plus `impl From<(sqlx::migrate::MigrateError,
525+
Driver)> for Error` wrap `MigrateError`, matching the existing tuple-`From` pattern
526+
used by every other `sqlx` error site (see finding F3).
392527
- [ ] `packages/tracker-core/migrations/README.md` is updated to document automatic migration
393528
behavior and the v4 upgrade requirement.
394529
- [ ] Guidance for `1525-08`: PostgreSQL migration files start from migration 1 following the

project-words.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ nologin
166166
nonblocking
167167
nonroot
168168
Norberg
169+
notnull
169170
numwant
170171
nvCFlJCq7fz7Qx6KoKTDiMZvns8l5Kw7
171172
obra

0 commit comments

Comments
 (0)