Skip to content

Support enum columns in transformers - #1147

Draft
kvch wants to merge 2 commits into
mainfrom
support-enum-columns
Draft

Support enum columns in transformers#1147
kvch wants to merge 2 commits into
mainfrom
support-enum-columns

Conversation

@kvch

@kvch kvch commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Description

Please provide a brief description of the changes in this PR.

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

Type of Change

Please select the relevant option(s):

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🔧 Refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧪 Test coverage improvement
  • 🔨 Build/CI changes
  • 🧹 Code cleanup

Changes Made

Testing

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

Checklist

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

Additional Notes

kvch and others added 2 commits September 3, 2026 18:35
A transformation rule on a user-defined enum column was rejected at
startup with "does not support pg data type: <enum> with OID: <n>",
unless it used a transformer declaring AllDataTypes.

Problem:
An enum's OID is assigned by the database, so it reaches no case of the
transformer type compatibility switch, whose fallback branch knows only
citext and hstore by name. The name alone cannot identify an enum, so
the parser had no way to decide what such a column accepts.

Cause:
Two further problems sat behind that one. greenmask_choice is the only
transformer that can be constrained to values an enum actually has, but
its choices parameter is required, and it returned the chosen label as
[]byte regardless of the input type, which reaches postgres hex encoded
and is rejected with "invalid input value for enum".

Change:
- Mapper.EnumForOID resolves an OID to the enum it names and returns its
  labels. Postgres reports a domain's base type in the row description,
  so a domain over an enum arrives already resolved; an array of an enum
  has its own OID, matches nothing, and is left to the existing type
  check to reject by name. Results are cached, a nil value being a
  cached negative.
- A new EnumDataType, declared only by greenmask_choice. Enums are
  deliberately not mapped to StringDataType: greenmask_string and
  masking would then pass validation and emit a label the enum does not
  have, failing per row against the target instead of at startup.
- The parser resolves the enum before building the transformer, so a
  greenmask_choice rule that omits choices gets the enum's labels, using
  the hook that already injects postgres_url for pg_anonymizer. Choices
  listed explicitly are checked against the labels. An explicitly empty
  list is left alone for the builder to reject, since it is usually a
  template that rendered nothing rather than a request for every label.
- greenmask_choice returns a string when the input was a string.

The defaulted labels are otherwise an invisible config value the
operator never wrote, so each one is logged at startup, and combining
them with the deterministic generator raises a warning: that generator
uses no secret, and an enum publishes its whole label set to anyone who
can read the target, so the mapping inverts by trying all of them.

Verification:
- make lint: 0 issues
- make test: passes
- PGSTREAM_INTEGRATION_TESTS=true go test ./pkg/stream/integration
  ./pkg/wal/processor/transformer ./internal/postgres: all pass
- Test_SnapshotToPostgres_EnumColumnTransformer asserts the exact rows a
  deterministic run must produce, computed by running the transformer
  the parser builds, over both writer paths. Membership in the label set
  would have passed even with the transformer skipped, since the source
  values are themselves labels
- TestPostgresTransformerParser_ParseAndValidate_enumColumns covers the
  wiring against a mocked catalog: defaulting, an explicit subset, an
  invalid label, a rejected transformer, and a failing lookup. Swallowing
  the EnumForOID error, which no test caught before, now fails it
- TestPostgresTransformerParser_EnumColumns_Integration covers the same
  against a real catalog, plus domains and enum arrays
- TestMapper_EnumForOID asserts the cache is used, not just populated

Caveats:
The labels are read once, at startup, so a replicated ALTER TYPE ...
RENAME VALUE is picked up only on restart. Constraints narrowing the
column further, such as a CHECK on a domain over the enum, are not
visible in the row description and are not honoured. Both are
documented, along with the requirement for a source Postgres URL.

Emitting the chosen value as a string changes what the Kafka, webhook
and search targets carry for every greenmask_choice column, not only
enum ones: they JSON encoded the previous []byte as base64. This matches
what greenmask_string already did and is documented, but consumers that
decoded base64 need updating.

Generated by an unattended AFK agent run. Not reviewed by a human.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KyBQAf6EhgQTrY3WccNcwf
@kvch kvch added ai-review Set for simple PRs that does not require human approval and removed ai-review Set for simple PRs that does not require human approval labels Sep 3, 2026
@kvch
kvch marked this pull request as draft September 3, 2026 18:37
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Coverage

Total: 61.4% (+0.2% vs main)

Coverage in packages changed by this PR:

Package Coverage Δ
internal/postgres 50.8% +0.7%
pkg/stream 47.9% ±0.0%
pkg/transformers 85.3% ±0.0%
pkg/transformers/greenmask 78.0% +0.2%
pkg/wal/processor/transformer 86.9% +1.5%

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.

1 participant