This project wraps the upstream Booklore application to add MariaDB, PostgreSQL, and SQLite support. The upstream only supports MariaDB. We use a Gradle source overlay pattern: upstream sources are pulled via a git submodule and filtered at build time, with our replacement files taking precedence.
- Git submodule:
upstream/→https://github.com/booklore-app/booklore.git - Sync tasks copy upstream sources to
build/generated/with exclusions for files we replace - Both overlay (
src/) and filtered upstream (build/generated/) directories are compiled together setupTestFixturescopies binary test resources (cbx/) that use filesystem paths (not classpath)
| File | Purpose |
|---|---|
src/main/java/.../config/DatabaseMigrationConfig.java |
Liquibase primary + legacy Flyway catch-up migration |
src/main/java/.../config/DataSourceConfig.java |
SQLite JDBC driver configuration (epoch millis timestamps) |
src/main/java/.../config/TimezoneFunctionContributor.java |
Hibernate FunctionContributor for cross-DB timezone HQL functions |
src/main/java/.../repository/ReadingSessionRepository.java |
Native queries rewritten as JPQL using custom HQL functions |
src/main/resources/application.yaml |
Multi-DB datasource config, defaults to SQLite |
src/main/resources/db/changelog/ |
Liquibase changelogs (replacing upstream Flyway migrations) |
src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor |
SPI registration |
src/test/resources/application-test.yml |
H2 in-memory test config with Liquibase disabled |
-
Hibernate FunctionContributor SPI registers
tz_convert,tz_date,tz_day_of_weekas custom HQL functions with dialect-specific SQL patterns (MariaDB/PostgreSQL/SQLite/H2). This avoids native queries entirely. -
Liquibase instead of Flyway for database-agnostic DDL. The
DatabaseMigrationConfigdetects existing Flyway installations and runs catch-up migrations before handing off to Liquibase. -
SQLite compatibility constraints:
addUniqueConstraint→createIndexwithunique: true(SQLite lacksALTER TABLE ADD CONSTRAINT)addPrimaryKey→ inlineprimaryKey: trueon columns increateTable- Auto-increment PKs use
${id.type}property substitution (integerfor SQLite,bigintelsewhere) DataSourceConfigappendsdate_class=INTEGER&date_precision=MILLISECONDSto SQLite JDBC URLs (Hibernate writes timestamps as epoch millis)
-
H2 for tests: Liquibase is skipped for
jdbc:h2:mem:URLs; Hibernate DDL manages test schemas. TheTimezoneFunctionContributorhas an H2 branch with identity/passthrough patterns.
# Build (requires JDK 25)
./gradlew build
# Run tests (all 3379 upstream tests should pass)
./gradlew test
# Docker
docker build -t booklore-multidb .When upstream releases a new version:
- Update submodule:
cd upstream && git fetch && git checkout <tag> && cd .. && git add upstream - Port new Flyway migrations to Liquibase changelogs in
src/main/resources/db/changelog/changelogs/ - Check for new native queries:
grep -r "nativeQuery = true" upstream/booklore-api/src/ - Diff upstream
build.gradlefor dependency changes - Build and run full test suite
Set DATABASE_URL environment variable:
| Database | URL Format |
|---|---|
| SQLite (default) | jdbc:sqlite:/path/to/booklore.db |
| MariaDB | jdbc:mariadb://host:3306/booklore |
| PostgreSQL | jdbc:postgresql://host:5432/booklore |
For MariaDB/PostgreSQL, also set DATABASE_USERNAME and DATABASE_PASSWORD.
- Generated/virtual columns require SQLite 3.31+ (2020-01-22)
- No
ALTER TABLE ADD CONSTRAINT— all constraints must be inline or use indexes AUTOINCREMENTrequiresINTEGER PRIMARY KEY(notBIGINT)- Timestamps stored as epoch milliseconds (configured via JDBC URL parameters)
- Single-writer concurrency model — use
maximum-pool-size=1for write-heavy workloads
- Java 25, Spring Boot 4.0.3, Hibernate 7.2.6, Gradle 9.4.0
- hibernate-community-dialects (SQLiteDialect)
- Liquibase (schema management), Flyway (legacy catch-up only)
- SQLite JDBC 3.49.1.0, PostgreSQL driver, MariaDB driver