Skip to content

fix: strip extension-owned type schema qualifiers to prevent false-positive diffs#517

Open
ayusssmaan wants to merge 1 commit into
pgplex:mainfrom
ayusssmaan:fix/extension-owned-type-schema-qualifier
Open

fix: strip extension-owned type schema qualifiers to prevent false-positive diffs#517
ayusssmaan wants to merge 1 commit into
pgplex:mainfrom
ayusssmaan:fix/extension-owned-type-schema-qualifier

Conversation

@ayusssmaan

@ayusssmaan ayusssmaan commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #518

pgschema was emitting spurious ALTER COLUMN TYPE / CREATE TABLE diffs for columns typed with extension-owned types (e.g. pgvector's vector) whenever the extension resolved to a different schema between the live database and the plan-time comparison environment. The type itself hadn't changed - only the schema it happened to be installed in - but the schema-unaware qualifier comparison treated public.vector and domain.vector as different types.

  • ir: added ExtensionName (Column, TypeColumn, Type) and HasOwnedSequence (Column) / IsOwned (Sequence), sourced from pg_depend/pg_extension joins in the schema-scoped column/domain/composite-type/sequence queries.
  • internal/diff: columnsEqual, typesEqual, and the CREATE-path type/column SQL generators now strip schema qualifiers unconditionally when both sides are owned by the same extension, while still catching genuine changes (e.g. a vector dimension bump). Ordinary cross-schema types (domains, enums, contrib types installed at a fixed location) are unaffected.
  • internal/diff: isSerialColumn and generateSequenceSQL's OWNED BY emission now require genuine sequence ownership (HasOwnedSequence/IsOwned) rather than pattern-matching on column type + nextval() default text, fixing a related false-positive where a column merely referencing another table's sequence was treated as SERIAL.
  • internal/diff/topological: added a dependency edge for columns whose nextval() default targets a sequence owned by a different table, so that owner is always emitted first.
  • testdata/dump/sakila: restored missing ALTER SEQUENCE ... OWNED BY statements that a real pg_dump would emit; their absence was masking the ownership-detection gap above.

Fixes the false-positive/false-negative pair end-to-end: a plan-time schema difference for an extension-owned type produces no diff, while a genuine change (dimension, real cross-schema move of a non-extension type) still does.

…sitive diffs

pgschema was emitting spurious ALTER COLUMN TYPE / CREATE TABLE diffs for
columns typed with extension-owned types (e.g. pgvector's `vector`) whenever
the extension resolved to a different schema between the live database and
the plan-time comparison environment. The type itself hadn't changed - only
the schema it happened to be installed in - but the schema-unaware qualifier
comparison treated `public.vector` and `domain.vector` as different types.

- ir: added ExtensionName (Column, TypeColumn, Type) and HasOwnedSequence
  (Column) / IsOwned (Sequence), sourced from pg_depend/pg_extension joins
  in the schema-scoped column/domain/composite-type/sequence queries.
- internal/diff: columnsEqual, typesEqual, and the CREATE-path type/column
  SQL generators now strip schema qualifiers unconditionally when both sides
  are owned by the same extension, while still catching genuine changes
  (e.g. a vector dimension bump). Ordinary cross-schema types (domains,
  enums, contrib types installed at a fixed location) are unaffected.
- internal/diff: isSerialColumn and generateSequenceSQL's OWNED BY emission
  now require genuine sequence ownership (HasOwnedSequence/IsOwned) rather
  than pattern-matching on column type + nextval() default text, fixing a
  related false-positive where a column merely referencing another table's
  sequence was treated as SERIAL.
- internal/diff/topological: added a dependency edge for columns whose
  nextval() default targets a sequence owned by a different table, so that
  owner is always emitted first.
- testdata/dump/sakila: restored missing ALTER SEQUENCE ... OWNED BY
  statements that a real pg_dump would emit; their absence was masking the
  ownership-detection gap above.

Fixes the false-positive/false-negative pair end-to-end: a plan-time schema
difference for an extension-owned type produces no diff, while a genuine
change (dimension, real cross-schema move of a non-extension type) still
does.
@tianzhou

Copy link
Copy Markdown
Contributor

@ayusssmaan please create an issue first and reference it from the PR. Thx

@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR enriches schema introspection with extension and sequence ownership metadata, normalizes extension-owned type comparisons and DDL, and adds shared-sequence dependency ordering.

  • Adds ExtensionName, HasOwnedSequence, and IsOwned fields populated from PostgreSQL catalog dependencies.
  • Uses genuine sequence ownership when detecting SERIAL columns and emitting OWNED BY.
  • Ignores plan-environment schema differences for extension-owned types while retaining typmod comparisons.
  • Adds table-ordering logic for defaults that reference another table's owned sequence.
  • Expands unit and fixture coverage for extension types, serial ownership, qualification, and ordering.

Confidence Score: 2/5

The PR should not be merged until extension type identity, modified standalone sequence handling, and schema-aware shared-sequence ordering are corrected.

Generated DDL can fail or bind the wrong type outside the configured search_path, structural changes to referenced-but-unowned sequences can be silently omitted, and same-named sequences across schemas can produce an incorrect table creation order.

internal/diff/table.go, internal/diff/type.go, internal/diff/diff.go, internal/diff/topological.go

Security Review

Unconditionally emitting extension-owned types without a schema allows migration-time search_path resolution to fail or select a shadowing same-named type.

Important Files Changed

Filename Overview
internal/diff/table.go Extension-aware column generation fixes false diffs but emits bare extension types that may not resolve to the intended target type.
internal/diff/diff.go Added and dropped sequence handling uses real ownership, while modified sequences retain the fallback owner-name heuristic and can lose structural changes.
internal/diff/topological.go Adds shared-sequence ordering, but schema-less sequence keys collide across schemas.
internal/diff/type.go Composite and domain comparisons account for extension ownership, while composite DDL shares the unqualified-type resolution issue.
ir/queries/queries.sql Adds coordinated catalog queries for extension and sequence dependency metadata.
ir/inspector.go Maps the new catalog-derived ownership fields into the IR consistently.
ir/ir.go Extends columns, sequences, and types with ownership metadata consumed by diff generation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Catalog[PostgreSQL catalogs] --> Inspector[IR Inspector]
  Inspector --> Metadata[Extension and sequence ownership metadata]
  Metadata --> Compare[Schema comparison]
  Compare --> DDL[DDL generation]
  Metadata --> Ordering[Sequence dependency ordering]
  Ordering --> DDL
  DDL --> Apply[Apply migration with target search_path]
Loading

Comments Outside Diff (1)

  1. internal/diff/diff.go, line 1109-1110 (link)

    P1 Referenced sequences remain misclassified

    When a standalone or shared sequence is referenced by a column default without a real ownership dependency, the fallback owner names make this branch treat it as owned and skip structural comparison, causing requested increment, bounds, cycle, or cache changes to be silently omitted.

    Context Used: CLAUDE.md (source)

    Knowledge Base Used:

Reviews (1): Last reviewed commit: "fix: strip extension-owned type schema q..." | Re-trigger Greptile

Comment thread internal/diff/table.go
Comment on lines +1153 to +1156
if column.ExtensionName != "" {
columnType = stripAnySchemaPrefix(columnType)
} else {
columnType = stripSchemaPrefix(columnType, targetSchema)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Extension type identity is lost

When an extension-owned type is installed outside the target schema and public, stripping its schema makes PostgreSQL resolve the bare name through search_path, causing the migration to fail with a missing type or bind the column to an unintended same-named type.

Context Used: CLAUDE.md (source)

Knowledge Base Used:

Comment on lines +68 to +77
sequenceOwnerTable := make(map[string]string)
for key, table := range tableMap {
for _, col := range table.Columns {
if col.HasOwnedSequence {
if seqName := nextvalTargetSequenceName(col); seqName != "" {
sequenceOwnerTable[seqName] = key
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Sequence owner keys lose schema

When two managed schemas contain same-named owned sequences, this bare-name map overwrites one owner and a qualified nextval reference can receive an edge to the wrong table, causing the referencing table to be created before the table whose SERIAL definition creates the actual sequence.

Knowledge Base Used: Diff Engine

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.

Extension-owned type schema mismatch causes false-positive diffs (pgvector, etc.)

2 participants