Skip to content

Fix batch-4 review findings: structural pull loss, invalid function SQL, view deps, and gate coverage#56

Merged
gmr merged 12 commits into
mainfrom
fix/review-findings-batch4
Jul 7, 2026
Merged

Fix batch-4 review findings: structural pull loss, invalid function SQL, view deps, and gate coverage#56
gmr merged 12 commits into
mainfrom
fix/review-findings-batch4

Conversation

@gmr

@gmr gmr commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Batch 4 of the review-findings work. Fixes the six pull/build bugs that batch 3's fixture work uncovered, then re-enables the deferred fixtures so each fix is locked in by the round-trip and deploy gates. Expanding the fixtures surfaced three further genuine bugs, also fixed here.

The six batch-4 bugs

  • Partition children dropped on pull — pg_dump emits partition children as a plain CREATE TABLE plus a separate TABLE ATTACH (ALTER TABLE ONLY parent ATTACH PARTITION child FOR VALUES ...); the parser only handled inline PARTITION OF, and the ingest had no arm for TableAttach, so partition membership was lost. Now parsed and folded into the parent.
  • Typed-table inline column constraints dropped — a typed table's element list is a distinct grammar production; pull now walks it.
  • SERIAL / separate SET DEFAULT dropped on pull — pg_dump emits column defaults (e.g. nextval(...)) as a separate ALTER TABLE ... ALTER COLUMN ... SET DEFAULT; pull now folds them back onto the column.
  • Trigger comments dropped on pullapply_comment gained a TRIGGER arm.
  • Zero-argument functions emitted invalid SQL — bare CREATE FUNCTION name with no ().
  • Views emitted no dependency edges — a view ordered before its underlying table failed to restore; pull now derives view→relation edges from the query.

