Repository navigation
fix(catalog): name columns of scalar subqueries and field selections like Postgres - #891
Merged
Merged
Conversation
…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.
# Conflicts: # crates/pgls_analyser/tests/snapshots/postgres_regress__typecheck_15.snap # crates/pgls_analyser/tests/snapshots/postgres_regress__typecheck_16.snap # crates/pgls_analyser/tests/snapshots/postgres_regress__typecheck_17.snap # crates/pgls_analyser/tests/snapshots/postgres_regress__typecheck_18.snap
psteinroe
added a commit
that referenced
this pull request
Oct 26, 2026
…quawk (#905) 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: ```sql 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`: ```sql 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The column names we give unnamed select list entries (our port of Postgres'
FigureColname) were wrong in three places, and bothbanDuplicateColumnAssignments(new in #885, recommended) andunknownColumntrust them:?column?. Postgres transforms the subquery first and takes the name of its column, so(select max(a) from t)ismax.(t.c).arr[1]was named afterc, because only the last indirection item was looked at. Postgres takes the last field name anywhere in the list:arr.CASEwas namedcase. Postgres uses the name of the ELSE result if that is a good name, socase when ... then 1 else a endisa.So valid migrations failed the linter:
The casts of subqueries follow from this:
(select max(a) from t)::textkeepsmax, and(select 1)::textstays?column?(the subquery's name is a strong name, so the cast doesn't replace it withtext).What changed
EXPR_SUBLINKtakes the name of the first column of the subquery: the alias of the first entry of its left-most select, otherwise the name figured for its value,column1for VALUES, and unknown for a*(it expands to a column of the relation). The name is strong, as in Postgres.ColumnRefandA_Indirectiontake the last field name in the list, ignoring*and subscripts, and an indirection without one falls back to its argument.CASEports the ELSE handling.MULTIEXPR_SUBLINKhas no name, as in Postgres (it only appears inUPDATE ... SET (a, b) = (select ...)).figure_column_namereturnsNonefor a top-level*, instead of?column?for*/t.*. The resolver already expanded those itself;output_name(ORDER BY / GROUP BY references) no longer gets a bogus?column?for them.Every name was checked against Postgres 15 (
create view+pg_attribute), and the three duplicates in the new spec are rejected by Postgres with 42701.banDuplicateColumnAssignmentsnow also runs inpostgres_regress.rs, so any finding on a statement Postgres accepted fails CI on every version. The corpus is clean; its three findings are on rejected statements (update.sql42601,create_table.sqlandtyped_table.sql42701) and show up in the per-version snapshots.The rule docs said
insert into t (a, a)fails with42601. Postgres raises42701 duplicate_column(column "a" specified more than once); only duplicate SET assignments are42601.Tests
Spec files
banDuplicateColumnAssignments/unnamed_columns.sql(accepted views,CREATE TABLE AS,SELECT INTOwith subqueries, set operations, VALUES, casts, field subscripts, CASE),unnamed_duplicates.sql(the cases Postgres does reject), andunknownColumn/unnamed_view_columns.sql. Regress snapshots for 15–18 gain the three true positives.Not covered
The subquery's first column is unknown if it is a
*(we'd need the relation's first column), so that case reports nothing.