Skip to content

Emit CREATE TRIGGER DDL events with the correct schema - #1201

Open
pujitha24 wants to merge 2 commits into
xataio:mainfrom
pujitha24:auto/issue-1146
Open

pujitha24 wants to merge 2 commits into
xataio:mainfrom
pujitha24:auto/issue-1146

Conversation

@pujitha24

Copy link
Copy Markdown

Description

CREATE TRIGGER statements were never replicated in postgres-to-postgres CDC, while DROP TRIGGER statements were. In strict mode, dropping a trigger that was never created downstream causes pgstream to stop with an error. This fixes the root cause in the pgstream.emit_ddl() Postgres event trigger function: pg_event_trigger_ddl_commands() reports a NULL schema_name for CREATE TRIGGER (triggers are not schema-qualified catalog objects the way tables or functions are), and emit_ddl() silently returns without emitting anything when the schema name is NULL. DROP TRIGGER uses a different function, pg_event_trigger_dropped_objects(), which does report the schema, so it was unaffected.

Related Issue(s)
  • Fixes #(issue number)

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • Added pgstream.get_trigger_schema(trigger_oid), a small SQL helper that resolves a trigger's schema via pg_trigger → pg_class → pg_namespace, mirroring the existing pgstream.get_table_metadata helper.
  • Redefined pgstream.emit_ddl() (both the core and injector migration variants) so that wherever it reads schema_name from pg_event_trigger_ddl_commands(), it falls back to pgstream.get_trigger_schema(objid) for trigger objects. This is needed in two places: the rec_schema_name lookup (which gates whether anything is emitted at all) and the cmd subquery used to build the objects array (whose WHERE filter also excluded NULL-schema rows, so the trigger object itself was missing from affected_objects too).
  • Added the new logic as migrations core/4_fix_create_trigger_schema and injector/6_fix_create_trigger_schema, following this repo's existing pattern for DDL emission fixes (e.g. core/3_fix_duplicate_ddl_emission). Regenerated migrations.go for both packages via make gen-migrations.

Testing

  • Manual testing performed
  • Unit tests added/updated
  • Integration tests added/updated
  • All existing tests pass