Further bugs surfaced by expanding the fixtures

  • for_values_to typo in the partition schema — the RANGE oneOf branch required a misspelled key, so no range-partitioned table could validate.
  • Functions emitted without schema qualificationCREATE FUNCTION name(...) fails pg_restore under its reset search_path; now schema-qualified (matches pg_dump; documented as build deviation Phase 3: ddl module — object, view, function, and trigger families #12).
  • SERIAL default inlined into CREATE TABLE — created an unrestorable dependency cycle (sequence OWNED BY the column); now split into a separate ALTER TABLE ... SET DEFAULT entry depending on the sequence, as pg_dump does.
  • Deploy dropped comments on child entriesCOMMENT ON TRIGGER/INDEX resolved to no owning item and was silently skipped on fresh deploy; deploy now resolves owners transitively through intermediate non-item entries.

Validation

  • just check — fmt, clippy -D warnings, and all unit + integration tests pass (including new unit tests for the ATTACH fold, the sequence-default split, and nextval target parsing, plus updated build_parity deviations).
  • just gates — round-trip and all three deploy gates (equivalence, convergence, safety) pass against Postgres 17 with the expanded fixtures.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for emitting and round-tripping more DDL, including column defaults from ALTER TABLE ... SET DEFAULT and ATTACH PARTITION folding into parent tables.
    • View dependency information is now included in generated YAML where applicable.
  • Bug Fixes
    • Improved pg_restore-compatible output by splitting sequence-backed defaults into separate DEFAULT entries.
    • Fixed rendering of function targets/comments for zero-argument and schema-qualified functions.
    • Improved deploy-plan ownership resolution and corrected partition/typed-table constraint handling.
    • Fixed range-partition schema validation for to bounds.

gmr and others added 11 commits July 7, 2026 15:30
COMMENT ON TRIGGER trg ON tbl already parsed correctly in ddl/object.rs
(target.schema carries the owning table's qualified name, target.name
the trigger name), but pull::Assembly::apply_comment had no TRIGGER
arm, so the comment fell into the `_ => false` case and was silently
discarded. Add apply_trigger_comment, mirroring the existing
apply_column_comment two-name lookup, and a unit test asserting the
comment lands on the trigger.

RULE/POLICY/CONSTRAINT ... ON ... comments are deferred: their ddl-side
parsing shares the same two-name shape (POLICY already has a ddl test),
but there is no models::Trigger-equivalent field to attach a POLICY/RULE
comment to without further model changes, which is out of scope here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CREATE TABLE x OF type (col WITH OPTIONS ..., ...) uses a distinct
element-list production (OptTypedTableElementList/TypedTableElement),
not TableElement, so create_table's single TableElement walk never saw
these columns/constraints and silently dropped them. TypedTableElement
is columnOptions (a ColId + ColQualList of the same ColConstraintElem/
ColConstraint shapes the plain column() parser already understands) or
TableConstraint (same as an inline table-level constraint). Add a
second walk over TypedTableElement in create_table (src/ddl/table.rs)
that feeds columnOptions nodes straight into the existing column()
helper (data_type stays empty, matching build::render_typed_table_column
which never emits it) and TableConstraint nodes into the existing
table_constraint()/apply_constraint() path. Added
parses_typed_table_inline_column_constraints and
parses_typed_table_check_constraint unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dump_function's fallback branch used the bare d.name for func_name
when a function had no structured `parameters`, which produced
`CREATE FUNCTION schema.foo RETURNS ...` (missing `()`) for
zero-argument functions that store a plain, unparenthesized name —
invalid SQL that also broke the matching DROP FUNCTION and, since the
default COMMENT ON FUNCTION target derives from the same name,
COMMENT ON FUNCTION. Names that already carry an embedded `(argtypes)`
signature (the on-disk convention used to disambiguate overloads) are
left untouched, so parameterized functions render exactly as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pg_dump emits column defaults (SERIAL's nextval(...), and any other
DEFAULT) as a standalone `ALTER TABLE ONLY t ALTER COLUMN c SET
DEFAULT ...` statement (libpgdump ObjectType::Default), not inline on
the CREATE TABLE column def. alter_table() only recognized `kw_add`
commands, so these fell through to Statement::Unsupported and the
default was silently lost — SERIAL columns pulled with no default at
all.

- src/ddl/mod.rs: add Statement::SetColumnDefault { table, column,
  default }.
- src/ddl/table.rs: alter_table() now also matches
  alter_table_cmd -> alter_column_default (kw_set + kw_default +
  a_expr; kw_drop + kw_default is left unhandled, same as before).
  Added parses_alter_table_set_column_default.
- src/pull/mod.rs: OT::Default is now routed through the parser (was
  falling into push_remaining); Assembly::apply folds the default onto
  the matching column of the already-ingested table, with a
  deferred_defaults retry list (mirroring deferred_indexes/
  deferred_partitions) for the case where the ALTER precedes its
  table. Added set_default_attaches_to_existing_column and
  deferred_default_attaches_after_table_is_ingested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Views got no dependency edges (only foreign-key-derived table edges
existed), so build ordered them purely by libpgdump's same-priority
alphabetical sort — a view could be emitted before a table it queries,
breaking restore.

pg_dump's archive TOC does carry per-entry dump-id dependencies, but
wiring that through would require plumbing the raw libpgdump::Dump
(and its dump-id → object mapping, built in ingest()) into the writer,
touching src/pull/mod.rs which another pass owns. Instead, re-parse
each view/matview's stored query text with tree-sitter-postgres and
walk the CST for schema-qualified relation_expr nodes, matching them
against the pulled inventory (tables/views/materialized_views).
Unqualified references (CTE names, or anything pg_dump left
unqualified) and references outside the inventory are skipped rather
than guessed at, and self-references are guarded against.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pg_dump does not emit `CREATE TABLE child PARTITION OF parent FOR
VALUES ...`; it emits the child as a plain `CREATE TABLE` followed by a
separate `TABLE ATTACH` TOC entry (`ALTER TABLE ONLY parent ATTACH
PARTITION child FOR VALUES ...`). The parser only recognized the inline
`PARTITION OF` form, and the ingest dispatch had no arm for the
`TABLE ATTACH` object type, so partition membership was silently lost on
round-trip: the child surfaced as a standalone table and the parent had
no partitions.

Parse the `partition_cmd` ATTACH form into a new
`Statement::AttachPartition`, route `ObjectType::TableAttach` through the
parser, and fold attached children into their parent during pull
assembly — moving the bounds (and any comment picked up on the child
table) into the parent's `partitions` and dropping the child's
standalone table, mirroring the existing inline-`PARTITION OF` fold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…' and 'fix/view-dependencies' into fix/review-findings-batch4
The RANGE-partition branch of the `partitions` oneOf required the
misspelled key `for_vlaues_to` instead of `for_values_to`, so no RANGE
partition (with `for_values_from`/`for_values_to`) could ever satisfy
the schema — project load rejected every range-partitioned table. This
was carried over verbatim from the Python schemata and only surfaced
now that a fixture exercises range partition children.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two invalid-SQL bugs in the built archive, both surfaced by round-tripping
a fixture through pg_restore:

1. CREATE/DROP FUNCTION rendered a bare, unqualified function name.
   pg_restore runs with search_path reset to '' and schema-qualifies
   every object, so a bare `CREATE FUNCTION name(...)` fails with "no
   schema has been selected to create in". Render the function reference
   schema-qualified (the COMMENT target was already qualified). Python
   emitted the bare form too, so the three test-project functions move
   into the documented build_parity deviations.

2. A SERIAL column's `nextval(...)` default was inlined into CREATE
   TABLE, but the sequence is OWNED BY that column: inlining makes the
   table depend on the sequence while the sequence depends on the table,
   an unrestorable cycle. Mirror pg_dump by holding the default out of
   CREATE TABLE and emitting it as a separate ALTER TABLE ... SET DEFAULT
   entry that depends on both the table and the sequence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploy mapped each archive entry to its owning inventory item by looking
one dependency level deep. A COMMENT ON TRIGGER (and COMMENT ON INDEX)
depends on the TRIGGER/INDEX entry, which is itself a child of the table
rather than a modeled inventory item, so its owner set resolved empty
and the entry was silently dropped: deploying a fresh table with a
commented trigger lost the comment (caught by the equivalence gate).

Walk the dependency graph through intermediate non-item entries until it
reaches inventory items. This only recovers entries that were previously
skipped for having no resolvable owner, so it cannot change any entry
that already mapped to an item.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-enable the objects batch 3 documented but had to leave out because
the underlying bugs made them fail. Each now exercises a fix through the
round-trip and deploy gates:

- partition children via ATTACH PARTITION (pull fold)
- a typed table with an inline column constraint
- a zero-argument trigger function (build's `()` + schema qualification)
- a trigger with a COMMENT ON TRIGGER (pull capture + deploy emission)
- a view ordered alphabetically before its table (view dependency edges)
- a SERIAL column (separate SET DEFAULT capture + build split)

The trigger function body is authored in libpgfmt's normalized two-space
form so the round-trip schema diff stays clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f3fa8d7f-8349-4198-a585-82016196b772

📥 Commits

Reviewing files that changed from the base of the PR and between 1f0f6b3 and 7026cca.

📒 Files selected for processing (1)
  • src/ddl/table.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/ddl/table.rs

📝 Walkthrough

Walkthrough

This PR adds parsing and round-trip support for attached partitions, column defaults, typed-table constraints, trigger comments, and zero-arg functions. It also splits sequence-backed defaults during build, adds view dependency metadata, updates deploy owner resolution, and refreshes fixtures and parity expectations.

Changes

Round-trip and planning updates

Layer / File(s) Summary
DDL contracts and parser
src/ddl/mod.rs, src/ddl/table.rs, schemata/table.yml
Adds partition-attach and column-default statement variants, parses typed-table constraints plus SET DEFAULT and ATTACH PARTITION, and fixes the range-partition schema key.
Pull assembly replay
src/pull/mod.rs
Buffers late defaults and attached partitions, finalizes them after ingest, and adds trigger comment handling plus tests for defaults and partition folding.
Build output and function rendering
src/build/mod.rs
Splits sequence-backed defaults into standalone entries, omits inline nextval defaults, and renders zero-argument functions with schema-qualified comment targets.
Deploy owner resolution
src/deploy/mod.rs, tests/deploy.rs
Walks transitive dependencies to resolve owners for indirect entries and updates the expected function SQL in the deploy test.
View dependency metadata
src/pull/writer.rs
Extracts schema-qualified view references and emits dependency metadata for views and materialized views, with parsing and dependency tests.
Fixtures and parity expectations
fixtures/schema.sql, tests/build_parity.rs
Updates schema fixtures for partitions, typed-table constraints, triggers, and serial defaults, and adjusts build parity expectations for schema-qualified functions.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Ingest
  participant Assembly
  participant ParentTable
  participant ChildTable
  Ingest->>Assembly: parse OT::Default and OT::TableAttach
  Assembly->>Assembly: buffer deferred defaults and attached partitions
  Assembly->>Assembly: finalize ingest
  Assembly->>ParentTable: apply_deferred_defaults()
  Assembly->>ChildTable: apply_attached_partitions()
  Assembly->>ParentTable: attach trigger comment and partition metadata
Loading
sequenceDiagram
  participant Builder
  participant TableDump
  participant FunctionDump
  participant ArchiveEntries
  Builder->>Builder: split_sequence_defaults()
  Builder->>TableDump: omit inline nextval defaults
  Builder->>ArchiveEntries: emit separate DEFAULT entry
  Builder->>FunctionDump: compute func_name and comment_target
  FunctionDump->>ArchiveEntries: add_item_with_comment_target()
Loading

Possibly related PRs

  • gmr/pglifecycle#15: Shares the same src/pull/mod.rs ingestion and assembly path, including the newer handling for defaults, partitions, and trigger comments.
  • gmr/pglifecycle#25: Connects through the same column-default pipeline, where parsed ALTER TABLE ... SET DEFAULT changes are rendered or planned later.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fixes: pull-loss bugs, function SQL issues, view dependencies, and test/gate coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/review-findings-batch4

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/pull/writer.rs (1)

51-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider deduplicating the view/materialized-view serialization loops.

The two loops (Lines 52-67 and 68-85) are identical except for the output path's plural key ("views" vs "materialized_views"). Extracting a small helper keeps the dependency-injection logic from silently diverging if it's later edited in only one place.

♻️ Optional helper extraction
let mut save_view = |view: &models::View, kind: &str| -> Result<(), String> {
    let mut value = serialize(view)?;
    if let (Some(map), Some(dependencies)) = (
        value.as_object_mut(),
        view_dependencies(
            &view.schema,
            &view.name,
            view.query.as_deref(),
            &relation_inventory,
        ),
    ) {
        map.insert(String::from("dependencies"), dependencies);
    }
    writer.save_value(nested(kind, &view.schema, &view.name)?, &value)
};
for view in &assembly.views {
    save_view(view, "views")?;
}
for view in &assembly.materialized_views {
    save_view(view, "materialized_views")?;
}

(Type/borrow details may need adjustment to satisfy the borrow checker against writer.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pull/writer.rs` around lines 51 - 85, The serialization logic for views
and materialized views is duplicated in the writer flow, so extract the shared
behavior into a small helper or closure around serialize, view_dependencies, and
writer.save_value. Reuse that helper for both assembly.views and
assembly.materialized_views, passing only the plural key ("views" or
"materialized_views") and preserving the dependency injection into the
serialized value. Make sure the helper works cleanly with writer and the borrow
checker.
src/pull/mod.rs (1)

1098-1129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate relation-splitting logic between column and trigger comment handlers.

apply_column_comment and apply_trigger_comment both split target.schema on . into (schema, table) and resolve the owning table identically. Consider extracting a small shared helper (e.g. resolve_relation(target) -> Option<&mut models::Table>) to avoid the duplication.

♻️ Proposed extraction
fn resolve_owning_table(
    &mut self,
    target: &QualifiedName,
) -> Option<&mut models::Table> {
    let relation = target.schema.as_ref()?;
    let (schema, table) = match relation.split_once('.') {
        Some((schema, table)) => (Some(schema.to_string()), table),
        None => (None, relation.as_str()),
    };
    self.find_table(&QualifiedName { schema, name: table.to_string() })
}

Also applies to: 1131-1164

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pull/mod.rs` around lines 1098 - 1129, Both apply_column_comment and
apply_trigger_comment duplicate the same target.schema split-and-table lookup
logic, so extract that resolution into a shared helper in this module (for
example, resolve_owning_table or similar) and have both comment handlers call it
before updating the column or trigger. Keep the helper responsible for splitting
the relation into schema/table and calling find_table on a QualifiedName so the
two handlers only contain their specific comment assignment logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/ddl/table.rs`:
- Around line 142-156: The TypedTableElement handling in the table parsing flow
is dropping index tablespace information for table constraints. Update the
TypedTableElement loop in table.rs so that, like the sibling TableElement path,
it captures OptConsTableSpace from parsed TableConstraint values and assigns it
to table.index_tablespace before calling apply_constraint. Ensure PRIMARY
KEY/UNIQUE constraints with USING INDEX TABLESPACE round-trip correctly through
table_constraint and apply_constraint.

---

Nitpick comments:
In `@src/pull/mod.rs`:
- Around line 1098-1129: Both apply_column_comment and apply_trigger_comment
duplicate the same target.schema split-and-table lookup logic, so extract that
resolution into a shared helper in this module (for example,
resolve_owning_table or similar) and have both comment handlers call it before
updating the column or trigger. Keep the helper responsible for splitting the
relation into schema/table and calling find_table on a QualifiedName so the two
handlers only contain their specific comment assignment logic.

In `@src/pull/writer.rs`:
- Around line 51-85: The serialization logic for views and materialized views is
duplicated in the writer flow, so extract the shared behavior into a small
helper or closure around serialize, view_dependencies, and writer.save_value.
Reuse that helper for both assembly.views and assembly.materialized_views,
passing only the plural key ("views" or "materialized_views") and preserving the
dependency injection into the serialized value. Make sure the helper works
cleanly with writer and the borrow checker.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fe25e4a4-59ea-4f3b-95d6-ff09cc167f0f

📥 Commits

Reviewing files that changed from the base of the PR and between 4e0a067 and 1f0f6b3.

📒 Files selected for processing (10)
  • fixtures/schema.sql
  • schemata/table.yml
  • src/build/mod.rs
  • src/ddl/mod.rs
  • src/ddl/table.rs
  • src/deploy/mod.rs
  • src/pull/mod.rs
  • src/pull/writer.rs
  • tests/build_parity.rs
  • tests/deploy.rs

Comment thread src/ddl/table.rs
The TypedTableElement loop applied table constraints via
apply_constraint but never captured OptConsTableSpace into
table.index_tablespace, unlike the sibling TableElement loop. A typed
table's PRIMARY KEY/UNIQUE ... USING INDEX TABLESPACE constraint
silently lost its tablespace on pull — the same round-trip data loss
class this branch fixes elsewhere. Mirror the sibling loop: first
constraint carrying a tablespace wins.

Reported by CodeRabbit. just check and just gates pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gmr

gmr commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

🤖 This comment was posted by Claude on behalf of @gmr

Addressed CodeRabbit's review (1 comment):

  • Major — typed-table constraints dropped index_tablespace (src/ddl/table.rs): Fixed in 7026cca. The TypedTableElement loop now mirrors the sibling TableElement loop, capturing OptConsTableSpace into table.index_tablespace so PRIMARY KEY/UNIQUE ... USING INDEX TABLESPACE on typed tables round-trips through pull.

just check (fmt + clippy -D warnings + tests) and just gates (round-trip + deploy equivalence/convergence/safety) both pass locally. Thread resolved.

@gmr

gmr commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

Automated monitoring complete — PR is ready to merge.

  • CI: all checks green on HEAD 7026cca
  • CodeRabbit: re-reviewed 7026cca, no outstanding findings; review current
  • Review threads: all resolved (1 thread addressed this session — typed-table index_tablespace fix, acknowledged by CodeRabbit)

No further blockers. The documented intentional design decisions (schema-qualified functions per build deviation #12, split SERIAL/sequence SET DEFAULT entries, transitive child-entry owner resolution, normalized trigger-function body) were preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

@gmr gmr merged commit 31d3485 into main Jul 7, 2026
2 checks passed
@gmr gmr deleted the fix/review-findings-batch4 branch July 7, 2026 20:52
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