Skip to content

Batch 3: dollar-quote safety, efficiency/dedup cleanups, and gate-fixture coverage#55

Merged
gmr merged 8 commits into
mainfrom
fix/review-findings-batch3
Jul 7, 2026
Merged

Batch 3: dollar-quote safety, efficiency/dedup cleanups, and gate-fixture coverage#55
gmr merged 8 commits into
mainfrom
fix/review-findings-batch3

Conversation

@gmr

@gmr gmr commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Third batch from the 2026-07-06 code review: the remaining low-severity
correctness items, the efficiency (O(n²)→indexed) and duplication
cleanups, and — most importantly — an expansion of the Postgres gate
fixtures to close the coverage gaps that let earlier bugs slip past the
gates. Split across isolated worktrees by disjoint file ownership,
integrated, and validated with just check and just gates.

Solution

  • L1 — collision-safe dollar quoting (utils.rs, build/mod.rs,
    deploy/alter.rs): new dollar_quote() helper picks a tag not present
    in the body ($$$c1$ → …) so a comment/value containing $$ no
    longer terminates the literal early. All three sites routed through it.
  • D3 — deploy comment/options dedup (deploy/alter.rs):
    push_comment/push_options fold 14 identical delta sites; emitted
    SQL and --allow-drop gating unchanged.
  • E1/L9/D1 — pull (pull/*): (schema,name) + index_location
    maps remove the O(n²) ingest scans; the overload filename counter no
    longer displaces a real fn_1; one shared symlink-skipping empty-dir
    reaper replaces the two divergent copies.
  • E3 — loader (project/load.rs): a (desc,schema,name) index makes
    lookup_item/dependency resolution O(1); the duplicate-object guard
    still works.
  • E4/D2 — ddl (ddl/*): single-pass direct-child scans replace
    repeated recursive has()/find() walks (genuinely dual-depth sites
    left recursive, with reasoning); shared column_elems() helper folds 7
    duplicated blocks.
  • E2/L6 — build ACLs + progress (build/acls.rs, main.rs,
    progress.rs): an ObjectIndex makes ACL lookups O(1); progress bars
    now initialize even with --log-file.
  • Gate fixtures (fixtures/schema.sql): added round-trip/deploy
    coverage for a partitioned table's PARTITION BY, a typed table, table
    CHECK constraints, a security_barrier+check_option view, a column
    COMMENT, and a bare unquoted public reference.

Verification

  • just check — fmt, clippy -D warnings, full suite: green.
  • just gates — round-trip + deploy equivalence/convergence/safety:
    green on the expanded fixtures.

⚠️ New product bugs uncovered by the fixture work (NOT fixed here)

Trying to fixture the gap paths surfaced six real, previously-uncaught
bugs. They're documented inline in fixtures/schema.sql; each is a
candidate for a follow-up (batch 4). Highest-impact first:

  1. Zero-argument functions build invalid SQLCREATE FUNCTION name RETURNS … missing (); breaks every trigger function. (HIGH)
  2. Partition children lost on round-trippg_dump emits
    ALTER TABLE ONLY parent ATTACH PARTITION child FOR VALUES …, a form
    pull doesn't recognize; the inline PARTITION OF form it does handle
    is never what pg_dump produces. (HIGH — N-H3 is incomplete for real
    dumps)
  3. Views have no dependency edge to queried tables — build orders
    views alphabetically, so a view can be emitted before the table it
    selects from. (HIGH, systemic)
  4. Typed-table inline column constraints dropped on pull.
  5. SERIAL column defaults dropped — pg_dump's separate
    ALTER TABLE … SET DEFAULT nextval(…) isn't captured.
  6. Trigger comments dropped on pullapply_comment has no
    TRIGGER/RULE/POLICY/CONSTRAINT arm.

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

Summary by CodeRabbit

  • New Features
    • Generated outputs now cover a broader set of SQL structures (e.g., partitioned tables, typed tables, table constraints, views with options, and column comments).
  • Bug Fixes
    • Improved SQL string rendering for COMMENT ON/option payloads using safer dollar-quoting, preserving tricky characters.
    • Fixed function YAML emission during --update --prune to avoid overwriting existing numbered overload files; improved empty-directory cleanup and symlink-safe traversal.
    • Faster, more reliable object/function resolution and dependency/ACL targeting via in-memory indexing.

gmr and others added 7 commits July 7, 2026 14:29
apply_cached_dependencies and add_definition's duplicate-object guard
both scanned the full inventory linearly via lookup_item, making
dependency resolution O(n^2) in project size. Maintain a HashMap
keyed by (desc, namespace, tag) incrementally as items are added and
resolve both the duplicate check and dependency edges through it
instead.

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

Comments and string values wrapped in bare $$...$$ could be terminated
early by an embedded $$ in the body (values come from YAML or a live
database). Add utils::dollar_quote, which picks a pg_dump-style tag
($$, then $c1$, $c2$, ...) that does not occur in the body, and route
build::add_comment, deploy::alter::comment_on, and utils::render_value
through it.

Also extract push_comment and push_options helpers in deploy/alter.rs
to replace the repeated "if repo.x != db.x { alters.push(...) }"
blocks across the table/column/sequence/domain/enum/extension/schema/
foreign-table/fdw/server/user-mapping resolvers. Pure refactor: emitted
SQL and --allow-drop gating are unchanged since every statement still
flows through the shared Alter::new/Alter::destructive constructors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- build/acls.rs: build a HashMap-backed ObjectIndex once per build
  (generic desc/schema/name lookups, plus function exact-identity and
  bare-name indexes) instead of scanning the full inventory per ACL
  group and scanning it twice for functions. Matching semantics are
  preserved: schemaless descs still ignore the parsed schema, function
  identity matches exactly first, and the bare-name fallback still
  requires the match to be unambiguous.
- main.rs: initialize progress::init() unconditionally in
  configure_logging, before checking --log-file, so progress bars are
  no longer disabled on a TTY just because logs are being written to a
  file. Non-file logging behavior is unchanged.

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

- mod.rs: index tables/materialized_views/indexes by (schema, name) in
  HashMaps maintained during ingest instead of doing a linear scan per
  index/constraint/trigger/comment merge, and per INDEX comment; ingest
  merge results are unchanged, only the lookup cost.
- writer.rs: skip counter-suffixed filenames that collide with another
  function's real name when disambiguating overloads, so a real `fn_1`
  is never displaced into `fn_1_1.yaml` by an `fn` overload. Adds a
  unit test for the `fn`, `fn`, `fn_1` scenario.
- writer.rs/update.rs: replace the two divergent empty-directory
  reapers with one shared `writer::remove_empty_directories`, keeping
  the safer symlink-skipping walk (update.rs's behavior) for both the
  fresh-write and `--update --prune` paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
E4: table::table_constraint, function::apply_common_option, and
trigger::create_trigger called has()/find() repeatedly over the same
small subtree to test mutually-exclusive keyword branches. Verified
against tree-sitter-postgres's node-types.json/grammar.js that the
relevant keywords are direct children of their parent node (one per
grammar alternative), so each hot spot now does a single child-kind
scan instead. Left two call sites recursive because they genuinely
need it: function.rs's SET clause (kw_set/set_rest_more nest inside a
FunctionSetResetClause child, not directly under common_func_opt_item)
and trigger.rs's `when`/`for_each` detection (the timing/for-each
keywords are nested for a plain trigger but bare direct children for a
CONSTRAINT TRIGGER, per the existing comment in that code).

D2: extracted `ddl::column_elems` to collect and unquote a node's
columnElem children, replacing the block duplicated across table.rs
(index include list, constraint include list, column_list, foreign
key ref_columns), acl.rs (privilege column list), and view.rs (view
column list). All of these sites used plain `unquote`, not
`unquote_role`, so the shared helper does the same.

No behavior change intended; `just check` (fmt-check, clippy -D
warnings, full test suite) passes unchanged.

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

Adds fixtures/schema.sql coverage for object shapes recent batches
added support for, so the round-trip and deploy gates would catch
regressions in them going forward:

- a RANGE-partitioned table's PARTITION BY clause (events)
- a typed table via CREATE TABLE ... OF a composite type (locations)
- table-level CHECK constraints (products)
- a view with security_barrier and check_option (verified_users)
- a per-column COMMENT (users.display_name)
- a bare, unquoted `public` schema reference (public.widgets)

Several closely-related additions were attempted but dropped after
they surfaced real, previously-unknown product bugs rather than
fixture mistakes (each documented inline at its would-be call site
in fixtures/schema.sql):

- Partition children (`... PARTITION OF parent FOR VALUES ...`,
  including a DEFAULT partition) round-trip as unattached plain
  tables: pg_dump always emits partition attachment as a separate
  `ALTER TABLE ONLY parent ATTACH PARTITION child FOR VALUES ...`
  statement, which nothing in src/ddl/ or src/pull/ recognizes. The
  existing `merges_partition_children` unit test only exercises the
  inline `PARTITION OF ... FOR VALUES` form pg_dump never actually
  produces, so it never caught this.
- A typed table's inline column constraint
  (`CREATE TABLE t OF type (CONSTRAINT ... CHECK (...))`) is silently
  dropped by pull: `create_table` (src/ddl/table.rs) only walks
  `TableElement` children, and a typed table's element list uses a
  different grammar production.
- A SERIAL column's DEFAULT is dropped: pg_dump emits it as a
  separate `ALTER TABLE ... ALTER COLUMN ... SET DEFAULT nextval(...)`
  statement, and pull only captures inline defaults.
- A trigger's COMMENT is dropped: `apply_comment` (src/pull/mod.rs)
  has no match arm for TRIGGER (nor RULE/POLICY/CONSTRAINT).
- A zero-argument function renders as invalid SQL on build
  (`CREATE FUNCTION name RETURNS ...` with no `()`), because the
  pull-side `src/ddl/function.rs` no longer appends `()` to a
  zero-parameter function's name, an invariant `dump_function`'s
  fallback branch (src/build/mod.rs) depends on.

Also documents (inline, at the view addition) a systemic gap that
predates this change but was newly exposed by adding the first
non-materialized view depending on a table: views never get an
explicit dependency edge to the tables/views they query (only
FK-derived table dependencies are recorded), so `build` orders them
purely by libpgdump's default same-priority alphabetical sort. The
new view is named to sort after `users` to avoid tripping this; a
materialized view happens to be unaffected only because
MATERIALIZED VIEW has its own, later, priority tier.

`just gates` and `just check` both pass with these fixtures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/loader-efficiency', 'fix/ddl-efficiency', 'fix/acls-and-progress' and 'test/gate-fixture-coverage' into fix/review-findings-batch3
@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: d9c099fa-ad0f-41bf-98dd-f0eab02bfc59

📥 Commits

Reviewing files that changed from the base of the PR and between b3073a5 and 05c2459.

📒 Files selected for processing (2)
  • src/pull/writer.rs
  • src/utils.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/utils.rs
  • src/pull/writer.rs

📝 Walkthrough

Walkthrough

This PR adds SQL fixture objects, introduces hash-map-based lookup indexes in ACL resolution, project loading, and pull ingestion, adds shared dollar-quoting for comment/string rendering, refactors DDL parsing helpers and direct child lookups, updates pull writer pruning and filename disambiguation, and adjusts main.rs progress bar setup.

Changes

Core PR changes

Layer / File(s) Summary
Fixture schema additions
fixtures/schema.sql
Adds partitioned, typed, constrained, and view-based fixture objects, plus a column comment and a public widgets table.
Shared dollar quoting
src/utils.rs, src/build/mod.rs, src/deploy/alter.rs
Adds dollar-quote selection logic in utils and uses it for comment rendering in build and deploy paths.
Lookup indexes for ACLs, loading, and pull ingest
src/build/acls.rs, src/project/load.rs, src/pull/mod.rs
Adds in-memory indexes for ACL target resolution, loader duplicate/dependency lookup, and pull ingestion object placement and comment lookup.
DDL helper and parser updates
src/ddl/mod.rs, src/ddl/acl.rs, src/ddl/table.rs, src/ddl/view.rs, src/ddl/trigger.rs, src/ddl/function.rs
Adds a shared column list helper and updates ACL, table, view, trigger, and function parsing to use it or direct child lookups.
Pull pruning and filename handling
src/pull/writer.rs, src/pull/update.rs
Refactors empty-directory pruning to use multiple top paths, skips symlink traversal, and keeps real numbered function filenames from being displaced.
Progress initialization order
src/main.rs
Initializes progress rendering before logger setup and branches on the stored progress handle.

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

Sequence Diagram(s)

sequenceDiagram
  participant CreateIndexStmt
  participant Assembly
  participant Loader
  participant ObjectIndex
  participant Inventory
  CreateIndexStmt->>Assembly: ingest table or matview index
  Assembly->>Assembly: update table_index/matview_index/index_location
  Loader->>Loader: store index_key for new definitions
  ObjectIndex->>Inventory: build object and function maps
  ObjectIndex->>ObjectIndex: resolve dependency targets from maps
Loading

Possibly related PRs

  • gmr/pglifecycle#9: Introduces the Loader-based project loading foundation directly extended by this PR's Loader.index change.
  • gmr/pglifecycle#16: Introduced the ACL emission in src/build/acls.rs that this PR's ObjectIndex resolution builds on.
  • gmr/pglifecycle#25: Modifies the same in-place ALTER/COMMENT emission logic in src/deploy/alter.rs.
🚥 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 is concise and accurately reflects the PR’s main themes: dollar-quote safety, cleanup/efficiency work, and expanded gate-fixture 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-batch3

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: 2

🤖 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/pull/writer.rs`:
- Around line 489-496: The remove_empty_directories path still follows symlinked
top entries because it uses top.is_dir() before calling walk_directories, which
can let --update --prune traverse outside the intended tree. Update
remove_empty_directories in the writer module to check each top with
symlink_metadata first, skip entries whose metadata indicates a symlink, and
only then proceed to walk_directories and prune empty directories for real
directories.

In `@src/utils.rs`:
- Around line 170-186: The dollar_quote helper currently checks only for the
full delimiter string, which can miss cases where the body ends with the
delimiter prefix and still create invalid SQL. Update dollar_quote to use
pg_dump-style collision checks by matching the delimiter prefix without the
trailing $ when deciding whether $$ or $c{n}$ is safe, and keep the fallback
loop in sync with that logic.
🪄 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: 0dcbbe19-8c01-47d2-8987-fa7f875925c6

📥 Commits

Reviewing files that changed from the base of the PR and between d14ab5c and b3073a5.

📒 Files selected for processing (16)
  • fixtures/schema.sql
  • src/build/acls.rs
  • src/build/mod.rs
  • src/ddl/acl.rs
  • src/ddl/function.rs
  • src/ddl/mod.rs
  • src/ddl/table.rs
  • src/ddl/trigger.rs
  • src/ddl/view.rs
  • src/deploy/alter.rs
  • src/main.rs
  • src/project/load.rs
  • src/pull/mod.rs
  • src/pull/update.rs
  • src/pull/writer.rs
  • src/utils.rs

Comment thread src/pull/writer.rs
Comment thread src/utils.rs
…fix check

- remove_empty_directories: use symlink_metadata so a top that is a
  symlink to an external directory is skipped rather than followed,
  preventing --update --prune from reaping empty dirs outside the
  project tree.
- dollar_quote: pick delimiters by their form without the trailing $
  (pg_dump appendStringLiteralDQ style) so a body ending in the
  delimiter prefix cannot merge with the closing $ into invalid SQL.
  Adds a regression test for the trailing-$ case.

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

gmr commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

pr-agent: PR is ready to merge.

  • CI: all GitHub Actions checks green (fmt-check + lint + test) on 05c2459
  • CodeRabbit: review completed=success on current head, no unresolved actionable findings
  • Review threads: all resolved (2 Major findings fixed this session)
    • src/pull/writer.rs — symlink-safe empty-directory reaper (uses symlink_metadata)
    • src/utils.rs — dollar-quote prefix-without-trailing-dollar check
  • The six documented, out-of-scope product bugs remain deferred to the follow-up as intended.

Not merging per instructions — this is a readiness notification only.

@gmr gmr merged commit 4e0a067 into main Jul 7, 2026
2 checks passed
@gmr gmr deleted the fix/review-findings-batch3 branch July 7, 2026 19:24
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