#787·lago

[BUG]: `enriched_events` is left partitioned but never registered with pg_partman, and `migrate.sh` exits 0

Author: mbucknamCreated Sep 1, 2026Updated Sep 12, 2026

Describe the bug

After a clean ./scripts/migrate.sh against an empty database, enriched_events exists and is declared PARTITION BY RANGE, but it is not registered with pg_partman. partman.part_config has no row for it, and the only child table is enriched_events_default. Every row therefore lands in the catch-all default partition, and no monthly partitions are ever created or maintained.

migrate.sh exits 0. There is no error, no warning, and nothing in the log at default level.

This is not a pg_partman problem and not a managed-database problem — it reproduces on getlago/postgres-partman:15.0-alpine, the image this project ships.

To Reproduce

  1. Start the reference database image:

    bash
    docker network create lagorepro
    docker run -d --name pgpartman --network lagorepro \
      -e POSTGRES_PASSWORD=repro -e POSTGRES_USER=lago -e POSTGRES_DB=lago \
      getlago/postgres-partman:15.0-alpine
  2. Confirm the extension is present — it is, version 5.4.0:

    bash
    docker exec pgpartman psql -U lago -d lago -tAc \
      "select extversion from pg_extension where extname='pg_partman'"
  3. Run the migration against it:

    bash
    docker run --rm --network lagorepro \
      -e RAILS_ENV=production \
      -e DATABASE_URL=postgresql://lago:repro@pgpartman:5432/lago \
      -e SECRET_KEY_BASE=... -e LAGO_RSA_PRIVATE_KEY=... \
      -e LAGO_ENCRYPTION_PRIMARY_KEY=... -e LAGO_ENCRYPTION_DETERMINISTIC_KEY=... \
      -e LAGO_ENCRYPTION_KEY_DERIVATION_SALT=... \
      getlago/api:v1.52.1 ./scripts/migrate.sh

    It exits 0.

  4. Inspect the result:

    sql
    SELECT relkind FROM pg_class
     WHERE relname='enriched_events' AND relnamespace='public'::regnamespace;
    -- p   (partitioned, as expected)
    
    SELECT count(*) FROM partman.part_config
     WHERE parent_table='public.enriched_events';
    -- 0   (expected: 1)
    
    SELECT c.relname FROM pg_inherits i JOIN pg_class c ON c.oid=i.inhrelid
     WHERE i.inhparent='public.enriched_events'::regclass;
    -- enriched_events_default   (and nothing else)

Expected behavior

partman.create_parent succeeds, partman.part_config gains a row for public.enriched_events, and pg_partman creates monthly partitions from 2024-12-01 plus three premade ahead — the parameters the migration itself passes.

Failing that, the migration should raise, so the operator learns about it during deployment rather than months later.

Check Expected Actual
migrate.sh exit code 0 0
pg_partman installed yes yes (5.4.0)
enriched_events is relkind='p' yes yes
Row in partman.part_config 1 0
Monthly partitions ~25 0 — only the default
Warning or error anywhere none

Screenshots

N/A — server-side, output shown above.

Support

Not a browser issue, so the equivalents:

  • Deployment: Docker, self-hosted
  • Lago version: v1.52.1 (getlago/api:v1.52.1); also reproduced on v1.52.0
  • Database: getlago/postgres-partman:15.0-alpine — PostgreSQL 15.0, pg_partman 5.4.0
  • OS: Linux x86_64

Additional context

pg_partman is working correctly. A control test in the same session, issuing the identical create_parent call from db/migrate/20260109132143_partition_enriched_events.rb against a freshly created partitioned table, returned t, wrote the part_config row, and built 26 partitions. So the extension functions; the migration's invocation of it does not take effect.

Probable mechanism. partman.create_parent appears to return false rather than raising. After the migration, both partman.template_public_enriched_events and enriched_events_default exist — artifacts create_parent creates early — but there is no part_config row. Because execute sees no exception, the migration is recorded as applied and the script exits 0.

Compounding it, both 20260109092932_setup_partman.rb and 20260109132143_partition_enriched_events.rb guard on pg_extension_present?("pg_partman") and return quietly if it is absent, logging only at debug. Between a silent guard and a create_parent that fails without raising, there is no signal at all — we confirmed zero lines mentioning partman or create_parent in the full migration output.

Why this matters. The failure is silent and the consequence is delayed. An operator has no reason to suspect anything is wrong until the table misbehaves months later, and by then every row is in an unbounded default partition — which is worse than no partitioning, since it carries all of the overhead and none of the benefit, and makes attaching real partitions progressively more expensive.

Suggested fix.

  • Check the boolean returned by create_parent and raise if it is false.
  • Log at warn rather than debug when the partman guard skips.

Workaround, safe only while the default partition is still empty:

sql
ALTER TABLE public.enriched_events DETACH PARTITION public.enriched_events_default;
DROP TABLE public.enriched_events_default;

SELECT partman.create_parent(
  p_parent_table    := 'public.enriched_events',
  p_control         := 'timestamp',
  p_interval        := '1 month',
  p_type            := 'range',
  p_premake         := 3,
  p_start_partition := '2024-12-01'
);

UPDATE partman.part_config
SET infinite_time_partitions = true,
    retention                = '14 months',
    retention_keep_table     = true
WHERE parent_table = 'public.enriched_events';