Skip to content

fix(analyser): precise spans and coverage for the rules ported from Squawk - #905

Merged
psteinroe merged 3 commits into
mainfrom
fix/squawk-rules-spans-coverage
Oct 26, 2026
Merged

psteinroe merged 3 commits into
mainfrom
fix/squawk-rules-spans-coverage

Conversation

@psteinroe

Copy link
Copy Markdown
Collaborator

Stacked on #891.

Follow-ups to the rules ported from Squawk in #885: where the diagnostics point, and what they cover. Every new error case was checked against Postgres 15 in a rolled-back transaction.

banDuplicateColumnAssignments

  • Diagnostics pointed at the whole statement. They now point at the second occurrence: the column name in SET, INSERT and column definitions, and the whole select list entry (e.g. coalesce(a, b) as x) in views, CREATE TABLE AS and SELECT INTO. View column lists and COPY column lists have no locations and still point at the statement.

  • A * anywhere in a view's select list turned the check off. It now only hides the names it may expand to:

    create view v as select t.*, 1 as x, 2 as x from t;
    -- before: nothing
    -- after:  × Column x is specified more than once.   (Postgres: 42701)

    If the view's explicit column names reach into the *, we still can't tell what the later entries are called, so create view v (a, c, d) as select t.*, 2 as x, 3 as x stays silent (Postgres accepts it when t has two columns).

  • New statements, all rejected by Postgres with 42701: alter table users add column email text, add column email text (also after ADD COLUMN IF NOT EXISTS email; a later IF NOT EXISTS is skipped by Postgres and not reported), create type pair as (a int, a int), copy users (id, id), and create foreign table ... (id int, id int). ADD COLUMN and COPY have their own detail text.

requireTableSchema (opt-in)

  • Points at the table name, and the note uses it (e.g. public.posts instead of public.my_table). DROP TABLE a, b points at each name.
  • Temporary tables created earlier in the file (create temp table, create table pg_temp.x, select ... into temp x) are no longer reported on later ALTER/DROP/CREATE INDEX, until they are dropped. An unqualified name refers to them since pg_temp comes first in the search path. Squawk reports these too.
  • Scope: statements that create, change or drop a table, and the tables they reference by name. Newly covered: CREATE FOREIGN TABLE, ALTER/DROP FOREIGN TABLE, ALTER TABLE ... RENAME (table, column, constraint), SET SCHEMA, SELECT INTO, CREATE INDEX ON, and inside those statements REFERENCES, PARTITION OF, INHERITS, LIKE, ATTACH PARTITION. Queries (the body of a view, CREATE TABLE AS ... SELECT ... FROM x, DML) stay out of scope, otherwise the rule would fire on every query. This is wider than Squawk, which only checks CREATE/ALTER/DROP TABLE; the rule is opt-in, and a user who enables it wants every table name in DDL qualified.

banDropConstraint, banDropDefault (opt-in)

One diagnostic per statement without a name became one per command, with the name, pointing at its DROP CONSTRAINT / DROP DEFAULT:

alter table users drop constraint users_email_key, drop constraint if exists users_name_check;
-- before: ! Dropping a constraint may break existing clients.
-- after:  ! Dropping constraint users_email_key may break existing clients.
--         ! Dropping constraint users_name_check may break existing clients.

renamingObject (opt-in)

Most of its findings on the regression corpus are ALTER INDEX ... RENAME and RENAME CONSTRAINT, which Squawk's renaming-object reports as well, so they stay. We also report collations, text search configurations, FDWs and other objects Squawk doesn't. I kept those: clients do reference collations and text search configurations by name, and the rule is opt-in. The one change is ALTER TYPE ... RENAME ATTRIBUTE, which moves to renamingColumn like in Squawk (an attribute is a column of a composite type).

requireEnumValueOrdering now has applies_to: Migration, like the other DDL rules from #885.

Tests

New spec files for the new duplicate cases (and the accepted ones next to them), the new requireTableSchema positions and temporary tables, multiple commands for banDropConstraint/banDropDefault, and RENAME ATTRIBUTE. The spec snapshots don't show spans, so there are unit tests per rule asserting the text each diagnostic points at, using a small test helper spanned_texts in lib.rs. StatementTokens gets a keyword_pairs helper for the DROP CONSTRAINT / DROP DEFAULT spans.

Not covered

Duplicates that need the catalog (ADD COLUMN of an existing column, LIKE t plus a column of t) and view column lists / COPY column lists still point at the statement.

…like Postgres

