Skip to content

Commit 4cce977

Browse files
authored
Merge pull request #310 from nanotaboada/feat/130-flyway-database-migrations
feat(db): integrate Flyway for database migrations (#130)
2 parents dc49f14 + d7c2b74 commit 4cce977

14 files changed

Lines changed: 186 additions & 36 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
HELP.md
22
.claude/settings.local.json
33
target/
4+
storage/*.db
45
!.mvn/wrapper/maven-wrapper.jar
56
!**/src/main/**/target/
67
!**/src/test/**/target/

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ Release names follow the **historic football clubs** naming convention (A–Z):
4242

4343
### Added
4444

45+
- Integrate Flyway for database schema versioning and automated migrations;
46+
add `spring-boot-starter-flyway` (Spring Boot 4.0 requires this dedicated
47+
starter for autoconfiguration — `flyway-core` alone is insufficient) and
48+
`flyway-database-postgresql` to `pom.xml`; create
49+
migration directory `src/main/resources/db/migration/` with three versioned
50+
scripts: `V1__Create_players_table.sql` (schema), `V2__Seed_starting11.sql`
51+
(11 Starting XI players), `V3__Seed_substitutes.sql` (15 substitute players);
52+
configure `spring.flyway.enabled=true` and `spring.flyway.locations` only —
53+
no baseline settings, Flyway runs V1→V2→V3 from scratch on every empty
54+
database; disable Flyway in test environment which continues to use SQLite
55+
in-memory with `ddl.sql`/`dml.sql`; switch `spring.jpa.hibernate.ddl-auto`
56+
from `none` to `validate` so Hibernate verifies entity mappings against the
57+
Flyway-managed schema (#130)
58+
4559
### Changed
4660

4761
- Switch runtime base image from `eclipse-temurin:25-jdk-alpine` to

Dockerfile

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,6 @@ COPY --chmod=444 README.md ./
4444
COPY --chmod=555 scripts/entrypoint.sh ./entrypoint.sh
4545
COPY --chmod=555 scripts/healthcheck.sh ./healthcheck.sh
4646

47-
# The 'hold' is our storage compartment within the image. Here, we copy a
48-
# pre-seeded SQLite database file, which Compose will mount as a persistent
49-
# 'storage' volume when the container starts up.
50-
COPY --chmod=555 storage/ ./hold/
51-
5247
# Install SQLite runtime libs, add non-root user and prepare volume mount point
5348
RUN apk add --no-cache sqlite-libs && \
5449
addgroup -S spring && \

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,53 @@ spring.datasource.url=jdbc:sqlite::memory:
269269
spring.jpa.hibernate.ddl-auto=create-drop
270270
```
271271

272+
## Database Migrations
273+
274+
Schema versioning is managed by [Flyway](https://documentation.red-gate.com/flyway), which runs automatically on application startup and applies any pending migrations in order.
275+
276+
### Migration files
277+
278+
Versioned SQL scripts live in `src/main/resources/db/migration/` and follow the Flyway naming convention:
279+
280+
```text
281+
V{version}__{description}.sql
282+
```
283+
284+
| File | Description |
285+
| ---- | ----------- |
286+
| `V1__Create_players_table.sql` | Creates the `players` table (schema) |
287+
| `V2__Seed_starting11.sql` | Seeds 11 Starting XI players (`starting11 = 1`) |
288+
| `V3__Seed_substitutes.sql` | Seeds 15 Substitute players (`starting11 = 0`) |
289+
290+
All migration SQL is written to be compatible with both **SQLite** (local dev) and **PostgreSQL** (see #286).
291+
292+
### First start
293+
294+
On first run, Flyway detects an empty database and applies V1 → V2 → V3 in sequence, creating the `players` table and seeding all 26 players. The database file (`storage/players-sqlite3.db`) is created automatically and is excluded from version control.
295+
296+
### Adding a new migration
297+
298+
Create a new file in `src/main/resources/db/migration/` with the next version number:
299+
300+
```bash
301+
touch src/main/resources/db/migration/V4__Add_nationality_column.sql
302+
```
303+
304+
Flyway applies it automatically on the next application startup. View the applied history by querying the `flyway_schema_history` table.
305+
306+
### Reset local database
307+
308+
Delete the SQLite file and restart — Flyway recreates the schema and seed data from scratch:
309+
310+
```bash
311+
rm storage/players-sqlite3.db
312+
./mvnw spring-boot:run
313+
```
314+
315+
### Tests
316+
317+
The test environment keeps `spring.flyway.enabled=false` and uses SQLite in-memory with `ddl.sql`/`dml.sql` via Spring SQL init for fast, isolated test execution.
318+
272319
## Contributing
273320

274321
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on:

pom.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,31 @@
169169
<groupId>org.hibernate.orm</groupId>
170170
<artifactId>hibernate-community-dialects</artifactId>
171171
</dependency>
172+
<!-- Spring Boot Starter Flyway ==================================== -->
173+
<!--
174+
Spring Boot 4.0 moved Flyway auto-configuration out of
175+
spring-boot-autoconfigure into this dedicated starter. Adding only
176+
flyway-core is no longer sufficient — this starter is required to
177+
enable FlywayAutoConfiguration and the spring.flyway.* properties.
178+
Transitively brings in flyway-core (version managed by Spring BOM).
179+
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-flyway
180+
-->
181+
<dependency>
182+
<groupId>org.springframework.boot</groupId>
183+
<artifactId>spring-boot-starter-flyway</artifactId>
184+
</dependency>
185+
<!-- Flyway Database PostgreSQL ==================================== -->
186+
<!--
187+
Flyway 10+ modular architecture requires explicit database support
188+
modules for non-community databases. Required for PostgreSQL support
189+
(see #286 — Add PostgreSQL support with unified migration-based
190+
initialization).
191+
https://mvnrepository.com/artifact/org.flywaydb/flyway-database-postgresql
192+
-->
193+
<dependency>
194+
<groupId>org.flywaydb</groupId>
195+
<artifactId>flyway-database-postgresql</artifactId>
196+
</dependency>
172197
<!-- H2 Database Engine ============================================ -->
173198
<!--
174199
Provides a fast in-memory database for testing. Used only in test

scripts/entrypoint.sh

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,38 @@
11
#!/bin/sh
22
set -e
33

4-
echo "✔ Executing entrypoint script..."
5-
6-
IMAGE_STORAGE_PATH="/app/hold/players-sqlite3.db"
7-
VOLUME_STORAGE_PATH="/storage/players-sqlite3.db"
8-
9-
echo "✔ Starting container..."
10-
11-
if [ ! -f "$VOLUME_STORAGE_PATH" ]; then
12-
echo "⚠️ No existing database file found in volume."
13-
if [ -f "$IMAGE_STORAGE_PATH" ]; then
14-
echo "Copying database file to writable volume..."
15-
if cp "$IMAGE_STORAGE_PATH" "$VOLUME_STORAGE_PATH"; then
16-
echo "✔ Database initialized at $VOLUME_STORAGE_PATH"
17-
else
18-
echo "❌ Failed to copy database from $IMAGE_STORAGE_PATH to $VOLUME_STORAGE_PATH"
19-
echo " Check file permissions and available disk space."
20-
exit 1
21-
fi
22-
else
23-
echo "⚠️ Database file missing at $IMAGE_STORAGE_PATH"
4+
# Helper function for formatted logging
5+
log() {
6+
echo "[ENTRYPOINT] $(date '+%Y/%m/%d - %H:%M:%S') | $1"
7+
return 0
8+
}
9+
10+
log "✔ Starting container..."
11+
12+
STORAGE_PATH="${STORAGE_PATH:-storage/players-sqlite3.db}"
13+
14+
mkdir -p "$(dirname "$STORAGE_PATH")"
15+
16+
if [ ! -w "$(dirname "$STORAGE_PATH")" ]; then
17+
log "❌ Storage directory is not writable: $(dirname "$STORAGE_PATH")"
2418
exit 1
25-
fi
19+
fi
20+
21+
if [ -f "$STORAGE_PATH" ] && [ ! -w "$STORAGE_PATH" ]; then
22+
log "❌ Database file is not writable: $STORAGE_PATH"
23+
exit 1
24+
fi
25+
26+
if [ ! -f "$STORAGE_PATH" ]; then
27+
log "⚠️ No existing database file found at $STORAGE_PATH."
28+
log "🗄️ Flyway migrations will initialize the database on first start."
2629
else
27-
echo "✔ Existing database file found. Skipping seed copy."
30+
log "✔ Existing database file found at $STORAGE_PATH."
2831
fi
2932

30-
echo "✔ Ready!"
31-
echo "🚀 Launching app..."
32-
echo ""
33-
echo "🔌 Endpoints:"
34-
echo " Health: http://localhost:9001/actuator/health"
35-
echo " Players: http://localhost:9000/players"
36-
echo ""
33+
log "✔ Ready!"
34+
log "🚀 Launching app..."
35+
log "🔌 API endpoints | http://localhost:9000"
36+
log "📚 Swagger UI | http://localhost:9000/swagger/index.html"
37+
log "❤️ Health check | http://localhost:9001/actuator/health"
3738
exec "$@"

src/main/resources/application.properties

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ springdoc.swagger-ui.path=/swagger/index.html
2020
spring.datasource.url=jdbc:sqlite:${STORAGE_PATH:storage/players-sqlite3.db}
2121
spring.datasource.driver-class-name=org.sqlite.JDBC
2222
spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect
23-
spring.jpa.hibernate.ddl-auto=none
23+
spring.jpa.hibernate.ddl-auto=validate
2424
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
2525
spring.jpa.show-sql=false
2626
spring.jpa.properties.hibernate.format_sql=true
27+
28+
# Flyway Database Migration Configuration
29+
# Flyway manages all schema creation and seed data via versioned SQL migrations.
30+
# On first start, Flyway creates the database and runs V1 → V2 → V3 in order.
31+
spring.flyway.enabled=true
32+
spring.flyway.locations=classpath:db/migration
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-- V1: Create players table
2+
-- Compatible with both SQLite (local dev) and PostgreSQL (see #286).
3+
-- TEXT columns use TEXT affinity in SQLite and the unlimited TEXT type in PostgreSQL.
4+
-- INTEGER is used for squadNumber (natural key) and starting11 (boolean flag: 1/0).
5+
-- UUID primary key is stored as VARCHAR(36) and generated at the application level.
6+
7+
CREATE TABLE IF NOT EXISTS players (
8+
id VARCHAR(36) NOT NULL,
9+
squadNumber INTEGER NOT NULL,
10+
firstName TEXT NOT NULL,
11+
middleName TEXT,
12+
lastName TEXT NOT NULL,
13+
dateOfBirth TEXT NOT NULL,
14+
position TEXT NOT NULL,
15+
abbrPosition TEXT NOT NULL,
16+
team TEXT NOT NULL,
17+
league TEXT NOT NULL,
18+
starting11 BOOLEAN NOT NULL,
19+
PRIMARY KEY (id),
20+
UNIQUE (squadNumber)
21+
);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- V2: Seed Starting XI players (starting11 = 1)
2+
-- Argentina 2022 FIFA World Cup squad — 11 players who started the final.
3+
-- Rolling back this migration removes only the Starting XI, leaving substitutes intact.
4+
5+
INSERT INTO players (id, squadNumber, firstName, middleName, lastName, dateOfBirth, position, abbrPosition, team, league, starting11) VALUES
6+
('01772c59-43f0-5d85-b913-c78e4e281452', 23, 'Damián', 'Emiliano', 'Martínez', '1992-09-02T00:00:00.000Z', 'Goalkeeper', 'GK', 'Aston Villa FC', 'Premier League', 1),
7+
('da31293b-4c7e-5e0f-a168-469ee29ecbc4', 26, 'Nahuel', NULL, 'Molina', '1998-04-06T00:00:00.000Z', 'Right-Back', 'RB', 'Atlético Madrid', 'La Liga', 1),
8+
('c096c69e-762b-5281-9290-bb9c167a24a0', 13, 'Cristian','Gabriel', 'Romero', '1998-04-27T00:00:00.000Z', 'Centre-Back', 'CB', 'Tottenham Hotspur', 'Premier League', 1),
9+
('d5f7dd7a-1dcb-5960-ba27-e34865b63358', 19, 'Nicolás', 'Hernán Gonzalo', 'Otamendi', '1988-02-12T00:00:00.000Z', 'Centre-Back', 'CB', 'SL Benfica', 'Liga Portugal', 1),
10+
('2f6f90a0-9b9d-5023-96d2-a2aaf03143a6', 3, 'Nicolás', 'Alejandro', 'Tagliafico','1992-08-31T00:00:00.000Z', 'Left-Back', 'LB', 'Olympique Lyon', 'Ligue 1', 1),
11+
('b5b46e79-929e-5ed2-949d-0d167109c022', 11, 'Ángel', 'Fabián', 'Di María', '1988-02-14T00:00:00.000Z', 'Right Winger', 'RW', 'SL Benfica', 'Liga Portugal', 1),
12+
('0293b282-1da8-562e-998e-83849b417a42', 7, 'Rodrigo', 'Javier', 'de Paul', '1994-05-24T00:00:00.000Z', 'Central Midfield','CM', 'Atlético Madrid', 'La Liga', 1),
13+
('d3ba552a-dac3-588a-b961-1ea7224017fd', 24, 'Enzo', 'Jeremías', 'Fernández', '2001-01-17T00:00:00.000Z', 'Central Midfield','CM', 'SL Benfica', 'Liga Portugal', 1),
14+
('9613cae9-16ab-5b54-937e-3135123b9e0d', 20, 'Alexis', NULL, 'Mac Allister','1998-12-24T00:00:00.000Z','Central Midfield','CM','Brighton & Hove Albion', 'Premier League', 1),
15+
('acc433bf-d505-51fe-831e-45eb44c4d43c', 10, 'Lionel', 'Andrés', 'Messi', '1987-06-24T00:00:00.000Z', 'Right Winger', 'RW', 'Paris Saint-Germain', 'Ligue 1', 1),
16+
('38bae91d-8519-55a2-b30a-b9fe38849bfb', 9, 'Julián', NULL, 'Álvarez', '2000-01-31T00:00:00.000Z', 'Centre-Forward', 'CF', 'Manchester City', 'Premier League', 1);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- V3: Seed Substitute players (starting11 = 0)
2+
-- Argentina 2022 FIFA World Cup squad — 15 players who did not start the final.
3+
-- Rolling back this migration removes only the substitutes, leaving the Starting XI intact.
4+
5+
INSERT INTO players (id, squadNumber, firstName, middleName, lastName, dateOfBirth, position, abbrPosition, team, league, starting11) VALUES
6+
('5a9cd988-95e6-54c1-bc34-9aa08acca8d0', 1, 'Franco', 'Daniel', 'Armani', '1986-10-16T00:00:00.000Z', 'Goalkeeper', 'GK', 'River Plate', 'Copa de la Liga', 0),
7+
('5fdb10e8-38c0-5084-9a3f-b369a960b9c2', 2, 'Juan', 'Marcos', 'Foyth', '1998-01-12T00:00:00.000Z', 'Right-Back', 'RB', 'Villarreal', 'La Liga', 0),
8+
('bbd441f7-fcfb-5834-8468-2a9004b64c8c', 4, 'Gonzalo', 'Ariel', 'Montiel', '1997-01-01T00:00:00.000Z', 'Right-Back', 'RB', 'Nottingham Forest', 'Premier League', 0),
9+
('9d140400-196f-55d8-86e1-e0b96a375c83', 5, 'Leandro', 'Daniel', 'Paredes', '1994-06-29T00:00:00.000Z', 'Defensive Midfield','DM', 'AS Roma', 'Serie A', 0),
10+
('d8bfea25-f189-5d5e-b3a5-ed89329b9f7c', 6, 'Germán', 'Alejo', 'Pezzella', '1991-06-27T00:00:00.000Z', 'Centre-Back', 'CB', 'Real Betis Balompié', 'La Liga', 0),
11+
('dca343a8-12e5-53d6-89a8-916b120a5ee4', 8, 'Marcos', 'Javier', 'Acuña', '1991-10-28T00:00:00.000Z', 'Left-Back', 'LB', 'Sevilla FC', 'La Liga', 0),
12+
('c62f2ac1-41e8-5d34-b073-2ba0913d0e31', 12, 'Gerónimo', NULL, 'Rulli', '1992-05-20T00:00:00.000Z', 'Goalkeeper', 'GK', 'Ajax Amsterdam', 'Eredivisie', 0),
13+
('d3b0e8e8-2c34-531a-b608-b24fed0ef986', 14, 'Exequiel', 'Alejandro', 'Palacios', '1998-10-05T00:00:00.000Z', 'Central Midfield', 'CM', 'Bayer 04 Leverkusen', 'Bundesliga', 0),
14+
('b1306b7b-a3a4-5f7c-90fd-dd5bdbed57ba', 15, 'Ángel', 'Martín', 'Correa', '1995-03-09T00:00:00.000Z', 'Right Winger', 'RW', 'Atlético Madrid', 'La Liga', 0),
15+
('ecec27e8-487b-5622-b116-0855020477ed', 16, 'Thiago', 'Ezequiel', 'Almada', '2001-04-26T00:00:00.000Z', 'Attacking Midfield','AM', 'Atlanta United FC', 'Major League Soccer',0),
16+
('7cc8d527-56a2-58bd-9528-2618fc139d30', 17, 'Alejandro', 'Darío', 'Gómez', '1988-02-15T00:00:00.000Z', 'Left Winger', 'LW', 'AC Monza', 'Serie A', 0),
17+
('191c82af-0c51-526a-b903-c3600b61b506', 18, 'Guido', NULL, 'Rodríguez', '1994-04-12T00:00:00.000Z', 'Defensive Midfield','DM', 'Real Betis Balompié', 'La Liga', 0),
18+
('7941cd7c-4df1-5952-97e8-1e7f5d08e8aa', 21, 'Paulo', 'Exequiel', 'Dybala', '1993-11-15T00:00:00.000Z', 'Second Striker', 'SS', 'AS Roma', 'Serie A', 0),
19+
('79c96f29-c59f-5f98-96b8-3a5946246624', 22, 'Lautaro', 'Javier', 'Martínez', '1997-08-22T00:00:00.000Z', 'Centre-Forward', 'CF', 'Inter Milan', 'Serie A', 0),
20+
('98306555-a466-5d18-804e-dc82175e697b', 25, 'Lisandro', NULL, 'Martínez', '1998-01-18T00:00:00.000Z', 'Centre-Back', 'CB', 'Manchester United', 'Premier League', 0);

0 commit comments

Comments
 (0)