From 2315cd084e5f7032df0f3b5e0a99255bb2c490c6 Mon Sep 17 00:00:00 2001 From: Sharad Gaur Date: Thu, 12 Nov 2026 18:09:20 -0500 Subject: [PATCH] Parse parameterized view calls with a database and non-literal arguments A parameterized view is called like a table function. Two parts of that call were rejected: - a database-qualified name, db.view(name = value) - an argument that is an array or a query parameter, such as ids = [1, 2] or id = {id:UInt64} ClickHouse prints the qualified call as `db.view`(...) when it formats a query. That form already parsed once the arguments did. --- parser/parser_query.go | 10 +- parser/parser_table.go | 5 + parser/parser_test.go | 6 + .../select_from_parameterized_view.sql | 13 + .../format/select_from_parameterized_view.sql | 8 + ...ct_from_parameterized_view.sql.golden.json | 369 ++++++++++++++++++ .../query/select_from_parameterized_view.sql | 3 + 7 files changed, 412 insertions(+), 2 deletions(-) create mode 100644 parser/testdata/query/format/beautify/select_from_parameterized_view.sql create mode 100644 parser/testdata/query/format/select_from_parameterized_view.sql create mode 100644 parser/testdata/query/output/select_from_parameterized_view.sql.golden.json create mode 100644 parser/testdata/query/select_from_parameterized_view.sql diff --git a/parser/parser_query.go b/parser/parser_query.go index 1470682..3f2d565 100644 --- a/parser/parser_query.go +++ b/parser/parser_query.go @@ -484,7 +484,7 @@ func (p *Parser) parseTableExpr(pos Pos) (*TableExpr, error) { return nil, err } // it's a table name - if tableIdentifier.Database != nil || !p.matchTokenKind(TokenKindLParen) { // database.table + if !p.matchTokenKind(TokenKindLParen) { // database.table expr = tableIdentifier } else { // table function expr @@ -492,8 +492,14 @@ func (p *Parser) parseTableExpr(pos Pos) (*TableExpr, error) { if err != nil { return nil, err } + // A parameterized view is called like a table function and can + // be qualified with its database: db.view(name = value). + var name Expr = tableIdentifier.Table + if tableIdentifier.Database != nil { + name = tableIdentifier + } expr = &TableFunctionExpr{ - Name: tableIdentifier.Table, + Name: name, Args: tableArgs, } } diff --git a/parser/parser_table.go b/parser/parser_table.go index 1da62a2..a0e558b 100644 --- a/parser/parser_table.go +++ b/parser/parser_table.go @@ -845,6 +845,11 @@ func (p *Parser) parseTableArgPrimaryExpr(pos Pos) (Expr, error) { case p.matchTokenKind(TokenKindInt), p.matchTokenKind(TokenKindFloat), p.matchTokenKind(TokenKindString), p.matchKeyword(KeywordNull): return p.parseLiteral(p.Pos()) + case p.matchTokenKind(TokenKindLBracket): + return p.parseArrayParams(p.Pos()) + case p.matchTokenKind(TokenKindLBrace): + // a query parameter such as {id:UInt64} + return p.parseQueryParam(p.Pos()) default: return nil, fmt.Errorf("unexpected token: %q, expected , ", p.currentTokenString()) } diff --git a/parser/parser_test.go b/parser/parser_test.go index a69f480..b1457d9 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -290,6 +290,12 @@ func TestParser_InvalidSyntax(t *testing.T) { "(SELECT 1) SETTINGS max_threads=1 UNION ALL SELECT 2", // AS at the very end of an ORDER BY item leaves no token to check for SELECT "SELECT * FROM t ORDER BY a AS", + // arguments of a parameterized view call + "SELECT * FROM db.v(a = {x})", // query parameter without a type + "SELECT * FROM db.v(a = {'k': 1})", // not a query parameter + "SELECT * FROM db.v(a = [1, )", // unterminated array + "SELECT * FROM db.v(a = 1)(b = 2)", // a call takes one argument list + "SELECT * FROM db.t (SELECT 1)", // a subquery cannot follow a table name } for _, sql := range invalidSQLs { parser := NewParser(sql) diff --git a/parser/testdata/query/format/beautify/select_from_parameterized_view.sql b/parser/testdata/query/format/beautify/select_from_parameterized_view.sql new file mode 100644 index 0000000..4f15796 --- /dev/null +++ b/parser/testdata/query/format/beautify/select_from_parameterized_view.sql @@ -0,0 +1,13 @@ +-- Origin SQL: +SELECT id +FROM db.v(min_id = {min_id:UInt64}, ids = [{id:UInt64}], since = toDateTime64({since:String}, 3)) AS a +INNER JOIN `db.v`(min_id = 1, ids = [1, 2], since = now()) AS b ON a.id = b.id + + +-- Beautify SQL: +SELECT + id +FROM + db.v(min_id={min_id: UInt64}, ids=[{id: UInt64}], since=toDateTime64({since: String}, 3)) AS a + INNER JOIN + `db.v`(min_id=1, ids=[1, 2], since=now()) AS b ON a.id = b.id; diff --git a/parser/testdata/query/format/select_from_parameterized_view.sql b/parser/testdata/query/format/select_from_parameterized_view.sql new file mode 100644 index 0000000..759decd --- /dev/null +++ b/parser/testdata/query/format/select_from_parameterized_view.sql @@ -0,0 +1,8 @@ +-- Origin SQL: +SELECT id +FROM db.v(min_id = {min_id:UInt64}, ids = [{id:UInt64}], since = toDateTime64({since:String}, 3)) AS a +INNER JOIN `db.v`(min_id = 1, ids = [1, 2], since = now()) AS b ON a.id = b.id + + +-- Format SQL: +SELECT id FROM db.v(min_id={min_id: UInt64}, ids=[{id: UInt64}], since=toDateTime64({since: String}, 3)) AS a INNER JOIN `db.v`(min_id=1, ids=[1, 2], since=now()) AS b ON a.id = b.id; diff --git a/parser/testdata/query/output/select_from_parameterized_view.sql.golden.json b/parser/testdata/query/output/select_from_parameterized_view.sql.golden.json new file mode 100644 index 0000000..9b6ff13 --- /dev/null +++ b/parser/testdata/query/output/select_from_parameterized_view.sql.golden.json @@ -0,0 +1,369 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 191, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "Name": "id", + "QuoteType": 1, + "NamePos": 7, + "NameEnd": 9 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": { + "FromPos": 10, + "Expr": { + "JoinPos": 15, + "Left": { + "Table": { + "TablePos": 15, + "TableEnd": 112, + "Alias": null, + "Expr": { + "Expr": { + "Name": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 15, + "NameEnd": 17 + }, + "Table": { + "Name": "v", + "QuoteType": 1, + "NamePos": 18, + "NameEnd": 19 + } + }, + "Args": { + "LeftParenPos": 19, + "RightParenPos": 106, + "Args": [ + { + "NamePos": 20, + "Name": { + "Name": "min_id", + "QuoteType": 0, + "NamePos": 20, + "NameEnd": 26 + }, + "Value": { + "LBracePos": 29, + "RBracePos": 43, + "Name": { + "Name": "min_id", + "QuoteType": 1, + "NamePos": 30, + "NameEnd": 36 + }, + "Type": { + "Name": { + "Name": "UInt64", + "QuoteType": 1, + "NamePos": 37, + "NameEnd": 43 + } + } + } + }, + { + "NamePos": 46, + "Name": { + "Name": "ids", + "QuoteType": 0, + "NamePos": 46, + "NameEnd": 49 + }, + "Value": { + "LeftBracketPos": 52, + "RightBracketPos": 64, + "Items": { + "ListPos": 53, + "ListEnd": 64, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LBracePos": 53, + "RBracePos": 63, + "Name": { + "Name": "id", + "QuoteType": 1, + "NamePos": 54, + "NameEnd": 56 + }, + "Type": { + "Name": { + "Name": "UInt64", + "QuoteType": 1, + "NamePos": 57, + "NameEnd": 63 + } + } + }, + "Alias": null + } + ] + } + } + }, + { + "NamePos": 67, + "Name": { + "Name": "since", + "QuoteType": 0, + "NamePos": 67, + "NameEnd": 72 + }, + "Value": { + "Name": { + "Name": "toDateTime64", + "QuoteType": 1, + "NamePos": 75, + "NameEnd": 87 + }, + "Args": { + "LeftParenPos": 75, + "RightParenPos": 105, + "Args": [ + { + "LBracePos": 88, + "RBracePos": 101, + "Name": { + "Name": "since", + "QuoteType": 1, + "NamePos": 89, + "NameEnd": 94 + }, + "Type": { + "Name": { + "Name": "String", + "QuoteType": 1, + "NamePos": 95, + "NameEnd": 101 + } + } + }, + { + "NumPos": 104, + "NumEnd": 105, + "Literal": "3", + "Base": 10 + } + ] + } + } + } + ] + } + }, + "AliasPos": 111, + "Alias": { + "Name": "a", + "QuoteType": 1, + "NamePos": 111, + "NameEnd": 112 + } + }, + "HasFinal": false + }, + "StatementEnd": 112, + "SampleRatio": null, + "HasFinal": false + }, + "Right": { + "JoinPos": 113, + "Left": { + "Table": { + "TablePos": 125, + "TableEnd": 176, + "Alias": null, + "Expr": { + "Expr": { + "Name": { + "Name": "db.v", + "QuoteType": 3, + "NamePos": 125, + "NameEnd": 129 + }, + "Args": { + "LeftParenPos": 130, + "RightParenPos": 170, + "Args": [ + { + "NamePos": 131, + "Name": { + "Name": "min_id", + "QuoteType": 0, + "NamePos": 131, + "NameEnd": 137 + }, + "Value": { + "NumPos": 140, + "NumEnd": 141, + "Literal": "1", + "Base": 10 + } + }, + { + "NamePos": 143, + "Name": { + "Name": "ids", + "QuoteType": 0, + "NamePos": 143, + "NameEnd": 146 + }, + "Value": { + "LeftBracketPos": 149, + "RightBracketPos": 154, + "Items": { + "ListPos": 150, + "ListEnd": 154, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "NumPos": 150, + "NumEnd": 151, + "Literal": "1", + "Base": 10 + }, + "Alias": null + }, + { + "Expr": { + "NumPos": 153, + "NumEnd": 154, + "Literal": "2", + "Base": 10 + }, + "Alias": null + } + ] + } + } + }, + { + "NamePos": 157, + "Name": { + "Name": "since", + "QuoteType": 0, + "NamePos": 157, + "NameEnd": 162 + }, + "Value": { + "Name": { + "Name": "now", + "QuoteType": 1, + "NamePos": 165, + "NameEnd": 168 + }, + "Args": { + "LeftParenPos": 165, + "RightParenPos": 169, + "Args": [] + } + } + } + ] + } + }, + "AliasPos": 175, + "Alias": { + "Name": "b", + "QuoteType": 1, + "NamePos": 175, + "NameEnd": 176 + } + }, + "HasFinal": false + }, + "StatementEnd": 176, + "SampleRatio": null, + "HasFinal": false + }, + "Right": null, + "Modifiers": [ + "INNER", + "JOIN" + ], + "Constraints": { + "OnPos": 177, + "On": { + "ListPos": 180, + "ListEnd": 191, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LeftExpr": { + "Fields": [ + { + "Name": "a", + "QuoteType": 1, + "NamePos": 180, + "NameEnd": 181 + }, + { + "Name": "id", + "QuoteType": 1, + "NamePos": 182, + "NameEnd": 184 + } + ] + }, + "Operation": "=", + "RightExpr": { + "Fields": [ + { + "Name": "b", + "QuoteType": 1, + "NamePos": 187, + "NameEnd": 188 + }, + { + "Name": "id", + "QuoteType": 1, + "NamePos": 189, + "NameEnd": 191 + } + ] + }, + "HasGlobal": false, + "HasNot": false + }, + "Alias": null + } + ] + } + } + }, + "Modifiers": null, + "Constraints": null + } + }, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "UnionAll": null, + "UnionDistinct": null, + "Except": null, + "Intersect": null + } +] \ No newline at end of file diff --git a/parser/testdata/query/select_from_parameterized_view.sql b/parser/testdata/query/select_from_parameterized_view.sql new file mode 100644 index 0000000..4aa7615 --- /dev/null +++ b/parser/testdata/query/select_from_parameterized_view.sql @@ -0,0 +1,3 @@ +SELECT id +FROM db.v(min_id = {min_id:UInt64}, ids = [{id:UInt64}], since = toDateTime64({since:String}, 3)) AS a +INNER JOIN `db.v`(min_id = 1, ids = [1, 2], since = now()) AS b ON a.id = b.id