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 <[email protected]>
This commit is contained in:
Silvan
2026-02-03 16:23:24 +00:00
committed by GitHub
co-authored by Copilot
parent 6363ee0d0a
commit 7a41fe968b
7 changed files with 61 additions and 6 deletions
+3 -3
View File
@@ -24,7 +24,7 @@ services:
- "4200:4200" - "4200:4200"
db: db:
image: postgres:17 image: postgres:18
restart: unless-stopped restart: unless-stopped
env_file: env_file:
- ./.env.db - ./.env.db
@@ -40,7 +40,7 @@ services:
- 5432:5432 - 5432:5432
db-api-integration: db-api-integration:
image: 'postgres:17' image: 'postgres:18'
env_file: env_file:
- ./.env.db - ./.env.db
environment: environment:
@@ -61,7 +61,7 @@ services:
- 6379:6379 - 6379:6379
db-functional-ui: db-functional-ui:
image: 'postgres:17' image: 'postgres:18'
env_file: env_file:
- ./.env.db - ./.env.db
environment: environment:
@@ -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). 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: Postgres can be configured as follows:
```yaml ```yaml
+8 -2
View File
@@ -1,6 +1,6 @@
create schema if not exists cache; 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, cache_name varchar not null,
id uuid not null default gen_random_uuid(), id uuid not null default gen_random_uuid(),
created_at timestamptz not null default now(), created_at timestamptz not null default now(),
@@ -11,7 +11,10 @@ create unlogged table if not exists cache.objects (
) )
partition by list (cache_name); 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 <> ''), cache_name varchar not null check (cache_name <> ''),
index_id integer not null check (index_id > 0), index_id integer not null check (index_id > 0),
index_key varchar not null check (index_key <> ''), 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 create index if not exists string_keys_object_id_idx
on cache.string_keys (cache_name, object_id); -- for delete cascade 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;
+27
View File
@@ -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"
}
+19
View File
@@ -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$
+1
View File
@@ -177,6 +177,7 @@ type Steps struct {
s66SessionRecoveryCodeCheckedAt *SessionRecoveryCodeCheckedAt s66SessionRecoveryCodeCheckedAt *SessionRecoveryCodeCheckedAt
s67SyncMemberRoleFields *SyncMemberRoleFields s67SyncMemberRoleFields *SyncMemberRoleFields
s68TargetAddPayloadTypeColumn *TargetAddPayloadTypeColumn s68TargetAddPayloadTypeColumn *TargetAddPayloadTypeColumn
s69CacheTablesLogged *CacheTablesLogged
} }
func MustNewSteps(v *viper.Viper) *Steps { func MustNewSteps(v *viper.Viper) *Steps {
+2
View File
@@ -249,6 +249,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s66SessionRecoveryCodeCheckedAt = &SessionRecoveryCodeCheckedAt{dbClient: dbClient} steps.s66SessionRecoveryCodeCheckedAt = &SessionRecoveryCodeCheckedAt{dbClient: dbClient}
steps.s67SyncMemberRoleFields = &SyncMemberRoleFields{dbClient: dbClient} steps.s67SyncMemberRoleFields = &SyncMemberRoleFields{dbClient: dbClient}
steps.s68TargetAddPayloadTypeColumn = &TargetAddPayloadTypeColumn{dbClient: dbClient} steps.s68TargetAddPayloadTypeColumn = &TargetAddPayloadTypeColumn{dbClient: dbClient}
steps.s69CacheTablesLogged = &CacheTablesLogged{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections") 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.s64ChangePushPosition,
steps.s65FixUserMetadata5Index, steps.s65FixUserMetadata5Index,
steps.s67SyncMemberRoleFields, steps.s67SyncMemberRoleFields,
steps.s69CacheTablesLogged,
} { } {
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
if setupErr != nil { if setupErr != nil {