FigureColname takes the name of a scalar subquery's column, the last field
name of an indirection, and the name of a CASE's ELSE result if it is a good
name. The port named the first two `?column?` or after the indirection's
argument, and every CASE `case`, so banDuplicateColumnAssignments reported
valid views as duplicates and unknownColumn rejected existing view columns.

Also run banDuplicateColumnAssignments over the Postgres regression suite and
fix the SQLSTATE of duplicate column lists in its docs.
…quawk

banDuplicateColumnAssignments now points at the duplicate, also checks ALTER
TABLE ... ADD COLUMN, CREATE TYPE ... AS and COPY column lists, and a `*`
in a view only hides the names it may expand to. requireTableSchema points
at the table, names it in the note, skips temporary tables created earlier
in the file, and covers foreign tables, RENAME, SET SCHEMA, SELECT INTO,
CREATE INDEX and referenced tables. banDropConstraint and banDropDefault
report each command with its name. RENAME ATTRIBUTE moves from
renamingObject to renamingColumn, and requireEnumValueOrdering only runs on
migrations.
@psteinroe
psteinroe merged commit 6e8494e into main Oct 26, 2026
11 checks passed
@psteinroe
psteinroe deleted the fix/squawk-rules-spans-coverage branch October 26, 2026 09:21
psteinroe added a commit that referenced this pull request Oct 26, 2026
…ction and banDuplicateColumnAssignments (#909)

Stacked on #905, and merges #897 (both change `identifierTooLong`), so until #897 lands the diff here includes it; the change of this PR is the last commit.

Three of the rules ported from Squawk in #885 get fixes, using the fix API from #859. Squawk has fixes for these; `requireEnumValueOrdering`'s Squawk fix inserts a placeholder that doesn't run, so it isn't ported.

**identifierTooLong: safe**

The fix spells out the name Postgres already uses: the identifier truncated to 63 bytes at a character boundary, the way Postgres truncates it. Since #897 reports each name once per statement, the fix rewrites every identifier of the statement that stands for the name:

```sql
select aaaa…(64).id from aaaa…(64) where AAAA…(64).id = 1;
-- fix: select aaaa…(63).id from aaaa…(63) where AAAA…(63).id = 1;
```

Unquoted identifiers keep their case as written, quoted ones are re-quoted from the truncated name (so a `""` cut in half can't break the quoting). `U&"..."` identifiers are left alone, because rewriting them would leave a `UESCAPE` clause behind.

The report proposed making it unsafe for quoted names. I made it safe for all: the statement does exactly the same afterwards, quoted or not, which is what `FixKind::Safe` means. Other statements that use the long name get their own diagnostic and fix, and leaving them as they are is still equivalent. The fix doesn't pick a good short name, that's still up to the user (the note says so).

**banUncommittedTransaction: unsafe**

Adds `commit;` on a new line after the last statement of the file, or of the migration section the `BEGIN` is in:

```sql
-- section: up transaction:false
begin;
create table users (id bigint);
commit;            -- added

-- section: down transaction:false
```

Unsafe because it changes what the migration does (its changes are now applied), and whether `COMMIT` belongs at the end, or a `ROLLBACK` was meant, is a guess. To know where the file or section ends, `AnalysedFileContext` now knows where its statements start and where the last one ends, including statements that don't parse, via `end_offset()`, relative to the current statement like other fix edits.

**banDuplicateColumnAssignments: unsafe**

Keeps the last assignment of a SET clause (UPDATE, ON CONFLICT DO UPDATE, MERGE) and removes the earlier ones with their commas. Only if every assignment to the column is a plain `col = value`: no fix for INSERT column lists (the values would have to move too), `(a, b) = ...` assignments, or subscripts and fields. Unsafe because we can't tell which value was meant.

**Tests**

Every spec snapshot of the three rules now shows the fixed SQL, and the test harness checks it still parses. New specs: `banDuplicateColumnAssignments/fixes.sql` (comments between assignments, ON CONFLICT, and the cases without a fix), `banUncommittedTransaction/fix_in_section.sql`.

**Risks**

The fixes are plain token edits, independent of the fix infrastructure changes in flight. One interaction to watch: #906 checks each fix by applying it to the statement text and re-parsing. The COMMIT fix's edit lies past the end of the `BEGIN` statement (at the end of the file or section), so `RuleFix::apply` on the statement text would slice out of range. Whichever of the two lands second has to handle edits outside the statement, e.g. skip the re-parse check for them or apply the fix to the file text.

**Follow-ups:** #906 keeps fixes that edit past the end of their statement without re-parsing them, so the `commit;` fix works with it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant