Skip to content

Allow CAST as a table function argument - #307

Merged
git-hulk merged 1 commit into
masterfrom
fix/cast-in-table-function-arg
Aug 6, 2026
Merged

Allow CAST as a table function argument#307
git-hulk merged 1 commit into
masterfrom
fix/cast-in-table-function-arg

Conversation

@git-hulk

@git-hulk git-hulk commented Aug 5, 2026

Copy link
Copy Markdown
Member

Table function arguments are parsed by parseTableArgPrimaryExpr, which
had no case for CAST. It read the keyword as an ordinary function name,
so CAST's own AS separator ended the argument list and
numbers(CAST(1 + 1 AS UInt64)) failed with "expected ')'". Route CAST to
parseColumnCastExpr, as parseColumnExpr already does. CAST is not
reserved, so it also matches TokenKindIdent and the new case has to come
before that one.

Only the argument list of a table function reaches this parser, never
the table name, so a table called cast still parses. Both CAST spellings
are covered, since parseColumnCastExpr reads AS and ',' alike.

Checked against clickhouse-local 26.7.1, which accepts the new
statements and their formatted output.

🤖 Generated with Claude Code

Table function arguments are parsed by parseTableArgPrimaryExpr, which
had no case for CAST. It read the keyword as an ordinary function name,
so CAST's own AS separator ended the argument list and
numbers(CAST(1 + 1 AS UInt64)) failed with "expected ')'". Route CAST to
parseColumnCastExpr, as parseColumnExpr already does. CAST is not
reserved, so it also matches TokenKindIdent and the new case has to come
before that one.

Only the argument list of a table function reaches this parser, never
the table name, so a table called cast still parses. Both CAST spellings
are covered, since parseColumnCastExpr reads AS and ',' alike.

Checked against clickhouse-local 26.7.1, which accepts the new
statements and their formatted output.

Assistant By Claude Opus 5
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@git-hulk
git-hulk merged commit 44047b0 into master Aug 6, 2026
4 checks passed
@git-hulk
git-hulk deleted the fix/cast-in-table-function-arg branch August 6, 2026 02:43
pull Bot pushed a commit to edisplay/signoz that referenced this pull request Aug 7, 2026
Bumps `clickhouse-sql-parser` to v0.5.5, fixes the false rejection that
was left over once it landed, and closes three holes in the same
validator that the first two changes brought to light.

## The bump

**Reserved keywords as expression operands**
([#305](AfterShip/clickhouse-sql-parser#305)).
`interval` was fixed in v0.5.4, but the same defect affected 36 other
keywords once the column appeared as an operand rather than bare.
Sweeping 94 candidates against ClickHouse 26.8.1.337, only `on` still
rejects — and ClickHouse runs that too. This one was live: `sum(limit)`
on a metric label.

**Panic on an unparseable `DEFAULT` expression**
([#306](AfterShip/clickhouse-sql-parser#306)).
Both known cases return a parse error now instead of dereferencing nil.
The `recover` in `ErrIfStatementIsNotValid` stays — it guards the next
one of these, not these two.

[#307](AfterShip/clickhouse-sql-parser#307) also
allows `CAST` in a table function's argument list.

## Table functions are only table functions in a table position

The parser types a call inside a table function's argument list as a
`TableFunctionExpr` as well, so the generator allow list only ever
cleared a generator whose argument was a literal. Every real dashboard
computes its row count — `numbers(greatest(1, intDiv(end_ns - start_ns,
step_ns) + 1))` — and every one was refused, on `intDiv` rather than on
`numbers`.

`TableExpr.Expr` is the only table position a SELECT can reach, so the
allow list asks that instead. Of the four places the parser builds a
`TableFunctionExpr`, two are `CREATE TABLE` paths rejected as
not-a-SELECT before the walk starts, one is `parseTableArgPrimaryExpr`,
and one is the `FROM`/`JOIN` path that wraps into a `TableExpr`.

## Three holes that were already open

Skipping argument position is only safe if nothing there can read, and
that turned out not to be true — not because of this change, but
independently of it.

**Reading functions.** `file` is both a table function and a scalar
function, and the validator never inspected scalar calls at all. On
`main` today, `SELECT file('/etc/passwd')` is accepted and returns the
file. A numeric wrapper passes ClickHouse's type check, so the row count
alone is an oracle: `numbers(length(file(x)))` yields one row per byte.
The same applies to the 42 dictionary accessors, which can be backed by
HTTP, ODBC or another database, to `catboostEvaluate`, and to the
introspection functions. All are now refused by name wherever they
appear, under `clickhouse_sql_reading_function`.

**`x IN db.table`.** ClickHouse reads this as `x IN (SELECT * FROM
db.table)`, and a qualified name on the right of `IN` parses as a
`Path`, not a `TableIdentifier` — so `SELECT * FROM t WHERE a IN
system.users` bypassed the internal-database rule entirely. Now checked,
including the `GLOBAL IN` and `NOT IN` forms.

**Quoted generator names.** The allow list matched on the formatted
name, which carries the quoting, so ``SELECT * FROM `numbers`(31)`` was
refused. It now reads the identifier the way the internal-database
branch already did.

## Effect

Replaying 72 distinct shapes of production `clickhouse_sql` that the
validator currently rejects: **64 pass, up from 59 on v0.5.4**. Two came
from the bump, three from the table-position change, and those three are
379 of the 1390 sampled occurrences. The three new rules add no false
positives to the corpus.

Of the eight left, four are correct rejections (`system` reads, `SHOW
TABLES`), one is a dashboard variable rendering as the literal `<no
value>`, one is SQL ClickHouse also rejects, and two are an open
upstream gap.

## Tests

`TestErrIfStatementIsNotValid_ShouldPassButFails` is back, holding what
remains: three forms of a parenthesised left operand of a set operator,
and `on` as a column name. It also stopped panicking — `errors.Asc`
dereferences the error it is given, so a case starting to pass took the
suite out with a SIGSEGV instead of reporting. Both refusal tables now
share one harness, bounded by the same timeout the passing table uses.

Known gap: no input is currently known to panic the parser, so the
`recover` has no test exercising it.
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