Skip to content

refactor(seed): the boards and the esports history load on start, not in a migration - #1328

Merged
ExtraToast merged 2 commits into
mainfrom
worktree-refactor+seeds-leave-the-migrator
Sep 18, 2026
Merged

ExtraToast merged 2 commits into
mainfrom
worktree-refactor+seeds-leave-the-migrator

Conversation

@ExtraToast

Copy link
Copy Markdown
Contributor

Closes #1295. Part of #1289.

The boards and the esports history were loaded by two repeatable Flyway migrations written in Kotlin, which read their CSV off the classpath and hashed the files to decide whether to re-run. Seed data is not schema, and the migrator is about to stop being able to run this at all: #1297 moves migrations into a Job, and #1296 replaces Flyway with Liquibase. A Liquibase CLI cannot execute Kotlin, so the seeds have to leave before the migrator can be swapped.

What this achieves

R__Boards_seed and R__Esports_seed become ShippedBoards and ShippedEsports, ordinary beans that run on ApplicationReadyEvent. The schema migrator is left with schema.

Nothing about what lands in the database changes. The upserts are the same raw JDBC, keyed the same way, with the same <=> no-op guards, the same "deletion outranks the files", and the same rule that an attached account is never re-matched. The five integration tests covering that behaviour are unchanged except for how they invoke the loader.

How

The files moved with git mv, so the diff reads as a move rather than a rewrite. migrate(Context) becomes apply(), wrapped in one TransactionTemplate with DataSourceUtils.getConnection — the same all-or-nothing transaction Flyway used to provide, taken from the transaction rather than opened beside it.

The shape is the one this codebase already uses for work that cannot happen in a migration. StoredImageRenditionsBackfill states it outright — in the application rather than a migration, and idempotent, so every start after the first does nothing — and both art steps are a worker bean plus a thin ApplicationReadyEvent listener that logs and swallows so a failure never blocks start-up. The new loaders are that, and SeedCsv stays a constructor default rather than a bean, as ShippedArt already has it.

The tests stop building a hand-rolled Flyway Context over a raw connection and call the bean. The two parsing tests moved out of the db.migration package, which no longer describes them.

Not in scope

Liquibase (#1296) and the pre-rollout migration Job (#1297). This only takes the Kotlin out of the migrator's way.

The seeds' own behaviour. Their idempotency was already thorough and is left alone deliberately — changing where code runs from and what it does in the same pull request would make the integration tests useless as a control.

Worth a reviewer's attention

  • flyway.ignore-migration-patterns: repeatable:missing is the load-bearing line here. Deleting a repeatable migration leaves its row in flyway_schema_history, and Flyway's default (*:future) makes validate reject an applied migration it can no longer resolve — the application refuses to boot. Those rows cannot be cleaned by a migration either, because validate runs before migrate. CI cannot catch this: it builds a fresh database with no history to be missing from. The pattern is scoped so a missing versioned migration is still an error.
  • Ordering was free before and is not now. Records had to exist before the art could be attached to them, which Flyway guaranteed by finishing before the context was up. Both are ApplicationReadyEvent listeners now, so SeedOrder puts records at 100 and art at 200. Without it a fresh database would intermittently get art with nothing to attach it to.
  • detekt. These files were exempt from 19 rules by way of **/db/migration/**, which they have left. Re-granting all 19 by filename would have silently exempted them from ClassNaming and SwallowedException they no longer need, so only the two that actually fire are named: MagicNumber, because the numbers are JDBC parameter positions, and LongMethod, which the existing comment already covers.
  • TestCleanUpListener restores game from a snapshot of what the migration left, and game is now populated at ApplicationReadyEvent instead. That still happens before the first test method, so the snapshot should be unchanged — but it is reasoning, and the integration run is what confirms it.

Verification

Locally: compileKotlin, compileTestKotlin, compileIntegrationTestKotlin and compileTestFixturesKotlin all build; ktlintCheck, detekt, detektIntegrationTestSourceSet and detektTestFixturesSourceSet are green; the parsing unit tests pass.

The database-backed integration tests — BoardSeedLoadIT, EsportsSeedLoadIT, RecoveredAttributionIT, ShippedBoardArtIT, ShippedArtIT — are the real proof that the semantics survived the move, and they need MariaDB, so CI runs them.

… in a migration

Both seeds were repeatable Flyway migrations written in Kotlin that read CSV off
the classpath. Seed data is not schema, and once migrations run in their own Job
the runner cannot execute Kotlin at all, so they had to leave first.

They become ShippedBoards and ShippedEsports: the same raw-JDBC upserts, keyed
the same way, in one transaction as Flyway gave them, behind the same
bean-plus-listener shape the art steps already use. Records are ordered ahead of
art, which the migrator used to guarantee by running before the context was up.

flyway.ignore-migration-patterns is set to repeatable:missing. Every database
that ran these still carries their history rows, validate refuses an applied
migration it cannot resolve, and that would stop the application booting rather
than fail a test. A versioned migration going missing stays an error.

detekt exempted these files through db/migration, which they have left; the two
rules that actually fire on them, MagicNumber and LongMethod, name them instead.
@ExtraToast ExtraToast self-assigned this Sep 18, 2026
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Changes

Bucket Files Added Removed
api · production 6 ███████ 147 ░░░ 54
api · unit tests 3 ███ 52 ░ 11
api · integration tests 5 █ 8 ░░░ 60
other · unclassified 1 █ 4 ░ 2
total 15 +211 −127

0.41 test lines per prod line.

Coverage

Statements Branches Functions Lines
api unit 40.40% 40.50% 32.68% 41.97%
main
api integration 68.49% 51.85% 68.29% 76.67%
main
frontend unit 67.64% 58.80% 60.46% 68.82%
main
frontend e2e 60.60% 37.81% 58.04% 64.12%
main

No baseline is cached from main yet, so there is nothing to compare against.

Patch coverage: 93.9%, 62 of 66 changed lines covered.

4 uncovered lines in this pull request

services/api/src/main/kotlin/net/blueshell/api/board/domain/ShippedBoards.kt 312, 313
services/api/src/main/kotlin/net/blueshell/api/esports/domain/ShippedEsports.kt 445, 446

BoardArtSystemTest failed with "No board in the seeded history carries a
photo": the art ran before the records it hangs on. @order was on the class,
and ApplicationListenerMethodAdapter resolves an @eventlistener's order from
the method alone, so it was never applied and the two listeners raced.

A class-level @order looks correct and does nothing, so a test asserts the
annotation is on each onReady and that records sort before art. Both cases
fail when the annotation is moved back to the class.
@ExtraToast
ExtraToast merged commit 3b7803c into main Sep 18, 2026
39 checks passed
@ExtraToast
ExtraToast deleted the worktree-refactor+seeds-leave-the-migrator branch September 18, 2026 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The board and esports seeds reconcile themselves, outside the migrator

1 participant