Skip to content

fix: stub cross-schema partition parents in plan database - #555

Merged
tianzhou merged 8 commits into
mainfrom
fix/issue-552-cross-schema-partition
Aug 21, 2026
Merged

fix: stub cross-schema partition parents in plan database#555
tianzhou merged 8 commits into
mainfrom
fix/issue-552-cross-schema-partition

Conversation

@tianzhou

Copy link
Copy Markdown
Contributor

Closes #552

Summary

  • Added ExtractPartitionOfTargets to detect PARTITION OF references to cross-schema parents in desired SQL
  • Added BuildPartitionedTableStubSQL to generate stub CREATE TABLE ... PARTITION BY DDL for partition parents
  • Created prependPartitionParentStubs in cmd/plan/ to wire up the stubbing before ApplySchema, mirroring the existing FK stub pattern (prependIgnoredTableStubs)
  • When a partition child references a parent in a different schema, the plan database now gets a stub parent table so the PARTITION OF clause resolves correctly

Test plan

  • Unit tests for ExtractPartitionOfTargets (5 cases: simple, qualified, dedup, partition-by-not-matched, quoted)
  • go build ./... passes
  • go vet ./... passes
  • All internal/postgres tests pass
  • CI integration tests

🤖 Generated with Claude Code

When a partition child is in a different schema from its partitioned
parent, the plan database fails because it tries to create the partition
on the real parent table. Fix by detecting PARTITION OF references to
cross-schema parents and prepending stub CREATE TABLE ... PARTITION BY
statements, mirroring the existing FK stub pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 21, 2026 04:47
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR detects desired-state cross-schema PARTITION OF references, introspects their live parents, and prepends partitioned-table stubs before materializing the desired IR. The generated partition clause is malformed, and external plan databases do not clean up the cross-schema objects.

  • Adds SQL extraction for partition-parent references.
  • Builds parent stubs from target catalog metadata.
  • Integrates stubbing into GeneratePlan before ApplySchema.
  • Adds extraction-focused unit tests.

Confidence Score: 3/5

This PR is not safe to merge until the malformed partition DDL and external plan-database cleanup boundary are fixed.

Every cross-schema partition stub currently receives an invalid duplicated partition strategy, while external providers also leave real-schema stub objects behind after validation.

Files Needing Attention: ir/stub.go, internal/postgres/external.go

Important Files Changed

Filename Overview
ir/stub.go Adds partition-parent stub generation, but duplicates the partition strategy in generated SQL and creates externally persistent cross-schema objects.
cmd/plan/partition_stubs.go Detects and prepends missing cross-schema parent stubs using metadata from the target database.
cmd/plan/plan.go Invokes partition stubbing for both embedded and external desired-state providers before ApplySchema.
internal/postgres/fk_refs.go Adds quote-aware extraction and deduplication of PARTITION OF targets.
internal/postgres/fk_refs_test.go Covers basic, qualified, deduplicated, negative, and quoted extraction cases.

Sequence Diagram

sequenceDiagram
    participant Plan as GeneratePlan
    participant Target as Target database
    participant Stub as Stub builder
    participant Provider as Desired-state provider
    Plan->>Target: Inspect cross-schema parent
    Target-->>Stub: Columns, constraints, partition definition
    Stub-->>Plan: CREATE SCHEMA/TABLE stub
    Plan->>Provider: Apply stub + desired SQL
    Provider-->>Plan: Syntax failure or desired IR
    Note over Provider: External provider only cleans its temporary schema
Loading

Reviews (1): Last reviewed commit: "fix: stub cross-schema partition parents..." | Re-trigger Greptile

Comment thread ir/stub.go Outdated
Comment thread ir/stub.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses #552 by ensuring pgschema plan can apply desired SQL that includes PARTITION OF references to a partitioned parent table in a different schema, by detecting those references and prepending stub DDL for the missing parent(s) before applying the desired state into the plan database.

Changes:

  • Added partition-parent stub generation in ir (BuildPartitionedTableStubSQL) including PARTITION BY metadata.
  • Added SQL scanning for PARTITION OF targets (ExtractPartitionOfTargets) with unit tests.
  • Wired new stub-prepending into cmd/plan before ApplySchema, mirroring the existing FK ignored-table stubbing flow.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
