From 441387520bf087ee7b10a79ae8967b92dbd2535f Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Wed, 5 Aug 2026 13:37:18 +0200 Subject: [PATCH 1/2] Allow any expression as the value of CAST CAST read its value with the primary expression parser, so an operator stopped it: CAST(a + 1 AS String) and CAST(a + 1, 'String') both failed at the plus sign. Parse the value at the lowest precedence instead, which also accepts a leading minus as in CAST(-1 AS Int8). --- parser/parser_column.go | 2 +- .../query/format/beautify/select_cast.sql | 10 + parser/testdata/query/format/select_cast.sql | 7 + .../query/output/select_cast.sql.golden.json | 181 ++++++++++++++++++ parser/testdata/query/select_cast.sql | 5 +- 5 files changed, 203 insertions(+), 2 deletions(-) diff --git a/parser/parser_column.go b/parser/parser_column.go index e273d60c..32439d28 100644 --- a/parser/parser_column.go +++ b/parser/parser_column.go @@ -640,7 +640,7 @@ func (p *Parser) parseColumnCastExpr(pos Pos) (Expr, error) { return nil, err } - columnExpr, err := p.parseColumnExpr(p.Pos()) + columnExpr, err := p.parseExpr(p.Pos()) if err != nil { return nil, err } diff --git a/parser/testdata/query/format/beautify/select_cast.sql b/parser/testdata/query/format/beautify/select_cast.sql index 075439f4..e4a278a9 100644 --- a/parser/testdata/query/format/beautify/select_cast.sql +++ b/parser/testdata/query/format/beautify/select_cast.sql @@ -3,6 +3,10 @@ select cast(1 as Float64) as value; select cast(1, 'Float64') as value; select (1 as Float64) as value; select 1::Float64 as value; +select cast(a + 1 as String) as value; +select cast(a + 1, 'String') as value; +select cast(-1 as Int8) as value; + -- Beautify SQL: SELECT @@ -13,3 +17,9 @@ SELECT (1 AS Float64) AS value; SELECT 1::Float64 AS value; +SELECT + CAST(a + 1 AS String) AS value; +SELECT + CAST(a + 1, 'String') AS value; +SELECT + CAST(-1 AS Int8) AS value; diff --git a/parser/testdata/query/format/select_cast.sql b/parser/testdata/query/format/select_cast.sql index 523582c6..e79bcb35 100644 --- a/parser/testdata/query/format/select_cast.sql +++ b/parser/testdata/query/format/select_cast.sql @@ -3,9 +3,16 @@ select cast(1 as Float64) as value; select cast(1, 'Float64') as value; select (1 as Float64) as value; select 1::Float64 as value; +select cast(a + 1 as String) as value; +select cast(a + 1, 'String') as value; +select cast(-1 as Int8) as value; + -- Format SQL: SELECT CAST(1 AS Float64) AS value; SELECT CAST(1, 'Float64') AS value; SELECT (1 AS Float64) AS value; SELECT 1::Float64 AS value; +SELECT CAST(a + 1 AS String) AS value; +SELECT CAST(a + 1, 'String') AS value; +SELECT CAST(-1 AS Int8) AS value; diff --git a/parser/testdata/query/output/select_cast.sql.golden.json b/parser/testdata/query/output/select_cast.sql.golden.json index 0cdcd6f9..81a9910b 100644 --- a/parser/testdata/query/output/select_cast.sql.golden.json +++ b/parser/testdata/query/output/select_cast.sql.golden.json @@ -216,5 +216,186 @@ "UnionDistinct": null, "Except": null, "Intersect": null + }, + { + "SelectPos": 132, + "StatementEnd": 169, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "CastPos": 139, + "Expr": { + "LeftExpr": { + "Name": "a", + "QuoteType": 1, + "NamePos": 144, + "NameEnd": 145 + }, + "Operation": "+", + "RightExpr": { + "NumPos": 148, + "NumEnd": 149, + "Literal": "1", + "Base": 10 + }, + "HasGlobal": false, + "HasNot": false + }, + "Separator": "as", + "AsPos": 150, + "AsType": { + "Name": { + "Name": "String", + "QuoteType": 1, + "NamePos": 153, + "NameEnd": 159 + } + } + }, + "Modifiers": [], + "Alias": { + "Name": "value", + "QuoteType": 1, + "NamePos": 164, + "NameEnd": 169 + } + } + ], + "From": 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 + }, + { + "SelectPos": 171, + "StatementEnd": 208, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "CastPos": 178, + "Expr": { + "LeftExpr": { + "Name": "a", + "QuoteType": 1, + "NamePos": 183, + "NameEnd": 184 + }, + "Operation": "+", + "RightExpr": { + "NumPos": 187, + "NumEnd": 188, + "Literal": "1", + "Base": 10 + }, + "HasGlobal": false, + "HasNot": false + }, + "Separator": ",", + "AsPos": 188, + "AsType": { + "LiteralPos": 191, + "LiteralEnd": 197, + "Literal": "String" + } + }, + "Modifiers": [], + "Alias": { + "Name": "value", + "QuoteType": 1, + "NamePos": 203, + "NameEnd": 208 + } + } + ], + "From": 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 + }, + { + "SelectPos": 210, + "StatementEnd": 242, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "CastPos": 217, + "Expr": { + "NumPos": 222, + "NumEnd": 224, + "Literal": "-1", + "Base": 10 + }, + "Separator": "as", + "AsPos": 225, + "AsType": { + "Name": { + "Name": "Int8", + "QuoteType": 1, + "NamePos": 228, + "NameEnd": 232 + } + } + }, + "Modifiers": [], + "Alias": { + "Name": "value", + "QuoteType": 1, + "NamePos": 237, + "NameEnd": 242 + } + } + ], + "From": 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_cast.sql b/parser/testdata/query/select_cast.sql index 899e2691..6144d763 100644 --- a/parser/testdata/query/select_cast.sql +++ b/parser/testdata/query/select_cast.sql @@ -1,4 +1,7 @@ select cast(1 as Float64) as value; select cast(1, 'Float64') as value; select (1 as Float64) as value; -select 1::Float64 as value; \ No newline at end of file +select 1::Float64 as value; +select cast(a + 1 as String) as value; +select cast(a + 1, 'String') as value; +select cast(-1 as Int8) as value; From 1dc638d03624e29382b74d888900e1d4bf8d210f Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Wed, 5 Aug 2026 13:36:58 +0200 Subject: [PATCH 2/2] Report an error instead of crashing when a column value fails to parse The end position of the column was read from the expression before the error was checked, so any parse failure in DEFAULT, MATERIALIZED or ALIAS dereferenced a nil expression. --- parser/parser_table.go | 10 +++++++--- parser/parser_test.go | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/parser/parser_table.go b/parser/parser_table.go index 809d5882..10d3f8d6 100644 --- a/parser/parser_table.go +++ b/parser/parser_table.go @@ -698,20 +698,24 @@ func (p *Parser) parseTableColumnExpr(pos Pos) (*ColumnDef, error) { columnEnd = notNull.End() } + var valueExpr Expr switch { case p.tryConsumeKeywords(KeywordDefault): column.DefaultExpr, err = p.parseExpr(p.Pos()) - columnEnd = column.DefaultExpr.End() + valueExpr = column.DefaultExpr case p.tryConsumeKeywords(KeywordMaterialized): column.MaterializedExpr, err = p.parseExpr(p.Pos()) - columnEnd = column.MaterializedExpr.End() + valueExpr = column.MaterializedExpr case p.tryConsumeKeywords(KeywordAlias): column.AliasExpr, err = p.parseExpr(p.Pos()) - columnEnd = column.AliasExpr.End() + valueExpr = column.AliasExpr } if err != nil { return nil, err } + if valueExpr != nil { + columnEnd = valueExpr.End() + } comment, err := p.tryParseColumnComment(p.Pos()) if err != nil { diff --git a/parser/parser_test.go b/parser/parser_test.go index 268ca1d2..d670f597 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -188,6 +188,10 @@ func TestParser_InvalidSyntax(t *testing.T) { // CREATE TABLE and the next clause in ALTER TABLE. "CREATE TABLE t (a DateTime, b String) ENGINE = MergeTree ORDER BY a, b", "ALTER TABLE t MODIFY ORDER BY a, b", + // A broken column value must report an error, not stop the parser + "CREATE TABLE t (x String DEFAULT CAST(a +, 'String'))", + "CREATE TABLE t (x String MATERIALIZED a +)", + "CREATE TABLE t (x String ALIAS a +)", // Invalid ARRAY JOIN types (only ARRAY JOIN, LEFT ARRAY JOIN, and INNER ARRAY JOIN are valid) "SELECT * FROM t RIGHT ARRAY JOIN arr AS a", // RIGHT ARRAY JOIN not supported "SELECT * FROM t FULL ARRAY JOIN arr AS a", // FULL ARRAY JOIN not supported