This bug lives entirely in a Postgres event trigger function (plpgsql), so it can only be proven against a real Postgres server, not a Go unit test. Docker/testcontainers were unavailable in the environment this change was built in, so I validated directly against a local PostgreSQL 16 server (Homebrew, wal_level=logical) instead of make integration-test:

  • Applied the actual pre-fix migration SQL (core 1-3) and reproduced the reported bug exactly: creating a table, function, and trigger, then dropping the trigger, and reading the real logical replication stream (pg_create_logical_replication_slot + pg_logical_slot_get_changes) showed 0 CREATE TRIGGER messages and 1 DROP TRIGGER message.
  • Applied the fix (core migrations 1-4) and reran the same scenario: CREATE TRIGGER now emits a pgstream.ddl message with the correct "schema_name": "public" and the trigger object listed with "schema": "public".
  • Verified the down migration round-trips correctly: applied up 1-4, ran the down migration for 4, confirmed ALTER TABLE ... DROP COLUMN still emits exactly one message (i.e. migration 3's duplicate-emission fix is preserved after reverting 4) and that CREATE TRIGGER goes back to emitting nothing (expected, since that reverts to the pre-fix state); then re-applied the up migration and confirmed CREATE TRIGGER works again.
  • go build, go vet, and golangci-lint run --config=.golangci.yml all pass cleanly against the changed packages (./migrations/..., ./internal/migrator/...).
  • ./bin/license-header-checker -r ./license-header.txt . go (the exact command make license-check runs) passes: "492 licenses ok, 0 licenses replaced, 0 licenses added".
  • I could not run go build ./..., make test, or make integration-test for the full repository in this environment: the sandbox's disk is nearly completely full for reasons unrelated to this change (confirmed the same "no space left on device" errors occur building unrelated large dependency subtrees, while the specific packages this change touches build and vet cleanly). Base branch CI (gh run list --branch main) is green, so this is an environment limitation, not a signal about the change itself.

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Code is well-commented
  • Documentation updated where necessary

Additional Notes

Note: make test / make integration-test could not be run locally due to a disk-space limitation in the build environment, unrelated to this change (see Testing section above for the live-Postgres validation performed instead).

Fixes #1146

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Coverage

Total: 63.1% (+4.7% vs main)

Coverage in packages changed by this PR:

Package Coverage Δ
migrations/postgres/core 0.0% ±0.0%
migrations/postgres/injector 0.0% ±0.0%

@pujitha24
pujitha24 force-pushed the auto/issue-1146 branch 2 times, most recently from 001cdf2 to 7a48214 Compare September 25, 2026 14:33
@pujitha24

Copy link
Copy Markdown
Author

This is rebased on main and CI is green; let me know if anything would make it easier to review.

@pujitha24
pujitha24 force-pushed the auto/issue-1146 branch 3 times, most recently from c16c59b to 520789e Compare October 7, 2026 13:52
CREATE TRIGGER statements were never replicated in postgres-to-postgres
CDC, while DROP TRIGGER statements were. In strict mode, dropping a
trigger downstream that was never created there causes pgstream to
stop with an error.

Motivation:
pg_event_trigger_ddl_commands() reports a NULL schema_name for CREATE
TRIGGER, since triggers are not schema-qualified catalog objects the
way tables or functions are. The pgstream.emit_ddl() event trigger
function picks rec_schema_name from the first row this function
returns and returns early without emitting anything when it is NULL,
so CREATE TRIGGER events were silently dropped. DROP TRIGGER goes
through pg_event_trigger_dropped_objects() instead, which does report
the schema, so it was emitted, leading to the asymmetry.

Approach:
Add pgstream.get_trigger_schema(trigger_oid), a small SQL helper that
resolves a trigger's schema via pg_trigger -> pg_class -> pg_namespace,
mirroring the existing pgstream.get_table_metadata helper. Use it as a
fallback wherever emit_ddl() reads schema_name from
pg_event_trigger_ddl_commands(): both the rec_schema_name lookup (which
gates whether anything is emitted) and the cmd subquery used to build
the objects array (whose WHERE filter also excluded the NULL-schema
trigger row from affected_objects). Added as new migrations,
core/4_fix_create_trigger_schema and injector/6_fix_create_trigger_schema,
following this repo's existing pattern for DDL emission fixes (see
core/3_fix_duplicate_ddl_emission). Regenerated migrations.go for both
packages via make gen-migrations.

Validation:
This bug lives entirely in a Postgres event trigger function (plpgsql),
so it can only be proven against a real Postgres server, not a Go unit
test. Docker/testcontainers were unavailable in this environment, so I
validated directly against a local PostgreSQL 16 server
(wal_level=logical) instead of make integration-test:
- Applied the pre-fix migration SQL (core 1-3) and reproduced the
  reported bug exactly via a real logical replication slot: creating a
  table, function and trigger, then dropping the trigger, showed 0
  CREATE TRIGGER messages and 1 DROP TRIGGER message.
- Applied the fix (core 1-4) and reran the same scenario: CREATE
  TRIGGER now emits a pgstream.ddl message with "schema_name": "public"
  and the trigger object correctly listed.
- Verified the down migration round-trips correctly: after applying up
  1-4 then down 4, ALTER TABLE ... DROP COLUMN still emits exactly one
  message (migration 3's duplicate-emission fix survives the revert),
  and CREATE TRIGGER correctly stops emitting again; re-applying up 4
  restores the fix.
- go build, go vet and golangci-lint run --config=.golangci.yml all
  pass cleanly against ./migrations/... and ./internal/migrator/....
- ./bin/license-header-checker -r ./license-header.txt . go (the exact
  command make license-check runs) passes: "492 licenses ok, 0
  licenses replaced, 0 licenses added".
- I could not run go build ./..., make test or make integration-test
  for the full repository: the sandbox's disk is nearly completely
  full for reasons unrelated to this change (confirmed the same
  disk-full errors occur building unrelated large dependency subtrees,
  while the packages this change touches build and vet cleanly). Base
  branch CI is green, so this is an environment limitation, not a
  signal about the change.

Report: xataio#1146
Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
Assisted-by: claude-sonnet-5 (via Claude Code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"CREATE TRIGGER" not replicated, "DROP TRIGGER" is replicated, leading to failure

1 participant