Skip to content

feat(grammar): parse the Postgres 18 syntax and complete its keywords - #1118

Open
psteinroe wants to merge 2 commits into
mainfrom
feat/grammar-pg18-syntax
Open

psteinroe wants to merge 2 commits into
mainfrom
feat/grammar-pg18-syntax

Conversation

@psteinroe

@psteinroe psteinroe commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Since #1055 we parse Postgres 18, but the tree-sitter grammar didn't know the new syntax, so completions inside it got an error tree and fell back to every keyword. This teaches the grammar the Postgres 18 additions to gram.y and adds their keywords to the completion list.

What changed

  • Keywords enforced, virtual, period, overlaps, large, objects, plus sequences, types, schemas and usage for the default-privilege targets. They are in ALL_KEYWORDS, whose tests check that it matches the grammar.
  • Column definitions: GENERATED ALWAYS AS (...) VIRTUAL, [NOT] ENFORCED, NOT NULL NO INHERIT, CONSTRAINT name before a column constraint, and REFERENCES t without a column list.
  • Table constraints: a public constraint_attributes node after a constraint ([NOT] DEFERRABLE, INITIALLY ..., [NOT] ENFORCED, NOT VALID, NO INHERIT), named foreign key and unique constraints, [CONSTRAINT n] NOT NULL col, WITHOUT OVERLAPS in key columns and PERIOD in foreign key columns (both sides). A column named period stays a column.
  • ALTER TABLE ... ALTER CONSTRAINT name with the attributes it accepts: [NO] INHERIT and no NOT VALID.
  • ALTER DEFAULT PRIVILEGES [FOR ROLE ...] [IN SCHEMA ...] GRANT | REVOKE ... ON {TABLES | SEQUENCES | FUNCTIONS | ROUTINES | TYPES | SCHEMAS | LARGE OBJECTS}, a new statement. Each part is a partial rule with end fields, so completions work while it is typed.
  • MERGE ... RETURNING (where old/new are new in 18), and a list of relations, each with its own ONLY, for VACUUM and ANALYZE. returning_with and ANALYZE ONLY now parse while they are typed (returning with (), analyze only).
  • grantable and role_specification mark their end. grantable also takes usage and create.

The RETURNING WITH (...), old.*/new.* and the new COPY options already parsed.

Backwards compatibility

I parsed every statement that Postgres accepts in the 15–18 regression fixtures (127,026) with the old and the new grammar: none that parsed before has an error now, and 837 more parse.

Tests

  • grammar_tests.rs: one snapshot per construct, each asserting that the tree has no error. On main all but three have errors: UPDATE ... RETURNING WITH and the COPY options already parsed, and postgres_18_period_column checks that a column named period still parses.
  • partial-sqls/pg18.sql runs the typing-state test over RETURNING WITH (OLD AS ...), old.*/new.* and ANALYZE ONLY t, ONLY u. It fails on main at returning with () and analyze only.
  • Completions: completes_default_privileges_targets and completes_constraint_attributes check the exact keywords after ... ON, ... ON LARGE, ALTER CONSTRAINT c and ... NOT. On main the tree has an error there, so every keyword is offered.
  • cargo test -p pgls_treesitter_grammar -p pgls_treesitter -p pgls_completions -p pgls_hover -p pgls_typecheck, cargo clippy --workspace --all-targets -- -D warnings.

Not covered

  • VACUUM (options), CREATE TABLE ... INHERITS, partitions and OVERLAPS as an operator didn't parse before and still don't.
  • CREATE TABLE and ALTER TABLE aren't partial rules yet, so completions inside a half-typed column definition still see an error tree. ALTER DEFAULT PRIVILEGES is, from alter default on.
  • A table, column or role named usage, large and so on is still fine.

Update: GRANT role TO role follows Postgres again. Without ON, the list after GRANT/REVOKE is a list of roles, and the unreserved privilege names (insert, update, delete, truncate, trigger, maintain, execute, usage) are role names there, so grant usage, update to bob parses with two role_identifiers. The grammar declares the conflict between grantable and these role names, and the ON decides. Keyword completion now skips a keyword that the grammar would read as a name at the cursor, so grant | to owner still offers roles first. New snapshots grant_role_named_usage, revoke_role_named_usage and grant_usage_privilege. The comparison over the regression fixtures still finds no statement that parsed on main and fails now.

$.grantable,
$.default_privileges_target,
$.keyword_to,
comma_list(field("end", $.role_specification), true),

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.

With a catalog containing bob, completing alter default privileges grant select on tables to b returns the bob Role item on the base commit, but an empty list here. The same regression occurs after REVOKE ... FROM, and existing roles also disappear after FOR ROLE.

The new tree correctly contains a role_identifier, but these clauses have no recognized wrapping context, so completable_context rejects catalog completions.

We should connect the new clauses to the completion context and add assertions for actual Role items alongside the keyword tests.

Comment on lines +3282 to +3283
optional($.default_privileges_for_role),
optional($.default_privileges_in_schema),

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.

alter default privileges in schema public for role admin grant select on tables to bob; is valid PostgreSQL syntax. I verified it with the PostgreSQL 18 parser and executed both option orders successfully on PostgreSQL 15.

This sequence only accepts FOR ROLE followed by IN SCHEMA; reversing them leaves an ERROR node around FOR ROLE admin.

I think these options should remain order-independent, with a grammar test covering the reversed order and its completion states.

SqlKeyword::new("parallel"),
SqlKeyword::new("partition"),
SqlKeyword::new("password"),
SqlKeyword::new("period"),

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.

With public.t.period in the catalog, completing create table t (period int, primary key (period)); inside period now returns a Keyword item with no description or type detail. The base returned a Column item with its catalog metadata.

The tree still identifies this token as a column_identifier, and hover remains correct; the inserted text is also unchanged.

Maybe we could prefer the catalog Column item over the keyword candidate in this context, and assert the completion kind and metadata in the compatibility test.

This branch has not been deployed

No deployments
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.

2 participants