Repository navigation
Create the changelog table in the manual migration export for an empty database - #51730
Conversation
There was a problem hiding this comment.
Pull request overview
Restores valid manual migration exports for empty databases by explicitly creating Liquibase’s changelog table when needed.
Changes:
- Tracks whether changelog tables existed before validation.
- Emits missing
DATABASECHANGELOGDDL while avoiding custom-table duplication. - Adds integration coverage and manual migration documentation.
Reviewed changes
Copilot reviewed 4 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
quarkus/runtime/.../QuarkusJpaUpdaterProvider.java |
Implements changelog-table tracking and DDL export. |
model/jpa/.../LiquibaseJpaUpdaterProvider.java |
Mirrors the fix in the legacy provider. |
ManualMigrationEmptyDatabaseDistTest.java |
Tests empty-database export ordering and uniqueness. |
ManualMigrationCustomProviderDistTest.java |
Verifies custom changelog-table export behavior. |
BasicDatabaseTest.java |
Restores cross-database export assertions. |
migrate_db.adoc |
Documents empty-database manual migration requirements. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (MASTER_CHANGELOG_TABLE.equalsIgnoreCase(changelogTableName) | ||
| && Boolean.FALSE.equals(changelogTablePreExisted.get(changelogTableName))) { |
There was a problem hiding this comment.
Thanks for spotting this. I don't think this PR changes that behaviour though.
The MySQLCustomChangeLogHistoryService block isn't touched here; a diff against main shows no changed line in it. And when changelogTablePreExisted is true, the generated script is identical to what main produces: main doesn't emit CREATE TABLE in that case either, because Liquibase skips it when the table already exists, and the same ALTER TABLE ... ADD PRIMARY KEY follows it. So the failure you describe already happens on main today.
I'd also be careful with the suggested fix. Liquibase's standard changelog table has no primary key, which is exactly why Keycloak adds one for MySQL. An administrator pre-creating the table for a user without DDL rights will normally create it from that standard DDL, so without the key. Suppressing the statement whenever the table pre-exists would leave those deployments without it. Getting it right means knowing whether the target database already has the key, and in the flow this PR is about, the target isn't the database the script was generated against.
Happy to open a separate issue for it if you think it's worth fixing.
| if (MASTER_CHANGELOG_TABLE.equalsIgnoreCase(changelogTableName) | ||
| && Boolean.FALSE.equals(changelogTablePreExisted.get(changelogTableName))) { |
There was a problem hiding this comment.
Thanks for spotting this. I don't think this PR changes that behaviour though.
The MySQLCustomChangeLogHistoryService block isn't touched here; a diff against main shows no changed line in it. And when changelogTablePreExisted is true, the generated script is identical to what main produces: main doesn't emit CREATE TABLE in that case either, because Liquibase skips it when the table already exists, and the same ALTER TABLE ... ADD PRIMARY KEY follows it. So the failure you describe already happens on main today.
I'd also be careful with the suggested fix. Liquibase's standard changelog table has no primary key, which is exactly why Keycloak adds one for MySQL. An administrator pre-creating the table for a user without DDL rights will normally create it from that standard DDL, so without the key. Suppressing the statement whenever the table pre-exists would leave those deployments without it. Getting it right means knowing whether the target database already has the key, and in the flow this PR is about, the target isn't the database the script was generated against.
Happy to open a separate issue for it if you think it's worth fixing.
| assertThat(output, containsString("Create Database Change Log Table")); | ||
|
|
||
| // The script inserts into the changelog table, so it has to create it - once, or applying it fails | ||
| var outputLowerCase = output.toLowerCase(); |
There was a problem hiding this comment.
Fixed, the assertion now uses toLowerCase(Locale.ROOT).
One correction on the impact though. The three database tests wouldn't have returned zero. The count adds two alternatives, public.databasechangelog and keycloak.databasechangelog, and these tests run against PostgreSQL, MySQL and MariaDB, whose exports qualify the table with the schema or database name, so CREATE TABLE keycloak.databasechangelog (. KEYCLOAK lowercases the same way under tr_TR, so that alternative still matches. Only the PUBLIC. one is affected by the dotless ı, and it isn't what those databases produce.
I checked the underlying behaviour at string level ("PUBLIC".toLowerCase(new Locale("tr","TR")) gives publıc and the match fails) and reran PostgreSQLDistTest after the change. The fix is right either way, since the assertion shouldn't depend on the host locale.
0bb8298 to
1be1244
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/QuarkusJpaUpdaterProvider.java:239
- When the table was initially absent, Liquibase has already created it on the live connection before this block—the root cause described by this PR. On databases with auto-committing DDL such as MySQL, this emitted
CREATE TABLEtherefore fails when the script is applied back to the database it was generated from, which is the documented manual-migration workflow. Prevent or clean up the validation-created table after export, or otherwise make this creation idempotent.
if (MASTER_CHANGELOG_TABLE.equalsIgnoreCase(changelogTableName)
&& Boolean.FALSE.equals(changelogTablePreExisted.get(changelogTableName))) {
// Same comment Liquibase emits when it writes this statement itself
loggingExecutor.comment("Create Database Change Log Table");
loggingExecutor.execute(new CreateDatabaseChangeLogTableStatement());
model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java:236
- This has the same same-database failure as the Quarkus implementation: when the table was absent at startup, Liquibase creates it on the live connection before export, and auto-committing databases retain it, so the generated
CREATE TABLEfails when applied to that database. Prevent or clean up the validation-created table after export, or otherwise make this creation idempotent.
if (MASTER_CHANGELOG_TABLE.equalsIgnoreCase(changelogTableName)
&& Boolean.FALSE.equals(changelogTablePreExisted.get(changelogTableName))) {
// Same comment Liquibase emits when it writes this statement itself
loggingExecutor.comment("Create Database Change Log Table");
loggingExecutor.execute(new CreateDatabaseChangeLogTableStatement());
|
The failing It looks like a follow-on from #51602: the URL was updated to |
1be1244 to
52fa878
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 6 changed files in this pull request and generated 6 comments.
Suppressed comments (2)
docs/documentation/upgrading/topics/migrate_db.adoc:77
- The CLI option formatting here (
--spi-connections-jpa--quarkus--initialize-empty=false) is inconsistent with the option actually used in tests in this PR (--spi-connections-jpa-quarkus-initialize-empty=false). Align the documentation with the correct CLI option spelling to prevent copy/paste failures.
For an empty database, also set the `initialize-empty` property to "false". Otherwise {project_name} creates the
schema itself and no SQL file is written, regardless of the `migration-strategy` value:
[source,bash]
----
kc.[sh|bat] start --spi-connections-jpa--quarkus--initialize-empty=false
----
quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/BasicDatabaseTest.java:104
- This assertion hard-codes schema names (
public/keycloak) and an exact formatting of the emitted SQL. If the export changes to omit schema qualification, quote identifiers, or uses a different default schema in some environments, this becomes brittle. Consider matching more flexibly (e.g., schema-optional search, or counting occurrences ofdatabasechangelog (combined with a stronger nearby marker like the 'Create Database Change Log Table' comment) while still ensuring the statement is emitted exactly once.
var outputLowerCase = output.toLowerCase(Locale.ROOT);
var count = countMatches(outputLowerCase, "create table public.databasechangelog (")
+ countMatches(outputLowerCase, "create table keycloak.databasechangelog (");
assertThat(count, is(1));
| @@ -68,11 +69,14 @@ public class QuarkusJpaUpdaterProvider implements JpaUpdaterProvider { | |||
| private static final Logger logger = Logger.getLogger(QuarkusJpaUpdaterProvider.class); | |||
|
|
|||
| public static final String CHANGELOG = "META-INF/jpa-changelog-master.xml"; | |||
| private static final String MASTER_CHANGELOG_TABLE = "DATABASECHANGELOG"; | |||
There was a problem hiding this comment.
The map is gone, replaced by a single flag for the master table.
| // Per changelog table: did it already exist when this server started, before validation created it? | ||
| private final Map<String, Boolean> changelogTablePreExisted = new HashMap<>(); |
| String changelogTableName = database.getDatabaseChangeLogTableName(); | ||
| if (MASTER_CHANGELOG_TABLE.equalsIgnoreCase(changelogTableName) | ||
| && Boolean.FALSE.equals(changelogTablePreExisted.get(changelogTableName))) { |
| @@ -68,10 +69,13 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider { | |||
| private static final Logger logger = Logger.getLogger(LiquibaseJpaUpdaterProvider.class); | |||
|
|
|||
| public static final String CHANGELOG = "META-INF/jpa-changelog-master.xml"; | |||
| private static final String MASTER_CHANGELOG_TABLE = "DATABASECHANGELOG"; | |||
There was a problem hiding this comment.
The map is gone, replaced by a single flag for the master table.
|
|
||
| public static final String DEPLOYMENT_ID_COLUMN = "DEPLOYMENT_ID"; | ||
|
|
||
| private final KeycloakSession session; | ||
| // Per changelog table: did it already exist when this server started, before validation created it? | ||
| private final java.util.Map<String, Boolean> changelogTablePreExisted = new java.util.HashMap<>(); |
| String changelogTableName = database.getDatabaseChangeLogTableName(); | ||
| if (MASTER_CHANGELOG_TABLE.equalsIgnoreCase(changelogTableName) | ||
| && Boolean.FALSE.equals(changelogTablePreExisted.get(changelogTableName))) { |
Unreported flaky test detectedIf the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR. org.keycloak.testsuite.federation.ldap.LDAPUserLoginTest#loginLDAPUserCredentialVaultAuthenticationSimpleEncryptionStartTLSKeycloak CI - Java Distribution IT (windows-latest - temurin - 21) |
52fa878 to
8888203
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 6 changed files in this pull request and generated 2 comments.
Suppressed comments (2)
docs/documentation/upgrading/topics/migrate_db.adoc:77
- The CLI option name in the added example appears to use double separators (
--spi-connections-jpa--quarkus--initialize-empty=false). In this PR’s tests the option is passed as--spi-connections-jpa-quarkus-initialize-empty=false(single separators). Update the docs example to the correct option spelling so users can copy/paste it successfully.
For an empty database, also set the `initialize-empty` property to "false". Otherwise {project_name} creates the
schema itself and no SQL file is written, regardless of the `migration-strategy` value:
[source,bash]
----
kc.[sh|bat] start --spi-connections-jpa--quarkus--initialize-empty=false
----
quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/BasicDatabaseTest.java:104
- This exact substring match is sensitive to SQL formatting and schema qualification (e.g., no schema prefix, quoted identifiers, multiple spaces/newlines). To make the test more robust, consider matching
create tablefordatabasechangelogwith a case-insensitive regex and flexible whitespace (and optionally allowing an arbitrary schema prefix), then assert the match count is exactly 1.
var outputLowerCase = output.toLowerCase(Locale.ROOT);
var count = countMatches(outputLowerCase, "create table public.databasechangelog (")
+ countMatches(outputLowerCase, "create table keycloak.databasechangelog (");
assertThat(count, is(1));
| String changelogTableName = database.getDatabaseChangeLogTableName(); | ||
| if (MASTER_CHANGELOG_TABLE.equalsIgnoreCase(changelogTableName) | ||
| && Boolean.FALSE.equals(changelogTablePreExisted.get(changelogTableName))) { | ||
| // Same comment Liquibase emits when it writes this statement itself | ||
| loggingExecutor.comment("Create Database Change Log Table"); | ||
| loggingExecutor.execute(new CreateDatabaseChangeLogTableStatement()); | ||
| } |
| String changelogTableName = database.getDatabaseChangeLogTableName(); | ||
| if (MASTER_CHANGELOG_TABLE.equalsIgnoreCase(changelogTableName) | ||
| && Boolean.FALSE.equals(changelogTablePreExisted.get(changelogTableName))) { | ||
| // Same comment Liquibase emits when it writes this statement itself | ||
| loggingExecutor.comment("Create Database Change Log Table"); | ||
| loggingExecutor.execute(new CreateDatabaseChangeLogTableStatement()); | ||
| } |
Unreported flaky test detectedIf the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR. org.keycloak.testsuite.broker.KcOidcBrokerTokenExchangeTest#org.keycloak.testsuite.federation.ldap.LDAPUserLoginTest#loginLDAPUserAuthenticationNoneEncryptionStartTLSKeycloak CI - Java Distribution IT (windows-latest - temurin - 21) org.keycloak.testsuite.webauthn.passwordless.WebAuthnPwdLessPropertyTest#timeout |
There was a problem hiding this comment.
Thank you for your PR. I think this PR is sound and solves something we need to fix. I only left minor comments.
|
|
||
| [source,bash] | ||
| < 10BC8 /td> | ---- | |
| kc.[sh|bat] start --spi-connections-jpa--quarkus--initialize-empty=false |
There was a problem hiding this comment.
| kc.[sh|bat] start --spi-connections-jpa--quarkus--initialize-empty=false | |
| <@kc.start parameters="--spi-connections-jpa--quarkus--initialize-empty=false"/> |
There was a problem hiding this comment.
That block is gone with the paragraph above.
| @@ -69,6 +69,24 @@ kc.[sh|bat] start --spi-connections-jpa--quarkus--migration-export=<path>/<file. | |||
|
|
|||
| After the changes have been written to the file, the server exits. | |||
|
|
|||
| For an empty database, also set the `initialize-empty` property to "false". Otherwise {project_name} creates the | |||
There was a problem hiding this comment.
My 2 cents (I don't know what maintainers thinks!):
It sounds like you are documenting bug. manual migration strategy must be explicitly as it is not a default strategy. If we ignore explicit configuration for scenario where the database is empty, I think that is validation error rather than expected behavior. They contradict each other.
There was a problem hiding this comment.
Agreed, createOrUpdateSchema checks initializeEmpty before it looks at the strategy, so an explicit manual or validate is silently ignored on an empty database. Dropped the paragraph.
Do you want that fixed in this PR, or in a separate issue? I'd lean towards separate since it's new behaviour rather than the regression this one restores.
There was a problem hiding this comment.
Do you want that fixed in this PR, or in a separate issue? I'd lean towards separate since it's new behaviour rather than the regression this one restores.
I think a separate issue is better, Please create it as at this point, you can describe it better than I do.
|
|
||
| public static final String DEPLOYMENT_ID_COLUMN = "DEPLOYMENT_ID"; | ||
|
|
||
| private final KeycloakSession session; | ||
| // Per changelog table: did it already exist when this server started, before validation created it? | ||
| private final java.util.Map<String, Boolean> changelogTablePreExisted = new java.util.HashMap<>(); |
There was a problem hiding this comment.
You only create the master changelog table, that is a flag, not map. Putting anything else to the map is a dead data.
There was a problem hiding this comment.
Fair, replaced with a single Boolean masterChangelogTablePreExisted. Same in the Quarkus provider.
| private static final String DEPLOYMENT_ID_COLUMN = "DEPLOYMENT_ID"; | ||
| public static final String VERIFY_AND_RUN_MASTER_CHANGELOG = "VERIFY_AND_RUN_MASTER_CHANGELOG"; | ||
|
|
||
| private final KeycloakSession session; | ||
| private Map<String, List<ChangeSet>> changeSets = new HashMap<>(); | ||
| // Per changelog table: did it already exist when this server started, before validation created it? | ||
| private final Map<String, Boolean> changelogTablePreExisted = new HashMap<>(); |
There was a problem hiding this comment.
same as in model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
There was a problem hiding this comment.
Done, same as in model/jpa.
8888203 to
384a019
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 6 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/liquibase/QuarkusJpaUpdaterProvider.java:241
- The new behavior has two branches: emit DDL when the changelog table was absent at server start, and do not emit it when it pre-existed. Current tests cover the empty-DB path; please add coverage for the pre-created-table path to ensure the script does not include the
CREATE TABLE DATABASECHANGELOGstatement (and remains applicable to the same DB it was generated from).
if (isMasterChangelogTable(database) && Boolean.FALSE.equals(masterChangelogTablePreExisted)) {
// Same comment Liquibase emits when it writes this statement itself
loggingExecutor.comment("Create Database Change Log Table");
loggingExecutor.execute(new CreateDatabaseChangeLogTableStatement());
}
| if (isMasterChangelogTable(database) && Boolean.FALSE.equals(masterChangelogTablePreExisted)) { | ||
| // Same comment Liquibase emits when it writes this statement itself | ||
| loggingExecutor.comment("Create Database Change Log Table"); | ||
| loggingExecutor.execute(new CreateDatabaseChangeLogTableStatement()); | ||
| } |
There was a problem hiding this comment.
It can't be null here: this method's only caller is updateChangeSet, which records the flag for the master table at its start, on the same Database, before reaching this call. ManualMigrationEmptyDatabaseDistTest covers exactly this path.
| if (isMasterChangelogTable(database) && Boolean.FALSE.equals(masterChangelogTablePreExisted)) { | ||
| // Same comment Liquibase emits when it writes this statement itself | ||
| loggingExecutor.comment("Create Database Change Log Table"); | ||
| loggingExecutor.execute(new CreateDatabaseChangeLogTableStatement()); | ||
| } |
There was a problem hiding this comment.
Same as in the Quarkus provider: the only caller is updateChangeSet, which sets the flag before this runs.
Unreported flaky test detectedIf the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR. org.keycloak.testsuite.adapter.servlet.SAMLClockSkewAdapterTest#testTokenTimeIsValidKeycloak CI - Adapter IT Strict Cookies |
…y database Liquibase writes the DDL of its changelog table into the generated script only while that table does not exist. Since Liquibase 4.33 parsing a changelog initializes the change log history service, which creates the table on the live connection, so by the time the export is written Liquibase no longer emits it. The script then inserts into a table it never creates and cannot be applied to an empty database, even though its header offers it as a creation script. Emit the statement explicitly, but only when the table did not exist before this server started - an administrator may have created it up front so that a user without DDL rights can produce the script at all, and in that case the target database already has it. This restores the guarantee added in keycloak#32535 along with the assertions that were dropped from BasicDatabaseTest in keycloak#41299. Closes keycloak#51729 Signed-off-by: Ali Tugrul Pinar <ali@keymate.io>
384a019 to
39f9d77
Compare
|
|
||
| After the changes have been written to the file, the server exits. | ||
|
|
||
| For an empty database the script also creates the Liquibase `DATABASECHANGELOG` table, which it records the applied |
Unreported flaky test detectedIf the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR. org.keycloak.testsuite.forms.BruteForceTest#testGrantInvalidOtpKeycloak CI - Forms IT (chrome) |
There was a problem hiding this comment.
LGTM, thanks for the fix.
|
Let's get a review from @Pepo48 or production readiness, then commit this. |
There was a problem hiding this comment.
I see in the docs the following statement:
It does not create the
DATABASECHANGELOGLOCKtable
I see that this PR fixes part of the problem, but reading this statement shows that it is not resolving it. If the CN team wants to proceed with it, I'm ok to proceed. Still there should be a follow-up issue for that extra table.
The following statement in the comments is also worrying:
An administrator may have pre-created it so that a user without DDL rights could produce the script at all
This also points to a process that is rather broken and a smooth experience.
Again, I'm ok to create a follow-up issue for that, which might be more docs, or a simplified process.
|
Thanks @ahus1 for having a look.
The
Point being is that we create it conditionally when it is missing, hence user without DDL rights can still execute migration script as long as all DDL operations are not necessary, because all tables exist. Please give me a hint, I definitely think we want a smooth experience, I just don't see what you see. |
|
@ahus1 , @michalvavrik Opened #53088 as the follow-up for the lock table. It turned out you had already proposed exactly this in #10149 back in 2022 and @hmlnarik agreed there, the issue just expired. The fix builds on the pre-existence flag introduced here, so I'll send the PR once this one is merged. Same rule as for the changelog table: emitted only when the table did not exist before the export run, so applying the script to the generating database keeps working. |
|
@Alnyli07 - thank you for this PR. I should have added some more context to my previous comment: A common scenario where people need those SQL scripts: The Keycloak user has (for good security practice) no permissions to run DDL/create tables. So the script is run by a different person / admin manually. This only works when it creates all the tables, including both the databasechangelog and the lock table. Otherwise, you run the script, and then see an error message later when Keycloak tries to create the table, but doesn't have permission. Thanks for creating the follow-up PR. I hope I'll be faster in providing feedback next time. |
Closes #51729
What
With
migration-strategy=manualagainst an empty database, the generated script offers itself as a creationscript for an empty database but inserts into
DATABASECHANGELOGwithout creating it, so it cannot be applied.Liquibase writes that DDL into the script only while the table does not exist. Since Liquibase 4.33 parsing a
changelog initializes the change log history service (
DatabaseChangeLog), which cre 8000 ates the table on the liveconnection, so by the time the export is written Liquibase no longer emits it.
This restores the guarantee added in #32535 / PR #37204, together with the two assertions that were dropped from
BasicDatabaseTestin #41299 / PR #41490.How
outputChangeLogTableCreationScriptemitsCreateDatabaseChangeLogTableStatementexplicitly, guarded on twoconditions:
tables up front so a user without DDL rights can produce the script at all, and in that case the target
database already has the table and the DDL would break the script with
already exists. This is the casePR Invalid migration export for empty database #37204 was originally fixing, so the guard is what keeps that fix intact;
JpaEntityProviderchangelog tables are still emitted by Liquibaseitself (verified:
DATABASECHANGELOG_MIGRATION_appears exactly once onmain), so emitting them here toowould duplicate.
Whether the table pre-existed is recorded in
validateChangeSetand inupdateChangeSet; the latter is neededbecause validation returns as soon as one changelog is out of date, so custom providers may not be seen there.
The same change is applied to the legacy
LiquibaseJpaUpdaterProvider, as in fe40730.Testing
New
ManualMigrationEmptyDatabaseDistTest(H2, raw dist, empty database) asserts the script:client-attributes-string-accomodation-fixed-post-create-indexasMARK_RAN(asserted as theabsence of its MySQL-only
VALUE(255)index syntax).changeSetExecutedpreconditions withare resolved against the physical changelog table, so this pins that the fix does notremove it.
ManualMigrationCustomProviderDistTestgains the same once/ordering assertions for a custom provider'sbookkeeping table.
The two assertions removed in PR #41490 are restored in
BasicDatabaseTest.assertManualDbInitialization, soPostgreSQLDistTest,MySQLDistTestandMariaDBDistTestguard this again.Verified end to end on PostgreSQL 17 with an empty schema — the generated script applies with
psql -v ON_ERROR_STOP=1(exit 0, 99 tables), after which the server starts withmigration-strategy=validate.Applying to a database whose bookkeeping tables were pre-created still works, i.e. no
already exists.Run against PostgreSQL (7 tests), MySQL (3) and MariaDB (3) through the
test-databaseprofile, which is wherethe restored
assertManualDbInitializationassertions apply. The MySQL path matters here becauseMySQLCustomChangeLogHistoryServiceadds the changelog table's primary key to the script; theCREATE TABLEis emitted before that
ALTER TABLE.The upgrade path is unaffected, which I checked separately on PostgreSQL: with an already migrated database
rolled back by one release, the generated script contains the 23 pending changesets and no
CREATE TABLEforeither bookkeeping table —
outputChangeLogTableCreationScriptis only reached when no changeset has run yet.Out of scope
DATABASECHANGELOGLOCKremains absent from the script — it is created byCustomLockServiceon startup, whichis why it was excluded in the first place, and that is unchanged here. So a database populated only by running
the script still lacks the lock table, and the first server start against it needs the privilege to create that
one table (in the run above the server was started with an owner role, which has it). The documentation change
in this PR states that; making manual mode work for a database user with no DDL privileges at all is a larger
question I would rather raise separately.
Documentation
docs/documentation/upgrading/topics/migrate_db.adoc— the "Manual relational database migration" section didnot mention
initialize-empty=false, although manual mode is silently skipped without it on an empty database.Added, together with a note that the two Liquibase bookkeeping tables are still created on startup, so a user
without DDL rights needs them created up front.
Note on AI assistance
AI agents were used while investigating this and while drafting the change, tests and documentation. I have
reviewed and understand every line, ran the verification described above myself, and will handle review feedback
directly.