From 7a41fe968b9fcb69b378336740b74e5448c1ff81 Mon Sep 17 00:00:00 2001 From: Silvan <27845747+adlerhurst@users.noreply.github.com> Date: Tue, 3 Feb 2026 17:23:24 +0100 Subject: [PATCH] fix(setup): ensure PostgreSQL 18 compatibility (#11484) # Which Problems Are Solved When starting Zitadel with Postgres version 18, setup fails with the following error: `level=error msg="migration failed" caller=".../cmd/setup/setup.go:373" code=0A000 detail= error="ERROR: partitioned tables cannot be unlogged (SQLSTATE 0A000)" hint= message="partitioned tables cannot be unlogged" name=34_add_cache_schema severity=ERROR` # How the Problems Are Solved - Modify setup step 34 to ensure compatibility with PostgreSQL 18 by changing the creation of the partitioned tables to`LOGGED` tables but keep the partitions `UNLOGGED`. - Added an additional setup step which alters the table persistence of the partitioned tables to `LOGGED`. # Additional Changes - Bumped Postgres compatibility to version 18 in docs. - Ensure default partitions for cache tables ## Additional Context - closes https://github.com/zitadel/zitadel/issues/10712 - backport to v4 - migration from PostgreSQL version 17 to 18 was verified using `pg_dumpall` and restoring the created backup file - and new setups using PostgreSQL version 18 directly --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .devcontainer/docker-compose.yaml | 6 ++--- .../manage/database/_postgres.mdx | 2 +- cmd/setup/34/34_cache_schema.sql | 10 +++++-- cmd/setup/69.go | 27 +++++++++++++++++++ cmd/setup/69.sql | 19 +++++++++++++ cmd/setup/config.go | 1 + cmd/setup/setup.go | 2 ++ 7 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 cmd/setup/69.go create mode 100644 cmd/setup/69.sql diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml index 1f4e313d4..2aeac9e3a 100644 --- a/.devcontainer/docker-compose.yaml +++ b/.devcontainer/docker-compose.yaml @@ -24,7 +24,7 @@ services: - "4200:4200" db: - image: postgres:17 + image: postgres:18 restart: unless-stopped env_file: - ./.env.db @@ -40,7 +40,7 @@ services: - 5432:5432 db-api-integration: - image: 'postgres:17' + image: 'postgres:18' env_file: - ./.env.db environment: @@ -61,7 +61,7 @@ services: - 6379:6379 db-functional-ui: - image: 'postgres:17' + image: 'postgres:18' env_file: - ./.env.db environment: diff --git a/apps/docs/content/self-hosting/manage/database/_postgres.mdx b/apps/docs/content/self-hosting/manage/database/_postgres.mdx index 5beff9fda..85a13a0f4 100644 --- a/apps/docs/content/self-hosting/manage/database/_postgres.mdx +++ b/apps/docs/content/self-hosting/manage/database/_postgres.mdx @@ -4,7 +4,7 @@ PostgreSQL is the default database for ZITADEL due to its reliability, robustnes If you are using Zitadel v2 and want to use a PostgreSQL database, you can [overwrite the default configuration](/self-hosting/manage/configure/configure). -Currently, versions 14 to 17 are supported. +Currently, versions 14 to 18 are supported. Postgres can be configured as follows: ```yaml diff --git a/cmd/setup/34/34_cache_schema.sql b/cmd/setup/34/34_cache_schema.sql index c2dd838c2..23fc1185c 100644 --- a/cmd/setup/34/34_cache_schema.sql +++ b/cmd/setup/34/34_cache_schema.sql @@ -1,6 +1,6 @@ create schema if not exists cache; -create unlogged table if not exists cache.objects ( +create table if not exists cache.objects ( cache_name varchar not null, id uuid not null default gen_random_uuid(), created_at timestamptz not null default now(), @@ -11,7 +11,10 @@ create unlogged table if not exists cache.objects ( ) partition by list (cache_name); -create unlogged table if not exists cache.string_keys( +create unlogged table if not exists cache.objects_default + PARTITION OF cache.objects DEFAULT; + +create table if not exists cache.string_keys( cache_name varchar not null check (cache_name <> ''), index_id integer not null check (index_id > 0), index_key varchar not null check (index_key <> ''), @@ -27,3 +30,6 @@ partition by list (cache_name); create index if not exists string_keys_object_id_idx on cache.string_keys (cache_name, object_id); -- for delete cascade + +create unlogged table if not exists cache.string_keys_default + PARTITION OF cache.string_keys DEFAULT; diff --git a/cmd/setup/69.go b/cmd/setup/69.go new file mode 100644 index 000000000..a9c1561d3 --- /dev/null +++ b/cmd/setup/69.go @@ -0,0 +1,27 @@ +package setup + +import ( + "context" + _ "embed" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 69.sql + cacheTablesLogged string +) + +type CacheTablesLogged struct { + dbClient *database.DB +} + +func (mig *CacheTablesLogged) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, cacheTablesLogged) + return err +} + +func (mig *CacheTablesLogged) String() string { + return "69_cache_tables_logged" +} diff --git a/cmd/setup/69.sql b/cmd/setup/69.sql new file mode 100644 index 000000000..d8d8d3ec3 --- /dev/null +++ b/cmd/setup/69.sql @@ -0,0 +1,19 @@ +DO +$do$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_class WHERE oid = 'cache.objects'::REGCLASS AND relpersistence = 'u') THEN + ALTER TABLE IF EXISTS cache.objects + SET LOGGED; + END IF; + IF EXISTS (SELECT 1 FROM pg_class WHERE oid = 'cache.string_keys'::REGCLASS AND relpersistence = 'u') THEN + ALTER TABLE IF EXISTS cache.string_keys + SET LOGGED; + END IF; + + CREATE UNLOGGED TABLE IF NOT EXISTS cache.objects_default + PARTITION OF cache.objects DEFAULT; + + CREATE UNLOGGED TABLE IF NOT EXISTS cache.string_keys_default + PARTITION OF cache.string_keys DEFAULT; +END +$do$ diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 807ec3355..2c1fcdbdf 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -177,6 +177,7 @@ type Steps struct { s66SessionRecoveryCodeCheckedAt *SessionRecoveryCodeCheckedAt s67SyncMemberRoleFields *SyncMemberRoleFields s68TargetAddPayloadTypeColumn *TargetAddPayloadTypeColumn + s69CacheTablesLogged *CacheTablesLogged } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index ca9196a2c..084145a09 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -249,6 +249,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s66SessionRecoveryCodeCheckedAt = &SessionRecoveryCodeCheckedAt{dbClient: dbClient} steps.s67SyncMemberRoleFields = &SyncMemberRoleFields{dbClient: dbClient} steps.s68TargetAddPayloadTypeColumn = &TargetAddPayloadTypeColumn{dbClient: dbClient} + steps.s69CacheTablesLogged = &CacheTablesLogged{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -303,6 +304,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s64ChangePushPosition, steps.s65FixUserMetadata5Index, steps.s67SyncMemberRoleFields, + steps.s69CacheTablesLogged, } { setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") if setupErr != nil {