ir/stub.go Adds partitioned-parent stub DDL builder and a catalog query for partition strategy/key.
internal/postgres/fk_refs.go Adds ExtractPartitionOfTargets SQL scanner to find cross-schema partition parents.
internal/postgres/fk_refs_test.go Adds unit tests covering ExtractPartitionOfTargets parsing behavior.
cmd/plan/plan.go Integrates partition-parent stubbing into plan generation before applying desired SQL.
cmd/plan/partition_stubs.go Implements prependPartitionParentStubs to discover and prepend required stubs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread cmd/plan/partition_stubs.go
Comment thread cmd/plan/partition_stubs.go Outdated
Comment thread cmd/plan/plan.go
tianzhou and others added 6 commits August 20, 2026 21:53
…rategy

pg_get_partkeydef returns the full partition key definition including
the strategy (e.g., "RANGE (id)"), so we must not prepend the strategy
separately. Simplify queryPartitionInfo → queryPartitionKeyDef.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… DB with pre-existing partitions

When the external plan DB already contains the parent table with its
partitions, CREATE TABLE IF NOT EXISTS is a no-op and the desired SQL's
PARTITION OF still conflicts with existing partitions. Fix by dropping
the parent CASCADE before creating the stub, so the desired SQL can
attach its partition children cleanly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…dle plan DB with pre-existing partitions"

This reverts commit 27e7779.
…SCADE

When the plan DB already has the cross-schema parent with attached
partitions, use a PL/pgSQL DO block to DETACH PARTITION each child
instead of DROP TABLE CASCADE. This is non-destructive: it only removes
the parent-child relationship without dropping any tables.

The DO block is a no-op when the parent does not exist (clean plan DB).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Redesign the cross-schema partition stub approach to stay within the
temp schema sandbox:

- BuildPartitionedTableStubSQL now emits unqualified table names so the
  stub lands in the temp schema via search_path
- PARTITION OF references to cross-schema parents are rewritten to strip
  the schema prefix, resolving to the stub via search_path
- Return an error when a referenced partition parent is not found on the
  target database instead of silently skipping
- Add unit tests for replacePartitionOfRef

This avoids creating or mutating persistent objects (schemas, tables)
outside the temp schema in the plan database.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@tianzhou tianzhou left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All 5 Copilot review comments addressed. Key change: redesigned the partition stub approach in 7a7444e6 to keep everything inside the temp schema sandbox — stubs use unqualified names and PARTITION OF references are rewritten to strip cross-schema prefixes.

Partition parent stubs now use _pgschema_partstub_<schema>__<table> as
the table name instead of the bare table name. This prevents collisions
when:
- the target schema has a table with the same name as the parent
- two cross-schema parents share the same table name (e.g.,
  public.events and archive.events)

PARTITION OF references are rewritten to point to the unique stub name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

cmd/plan/partition_stubs.go:75

  • The returned stub is created unqualified in the same temporary schema that plan.go later inspects. It therefore enters desiredStateIR, is normalized to the target schema, and is treated as a newly desired table; the #552 no-op case will produce a spurious CREATE TABLE annotations_data.annotation_shapes ... plan. Keep helper parents outside the inspected schema or explicitly remove them from the desired IR before diffing.
			return "", fmt.Errorf("cross-schema partition parent %s.%s not found or not a partitioned table on the target database", ref.Schema, ref.Table)

