Skip to content

sql-parser: support OPERATOR(pg_catalog.=) and prefix OPERATOR(...)#37643

Draft
ggevay wants to merge 2 commits into
MaterializeInc:mainfrom
ggevay:gabor/sql-486-operator-function-doesnt-work
Draft

sql-parser: support OPERATOR(pg_catalog.=) and prefix OPERATOR(...)#37643
ggevay wants to merge 2 commits into
MaterializeInc:mainfrom
ggevay:gabor/sql-486-operator-function-doesnt-work

Conversation

@ggevay

@ggevay ggevay commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Fixes SQL-486.

Motivation

expr OPERATOR(pg_catalog.=) expr failed to parse with Expected operator, found equals sign, while every other operator worked inside OPERATOR(...). psqlODBC generates this construct routinely, so the gap breaks real ODBC clients. PostgreSQL additionally accepts the prefix form, e.g. SELECT OPERATOR(pg_catalog.-) 1, which Materialize rejected entirely.

Description

Two commits:

  1. Accept = inside OPERATOR(...). The lexer emits = (and *) as dedicated tokens rather than general operator tokens, and the OPERATOR(...) name parser only handled the general token and *. Map = back to its operator name, mirroring the existing * special case.

  2. Support the prefix (unary) OPERATOR(...) form. Matching PostgreSQL, the operand parses at the precedence of "any other operator", looser than bare unary +/-, so OPERATOR(pg_catalog.-) 1 + 2 means - (1 + 2). Precedence behavior was verified against PostgreSQL 18, including compositions like 2 * OPERATOR(pg_catalog.-) 3 + 4. Operator resolution reuses the infix path, so non-unary operators fail with operator does not exist, like PostgreSQL. The OPERATOR( construct is only claimed in expression position when followed by (, so operator remains usable as an identifier.

    The second commit also makes the AST printer's parenthesization of prefix operands precedence-aware. The printer used to over-parenthesize operands of Other-level prefix operators (~ and namespaced OPERATOR(...)), which broke print/reparse fidelity for the new syntax. The rule now lives in one helper shared by AstDisplay and mz-sql-pretty, which previously duplicated the logic.

User-facing behavior change: OPERATOR(pg_catalog.=) (and unqualified OPERATOR(=)) now parses, and the prefix operator call syntax OPERATOR(pg_catalog.-) expr is now supported, improving PostgreSQL compatibility, in particular for psqlODBC.

Verification

New parser datadriven tests in src/sql-parser/tests/testdata/scalar (ASTs, round-trips, and precedence pins) and new sqllogictest coverage in test/sqllogictest/operator.slt (results, precedence, error cases, and SHOW CREATE VIEW round-trips through the catalog). The existing mz-sql-pretty fidelity test now exercises the shared parenthesization rule across all parser testdata. Edge-case behavior (precedence, invalid prefix operators, operator as an identifier) was compared against a live PostgreSQL 18.

🤖 Generated with Claude Code

The lexer emits = as a dedicated Token::Eq rather than a general
Token::Op, and parse_operator only accepted Token::Op and Token::Star.
As a result OPERATOR(pg_catalog.=) failed to parse with "Expected
operator, found equals sign" while all other operators worked.
psqlODBC generates this construct, so the gap breaks real clients.

Handle Token::Eq like Token::Star and map it back to the operator
name, matching PostgreSQL, which accepts = (and *) inside OPERATOR().

Fixes SQL-486.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ggevay ggevay added the A-ADAPTER Topics related to the ADAPTER layer label Jul 14, 2026
PostgreSQL allows OPERATOR(...) in prefix position, for example
OPERATOR(pg_catalog.-) 1, but Materialize only parsed the infix form.
Add a prefix arm that parses the operand at the precedence of "any
other operator", matching PostgreSQL, where a prefix OPERATOR() binds
looser than bare unary + and -, so OPERATOR(pg_catalog.-) 1 + 2 means
- (1 + 2). Resolution of the named operator reuses the infix path, so
non-unary operators fail with "operator does not exist", like
PostgreSQL.

Printing a prefix operator previously parenthesized any operand with
an exposed operator spine. That is wrong for Other-level prefixes
(~ and namespaced OPERATOR(...)), whose operand only re-associates
when its left spine exposes the Other level or looser. Encode that
rule in a prefix_operand_needs_parens helper shared with mz-sql-pretty,
which had duplicated the old logic and must agree with AstDisplay for
pretty-printed statements to reparse faithfully.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ggevay ggevay force-pushed the gabor/sql-486-operator-function-doesnt-work branch 2 times, most recently from 05def4b to aa8f003 Compare July 14, 2026 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ADAPTER Topics related to the ADAPTER layer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant