io.github.yrashid.actual-schema generates the final PostgreSQL DDL produced by your Liquibase
migrations.
You do not need an existing database, a PostgreSQL installation, pg_dump, or the Liquibase CLI.
The plugin starts a disposable PostgreSQL container, applies the changelog, exports the resulting
schema, and removes the container when the task finishes.
This is useful when you want to:
- keep a reviewable
schema.sqlsnapshot in Git; - detect migrations that changed the final schema without updating that snapshot;
- provide the current DDL to code generators, documentation tools, or reviewers;
- inspect the final database state instead of the individual migration statements.
Unlike liquibase update-sql, which describes migration steps, this plugin captures the state of a
real PostgreSQL database after those steps have been executed.
- Gradle 8.5 or newer.
- Gradle running on Java 17 or newer.
- A running Docker-compatible daemon accessible to the current user.
- A Liquibase changelog in your project.
The PostgreSQL image is downloaded automatically by Testcontainers when it is not already available locally. No PostgreSQL server or client tools need to be installed on the host.
Kotlin DSL:
plugins {
id("io.github.yrashid.actual-schema") version "0.1.2"
}Published versions are listed on the Gradle Plugin Portal.
actualSchema {
changelogFile.set(
layout.projectDirectory.file("src/main/resources/db/changelog/db.changelog-master.yaml")
)
}The default resource root is src/main/resources. The generated file is written to
build/generated/actual-schema/schema.sql unless outputFile is configured.
./gradlew generateActualSchemaThe plugin will pull postgres:16 if necessary, start a temporary container, apply the changelog,
run pg_dump --schema-only, write the SQL file, and stop the container.
A common project layout looks like this:
src/main/resources/
├── application.yaml
└── db/changelog/
├── db.changelog-master.yaml
└── changes/
├── 001-create-users.yaml
└── 002-add-user-status.yaml
database/
└── schema.sql
Configure Spring Boot and the plugin to use the same root changelog:
plugins {
id("org.springframework.boot") version "<your Spring Boot version>"
id("io.spring.dependency-management") version "<compatible version>"
id("io.github.yrashid.actual-schema") version "0.1.2"
}
dependencies {
implementation("org.liquibase:liquibase-core")
runtimeOnly("org.postgresql:postgresql")
}
actualSchema {
changelogFile.set(
layout.projectDirectory.file("src/main/resources/db/changelog/db.changelog-master.yaml")
)
resourceBaseDir.set(layout.projectDirectory.dir("src/main/resources"))
// Store the snapshot in Git so checkActualSchema can verify it in CI.
outputFile.set(layout.projectDirectory.file("database/schema.sql"))
postgresImage.set("postgres:16")
schemas.set(listOf("public"))
}# src/main/resources/application.yaml
spring:
liquibase:
change-log: classpath:/db/changelog/db.changelog-master.yamlCreate and commit the initial snapshot:
./gradlew generateActualSchema
git add database/schema.sqlThe application and the plugin configure Liquibase independently. If your application uses
contexts, labels, or changelog parameters, configure the same values in actualSchema as shown
below. The plugin currently runs migrations with Liquibase 5.0.3.
Applies all selected migrations to a new temporary database and writes the resulting DDL to
outputFile.
./gradlew generateActualSchemaUse this command after adding or changing migrations. Review and commit the updated snapshot if it is stored in the repository.
Generates a separate candidate without overwriting outputFile, compares the files byte-for-byte,
and fails when the committed snapshot is missing or stale.
./gradlew checkActualSchemaFor this task to be useful, configure outputFile outside build/, for example
database/schema.sql, and commit that file.
actualSchema {
outputFile.set(layout.projectDirectory.file("database/schema.sql"))
postgresImage.set("postgres:16")
postgresStartupTimeoutSeconds.set(60)
schemas.set(listOf("public", "reporting"))
excludeTables.set(setOf("public.audit_log"))
includeLiquibaseTables.set(false)
normalizeOutput.set(true)
}- An empty
schemaslist means “dump all schemas”. excludeTablesvalues are passed topg_dump --exclude-table.- Liquibase metadata tables are omitted by default.
- Output normalization removes PostgreSQL version headers, random
\\restrict/\\unrestrictkeys, and trailing whitespace differences.
actualSchema {
liquibaseContexts.set(listOf("production"))
liquibaseLabels.set(listOf("core"))
liquibaseParameters.putAll(
mapOf(
"schemaName" to "public",
"tablePrefix" to "app_"
)
)
liquibaseDefaultSchema.set("public")
liquibaseSchema.set("public")
liquibaseChangeLogTable.set("databasechangelog")
liquibaseChangeLogLockTable.set("databasechangeloglock")
}Only configure values that your changelog needs. Empty context and label lists apply migrations using Liquibase's default selection behavior.
| Property | Default | Purpose |
|---|---|---|
changelogFile |
required | Root Liquibase changelog |
resourceBaseDir |
src/main/resources |
Base directory for included changelog resources |
outputFile |
build/generated/actual-schema/schema.sql |
Generated schema snapshot |
postgresImage |
postgres:16 |
PostgreSQL-compatible container image |
postgresImageCompatibleSubstituteFor |
postgres |
Canonical image name used by Testcontainers |
postgresStartupTimeoutSeconds |
60 |
Startup timeout for the temporary PostgreSQL container |
databaseName |
actual_schema |
Name of the temporary database |
username / password |
actual_schema |
Credentials used only for the temporary database |
schemas |
public |
Schemas passed to pg_dump; empty means all schemas |
excludeTables |
empty | Table patterns excluded from the dump |
liquibaseContexts |
empty | Liquibase contexts to enable |
liquibaseLabels |
empty | Liquibase labels to enable |
liquibaseParameters |
empty | Changelog parameters |
liquibaseDefaultSchema |
unset | Default schema used by Liquibase |
liquibaseSchema |
unset | Schema containing Liquibase metadata tables |
liquibaseChangeLogTable |
databasechangelog |
Name of the Liquibase changelog metadata table |
liquibaseChangeLogLockTable |
databasechangeloglock |
Name of the Liquibase changelog lock table |
includeLiquibaseTables |
false |
Include Liquibase metadata tables in the snapshot |
normalizeOutput |
true |
Remove volatile pg_dump output and group index-like statements by table |
Testcontainers automatically pulls the configured image through Docker. To use another PostgreSQL
version, set postgresImage:
actualSchema {
postgresImage.set("postgres:17")
}PostgreSQL-compatible images such as PostGIS can be declared as substitutes for the official image:
actualSchema {
postgresImage.set("postgis/postgis:16-3.4")
postgresImageCompatibleSubstituteFor.set("postgres")
}For reproducible snapshots, pin the image by digest. Registry authentication is handled by Docker and Testcontainers using the host's normal registry configuration.
Add extension JARs, parsers, serializers, or project classes to the plugin's isolated Liquibase runtime:
dependencies {
add("actualSchemaLiquibaseRuntime", "org.liquibase.ext:liquibase-hibernate6:VERSION")
add("actualSchemaLiquibaseRuntime", project(":database-custom-changes"))
}Extensions must be compatible with the Liquibase version used by the plugin.
plugins {
id 'io.github.yrashid.actual-schema' version '0.1.2'
}
actualSchema {
changelogFile.set(
layout.projectDirectory.file('src/main/resources/db/changelog/db.changelog-master.yaml')
)
resourceBaseDir.set(layout.projectDirectory.dir('src/main/resources'))
outputFile.set(layout.projectDirectory.file('database/schema.sql'))
postgresImage.set('postgres:16')
postgresStartupTimeoutSeconds.set(60)
schemas.set(['public'])
excludeTables.set(['public.audit_log'] as Set)
liquibaseContexts.set(['production'])
liquibaseLabels.set(['core'])
liquibaseParameters.put('schemaName', 'public')
includeLiquibaseTables.set(false)
normalizeOutput.set(true)
}Generate database/schema.sql locally and commit it. CI then needs to run only
checkActualSchema:
name: Check database schema
on:
pull_request:
push:
branches: [main]
jobs:
schema:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
- uses: gradle/actions/setup-gradle@v6
- run: ./gradlew checkActualSchema --configuration-cacheGitHub-hosted Ubuntu runners already provide Docker. A self-hosted runner must expose a compatible Docker daemon to the build user.
Start Docker and verify that it is reachable from the same user that runs Gradle:
docker infoFor Colima, Podman, remote Docker, or other compatible runtimes, configure Testcontainers using the runtime's normal environment variables and socket settings.
Use a project-relative Gradle file provider and make sure resourceBaseDir contains the changelog
resources:
actualSchema {
changelogFile.set(
layout.projectDirectory.file("src/main/resources/db/changelog/db.changelog-master.yaml")
)
resourceBaseDir.set(layout.projectDirectory.dir("src/main/resources"))
}Also verify the file name and extension: .yaml, .yml, .xml, .json, or formatted SQL.
Regenerate it, inspect the diff, and commit the expected change:
./gradlew generateActualSchema
git diff -- database/schema.sqlTry pulling the configured image directly:
docker pull postgres:16Check network access, registry credentials, proxy configuration, the image name, and the requested tag. Private registries must be authenticated through Docker before running Gradle.
If the image is available but PostgreSQL starts too slowly on a self-hosted runner, increase
postgresStartupTimeoutSeconds.
Add it to actualSchemaLiquibaseRuntime, not only to the application's implementation
configuration.
Confirm that the requested version is visible on the Gradle Plugin Portal. If you use a newly released version, allow a short time for Gradle caches and mirrors to refresh.
Liquibase changelogs, SQL files, and custom change classes are executable build inputs. Run the plugin only on trusted repository contents, particularly in CI jobs with Docker or secret access. The database itself is temporary and isolated, but access to the Docker daemon is privileged.
Report suspected vulnerabilities privately as described in SECURITY.md.
- PostgreSQL is the only supported database.
- Docker or a Testcontainers-compatible runtime is required.
- Generated DDL can change when the PostgreSQL image or Liquibase version changes.
- Build-cache storage is disabled because Docker image tags may be mutable.
User-visible changes are documented in CHANGELOG.md. If you want to contribute to the plugin itself, see CONTRIBUTING.md.
Licensed under the Apache License 2.0.