cmd/plan/partition_stubs.go:94

  • Quoted lowercase references are not handled: for schema public and table parent, QuoteIdentifier returns bare names, so the quoted patterns are never added and PARTITION OF "public"."parent" remains qualified. The stub is then unused, causing the original missing/conflicting-parent failure. Match source identifier tokens rather than inferring whether they were quoted from their values.
	// Build the qualified reference patterns as they may appear in SQL.
	patterns := []struct{ old, new string }{
		// unquoted: schema.table → stubName
		{

cmd/plan/partition_stubs.go:119

  • This scans the entire SQL string, unlike extraction which skips literals, comments, and dollar-quoted bodies. Once a real reference triggers rewriting, matching text inside a table comment or dynamic SQL function body is also changed (for example, 'PARTITION OF public.parent' becomes 'PARTITION OF parent'), silently altering desired schema content. Restrict replacements to executable SQL tokens.
	for _, p := range patterns {
		result = replacePartitionOfRef(result, p.old, p.new)
	}

ir/stub.go:120

  • This partition-parent stub copies only type and NOT NULL, omitting parent column defaults. PARTITION OF inherits those defaults, so the temporary child is inspected without defaults that exist on the real child; columnsEqual then reports changes and can emit erroneous DROP DEFAULT operations. Clone defaults needed for inherited partition-column state rather than reusing the FK-only column shape.
		b.WriteString(QuoteIdentifier(col.name))
		b.WriteString(" ")
		b.WriteString(col.dataType)
		if col.notNull {
			b.WriteString(" NOT NULL")

Comment thread cmd/plan/partition_stubs.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (5)

cmd/plan/partition_stubs.go:68

  • The temporary stub is later inspected as part of the desired schema, and the rewritten child is recorded with this stub as its PartitionOf parent. Nothing removes the _pgschema_partstub_* table or restores the original cross-schema parent before GenerateMigration, so even a no-op plan will propose creating the stub in the target schema; a newly added child will also generate PARTITION OF against that fake parent. Preserve the original-to-stub mapping and sanitize the desired IR after inspection.
		stubName := partitionStubName(ref.Schema, ref.Table)
		ddl, err := ir.BuildPartitionedTableStubSQL(ctx, conn, ref.Schema, ref.Table, stubName)

cmd/plan/partition_stubs.go:128

  • This replacement scans the raw SQL, unlike extraction which skips literals, comments, and dollar-quoted bodies. Once a real partition reference selects a parent for rewriting, the same text inside a function body, string, or comment is also changed, potentially altering persisted function behavior or literal defaults. Restrict replacements to parsed SQL-code spans.
func replacePartitionOfRef(sql, oldRef, newRef string) string {
	lower := strings.ToLower(sql)
	lowerOld := strings.ToLower(oldRef)

cmd/plan/partition_stubs.go:155

  • This match has no identifier boundary after oldRef. If the SQL references both ext.events and ext.events_archive, rewriting ext.events first also consumes the prefix of the latter and produces an invalid target such as "stub"_archive. It also case-folds quoted identifiers, which PostgreSQL treats as case-sensitive. Match the parsed qualified identifier exactly.
			if k+len(lowerOld) <= len(sql) && strings.EqualFold(sql[k:k+len(lowerOld)], lowerOld) {
				b.WriteString(sql[pos:afterOf])
				b.WriteString(sql[afterOf:k])
				b.WriteString(newRef)
				i = k + len(lowerOld)

ir/stub.go:120

  • A partition child inherits column defaults from its parent, but this stub emits only type and NOT NULL. For a parent column with a default, the child created against the stub is inspected without that default, while the real child has it; the diff then emits a spurious DROP DEFAULT. Partition-parent stubs need to reproduce inherited column attributes rather than reuse the deliberately minimal FK-stub column model.
	for i, col := range cols {
		b.WriteString("    ")
		b.WriteString(QuoteIdentifier(col.name))
		b.WriteString(" ")
		b.WriteString(col.dataType)
		if col.notNull {
			b.WriteString(" NOT NULL")

cmd/plan/partition_stubs.go:119

  • The extractor accepts valid whitespace around the qualification dot, but the rewrite patterns do not. For example, PARTITION OF public . parent is detected and a stub is created, yet this loop leaves the clause pointing at public.parent, so the original cross-schema failure remains. Rewrite the exact parsed target span instead of matching a few rendered spellings.

This issue also appears in the following locations of the same file:

  • line 126
  • line 151
	result := sql
	for _, p := range patterns {
		result = replacePartitionOfRef(result, p.old, p.new)
	}

@tianzhou
tianzhou merged commit 9c81d46 into main Aug 21, 2026
2 checks passed
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.

[Bug] plan fails when a partition child is in a different schema from its parent

2 participants