From e1b235e1359d43473b32e550b66885ca56dcca91 Mon Sep 17 00:00:00 2001 From: Ostap Demkovych Date: Tue, 16 Jun 2026 13:38:58 +0300 Subject: [PATCH 1/2] add more unittests for $$ escaped literals; --- parser/format_test.go | 71 ++++++++++++++++++++++++++++++++++++++ parser/lexer_test.go | 80 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 143 insertions(+), 8 deletions(-) diff --git a/parser/format_test.go b/parser/format_test.go index 27d97ab6..edcce673 100644 --- a/parser/format_test.go +++ b/parser/format_test.go @@ -1,6 +1,7 @@ package parser import ( + "strings" "testing" "github.com/stretchr/testify/require" @@ -71,3 +72,73 @@ func TestFormatter_DefaultIndent(t *testing.T) { formatter := NewFormatter() require.Equal(t, " ", formatter.indent) } + +// Dollar-quoted `$$ … $$` literals are a lexer-only extension: the formatter +// normalises them to single-quoted form. This exercises the full pipeline +// (parse → format → parse → format) and asserts the second format is a fixed +// point — the canonical idempotence property already used elsewhere in the +// suite via validFormatSQL. +func TestFormatter_DollarQuotedRoundtrip(t *testing.T) { + testCases := []struct { + name string + input string + format string + }{ + { + name: "Simple text block", + input: "SELECT $$hello world$$", + format: "SELECT 'hello world';\n", + }, + { + name: "Numeric text block", + input: "SELECT $$123$$", + format: "SELECT '123';\n", + }, + { + name: "Empty text block", + input: "SELECT $$$$", + format: "SELECT '';\n", + }, + { + name: "Brace-wrapped placeholder is preserved verbatim", + input: "SELECT $$${variable:format}$$", + format: "SELECT '${variable:format}';\n", + }, + { + name: "Comment-like content is preserved verbatim", + input: "SELECT $$-- not a comment$$", + format: "SELECT '-- not a comment';\n", + }, + { + name: "Block-comment-like content is preserved verbatim", + input: "SELECT $$/* nor this */$$", + format: "SELECT '/* nor this */';\n", + }, + { + name: "Single dollar still flows into identifier path", + input: "SELECT $col FROM t", + format: "SELECT $col FROM t;\n", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + parser := NewParser(tc.input) + stmts, err := parser.ParseStmts() + require.NoError(t, err, "Failed to parse: %s", tc.input) + + var builder strings.Builder + for _, stmt := range stmts { + builder.WriteString(Format(stmt)) + builder.WriteByte(';') + builder.WriteByte('\n') + } + formatted := builder.String() + require.Equal(t, tc.format, formatted) + + // Second iteration must be a fixed point: re-parsing and + // re-formatting `formatted` yields `formatted` byte-for-byte. + validFormatSQL(t, formatted) + }) + } +} diff --git a/parser/lexer_test.go b/parser/lexer_test.go index 58043b0c..da8c9594 100644 --- a/parser/lexer_test.go +++ b/parser/lexer_test.go @@ -140,17 +140,81 @@ func TestConsumeString(t *testing.T) { } func TestConsumeTextBlock(t *testing.T) { - strs := []string{ - "$$hello world$$", - "$$123$$", - "$$${variable:format} and 'string' $$", - } - for _, s := range strs { - lexer := NewLexer(s) + t.Run("Simple, numeric, and embedded-content text blocks", func(t *testing.T) { + strs := []string{ + "$$hello world$$", + "$$123$$", + "$$${variable:format} and 'string' $$", + } + for _, s := range strs { + lexer := NewLexer(s) + err := lexer.consumeToken() + require.NoError(t, err) + require.Equal(t, TokenKindString, lexer.lastToken.Kind) + require.Equal(t, s[2:len(s)-2], lexer.lastToken.String) + require.True(t, lexer.isEOF()) + } + }) + + t.Run("Empty text block", func(t *testing.T) { + lexer := NewLexer("$$$$") err := lexer.consumeToken() require.NoError(t, err) require.Equal(t, TokenKindString, lexer.lastToken.Kind) - require.Equal(t, s[2:len(s)-2], lexer.lastToken.String) + require.Equal(t, "", lexer.lastToken.String) + require.True(t, lexer.isEOF()) + }) + + t.Run("Verbatim content: quotes, backslashes, comment-like sequences", func(t *testing.T) { + testCases := []struct { + input string + expected string + }{ + {`$$it's a 'test'$$`, `it's a 'test'`}, + {`$$a\nb\\c$$`, `a\nb\\c`}, + {`$$-- not a comment$$`, `-- not a comment`}, + {`$$/* nor this */$$`, `/* nor this */`}, + } + for _, tc := range testCases { + lexer := NewLexer(tc.input) + err := lexer.consumeToken() + require.NoError(t, err, "Failed to parse: %s", tc.input) + require.Equal(t, TokenKindString, lexer.lastToken.Kind) + require.Equal(t, tc.expected, lexer.lastToken.String) + require.True(t, lexer.isEOF()) + } + }) + + t.Run("Unterminated text block returns invalid string error", func(t *testing.T) { + unterminated := []string{ + "$$hello world", + "$$", + "$$$", + } + for _, s := range unterminated { + lexer := NewLexer(s) + err := lexer.consumeToken() + require.Error(t, err, "Expected error for unterminated input: %q", s) + require.Equal(t, "invalid string", err.Error()) + } + }) +} + +// A single `$` not immediately followed by another `$` must continue to flow +// into the identifier path — preserving bare `$ident` and brace-wrapped +// `${name}` / `${name:format}` placeholders. +func TestConsumeDollarIdent(t *testing.T) { + testCases := []string{ + "$col", + "${tbl}", + "${y:sqlstring}", + } + for _, s := range testCases { + lexer := NewLexer(s) + err := lexer.consumeToken() + require.NoError(t, err, "Failed to parse: %s", s) + require.Equal(t, TokenKindIdent, lexer.lastToken.Kind) + require.Equal(t, s, lexer.lastToken.String) require.True(t, lexer.isEOF()) } } From 4e145f7b2fcaaa85cc851638c849e0eed59eb47b Mon Sep 17 00:00:00 2001 From: Ostap Demkovych Date: Tue, 16 Jun 2026 18:28:35 +0300 Subject: [PATCH 2/2] Disambiguate SETTINGS placement around paren-wrapped set-op legs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track paren-wrapping on SelectQuery directly so the four placements of SETTINGS around a paren-bounded set-op leg become distinct, round-trippable AST shapes. SELECT 1 UNION ALL (SELECT 2 SETTINGS x=1) (per-leg) and SELECT 1 UNION ALL (SELECT 2) SETTINGS x=1 (chain-level on the leg) previously collapsed to byte-identical ASTs; chain-level SETTINGS on a paren-wrapped chain (e.g. (SELECT 1 UNION ALL SELECT 2) SETTINGS x=1) failed to parse at all. After this change each form parses to a distinct shape and re-formats to itself byte-for-byte. SelectQuery gains two additive fields — HasParen bool and OuterSettings *SettingsClause — populated by parseSelectQuery when it itself consumes the wrapping parens. The dispatcher now routes a leading `(` to parseSelectQuery so top-level wrapped chains parse end-to-end. parseCTEStmt is updated to consume the CTE-body parens at its own layer (mirroring parseSubQuery), so inner CTE SelectQueries keep HasParen=false and every pre-existing format/beautify golden stays byte-identical. Test suite gains TestParser_With_ChainSettingsDisambiguation plus four new fixtures (output/format/beautify goldens each) covering the four placements. JSON goldens regenerate uniformly with two added lines per SelectQuery rendering; format and beautify goldens are unchanged on every pre-existing fixture. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../.openspec.yaml | 2 + .../design.md | 185 ++++++++++++++++++ .../proposal.md | 75 +++++++ .../specs/paren-wrapped-select-query/spec.md | 177 +++++++++++++++++ .../tasks.md | 157 +++++++++++++++ openspec/config.yaml | 109 +++++++++-- .../specs/paren-wrapped-select-query/spec.md | 183 +++++++++++++++++ parser/ast.go | 13 ++ parser/format.go | 10 + parser/parser_query.go | 16 ++ parser/parser_table.go | 2 +- parser/parser_test.go | 109 +++++++++++ .../output/quantile_functions.sql.golden.json | 2 + .../ddl/output/bug_001.sql.golden.json | 2 + .../create_live_view_basic.sql.golden.json | 2 + ...te_materialized_view_basic.sql.golden.json | 2 + ...iew_with_comment_before_as.sql.golden.json | 2 + ...rialized_view_with_definer.sql.golden.json | 2 + ...ew_with_empty_table_schema.sql.golden.json | 4 + ...materialized_view_with_gcs.sql.golden.json | 2 + ...rialized_view_with_refresh.sql.golden.json | 2 + .../create_mv_with_not_op.sql.golden.json | 2 + .../create_mv_with_order_by.sql.golden.json | 4 + .../output/create_or_replace.sql.golden.json | 2 + .../output/create_view_basic.sql.golden.json | 2 + ..._view_on_cluster_with_uuid.sql.golden.json | 2 + .../create_view_with_comment.sql.golden.json | 2 + .../output/describe_subquery.sql.golden.json | 2 + .../alter_table_modify_query.sql.golden.json | 2 + ...insert_select_without_from.sql.golden.json | 2 + .../output/insert_with_select.sql.golden.json | 2 + .../select_with_paren_chain_settings.sql | 12 ++ .../select_with_paren_leg_no_settings.sql | 10 + .../select_with_paren_leg_settings_inside.sql | 12 ++ ...select_with_paren_leg_settings_outside.sql | 12 ++ .../select_with_paren_chain_settings.sql | 6 + .../select_with_paren_leg_no_settings.sql | 6 + .../select_with_paren_leg_settings_inside.sql | 6 + ...select_with_paren_leg_settings_outside.sql | 6 + .../access_tuple_with_dot.sql.golden.json | 4 + .../output/create_window_view.sql.golden.json | 2 + .../query_with_expr_compare.sql.golden.json | 4 + .../select_case_multiple_when.sql.golden.json | 2 + .../select_case_when_exists.sql.golden.json | 4 + .../select_case_when_regexp.sql.golden.json | 2 + .../query/output/select_cast.sql.golden.json | 8 + ...select_column_alias_string.sql.golden.json | 4 + .../output/select_concat_expr.sql.golden.json | 6 + .../select_except_bare_ident.sql.golden.json | 2 + ...ect_except_mixed_modifiers.sql.golden.json | 2 + .../query/output/select_expr.sql.golden.json | 2 + .../select_extract_with_regex.sql.golden.json | 2 + ...select_item_with_modifiers.sql.golden.json | 6 + .../output/select_json_type.sql.golden.json | 12 ++ ...select_keyword_alias_no_as.sql.golden.json | 2 + .../output/select_not_regexp.sql.golden.json | 2 + .../select_order_by_timestamp.sql.golden.json | 2 + ...t_order_by_with_fill_basic.sql.golden.json | 4 + ...order_by_with_fill_from_to.sql.golden.json | 4 + ...r_by_with_fill_interpolate.sql.golden.json | 4 + ...ill_interpolate_no_columns.sql.golden.json | 4 + ...der_by_with_fill_staleness.sql.golden.json | 2 + ...ct_order_by_with_fill_step.sql.golden.json | 4 + .../output/select_regexp.sql.golden.json | 2 + .../output/select_simple.sql.golden.json | 2 + .../select_simple_field_alias.sql.golden.json | 2 + ...select_simple_with_bracket.sql.golden.json | 2 + ...th_cte_with_column_aliases.sql.golden.json | 4 + ..._group_by_with_cube_totals.sql.golden.json | 2 + ...ct_simple_with_is_not_null.sql.golden.json | 2 + ...select_simple_with_is_null.sql.golden.json | 2 + .../select_simple_with_limit.sql.golden.json | 6 + ...ect_simple_with_top_clause.sql.golden.json | 2 + ...ct_simple_with_with_clause.sql.golden.json | 6 + ...able_alias_without_keyword.sql.golden.json | 2 + ..._table_function_with_query.sql.golden.json | 6 + .../select_when_condition.sql.golden.json | 2 + ...elect_window_comprehensive.sql.golden.json | 2 + .../output/select_window_cte.sql.golden.json | 6 + ...dow_keyword_name_in_parens.sql.golden.json | 2 + ...ect_window_named_in_parens.sql.golden.json | 2 + ...named_reference_extensions.sql.golden.json | 2 + .../select_window_params.sql.golden.json | 2 + .../select_with_bare_union.sql.golden.json | 4 + .../select_with_distinct.sql.golden.json | 2 + ...lect_with_distinct_keyword.sql.golden.json | 2 + ...distinct_on_dotted_columns.sql.golden.json | 2 + ...t_with_distinct_on_keyword.sql.golden.json | 2 + .../select_with_except_all.sql.golden.json | 4 + ...elect_with_except_distinct.sql.golden.json | 4 + .../select_with_group_by.sql.golden.json | 4 + .../select_with_intersect.sql.golden.json | 4 + ...t_with_intersect_modifiers.sql.golden.json | 6 + .../select_with_join_only.sql.golden.json | 2 + ...t_with_keyword_in_group_by.sql.golden.json | 2 + ...t_with_keyword_placeholder.sql.golden.json | 4 + .../select_with_left_join.sql.golden.json | 6 + ...ct_with_literal_table_name.sql.golden.json | 2 + ...multi_array_and_inner_join.sql.golden.json | 2 + ...lect_with_multi_array_join.sql.golden.json | 2 + .../select_with_multi_except.sql.golden.json | 6 + .../select_with_multi_join.sql.golden.json | 8 + ...ct_with_multi_line_comment.sql.golden.json | 2 + .../select_with_multi_union.sql.golden.json | 6 + ..._with_multi_union_distinct.sql.golden.json | 6 + .../select_with_number_field.sql.golden.json | 2 + ..._with_paren_chain_settings.sql.golden.json | 101 ++++++++++ ...with_paren_leg_no_settings.sql.golden.json | 81 ++++++++ ..._paren_leg_settings_inside.sql.golden.json | 101 ++++++++++ ...paren_leg_settings_outside.sql.golden.json | 101 ++++++++++ .../select_with_placeholder.sql.golden.json | 2 + ...elect_with_query_parameter.sql.golden.json | 4 + ...s_additional_table_filters.sql.golden.json | 14 ++ ...ct_with_single_quote_table.sql.golden.json | 2 + .../select_with_string_expr.sql.golden.json | 4 + ...select_with_union_distinct.sql.golden.json | 4 + ...select_with_union_settings.sql.golden.json | 4 + .../select_with_variable.sql.golden.json | 4 + ...elect_with_window_function.sql.golden.json | 2 + .../select_without_from_where.sql.golden.json | 4 + .../select_with_paren_chain_settings.sql | 1 + .../select_with_paren_leg_no_settings.sql | 1 + .../select_with_paren_leg_settings_inside.sql | 1 + ...select_with_paren_leg_settings_outside.sql | 1 + parser/walk.go | 3 + 125 files changed, 1798 insertions(+), 19 deletions(-) create mode 100644 openspec/changes/archive/2026-06-16-disambiguate-chain-settings/.openspec.yaml create mode 100644 openspec/changes/archive/2026-06-16-disambiguate-chain-settings/design.md create mode 100644 openspec/changes/archive/2026-06-16-disambiguate-chain-settings/proposal.md create mode 100644 openspec/changes/archive/2026-06-16-disambiguate-chain-settings/specs/paren-wrapped-select-query/spec.md create mode 100644 openspec/changes/archive/2026-06-16-disambiguate-chain-settings/tasks.md create mode 100644 openspec/specs/paren-wrapped-select-query/spec.md create mode 100644 parser/testdata/query/format/beautify/select_with_paren_chain_settings.sql create mode 100644 parser/testdata/query/format/beautify/select_with_paren_leg_no_settings.sql create mode 100644 parser/testdata/query/format/beautify/select_with_paren_leg_settings_inside.sql create mode 100644 parser/testdata/query/format/beautify/select_with_paren_leg_settings_outside.sql create mode 100644 parser/testdata/query/format/select_with_paren_chain_settings.sql create mode 100644 parser/testdata/query/format/select_with_paren_leg_no_settings.sql create mode 100644 parser/testdata/query/format/select_with_paren_leg_settings_inside.sql create mode 100644 parser/testdata/query/format/select_with_paren_leg_settings_outside.sql create mode 100644 parser/testdata/query/output/select_with_paren_chain_settings.sql.golden.json create mode 100644 parser/testdata/query/output/select_with_paren_leg_no_settings.sql.golden.json create mode 100644 parser/testdata/query/output/select_with_paren_leg_settings_inside.sql.golden.json create mode 100644 parser/testdata/query/output/select_with_paren_leg_settings_outside.sql.golden.json create mode 100644 parser/testdata/query/select_with_paren_chain_settings.sql create mode 100644 parser/testdata/query/select_with_paren_leg_no_settings.sql create mode 100644 parser/testdata/query/select_with_paren_leg_settings_inside.sql create mode 100644 parser/testdata/query/select_with_paren_leg_settings_outside.sql diff --git a/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/.openspec.yaml b/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/.openspec.yaml new file mode 100644 index 00000000..f617bd18 --- /dev/null +++ b/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-04 diff --git a/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/design.md b/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/design.md new file mode 100644 index 00000000..f735b318 --- /dev/null +++ b/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/design.md @@ -0,0 +1,185 @@ +## Context + +`SelectQuery` in `parser/ast.go` (around line 5119) is the AST node for a SELECT statement and the head of any UNION / EXCEPT / INTERSECT chain rooted on it. The set-op fields (`Union *SelectQuery`, `Except *SelectQuery`, `Intersect *SelectQuery` and their mode discriminators) were introduced by the prior `add-set-operator-modes` change; per-leg `SETTINGS` already works through `parseSelectStmt`'s `tryParseSettingsClause` call at `parser/parser_query.go:1151`. Per-leg SETTINGS lands on the leg's own `Settings` field. + +`parseSelectQuery` (`parser/parser_query.go:988-1030`) is the recursive entry point for any `SelectQuery`. Its flow: + +1. Optionally consume `(` (line 993 — `hasParen := …`). +2. `parseSelectStmt` — parses one SELECT body including its in-clause-list `SETTINGS`. +3. Set-op switch (lines 998–1023): consumes `UNION` / `EXCEPT` / `INTERSECT` (with optional `ALL`/`DISTINCT`), recurses into the RHS, stores it in the corresponding pointer + mode pair. +4. If `hasParen` was true, expects `)` (lines 1024–1028). +5. Returns the SelectQuery. + +`parseSubQuery` (`parser/parser_query.go:957-975`) is the parallel entry point for SELECTs used as subexpressions (FROM clauses, scalar subqueries, view bodies). It consumes its own optional `(` *before* delegating to `parseSelectQuery`, then expects its own `)` after. The two paren-consuming sites — `parseSubQuery:959` and `parseSelectQuery:993` — are mutually exclusive: when a SELECT appears as a subexpression, `parseSubQuery` consumes the parens and the inner `parseSelectQuery` sees only the keyword. When a SELECT appears as a set-op leg (no `SubQuery` wrapper), the parens go through `parseSelectQuery` itself. + +`parseStmt` (`parser/parser_table.go:1425-1482`) is the top-level statement dispatcher. Line 1437 routes `KeywordSelect` and `KeywordWith` to `parseSelectQuery`, but does **not** match `TokenKindLParen` — so a top-level `(SELECT …)` falls into the default-case error "unexpected token: `(`". + +`SubQuery` already carries `HasParen bool` (`parser/ast.go:5106`-ish): the in-repo precedent for parens-as-AST-state on a SELECT-wrapping node. + +`Format` for `SelectQuery` (in `parser/format.go`, the three set-op arms around line ~2342 after the `add-set-operator-modes` change) emits the body, then the set-op chain. It currently does not emit parens around any `SelectQuery` (parens are lost on round-trip when consumed by `parseSelectQuery` itself). + +## Goals / Non-Goals + +**Goals:** +- Distinguish in the AST between SETTINGS inside vs outside the closing `)` of a parenthesised set-op leg, so the four cases enumerated in the proposal each have distinct, round-trippable AST shapes. +- Accept `(SELECT … UNION … SELECT …) SETTINGS …` as a top-level statement — today it fails at the leading `(`. +- Accept `SELECT 1 UNION ALL (SELECT 2) SETTINGS …` — today the trailing SETTINGS errors at the statement boundary. +- Preserve byte-identical format and beautify goldens for every existing fixture (no `.sql` input currently produces a `parseSelectQuery`-consumed `(`, so no existing fixture's printed output should shift). +- Make the new fields additive on `SelectQuery`: existing consumers compile unchanged; JSON goldens gain exactly two new lines per `SelectQuery` rendering with no field renames or reorderings. + +**Non-Goals:** +- Retroactively reinterpreting `SELECT 1 UNION ALL SELECT 2 SETTINGS x=1` (no parens) as chain-level SETTINGS. ClickHouse's runtime may treat it as such, but this parser keeps the existing per-leg attachment and uses parens as the explicit disambiguator. +- Fixing the broader mixed-operator precedence question (INTERSECT-tighter-than-UNION, left-associativity for UNION/EXCEPT). That is `add-set-operator-modes`'s Decision 8 and is anticipated as a separate future change. +- Generalising paren tracking to every SELECT context. `SubQuery` already tracks its own parens; this change does NOT touch `SubQuery`. `WITH` CTEs, `INSERT … SELECT`, view bodies, and other SELECT-bearing constructs that go through `parseSubQuery` are unaffected. +- Adding `omitempty` JSON tags to suppress the new `"HasParen": false` / `"OuterSettings": null` lines on every SelectQuery. The repo convention is explicit rendering — see the archived `add-describe-settings-clause` Decision 4. + +## Decisions + +### Decision 1: Two additive fields on `SelectQuery`, not a wrapper node + +`SelectQuery` gains exactly two new fields: `HasParen bool` and `OuterSettings *SettingsClause`. No new AST type, no wrapper node, no visitor method. + +**Why:** Mirrors `SubQuery.HasParen` (in-repo precedent for paren-as-state on a SELECT-bearing node). Adding fields is strictly additive — every existing consumer that reflects on `SelectQuery` continues to compile; JSON goldens gain two new lines per rendering with no positional movement. A wrapper node (e.g. `ParenSelectQuery { Inner *SelectQuery; OuterSettings *SettingsClause }`) would force every consumer that handles `SelectQuery` to also handle the wrapper, and would force visitor changes — a much larger blast radius for a feature with a narrow surface area. + +**Alternative considered:** A dedicated `ParenSelectQuery` AST node carrying `Inner *SelectQuery` and `OuterSettings *SettingsClause`. **Rejected.** Cleaner type model on paper (the new state is on a dedicated node rather than overloaded on `SelectQuery`), but the downstream cost is high: every callsite that handles `SelectQuery` directly (FROM subqueries, CTEs, INSERT-SELECT, view bodies) would need to also handle the wrapper, or unwrap it at the boundary; the visitor protocol would need a new `VisitParenSelectQuery`; the JSON shape changes in a way that complicates diffs against pre-change goldens. The two-field additive shape is strictly less invasive for the same expressive power. + +### Decision 2: Name `HasParen` (mirror `SubQuery.HasParen`), not `WrappedInParens` / `OpenParen` + +The existing `SubQuery` carries the boolean `HasParen`. Reusing that exact name on `SelectQuery` keeps the convention uniform — a reader who already understands `SubQuery.HasParen` understands `SelectQuery.HasParen` immediately. + +**Alternative considered:** `WrappedInParens` (more descriptive) or `OpenParen` (terser, mirrors a token name). **Rejected** in favour of the in-repo precedent. A descriptive doc comment on the field carries the intent without requiring a name divergence. + +### Decision 3: `parseSubQuery` consumes parens *before* `parseSelectQuery` is called — so the inner `SelectQuery.HasParen` stays false in subquery contexts + +This is the load-bearing invariant for the format/beautify-golden stability claim. `parseSubQuery` (`parser/parser_query.go:957-975`) consumes its optional `(` at line 959 *before* calling `parseSelectQuery` at line 961. So when a SELECT appears as a FROM subquery, scalar subquery, INSERT-SELECT body, view body, or any other `SubQuery`-wrapped position, the `(` is consumed by the wrapper and `parseSelectQuery` sees `SELECT`/`WITH` next — its `hasParen` is `false`, the new `SelectQuery.HasParen` stays `false`, and the formatter emits no extra parens for this node. The `SubQuery` wrapper still emits its own parens via `SubQuery.HasParen` (unchanged from today). + +When a SELECT appears as a set-op leg (`SELECT 1 UNION ALL (SELECT 2)`), the set-op recursion at `parser/parser_query.go:1001` calls `parseSelectQuery` directly without a `parseSubQuery` wrapper. The `(` is consumed by `parseSelectQuery` itself at line 993, `HasParen` is set to `true`, and the formatter emits the parens for this leg. + +**Implication for existing fixtures:** the only `.sql` fixture in the repo whose input contains `(SELECT … UNION … SELECT …)` is `parser/testdata/query/compatible/1_stateful/00080_array_join_and_union.sql` — `SELECT count() FROM (SELECT … UNION ALL SELECT …);`. Here the outer parens belong to the FROM-clause subquery (`parseSubQuery`-consumed), so the inner `SelectQuery.HasParen` stays false. Verified by grep + manual inspection; this is the regression guard. + +### Decision 4: Trailing SETTINGS is consumed *after* the matching `)`, inside the `if hasParen` block + +The new parser logic in `parseSelectQuery` lives strictly *inside* the existing `if hasParen { … }` block (currently lines 1024–1028), immediately after the `expectTokenKind(TokenKindRParen)`. Shape: + +```go +if hasParen { + if err := p.expectTokenKind(TokenKindRParen); err != nil { + return nil, err + } + selectStmt.HasParen = true + outerSettings, err := p.tryParseSettingsClause(p.Pos()) + if err != nil { + return nil, err + } + if outerSettings != nil { + selectStmt.OuterSettings = outerSettings + } +} +``` + +**Why this placement, not after the function or outside the `if`:** +- Gating on `hasParen` enforces the per-node invariant: `OuterSettings` non-nil implies `HasParen` true. A nil-HasParen-but-non-nil-OuterSettings AST state is unreachable. +- `tryParseSettingsClause` is exactly the helper used inside `parseSelectStmt` for the in-body `SETTINGS`, so the parse behaviour is symmetric — same lexer, same comma handling, same key=value grammar. +- The set-op recursion at the inner level has already returned by this point. The inner leg has finished parsing (`)` consumed). The trailing SETTINGS naturally belongs to the *current* `parseSelectQuery` frame, not to the inner. + +**Where the SETTINGS lands semantically.** In `SELECT 1 UNION ALL (SELECT 2) SETTINGS x=1`: +- Outer `parseSelectQuery` enters with `hasParen=false`, parses SELECT 1 (no SETTINGS), consumes UNION ALL, recurses. +- Inner `parseSelectQuery` enters with `hasParen=true`, parses SELECT 2 (no SETTINGS), no set-op, consumes `)`, parses trailing SETTINGS into `inner.OuterSettings`, sets `inner.HasParen=true`, returns. +- Outer assigns `outer.Union = inner`, outer has `hasParen=false` so the new block does not fire, returns. + +So `OuterSettings` lands on the *inner* `SelectQuery` — the one whose parens were consumed. Semantically a consumer interprets this as "trailing SETTINGS for the parens-wrapped expression rooted at this node, i.e. for the leg as wrapped". The leg-shape SETTINGS reads naturally as "applied to this paren-bounded sub-expression", which is what the SQL text actually expresses. + +**Alternative considered:** bubble the trailing SETTINGS up to the chain head (`outer.OuterSettings` in the example above). **Rejected for this scope.** Bubbling would more closely match a "chain-level" interpretation, but it requires a second post-parse pass over the chain, complicates the field's semantics (it would no longer be a local property of the parens-bounded node), and gains nothing for round-trip correctness — the formatter emits SETTINGS where the AST node sits, and the AST node sits where the SQL text places it. The local-placement rule is what the SQL text shape supports. + +### Decision 5: `parseStmt` accepts `TokenKindLParen` as a SELECT dispatch path + +`parser/parser_table.go:1437` is extended from: +```go +case p.matchKeyword(KeywordSelect), p.matchKeyword(KeywordWith): + expr, err = p.parseSelectQuery(pos) +``` +to: +```go +case p.matchKeyword(KeywordSelect), p.matchKeyword(KeywordWith), p.matchTokenKind(TokenKindLParen): + expr, err = p.parseSelectQuery(pos) +``` + +**Why:** Without this, `(SELECT 1 UNION ALL SELECT 2) SETTINGS x=1` errors at the leading `(` with "unexpected token: `(`" (verified by probe). `parseSelectQuery` is already capable of consuming a leading `(` (line 989's match check, line 993's consumption); the dispatcher just needs to route `(`-starting input there. There is no other top-level statement that legitimately starts with `(`, so the dispatch is unambiguous. + +**Side effect.** A top-level bare `(SELECT 1)` (no chain, no SETTINGS) — which is rejected today — will parse after this change. The result is a `SelectQuery` with `HasParen=true`, `OuterSettings=nil`, no chain. The formatter will emit `(SELECT 1)`. This is intentional: the parser is now able to represent parens-wrapped statements end-to-end. No existing fixture starts with `(`, so no golden drift. + +### Decision 6: Formatter emits parens around the *entire* SelectQuery (body + chain), then OuterSettings after `)` + +`SelectQuery.FormatSQL` is restructured so that when `s.HasParen == true`: +1. Emit `(`. +2. Emit the SELECT body and any set-op chain (the existing logic, unchanged in shape). +3. Emit `)`. +4. If `s.OuterSettings != nil`, emit it after the `)`. + +When `s.HasParen == false`, the format path is unchanged — no parens, no `OuterSettings` consideration (the per-node invariant guarantees `OuterSettings` is nil in this case). + +**Why "around the entire SelectQuery, not just the body":** This matches what `parseSelectQuery` actually parsed. The opening `(` was consumed before the body, and the closing `)` was expected after the set-op recursion completed. So for `(SELECT 1 UNION ALL SELECT 2) SETTINGS x=1`: +- Outer `parseSelectQuery` enters with `hasParen=true`, parses SELECT 1, consumes UNION ALL, recurses for SELECT 2 (inner.hasParen=false), assigns `outer.Union = inner`, consumes `)`, parses trailing SETTINGS into `outer.OuterSettings`, sets `outer.HasParen=true`. +- Formatter emits: `(` + body of outer + ` UNION ALL ` + body of inner + `)` + ` SETTINGS …`. → `(SELECT 1 UNION ALL SELECT 2) SETTINGS x=1`. ✓ + +**Beautified output.** The beautified formatter places set-op keywords on their own line. The opening `(` precedes the first line of the outer's body; the closing `)` follows the last line of the inner's body; the SETTINGS lands on its own line after `)` (matching the existing beautified shape for SETTINGS inside a SelectQuery body). The three new beautify goldens lock this in. + +### Decision 7: `Accept` and `Walk` traverse `OuterSettings` after the set-op block + +`(*SelectQuery).Accept` is extended to call `s.OuterSettings.Accept(visitor)` (gated on non-nil) *after* the set-op traversal blocks (the existing `Union` / `Except` / `Intersect` traversal blocks from `add-set-operator-modes`) and *before* `visitor.VisitSelectQuery(s)`. The traversal order mirrors lexical order: body → set-op chain → trailing SETTINGS → outer visitor call. + +`Walk`'s `SelectQuery` case in `parser/walk.go` gains a parallel `if !Walk(n.OuterSettings, fn) { return false }` immediately after the existing `Walk(n.Settings, fn)` call (or, more precisely, immediately after the set-op `Walk` calls — wherever the existing `Settings` walk sits in the function, the new one sits one step further into the lexical order). + +**Why this order:** Visitors that consume the AST top-down expect the chain to be traversed before any post-chain trailing clauses. Putting `OuterSettings` after the set-op blocks matches the "trailing clause" mental model. + +### Decision 8: Golden regeneration footprint — explicit across all three golden families + +This is the regen contract for code review. Three families of golden files exist under `parser/testdata/`: JSON ASTs (`**/output/*.sql.golden.json`), compact-formatted SQL (`**/format/*.sql`), and beautified SQL (`**/format/beautify/*.sql`). The expected diff against each family after this change is precisely characterised below. + +**JSON family — mechanical two-line addition per SelectQuery rendering.** Every pre-existing JSON golden whose AST contains a `SelectQuery` gains exactly two added lines at each `SelectQuery` rendering: `"HasParen": false,` and `"OuterSettings": null`. No line is removed; no positional movement of any other field. Approximate scope: ~90 SelectQuery-containing fixtures with 1–3 renderings each — total addition in the low hundreds of lines. + +**Format family — byte-identical for every pre-existing fixture.** The formatter only emits `(...)` and trailing `OuterSettings` when `HasParen == true`. By D3, every pre-existing fixture's parser-path produces `HasParen == false` (parens that appear today are consumed by `parseSubQuery`, not by `parseSelectQuery` itself). So no existing format golden shifts. + +**Beautify family — byte-identical for every pre-existing fixture.** Same reasoning as the format family. The beautify path threads the same writer and the new branch is gated on the same `HasParen` flag. + +**New fixtures.** Four new `.sql` inputs under `parser/testdata/query/` each carry three goldens (`output/`, `format/`, `format/beautify/`) → 4 inputs + 12 new goldens. The JSON goldens render populated `"HasParen": true` and either `Settings`-or-`OuterSettings`-populated subtrees at the affected nodes. + +**Regression guard for the byte-identical claim.** `parser/testdata/query/compatible/1_stateful/00080_array_join_and_union.sql` is the canonical existing fixture whose input contains `(SELECT … UNION … SELECT …)` — and crucially those parens are FROM-subquery-wrapped (`parseSubQuery`-consumed), so the inner `SelectQuery.HasParen` stays `false` post-change. Its format and beautify goldens MUST match byte-for-byte; any drift indicates D3's parser-path invariant has broken. + +**Workflow.** Land the AST + parser + formatter + walk changes together (partial state fails to compile or fails round-trip). Then: + +1. `go test ./parser/... -run 'TestParser_ParseStatements' -count=1 -update` to regenerate JSON goldens. +2. `git diff --stat parser/testdata` — expected: only files under `**/output/*.sql.golden.json` change, no `.sql` under `**/format/` or `**/format/beautify/` changes. +3. Spot-check three goldens: one no-parens fixture (pure two-line addition), one set-op fixture with SETTINGS (two lines per leg), the FROM-subquery `00080_…` fixture (the regression guard; inner-leg `"HasParen"` must be `false`). +4. `go test ./parser/... -run 'TestParser_Format|TestParser_FormatBeautify' -count=1` *without* `-update`. This MUST pass — no `-update` is the proof the byte-identical claim holds. +5. Add the four new fixtures and generate their three goldens each, then re-run all three suites without `-update`. + +### Decision 9: Per-node invariant is enforced by the parser, not the type system + +Go's type system can't natively express "`OuterSettings` non-nil ⇒ `HasParen` true". The invariant is enforced operationally: the only code path that writes `OuterSettings` is the new block inside `if hasParen { … }` in `parseSelectQuery`, which also writes `HasParen = true` on the same SelectQuery. Manual construction outside the parser (e.g. in tests) could in principle violate the invariant; this is acceptable — the parser is the only normative producer of `SelectQuery` instances in this codebase, and downstream consumers reading the AST should not need to handle the violated state. + +**Why not a constructor or validator:** Overkill for two fields. The existing AST uses bare struct types throughout (`SelectQuery`, `SubQuery`, etc.) — adding a constructor for just these two would diverge from convention without commensurate benefit. + +### Decision 10: No interaction with `set-operator-modes` spec — it stays unchanged + +The set-operator-modes spec (introduced in `add-set-operator-modes`) covers Union/Except/Intersect parsing, modes, formatter shape, and walk/accept traversal. Its scenarios that mention SETTINGS (e.g. "Bare UNION combined with per-leg SETTINGS", "INTERSECT ALL with trailing SETTINGS on the right leg") are about *no-parens* per-leg SETTINGS attached to the inner SelectQuery's `Settings` field. Those scenarios remain byte-for-byte true after this change — the new fields are additive and don't displace `Settings`. So `set-operator-modes` is not modified; the new behaviour lives entirely in the new `paren-wrapped-select-query` spec. + +## Risks / Trade-offs + +- **Risk: an existing fixture's input contains parens that turn out to be `parseSelectQuery`-consumed, not `parseSubQuery`-consumed, and its format golden drifts.** *Mitigation:* Decision 3 is the load-bearing invariant. Before implementation, grep for `\(\s*SELECT` across `parser/testdata/` and inspect each match's parser path (FROM subquery, scalar, CTE, INSERT-SELECT all go through `parseSubQuery`; only direct set-op-leg parens go through `parseSelectQuery`). The known case is `parser/testdata/query/compatible/1_stateful/00080_array_join_and_union.sql`, which is FROM-clause-wrapped and safe. If any other case surfaces, it's either (a) already a set-op-leg case (in which case the format change is *correct* — the parens were silently dropped before and should now be preserved) or (b) reveals a parser-path assumption to investigate before merging. +- **Risk: `parseStmt` accepting `TokenKindLParen` shadows some other statement form that starts with `(`.** *Mitigation:* No other statement form starts with `(` — DDL, INSERT, USE, SET, SETTINGS, SYSTEM, OPTIMIZE, CHECK, EXPLAIN, GRANT, SHOW, DESC, SELECT, WITH all start with their respective keywords. The `(` dispatch is unambiguous. Verified by inspection of `parser/parser_table.go:1428-1467`. +- **Risk: `parseSubQuery`-vs-`parseSelectQuery` paren-consumption boundary is subtle and easy to break by refactoring.** *Mitigation:* the spec scenario "SubQuery wraps SelectQuery and the inner's HasParen stays false" is the regression test; any future refactor that violates the invariant will break the corresponding existing fixture's format golden, surfacing the drift. +- **Risk: visitor traversal order change for the new `OuterSettings` node could surprise consumers.** *Mitigation:* the order chosen (set-op traversal → OuterSettings → visitor call) matches lexical order; no existing visitor depends on the new node (it didn't exist before); documented in the spec. +- **Trade-off: parens-tracking is now state on `SelectQuery` itself, not on a wrapper.** Future structural cleanups (e.g. introducing a chain-AST type per `add-set-operator-modes` Decision 8) inherit `HasParen` as a SelectQuery property and must migrate it. Acceptable — that future change is already known to require an AST shape rework; carrying `HasParen` along is a known small detail. + +## Migration Plan + +Single commit, no external dependencies. AST field additions, parser changes, formatter changes, walk update, and the new `parseStmt` dispatch all land together — partial state would either fail to compile or fail tests (the new fields are populated by the parser but not yet emitted by the formatter would cause round-trip mismatches). The ~90 SelectQuery-containing JSON goldens are regenerated in the same commit (uniform two-line addition per rendering); four new `.sql` fixtures and twelve new goldens (3 per fixture × 4 fixtures) are committed alongside. Format and beautify goldens for every existing fixture remain byte-identical — this is the visible regression guard. + +Rollback is `git revert`. No data or config involvement; no runtime semantics changed for SQL that previously parsed. + +After this change ships, the four new fixtures plus the new inline test serve as the executable specification of the disambiguation. Any future change touching `parseSelectQuery`'s paren-consumption path or the formatter's `SelectQuery` arm must keep these passing. + +## Open Questions + +None blocking. The semantics of `OuterSettings` on a non-head leg ("trailing SETTINGS for the parens-wrapped sub-expression rooted at this node") is a local property well-defined by the parser flow; downstream consumers that need "chain-level SETTINGS" can read `OuterSettings` off whichever node was the top of their chain. If a future need emerges to bubble OuterSettings to the chain head as a structural property of the chain, it can be added as a separate change without invalidating this one. diff --git a/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/proposal.md b/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/proposal.md new file mode 100644 index 00000000..b3cc1776 --- /dev/null +++ b/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/proposal.md @@ -0,0 +1,75 @@ +## Why + +When a UNION / EXCEPT / INTERSECT chain ends in a parenthesised SELECT and a `SETTINGS` clause trails the chain, the parser today cannot tell whether the SETTINGS was authored *inside* the parens (per-leg, scoped to the subquery) or *outside* the parens (chain-level, scoped to the whole set-op expression). The current AST drops the parens entirely and folds both forms onto the inner leg's `Settings` field, so: + +- `SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads=1)` — author intent: per-leg +- `SELECT 1 UNION ALL SELECT 2 SETTINGS max_threads=1` — no parens, current per-leg attachment + +produce **byte-identical** ASTs and round-trip to the same SQL text. Worse, the third form ClickHouse accepts: + +- `SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads=1` — author intent: chain-level + +**fails to parse today** with ` or ';' was expected, but got: "SETTINGS"`, and `(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads=1` fails at the leading `(`. Tools that round-trip parenthesised SQL through this parser silently lose author intent for the paren-bounded form, and chain-level SETTINGS on a paren-wrapped chain cannot be expressed at all. + +This change disambiguates the placements end-to-end: the AST records whether a `SelectQuery` was wrapped in parens, and trailing SETTINGS after the closing `)` is captured in a separate field distinct from the inner SELECT's own `Settings`. + +## What Changes + +- **The four placements of SETTINGS around a paren-wrapped set-op leg become distinguishable in the AST and round-trip-stable through the formatter.** `SELECT 1 UNION ALL (SELECT 2 SETTINGS x=1)` and `SELECT 1 UNION ALL (SELECT 2) SETTINGS x=1` parse to different shapes; each re-formats to itself byte-for-byte. + +- **`SelectQuery` gains two additive fields** as part of its exported AST surface: + - `HasParen bool` — true when the parser consumed wrapping parens around this `SelectQuery` itself (mirrors the existing `SubQuery.HasParen` flag for the existing in-repo precedent). + - `OuterSettings *SettingsClause` — the optional `SETTINGS` clause appearing *after* the closing `)`, distinct from the existing `Settings` field (which continues to mean "SETTINGS inside the SELECT body"). + +- **Per-node invariant**: `OuterSettings != nil` ⇒ `HasParen == true`. When `HasParen` is false, `OuterSettings` is nil. + +- **`(SELECT … UNION … SELECT …) SETTINGS …` parses as a top-level statement.** Today this fails at the leading `(`; after the change it produces a `SelectQuery` with `HasParen=true` and `OuterSettings` populated. As a side effect, a bare top-level `(SELECT 1)` also parses (today: "unexpected token: `(`"). + +- **`SELECT 1 UNION ALL (SELECT 2) SETTINGS …` parses.** Today the trailing SETTINGS errors at the statement boundary; after the change it lands on the inner leg's `OuterSettings`. + +- **The no-parens form is unchanged.** `SELECT 1 UNION ALL SELECT 2 SETTINGS x=1` continues to attach the SETTINGS to the inner leg's `Settings` field (`HasParen=false`, `OuterSettings=nil`). This change does NOT retroactively reinterpret no-parens trailing SETTINGS as chain-level; parens are the explicit disambiguator. + +- **Subquery contexts are unchanged.** `FROM (SELECT …)`, scalar subqueries, view bodies, and INSERT-SELECT keep their existing parse shape — the wrapping `SubQuery.HasParen` continues to drive paren emission there, and the inner `SelectQuery.HasParen` stays false in those contexts. + +- **`Format`, `Beautify`, `Accept`, and `Walk` all honour the new fields.** `Format(stmt)` emits `(…)` around a `HasParen=true` SelectQuery and appends the `OuterSettings` clause after the `)`. The visitor and walker traverse `OuterSettings` in lexical order (after the set-op chain, before the outer visitor call). + +## Capabilities + +### New Capabilities +- `paren-wrapped-select-query`: Track whether a `SelectQuery` was parsed as a parenthesised expression and, when so, capture an optional trailing `SETTINGS` clause that appears *after* the closing `)`. Disambiguates "SETTINGS inside the parens" (per-leg) from "SETTINGS outside the parens" (chain-level) in set-op chains, and unlocks the top-level `(chain) SETTINGS …` form. + +### Modified Capabilities + + +## Impact + +- **AST API compatibility**: additive only. No existing exported field is renamed, removed, or reordered. Code that depends on the current `SelectQuery` surface compiles unchanged. Code that switch-cases on the AST gains two new fields it can ignore. + +- **JSON-golden footprint**: every committed JSON golden whose AST contains a `SelectQuery` (approximately 90 fixtures across `parser/testdata/`) gains exactly two added lines per `SelectQuery` rendering: `"HasParen": false,` and `"OuterSettings": null`. No field is removed; no positional movement of any other field. The four new fixtures additionally render populated values on the affected nodes. + +- **Format and beautify golden footprint**: every existing fixture's `format/` and `format/beautify/` golden remains **byte-identical**. This is the strong claim. It depends on the parser-side boundary that subquery parens are consumed by the subquery wrapper (so the inner `SelectQuery.HasParen` stays false), and is locked in by the existing `compatible/1_stateful/00080_array_join_and_union.sql` fixture — a `SELECT count() FROM (SELECT … UNION ALL SELECT …)` — which acts as the regression guard. + +- **New `.sql` fixtures** under `parser/testdata/query/`, one per placement plus the no-SETTINGS round-trip case: + - `select_with_paren_leg_settings_inside.sql` — `SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads = 1)` (per-leg). + - `select_with_paren_leg_settings_outside.sql` — `SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads = 1` (chain-level on the leg). + - `select_with_paren_chain_settings.sql` — `(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads = 1` (top-level wrapped chain). + - `select_with_paren_leg_no_settings.sql` — `SELECT 1 UNION ALL (SELECT 2)` (parens preserved with no SETTINGS). + Each carries the standard three goldens (`output/`, `format/`, `format/beautify/`). + +- **New inline test** `TestParser_With_ChainSettingsDisambiguation` in `parser/parser_test.go` parses each of the four new SQLs plus a "both placements coexist" SQL, and asserts the expected `HasParen` / `Settings` / `OuterSettings` shape on the inner and outer `SelectQuery`. Today's parser fails on three of the five inputs; after the change all five PASS. + +- **Round-trip property**: `Format(Parse(sql)) == Format(Parse(Format(Parse(sql))))` continues to hold for every fixture, including the four new ones. + +- **No dependencies added.** No lexer changes. No new visitor method. No new keyword. + +- **Rollback**: `git revert`. The fields are additive; reverting the commit restores the pre-change JSON-golden shape mechanically. + +- **Performance posture**: one additional optional `SettingsClause` parse attempt at the close of any paren-wrapped `SelectQuery` (a single keyword lookahead in the common no-trailing-SETTINGS case). No hot-path concern. + +- **Out of scope**: retroactively reinterpreting `SELECT 1 UNION ALL SELECT 2 SETTINGS x=1` (no parens) as chain-level SETTINGS; mixed-operator precedence (already a known limitation from the prior `add-set-operator-modes` change); generalising paren tracking to constructs already handled by `SubQuery`. diff --git a/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/specs/paren-wrapped-select-query/spec.md b/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/specs/paren-wrapped-select-query/spec.md new file mode 100644 index 00000000..638e58b3 --- /dev/null +++ b/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/specs/paren-wrapped-select-query/spec.md @@ -0,0 +1,177 @@ +## ADDED Requirements + +### Requirement: `SelectQuery` SHALL expose `HasParen` and `OuterSettings` on its exported AST surface + +The `SelectQuery` struct SHALL expose two additional optional fields: + +- `HasParen bool` — true when the parser consumed wrapping parens around this `SelectQuery` itself (as opposed to parens consumed by a surrounding `SubQuery` wrapper). +- `OuterSettings *SettingsClause` — the optional `SETTINGS` clause that appears immediately after the closing `)` of a paren-wrapped `SelectQuery`. Distinct from the existing `Settings` field, which continues to mean "SETTINGS inside the SELECT body's clause list". + +The placement of the new fields in the struct SHALL fall between the existing `Settings` field and the existing `Format` field, so the JSON-golden diff per `SelectQuery` rendering is two contiguous added lines (see D8 in design.md). + +#### Scenario: Exported AST surface gains the two new fields +- **WHEN** a Go consumer reflects on `parser.SelectQuery` after this change +- **THEN** the struct has fields `HasParen bool` and `OuterSettings *SettingsClause` in addition to every pre-existing field, with no pre-existing field renamed, removed, or reordered + +#### Scenario: Default zero values for any SelectQuery without parser-consumed parens +- **WHEN** any SQL that does not include parens directly around a `SelectQuery` is parsed (e.g. `SELECT 1`, `SELECT 1 UNION ALL SELECT 2`, `SELECT * FROM (SELECT 1)`) +- **THEN** every resulting `*SelectQuery` has `HasParen == false` AND `OuterSettings == nil` + +### Requirement: Parsing a parenthesised `SelectQuery` SHALL set `HasParen` on that node + +When the parser consumes a matching `(` … `)` pair around a `SelectQuery` (and any set-op chain rooted on it), the resulting `*SelectQuery` SHALL have `HasParen == true`. When the parens belong to a surrounding `SubQuery` wrapper (FROM-clause subquery, scalar subquery, view body, INSERT-SELECT body, etc.), the inner `*SelectQuery.HasParen` SHALL stay `false` and the surrounding `SubQuery.HasParen` SHALL be `true` (its pre-existing behaviour, unchanged). + +#### Scenario: Paren-wrapped set-op leg has HasParen true +- **WHEN** `SELECT 1 UNION ALL (SELECT 2)` is parsed +- **THEN** `ParseStmts` returns no error AND the outer `*SelectQuery.Union` is non-nil AND `outer.Union.HasParen == true` AND `outer.HasParen == false` + +#### Scenario: Top-level paren-wrapped chain has HasParen true on the head +- **WHEN** `(SELECT 1 UNION ALL SELECT 2)` is parsed +- **THEN** `ParseStmts` returns no error AND the resulting `*SelectQuery` has `HasParen == true` AND its `Union` is non-nil AND `outer.Union.HasParen == false` + +#### Scenario: SubQuery-wrapped SELECT keeps the inner HasParen false +- **WHEN** `SELECT * FROM (SELECT 1 UNION ALL SELECT 2)` is parsed +- **THEN** `ParseStmts` returns no error AND the FROM-clause `*SubQuery` has its own `HasParen == true` AND the wrapped `*SelectQuery` (`SubQuery.Select`) has `HasParen == false` + +### Requirement: Trailing `SETTINGS` after `)` SHALL land in `OuterSettings` + +When a `SETTINGS` clause appears immediately after the closing `)` of a paren-wrapped `SelectQuery`, the parser SHALL attach it to that node's `OuterSettings` field. SETTINGS appearing *before* the `)` (inside the parens, as part of the SELECT body) continues to attach to the node's `Settings` field — unchanged from today. + +#### Scenario: SETTINGS inside parens attaches to the leg's `Settings` +- **WHEN** `SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads=1)` is parsed +- **THEN** `outer.Union.HasParen == true` AND `outer.Union.Settings` is non-nil AND `outer.Union.OuterSettings == nil` + +#### Scenario: SETTINGS outside parens attaches to the leg's `OuterSettings` +- **WHEN** `SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads=1` is parsed +- **THEN** `outer.Union.HasParen == true` AND `outer.Union.Settings == nil` AND `outer.Union.OuterSettings` is non-nil + +#### Scenario: Both SETTINGS placements coexist on the same paren-wrapped leg +- **WHEN** `SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads=1) SETTINGS max_threads=2` is parsed +- **THEN** `outer.Union.HasParen == true` AND `outer.Union.Settings` is non-nil (the inner `max_threads=1`) AND `outer.Union.OuterSettings` is non-nil (the trailing `max_threads=2`) + +#### Scenario: Trailing SETTINGS on a top-level wrapped chain attaches to the head's `OuterSettings` +- **WHEN** `(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads=1` is parsed +- **THEN** the outer `*SelectQuery.HasParen == true` AND `outer.OuterSettings` is non-nil AND `outer.Union` is non-nil AND `outer.Union.HasParen == false` AND `outer.Union.OuterSettings == nil` + +#### Scenario: No-parens trailing SETTINGS continues to attach to the inner `Settings` +- **WHEN** `SELECT 1 UNION ALL SELECT 2 SETTINGS max_threads=1` is parsed +- **THEN** `outer.Union` is non-nil AND `outer.Union.Settings` is non-nil AND `outer.Union.HasParen == false` AND `outer.Union.OuterSettings == nil` AND `outer.Settings == nil` + +### Requirement: `OuterSettings` non-nil SHALL imply `HasParen` true (per-node invariant) + +For every `*SelectQuery` produced by `ParseStmts`, the invariant `OuterSettings != nil ⇒ HasParen == true` SHALL hold. Equivalently: a node with `HasParen == false` MUST have `OuterSettings == nil`. + +#### Scenario: Invariant holds across all four new fixtures +- **WHEN** each of `select_with_paren_leg_settings_inside.sql`, `select_with_paren_leg_settings_outside.sql`, `select_with_paren_chain_settings.sql`, `select_with_paren_leg_no_settings.sql` is parsed +- **THEN** every `*SelectQuery` node reachable from the result satisfies `OuterSettings == nil OR HasParen == true` + +### Requirement: Top-level paren-wrapped SELECT statements SHALL parse + +`ParseStmts` SHALL accept SQL whose first non-whitespace token is `(` and whose remainder forms a parenthesised `SelectQuery` (with or without a set-op chain inside, with or without a trailing `SETTINGS` clause). + +#### Scenario: Top-level paren-wrapped chain with trailing SETTINGS parses +- **WHEN** `(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads=1` is parsed by `ParseStmts` +- **THEN** no error is returned AND the resulting statement is a `*SelectQuery` with `HasParen == true` AND `OuterSettings` non-nil + +#### Scenario: Bare top-level paren-wrapped SELECT parses +- **WHEN** `(SELECT 1)` is parsed by `ParseStmts` +- **THEN** no error is returned AND the resulting statement is a `*SelectQuery` with `HasParen == true` AND `OuterSettings == nil` AND `Settings == nil` AND no set-op pointer populated + +### Requirement: `Format` SHALL preserve parens and emit `OuterSettings` + +`Format(stmt)` over a `*SelectQuery` SHALL: + +- Emit `(` before the SELECT body and `)` after the (optional) set-op chain when `HasParen == true`. +- Emit the `OuterSettings` clause after the closing `)` when `OuterSettings != nil`. +- Emit no parens and no `OuterSettings` when `HasParen == false` (the invariant guarantees `OuterSettings == nil` in that case). + +The existing emission of an inner `Settings` clause (when non-nil) SHALL be unchanged in all cases. + +#### Scenario: Paren-wrapped leg with inside-SETTINGS round-trips with parens preserved +- **WHEN** `SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads = 1)` is parsed and re-formatted +- **THEN** the formatted output contains `UNION ALL (SELECT 2 SETTINGS max_threads = 1)` AND does NOT collapse to `UNION ALL SELECT 2 SETTINGS max_threads = 1` + +#### Scenario: Paren-wrapped leg with outside-SETTINGS round-trips with parens preserved and SETTINGS after `)` +- **WHEN** `SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads = 1` is parsed and re-formatted +- **THEN** the formatted output contains `UNION ALL (SELECT 2) SETTINGS max_threads = 1` + +#### Scenario: Top-level wrapped chain with trailing SETTINGS round-trips identically +- **WHEN** `(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads = 1` is parsed and re-formatted +- **THEN** the formatted output starts with `(` AND contains `UNION ALL` AND ends with `) SETTINGS max_threads = 1` + +#### Scenario: Round-trip is a fixed point for every new fixture +- **WHEN** each of the four new `.sql` fixtures is parsed, formatted, re-parsed, and re-formatted +- **THEN** the second formatting result is byte-identical to the first (the canonical `validFormatSQL` property) + +### Requirement: `Accept` and `Walk` SHALL traverse `OuterSettings` + +`(*SelectQuery).Accept(visitor)` and `Walk(node, fn)` SHALL traverse the `OuterSettings` subtree (when non-nil) immediately after the set-op chain (`Union`, `Except`, `Intersect`) is traversed and before the outer node's visit callback fires. The lexical order — body → set-op chain → trailing SETTINGS → outer node — SHALL be preserved. + +#### Scenario: Visitor and walker see OuterSettings when present +- **WHEN** an `ASTVisitor` traverses a `*SelectQuery` whose `OuterSettings` is non-nil, or `Walk(outer, fn)` is invoked on it +- **THEN** at least one visit / `fn` invocation hits the `OuterSettings` subtree before the outer `*SelectQuery` itself is visited + +#### Scenario: Traversal is unchanged when OuterSettings is nil +- **WHEN** an `ASTVisitor` traverses a `*SelectQuery` whose `OuterSettings` is nil, or `Walk` is invoked on it +- **THEN** no additional `OuterSettings` visit / `fn` invocation occurs beyond what was triggered today by the existing `Settings` traversal + +### Requirement: Existing format and beautify goldens SHALL remain byte-identical + +Adding the two new fields SHALL NOT cause any pre-existing `.sql` fixture's `format/` or `format/beautify/` golden to drift. The load-bearing invariant that makes this possible — parens consumed by the `SubQuery` wrapper do NOT set the inner `SelectQuery.HasParen` — is locked in by the existing `compatible/1_stateful/00080_array_join_and_union.sql` fixture, which has a UNION chain inside a FROM subquery (see D3 in design.md). + +#### Scenario: All pre-existing format and beautify goldens pass without -update +- **WHEN** `TestParser_Format` and `TestParser_FormatBeautify` are run after this change against every pre-existing fixture +- **THEN** every golden file matches byte-for-byte without `-update` + +#### Scenario: FROM-subquery-wrapped UNION fixture is the regression guard +- **WHEN** `TestParser_Format/00080_array_join_and_union.sql` and `TestParser_FormatBeautify/00080_array_join_and_union.sql` are run after this change +- **THEN** both pass byte-identical, confirming that subquery-consumed parens do NOT trigger the new paren-emitting branch + +### Requirement: Pre-existing JSON goldens SHALL gain exactly two added lines per `SelectQuery` rendering + +Every pre-existing JSON golden whose AST contains a `SelectQuery` SHALL gain exactly two added lines at each `SelectQuery` rendering: `"HasParen": false,` and `"OuterSettings": null`. No line SHALL be removed; no other field SHALL move position. + +#### Scenario: Non-paren JSON golden diff is mechanical +- **WHEN** `TestParser_ParseStatements/select_expr.sql` (any small SELECT golden without parser-consumed parens) is regenerated against the post-change parser +- **THEN** the diff against the pre-change golden at each `SelectQuery` rendering consists of exactly two added lines (`"HasParen": false,` and `"OuterSettings": null`) and no other change + +#### Scenario: Set-op JSON golden gains the same two lines per leg +- **WHEN** `TestParser_ParseStatements/select_with_union_settings.sql` is regenerated against the post-change parser +- **THEN** each of the two `SelectQuery` renderings (outer and inner via `Union`) gains the same two added lines AND the existing `Settings`-populated subtree on the inner is unchanged + +### Requirement: Four new `.sql` fixtures SHALL exercise the four placements end-to-end + +Four `.sql` fixtures SHALL be added under `parser/testdata/query/`, each carrying its three goldens (`output/`, `format/`, `format/beautify/`): + +- `select_with_paren_leg_settings_inside.sql` — `SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads = 1)`. +- `select_with_paren_leg_settings_outside.sql` — `SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads = 1`. +- `select_with_paren_chain_settings.sql` — `(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads = 1`. +- `select_with_paren_leg_no_settings.sql` — `SELECT 1 UNION ALL (SELECT 2)`. + +#### Scenario: All four new fixtures flow through all three goldens +- **WHEN** the four fixtures are added with their corresponding goldens under `output/`, `format/`, and `format/beautify/` +- **THEN** `TestParser_ParseStatements`, `TestParser_Format`, and `TestParser_FormatBeautify` pass without `-update` + +#### Scenario: The two inside-vs-outside fixtures are byte-distinct in the JSON golden +- **WHEN** `select_with_paren_leg_settings_inside.sql.golden.json` and `select_with_paren_leg_settings_outside.sql.golden.json` are compared +- **THEN** they differ in the inner-leg's `Settings` vs `OuterSettings` field placement — the pre-change parser would have produced identical AST shapes for these two SQLs + +### Requirement: Inline tests SHALL assert the disambiguation contract + +A new inline test `TestParser_With_ChainSettingsDisambiguation` SHALL be added to the parser test suite. It SHALL parse each of the four new fixtures plus the "both placements coexist" SQL (`SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads=1) SETTINGS max_threads=2`) and assert the expected `HasParen` / `Settings` / `OuterSettings` field values on the inner and outer `*SelectQuery` per the scenarios above. The test SHALL include at least one assertion that the per-node invariant holds on a no-parens fixture. + +#### Scenario: All disambiguation forms pass the inline test +- **WHEN** `TestParser_With_ChainSettingsDisambiguation` is executed against the post-change parser +- **THEN** every SQL string in the test passes `require.NoError(t, err)` after `ParseStmts` AND every per-input field-shape assertion holds + +### Requirement: Pre-existing parser, AST, formatter, walker, and unrelated golden behaviour SHALL be preserved + +This change SHALL be additive on the exported AST: no pre-existing `SelectQuery` field renamed, removed, or reordered; no visitor method introduced or renamed; no `SubQuery` behaviour changed; no `omitempty` or `-` JSON tag added to any field. Any pre-existing parse-error contract that is asserted only by `require.Error` (not by error-message content) SHALL continue to pass. + +#### Scenario: TestParser_InvalidSyntax keeps passing +- **WHEN** `TestParser_InvalidSyntax` is run after this change +- **THEN** every input that errors today continues to error (note: the specific error *message* for some leading-`(` invalid inputs may change because the dispatch path is different, but `require.Error` is the only assertion) + +#### Scenario: SubQuery-wrapped SELECTs preserve their pre-change goldens +- **WHEN** any pre-existing fixture whose AST contains a `*SubQuery` wrapping a `*SelectQuery` is run through `TestParser_Format` and `TestParser_FormatBeautify` +- **THEN** the format and beautify goldens match byte-for-byte without `-update`, confirming that `SubQuery.HasParen` continues to drive paren emission for subquery contexts and the inner `SelectQuery.HasParen` stays false diff --git a/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/tasks.md b/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/tasks.md new file mode 100644 index 00000000..409355ca --- /dev/null +++ b/openspec/changes/archive/2026-06-16-disambiguate-chain-settings/tasks.md @@ -0,0 +1,157 @@ +## 1. Baseline + +- [x] 1.1 Add the new inline test `TestParser_With_ChainSettingsDisambiguation` to `parser/parser_test.go` (alongside the other `TestParser_With_*` helpers). Cover the five SQLs listed in the spec: + - `SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads=1)` — inside-parens SETTINGS, attaches to inner `Settings`. + - `SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads=1` — outside-parens SETTINGS, attaches to inner `OuterSettings`. + - `(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads=1` — top-level wrapped chain. + - `SELECT 1 UNION ALL (SELECT 2)` — parens preserved, no SETTINGS. + - `SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads=1) SETTINGS max_threads=2` — both placements coexist on the same leg. + For each, assert the expected `HasParen` / `Settings` / `OuterSettings` field values on the inner and outer `*SelectQuery`. +- [x] 1.2 Confirm the currently-failing forms FAIL today: `go test ./parser/... -run 'TestParser_With_ChainSettingsDisambiguation' -v -count=1`. Expected starting state: the two "inside parens" cases PARSE but the AST does NOT yet have `HasParen` / `OuterSettings` fields, so the test will fail at compile-time on the field reference. The three "outside parens" / "top-level wrapped" cases would parse-error too. Treat this baseline as: test exists but is non-compiling until task 2.x. +- [x] 1.3 Capture the full test baseline (excluding the new test): `go test ./parser/... -count=1 -skip TestParser_With_ChainSettingsDisambiguation 2>&1 | tee /tmp/baseline-test-output.txt`. Save passing-count summaries for `TestParser_ParseStatements`, `TestParser_Format`, `TestParser_FormatBeautify` for post-change comparison. +- [x] 1.4 Snapshot three representative pre-change JSON goldens for spot-checking: + - `cp parser/testdata/query/output/select_expr.sql.golden.json /tmp/select_expr.before.json` — non-set-op fixture (pure two-line addition). + - `cp parser/testdata/query/output/select_with_union_settings.sql.golden.json /tmp/select_with_union_settings.before.json` — set-op fixture with SETTINGS (gains two lines per SelectQuery rendering). + - `cp parser/testdata/query/compatible/1_stateful/00080_array_join_and_union.sql.golden.json /tmp/00080_array_join_and_union.before.json` — FROM-subquery-wrapped UNION fixture (HasParen=false on the inner SelectQuery; format/beautify goldens stay byte-identical). + Plus snapshot the format/beautify goldens for `00080_array_join_and_union.sql`: + - `cp parser/testdata/query/compatible/1_stateful/format/00080_array_join_and_union.sql /tmp/00080_array_join_and_union.format.before.sql` + - `cp parser/testdata/query/compatible/1_stateful/format/beautify/00080_array_join_and_union.sql /tmp/00080_array_join_and_union.beautify.before.sql` + All deleted in task 7.x. +- [x] 1.5 Confirm no source consumers exist outside `parser/` that would break on the additive fields: `grep -rn -E '\bSelectQuery\b' . --include='*.go' | grep -v /parser/ | head -10`. Expected: a small number of imports, none of which pattern-match all fields exhaustively. Verify by inspection. +- [x] 1.6 Grep for all `\(\s*SELECT` occurrences in `parser/testdata/` and categorise each match's parser path (FROM subquery / scalar / CTE / set-op-leg). Expected: the only set-op-leg match is none today (the existing `00080_…` is FROM-subquery-wrapped). If any other set-op-leg paren is found, expect its format/beautify golden to change after the formatter update — flag it before implementation. + +## 2. AST changes in `parser/ast.go` + +- [x] 2.1 In `type SelectQuery struct { … }` (around line 5119), add two new fields immediately after the existing `Settings *SettingsClause` field (and before `Format *FormatClause`): + ```go + HasParen bool + OuterSettings *SettingsClause + ``` +- [x] 2.2 Add a one-line doc comment on each field. `HasParen`: "True when `parseSelectQuery` itself consumed the wrapping parens (vs. the parens being consumed by an enclosing `parseSubQuery`)." `OuterSettings`: "SETTINGS clause that appears AFTER the closing `)` of a paren-wrapped SelectQuery. Distinct from `Settings`, which holds SETTINGS inside the SELECT body. Non-nil only when `HasParen` is true." +- [x] 2.3 In `func (s *SelectQuery) Accept(visitor ASTVisitor) error`, locate the end of the set-op traversal blocks (`Union`, `Except`, `Intersect`) and add a new traversal block immediately after them, before `visitor.VisitSelectQuery(s)`: + ```go + if s.OuterSettings != nil { + if err := s.OuterSettings.Accept(visitor); err != nil { + return err + } + } + ``` +- [x] 2.4 Do not yet build — `walk.go`, `format.go`, `parser_query.go`, `parser_table.go` still need updates. + +## 3. Walk update in `parser/walk.go` + +- [x] 3.1 Locate the `SelectQuery` case in `Walk`. After the existing set-op `Walk` calls (`Walk(n.Union, fn)`, `Walk(n.Except, fn)`, `Walk(n.Intersect, fn)`) and after the `Walk(n.Settings, fn)` call, add: + ```go + if !Walk(n.OuterSettings, fn) { return false } + ``` + matching the short-circuit shape used by the surrounding `Walk` calls. +- [x] 3.2 Do not yet build — `format.go` and the parser files still need updates. + +## 4. Formatter update in `parser/format.go` + +- [x] 4.1 Locate `SelectQuery.FormatSQL`. Identify the entry point of the body emission (the WITH clause / SELECT keyword emission). Wrap the entire current emission (body + set-op chain) with a conditional `(` … `)` based on `s.HasParen`: + - Before the existing first emission (typically the WITH or SELECT keyword), add: + ```go + if s.HasParen { + formatter.WriteString("(") + } + ``` + - After the existing last emission of the set-op chain (the end of the three `if … else if …` arms for Union / Except / Intersect), add: + ```go + if s.HasParen { + formatter.WriteString(")") + if s.OuterSettings != nil { + formatter.WriteByte(' ') + formatter.WriteExpr(s.OuterSettings) + } + } + ``` +- [x] 4.2 Verify beautified output behaves sensibly. For the beautified formatter, the `(` should precede the first beautified line and the `)` should follow the last set-op-chain line, with the `OuterSettings` appearing on its own line after the `)` (matching the existing per-SelectQuery clause indent). If the beautify path uses different helpers (e.g. `Break` calls), apply the same shape there. +- [x] 4.3 Do not yet build — the parser still doesn't populate `HasParen` / `OuterSettings`. + +## 5. Parser update in `parser/parser_query.go` + +- [x] 5.1 Locate `parseSelectQuery` (line 988-1030). Inside the existing `if hasParen { … }` block (currently lines 1024-1028), after the existing `expectTokenKind(TokenKindRParen)` call, add: + ```go + selectStmt.HasParen = true + outerSettings, err := p.tryParseSettingsClause(p.Pos()) + if err != nil { + return nil, err + } + if outerSettings != nil { + selectStmt.OuterSettings = outerSettings + } + ``` +- [x] 5.2 Verify `parseSubQuery` is left untouched (it must continue to consume its own outer parens before delegating to `parseSelectQuery`, so the inner `SelectQuery.HasParen` stays false in subquery contexts). +- [x] 5.3 `go build ./parser/...`. Expected: compiles. + +## 6. Parser dispatch update in `parser/parser_table.go` + +- [x] 6.1 Locate `parseStmt` (line 1425). Change the SELECT dispatch (line 1437) from: + ```go + case p.matchKeyword(KeywordSelect), p.matchKeyword(KeywordWith): + expr, err = p.parseSelectQuery(pos) + ``` + to: + ```go + case p.matchKeyword(KeywordSelect), p.matchKeyword(KeywordWith), p.matchTokenKind(TokenKindLParen): + expr, err = p.parseSelectQuery(pos) + ``` +- [x] 6.2 `go build ./parser/...`. Expected: compiles. +- [x] 6.3 `go vet ./parser/...`. Expected: no new warnings. + +## 7. Verify behavioural fix and regenerate JSON goldens + +- [x] 7.1 `go test ./parser/... -run 'TestParser_With_ChainSettingsDisambiguation' -v -count=1`. Expected: all five SQLs PASS, and all field-shape assertions hold. +- [x] 7.2 `go test ./parser/... -run 'TestParser_InvalidSyntax' -v -count=1`. Expected: PASS (error-message changes are acceptable; only `require.Error` is asserted). +- [x] 7.3 `go test ./parser/... -run 'TestParser_ParseStatements' -count=1`. Expected: many failures — every SelectQuery-containing JSON golden lacks the new `HasParen` / `OuterSettings` lines. +- [x] 7.4 Regenerate JSON goldens: `go test ./parser/... -run 'TestParser_ParseStatements' -count=1 -update`. +- [x] 7.5 Sanity-check the regen scope: `git diff --stat parser/testdata | head -120`. The changed-files list should be JSON goldens only (under `**/output/*.sql.golden.json`). No `.sql` file under `parser/testdata/**/format/` or `parser/testdata/**/format/beautify/` should appear in the diff. +- [x] 7.6 Spot-check the non-set-op fixture diff: `diff /tmp/select_expr.before.json parser/testdata/query/output/select_expr.sql.golden.json`. Expected: at each `SelectQuery` rendering, exactly two added lines (`"HasParen": false,` and `"OuterSettings": null`) and no other changes. +- [x] 7.7 Spot-check the SETTINGS-in-chain fixture diff: `diff /tmp/select_with_union_settings.before.json parser/testdata/query/output/select_with_union_settings.sql.golden.json`. Expected: each of the two SelectQuery renderings (outer and inner via `Union`) gains exactly two added lines (`"HasParen": false,` and `"OuterSettings": null`) and the existing `Settings` populated subtree stays in place on the inner. +- [x] 7.8 Spot-check the FROM-subquery-wrapped UNION fixture diff: `diff /tmp/00080_array_join_and_union.before.json parser/testdata/query/compatible/1_stateful/output/00080_array_join_and_union.sql.golden.json`. Expected: each `SelectQuery` rendering (outer plus the two legs inside the FROM subquery) gains exactly two added lines (`"HasParen": false,` and `"OuterSettings": null`). The inner-leg `"HasParen"` MUST be `false` — this confirms the load-bearing invariant that `parseSubQuery`-consumed parens do NOT set the inner `SelectQuery.HasParen`. +- [x] 7.9 Confirm format and beautify goldens did NOT shift for the FROM-subquery fixture: + - `diff /tmp/00080_array_join_and_union.format.before.sql parser/testdata/query/compatible/1_stateful/format/00080_array_join_and_union.sql` + - `diff /tmp/00080_array_join_and_union.beautify.before.sql parser/testdata/query/compatible/1_stateful/format/beautify/00080_array_join_and_union.sql` + Expected: empty diff for both. +- [x] 7.10 Run `go test ./parser/... -run 'TestParser_Format|TestParser_FormatBeautify' -count=1` (no `-update`). Expected: PASS — no format/beautify golden should have drifted. + +## 8. Add new `.sql` fixtures and goldens for the four disambiguation forms + +- [x] 8.1 Create `parser/testdata/query/select_with_paren_leg_settings_inside.sql` (single line): + ``` + SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads = 1) + ``` +- [x] 8.2 Create `parser/testdata/query/select_with_paren_leg_settings_outside.sql` (single line): + ``` + SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads = 1 + ``` +- [x] 8.3 Create `parser/testdata/query/select_with_paren_chain_settings.sql` (single line): + ``` + (SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads = 1 + ``` +- [x] 8.4 Create `parser/testdata/query/select_with_paren_leg_no_settings.sql` (single line): + ``` + SELECT 1 UNION ALL (SELECT 2) + ``` +- [x] 8.5 Generate JSON goldens: `go test ./parser/... -run 'TestParser_ParseStatements/(select_with_paren_leg_settings_inside|select_with_paren_leg_settings_outside|select_with_paren_chain_settings|select_with_paren_leg_no_settings)\.sql$' -count=1 -update`. **Visually inspect each generated JSON**: + - `select_with_paren_leg_settings_inside.sql.golden.json` — outer `Union` non-nil; inner `HasParen == true`, inner `Settings` non-nil, inner `OuterSettings == null`. + - `select_with_paren_leg_settings_outside.sql.golden.json` — outer `Union` non-nil; inner `HasParen == true`, inner `Settings == null`, inner `OuterSettings` non-nil. + - `select_with_paren_chain_settings.sql.golden.json` — outer `HasParen == true`, outer `OuterSettings` non-nil, outer `Union` non-nil; inner `HasParen == false`. + - `select_with_paren_leg_no_settings.sql.golden.json` — outer `Union` non-nil; inner `HasParen == true`, inner `Settings == null`, inner `OuterSettings == null`. +- [x] 8.6 Confirm the two inside-vs-outside fixtures' JSON goldens are byte-distinct: `diff parser/testdata/query/output/select_with_paren_leg_settings_inside.sql.golden.json parser/testdata/query/output/select_with_paren_leg_settings_outside.sql.golden.json | head -40`. Expected: the diff shows the inner's `Settings` populated in the first and `OuterSettings` populated in the second (and vice-versa for the unpopulated fields). +- [x] 8.7 Generate format goldens: `go test ./parser/... -run 'TestParser_Format/(select_with_paren_leg_settings_inside|select_with_paren_leg_settings_outside|select_with_paren_chain_settings|select_with_paren_leg_no_settings)\.sql$' -count=1 -update`. **Visually inspect each**: + - inside: contains `UNION ALL (SELECT 2 SETTINGS max_threads = 1)` (parens preserved around the leg, SETTINGS inside). + - outside: contains `UNION ALL (SELECT 2) SETTINGS max_threads = 1` (parens preserved around the leg, SETTINGS after `)`). + - chain-settings: starts with `(`, contains `UNION ALL`, contains `) SETTINGS max_threads = 1`. + - leg-no-settings: contains `UNION ALL (SELECT 2)` (parens preserved, no SETTINGS). +- [x] 8.8 Generate beautify goldens: `go test ./parser/... -run 'TestParser_FormatBeautify/(select_with_paren_leg_settings_inside|select_with_paren_leg_settings_outside|select_with_paren_chain_settings|select_with_paren_leg_no_settings)\.sql$' -count=1 -update`. **Visually inspect each** for sensible line breaks around the parens and the `SETTINGS` clause. +- [x] 8.9 Re-run all three test suites without `-update`: `go test ./parser/... -run 'TestParser_ParseStatements|TestParser_Format|TestParser_FormatBeautify' -count=1`. All goldens (regenerated + new) must pass. +- [x] 8.10 Round-trip idempotence check: for each of the four new fixtures, parse the format-golden, re-format, and confirm the second formatting is byte-identical to the first (the canonical "fixed-point" property already exercised by `validFormatSQL`). If any input does not reach a fixed point in two iterations, the formatter has a bug — investigate before proceeding. + +## 9. Close out + +- [x] 9.1 `go test ./parser/... -count=1`. Compare against the baseline captured in 1.3: `TestParser_With_ChainSettingsDisambiguation` flips FAIL → PASS; ~90 pre-existing JSON goldens are regenerated (uniform +2 lines per SelectQuery rendering); four new fixtures × 3 goldens = 12 new golden sub-tests appear and PASS. Nothing previously passing moves to fail. +- [x] 9.2 `go vet ./parser/...` produces no new warnings. +- [x] 9.3 `openspec validate disambiguate-chain-settings` reports the change as valid. +- [x] 9.4 Delete the temporary snapshots from tasks 1.3/1.4: `rm /tmp/baseline-test-output.txt /tmp/select_expr.before.json /tmp/select_with_union_settings.before.json /tmp/00080_array_join_and_union.before.json /tmp/00080_array_join_and_union.format.before.sql /tmp/00080_array_join_and_union.beautify.before.sql`. diff --git a/openspec/config.yaml b/openspec/config.yaml index 392946c6..0c00b287 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -1,20 +1,93 @@ schema: spec-driven -# Project context (optional) -# This is shown to AI when creating artifacts. -# Add your tech stack, conventions, style guides, domain knowledge, etc. -# Example: -# context: | -# Tech stack: TypeScript, React, Node.js -# We use conventional commits -# Domain: e-commerce platform - -# Per-artifact rules (optional) -# Add custom rules for specific artifacts. -# Example: -# rules: -# proposal: -# - Keep proposals under 500 words -# - Always include a "Non-goals" section -# tasks: -# - Break tasks into chunks of max 2 hours +context: | + ## Purpose + + A ClickHouse SQL parser and formatter in Go. Given ClickHouse SQL text it produces a typed AST (`parser` + package: `SelectQuery`, `InsertStmt`, `AlterTable`, `DescribeStmt`, …) and a canonical re-serialisation + via `Format(stmt)` / `(*Formatter).Beautify`. Consumed both as a Go library and as the + `clickhouse-sql-parser` CLI. The library is bidirectional: parse → format must be a fixed point on every + committed `.sql` fixture, so downstream tooling (linters, IDE integrations, dashboard rewriters) can + round-trip user SQL without losing intent. + + This is the Hydrolix fork of upstream `AfterShip/clickhouse-sql-parser`. Hydrolix changes land on `main`; + upstream syncs come in via `sync/upstream-vX.Y.Z-*` merge branches. + + Authoritative coding standards live in `AGENTS.md` (and `.claude/CLAUDE.md`) — proposals, designs, specs, + and tasks should assume those standards apply. + + ## Domain vocabulary + + - **Exported AST surface** — `SelectQuery`, `SubQuery`, `SettingsClause`, `UnionMode`/`ExceptMode`/ + `IntersectMode`, `Format`, `Beautify`, `Accept`, `ASTVisitor`, `Walk`, `ParseStmts`. These names ARE the + user contract; the proposal and spec layers can name them freely. Unexported helpers (`parseSelectQuery`, + `parseSubQuery`, `tryParseSettingsClause`, …) are implementation and stay in design.md / tasks.md. + - **Round-trip / fixed-point property** — `Format(Parse(sql)) == Format(Parse(Format(Parse(sql))))`. Every + `.sql` fixture under `parser/testdata/` upholds this; `validFormatSQL` in `parser/format_test.go` is the + canonical exerciser. + - **Three golden families per fixture** — JSON AST (`/output/.sql.golden.json`), compact + SQL (`/format/.sql`), beautified SQL (`/format/beautify/.sql`). Goldens + are regenerated via `make update_test` or `go test … -update`. Adding an AST field shifts the JSON-golden + line count by the number of node renderings; format/beautify goldens stay byte-identical for additive + changes that don't reach the formatter. + - **Set-op chain** — UNION / EXCEPT / INTERSECT × bare / ALL / DISTINCT (the 3×3 matrix), encoded on + `SelectQuery` as three optional pointer-pairs (`Union`/`UnionMode`, `Except`/`ExceptMode`, + `Intersect`/`IntersectMode`). Right-recursive today; mixed-operator precedence is a KNOWN limitation, see + the archived `add-set-operator-modes` change. + - **SubQuery-vs-set-op paren boundary** — parens around a `SELECT` used as a subexpression (FROM clause, + scalar subquery, view body, INSERT-SELECT) are consumed by the `SubQuery` wrapper, which carries its own + `HasParen` flag. Parens around a set-op leg or a top-level chain are consumed by `SelectQuery`'s parser + itself. This boundary is load-bearing for round-trip correctness — surface-level changes that touch + paren consumption must keep it intact. + + ## Constraints that shape proposals + + - **Exported AST is a public contract.** Adding fields is additive (only shifts JSON-golden line counts). + Renaming, removing, or reordering exported fields is a breaking API change and must be called out + explicitly in the proposal's Impact section. + - **Round-trip preservation is the central guarantee.** Any AST shape change must come with regenerated + goldens that prove the fixed-point property still holds for every fixture. + - **JSON-golden rendering is explicit** — every field renders (`null` / `""` included). The repo + convention does NOT use `omitempty`; AST snapshots are deliberately self-describing. + - **Lexer additions can reshape parses.** Reserving a new keyword can break previously-parseable inputs + that used the word as an identifier; grep `parser/testdata/` before reserving. + - **Upstream sync hygiene matters.** Structural changes that conflict with upstream + (`AfterShip/clickhouse-sql-parser`) make future merges harder; favour additive shapes when possible. + +rules: + proposal: + - "Proposal carries WHY and user-visible OUTCOMES. HOW belongs in design; behavioral contracts belong in spec." + - "Write the proposal so a reader who has not opened design.md or spec.md can follow it end-to-end." + - "Open the Why section with the problem in plain language: what the parser accepts/rejects today, what round-trip behaviour breaks, what an AST consumer cannot tell from the current shape. Avoid file paths, line numbers, and unexported-helper names in the Why — those belong in design.md." + - "In What Changes, lead each bullet with the user-observable change (\"Parens around a set-op leg round-trip preserved\", \"Trailing SETTINGS after `)` lands in a distinct AST field\"), not the implementation step. Outcome first, optional one-sentence context after." + - "The exported AST surface IS user-facing in this project — naming `SelectQuery.Union`, `SettingsClause`, `UnionMode`, the `FormatSQL` contract, etc. in the proposal is correct and expected. What does NOT belong: file paths (`parser/parser_query.go:993`), line numbers, unexported helpers (`tryParseSettingsClause`, `parseSelectStmt`), lexer token names, and ClickHouse server version pins. Frame internal-mechanism constraints in terms of the observable AST or formatted-output effect; design.md carries the mechanism." + - "Show user-facing contracts that already exist in the spec (the exported AST shape, the round-trip / fixed-point property, JSON-golden diff shape, the set of accepted surface forms) in their final form. Don't duplicate the algorithmic detail behind them." + - "Keep the proposal 1–2 pages. If a bullet runs past ~3 lines it is probably describing HOW — move it to design.md and replace with an outcome sentence." + - "Use concrete SQL examples sparingly and only where they make the behavior clearer at a glance (e.g. `SELECT 1 UNION ALL (SELECT 2) SETTINGS x=1`). Skip them when the prose already conveys the behavior." + - "Impact section enumerates JSON-golden diff shape (\"+N lines per X rendering across ~M fixtures\"), format/beautify-golden footprint (\"byte-identical for all existing fixtures\" is the strong claim worth stating when true), new `.sql` fixtures, compatibility guarantees for the exported AST, rollback story, performance posture, and explicit out-of-scope items. Code-level reach (which files change) is implementation detail and belongs in design.md." + + # Applies to spec.md files inside change directories AND capability specs in openspec/specs/. + specs: + - "Describe WHAT the parser guarantees and WHY it matters, not HOW. Algorithmic steps, helper-function names, file paths, line numbers, and lexer-internal mechanics belong in design.md." + - "Anchor each Requirement to an observable behavioral contract — a parse outcome, an AST shape invariant, a formatted-output guarantee, a golden-file diff shape, or a round-trip property. Avoid restating the implementation." + - "Each Requirement body MUST contain a SHALL or MUST statement (the validator enforces this). Open with the contract sentence, then narrow it with constraints — don't bury the rule under context." + - "Use the exported AST API surface in requirement bodies and scenarios (`SelectQuery.Union`, `Settings`, `HasParen`, `OuterSettings`, `UnionMode`, `ParseStmts`, `Format`) because those names ARE the user contract. Avoid unexported helpers (`parseSelectStmt`, `tryParseSettingsClause`) — those are implementation. If you find yourself reaching for an unexported name, you're describing HOW; move it to design.md." + - "Don't pin requirements to a specific ClickHouse server version or upstream parser SHA. When behaviour depends on a server semantic (e.g. `union_default_mode` runtime resolution), frame the requirement as the syntactic shape the parser must accept and document the version coupling in design.md." + - "Scenarios MUST use concrete inputs and observable outcomes: a `.sql` text in, the resulting AST field values OR the re-formatted SQL OR the golden-file diff OR a parse-error condition out. Use `**WHEN** … **THEN** …` (and `**AND**` for conjunctions). Use `**GIVEN**` only when the surrounding domain fact is non-obvious (e.g. ClickHouse server semantics)." + - "Performance optimisations, allocation invariants, and parser-internal quirks are design decisions, not requirements. A Requirement about a zero-allocation lexer fast-path or a particular lookahead depth belongs in design.md." + - "When a requirement's body grows past ~6 lines, check whether you've started describing the algorithm. Replace step-by-step prose with the invariant the algorithm is meant to uphold." + + design: + - "Design is where HOW lives: parser flow (which function consumes which token), AST field placement and ordering decisions, formatter emission order, walker/visitor traversal order, asymmetric edge cases (e.g. the `parseSubQuery`-vs-`parseSelectQuery` paren boundary), and ClickHouse-version coupling notes. Spec stays free of these." + - "When a design decision affects observable behaviour, surface the *observable* part in spec.md and the *cause* in design.md. Cross-reference by decision number (e.g. \"see D3\") rather than duplicating." + - "Each Decision states the choice, the rationale, the trade-offs accepted, and (when relevant) the alternatives considered. Don't silently merge unrelated decisions — squash only when they answer the same underlying question." + - "Call out the JSON-golden / format-golden / beautify-golden regeneration footprint explicitly: how many fixtures change, what the per-rendering diff looks like, which fixtures act as regression guards. This catches drift between intent and reality during code review." + + tasks: + - "Every spec Requirement's scenarios MUST trace to at least one task entry. An implementation task whose work satisfies the scenario is sufficient; a separate test task is the cleaner option for invariants whose verification is the load-bearing part. Goal: a future reader following tasks.md as the work-record can find every spec scenario's verification without grep." + - "When a test exists in the codebase but no task names it (ad-hoc tests added during implementation, regression guards added during bug-fix sessions), add a `[x]` task entry that names both the test and the spec scenario(s) it pins. Backfilling these is cheap and prevents the spec→test trace from being lost." + - "Tasks legitimately name implementation artifacts (file paths, function names, test names) — unlike spec.md and design.md." + - "Group tasks by phase, not by spec Requirement. A single section can carry tasks tied to multiple Requirements." + - "Keep tasks small enough to verify individually. A task that says \"add a test\" names the test function; a task that says \"add a helper\" names the helper and its signature. Vague tasks create coverage gaps that surface only at audit time." + - "Order tasks so the baseline (capture pre-change test state, snapshot reference goldens) runs first, build-affecting changes that won't compile in isolation run together (AST + walker + formatter + parser), golden regen runs once after the build is green, and new fixtures are added last. Close out with full-suite verification and snapshot cleanup." + - "When a task regenerates goldens with `-update`, the immediately-following task MUST be a `git diff` / `diff` spot-check that asserts the diff shape, not just the file count. Drive-by golden updates without diff inspection are how subtle regressions land." diff --git a/openspec/specs/paren-wrapped-select-query/spec.md b/openspec/specs/paren-wrapped-select-query/spec.md new file mode 100644 index 00000000..c9381113 --- /dev/null +++ b/openspec/specs/paren-wrapped-select-query/spec.md @@ -0,0 +1,183 @@ +## Purpose + +Track whether a `SelectQuery` was parsed as a parenthesised expression and, when so, capture an optional trailing `SETTINGS` clause that appears *after* the closing `)`. Disambiguates "SETTINGS inside the parens" (per-leg, scoped to the subquery) from "SETTINGS outside the parens" (chain-level) in set-op chains, and unlocks the top-level `(chain) SETTINGS …` form that the parser previously rejected at the leading `(`. + +The capability adds two additive fields to `SelectQuery` — `HasParen bool` and `OuterSettings *SettingsClause` — and threads them through the parser, formatter, walker, and visitor. It does not introduce a new AST node or visitor method, and it does not retroactively reinterpret no-parens trailing `SETTINGS` (which continues to attach to the inner leg's `Settings`). Parens are the explicit disambiguator. + +## Requirements + +### Requirement: `SelectQuery` SHALL expose `HasParen` and `OuterSettings` on its exported AST surface + +The `SelectQuery` struct SHALL expose two additional optional fields: + +- `HasParen bool` — true when the parser consumed wrapping parens around this `SelectQuery` itself (as opposed to parens consumed by a surrounding `SubQuery` wrapper). +- `OuterSettings *SettingsClause` — the optional `SETTINGS` clause that appears immediately after the closing `)` of a paren-wrapped `SelectQuery`. Distinct from the existing `Settings` field, which continues to mean "SETTINGS inside the SELECT body's clause list". + +The placement of the new fields in the struct SHALL fall between the existing `Settings` field and the existing `Format` field, so the JSON-golden diff per `SelectQuery` rendering is two contiguous added lines. + +#### Scenario: Exported AST surface gains the two new fields +- **WHEN** a Go consumer reflects on `parser.SelectQuery` +- **THEN** the struct has fields `HasParen bool` and `OuterSettings *SettingsClause` in addition to every pre-existing field, with no pre-existing field renamed, removed, or reordered + +#### Scenario: Default zero values for any SelectQuery without parser-consumed parens +- **WHEN** any SQL that does not include parens directly around a `SelectQuery` is parsed (e.g. `SELECT 1`, `SELECT 1 UNION ALL SELECT 2`, `SELECT * FROM (SELECT 1)`) +- **THEN** every resulting `*SelectQuery` has `HasParen == false` AND `OuterSettings == nil` + +### Requirement: Parsing a parenthesised `SelectQuery` SHALL set `HasParen` on that node + +When the parser consumes a matching `(` … `)` pair around a `SelectQuery` (and any set-op chain rooted on it), the resulting `*SelectQuery` SHALL have `HasParen == true`. When the parens belong to a surrounding `SubQuery` wrapper (FROM-clause subquery, scalar subquery, view body, INSERT-SELECT body, etc.) or to a `WITH` CTE binding, the inner `*SelectQuery.HasParen` SHALL stay `false` and the surrounding wrapper SHALL own the paren emission. + +#### Scenario: Paren-wrapped set-op leg has HasParen true +- **WHEN** `SELECT 1 UNION ALL (SELECT 2)` is parsed +- **THEN** `ParseStmts` returns no error AND the outer `*SelectQuery.Union` is non-nil AND `outer.Union.HasParen == true` AND `outer.HasParen == false` + +#### Scenario: Top-level paren-wrapped chain has HasParen true on the head +- **WHEN** `(SELECT 1 UNION ALL SELECT 2)` is parsed +- **THEN** `ParseStmts` returns no error AND the resulting `*SelectQuery` has `HasParen == true` AND its `Union` is non-nil AND `outer.Union.HasParen == false` + +#### Scenario: SubQuery-wrapped SELECT keeps the inner HasParen false +- **WHEN** `SELECT * FROM (SELECT 1 UNION ALL SELECT 2)` is parsed +- **THEN** `ParseStmts` returns no error AND the FROM-clause `*SubQuery` has its own `HasParen == true` AND the wrapped `*SelectQuery` (`SubQuery.Select`) has `HasParen == false` + +#### Scenario: CTE-body SELECT keeps the inner HasParen false +- **WHEN** `WITH t AS (SELECT 1) SELECT * FROM t` is parsed +- **THEN** the CTE body's `*SelectQuery` has `HasParen == false` — the CTE wrapper consumes the parens, not the inner `parseSelectQuery` + +### Requirement: Trailing `SETTINGS` after `)` SHALL land in `OuterSettings` + +When a `SETTINGS` clause appears immediately after the closing `)` of a paren-wrapped `SelectQuery`, the parser SHALL attach it to that node's `OuterSettings` field. SETTINGS appearing *before* the `)` (inside the parens, as part of the SELECT body) continues to attach to the node's `Settings` field. + +#### Scenario: SETTINGS inside parens attaches to the leg's `Settings` +- **WHEN** `SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads=1)` is parsed +- **THEN** `outer.Union.HasParen == true` AND `outer.Union.Settings` is non-nil AND `outer.Union.OuterSettings == nil` + +#### Scenario: SETTINGS outside parens attaches to the leg's `OuterSettings` +- **WHEN** `SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads=1` is parsed +- **THEN** `outer.Union.HasParen == true` AND `outer.Union.Settings == nil` AND `outer.Union.OuterSettings` is non-nil + +#### Scenario: Both SETTINGS placements coexist on the same paren-wrapped leg +- **WHEN** `SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads=1) SETTINGS max_threads=2` is parsed +- **THEN** `outer.Union.HasParen == true` AND `outer.Union.Settings` is non-nil (the inner `max_threads=1`) AND `outer.Union.OuterSettings` is non-nil (the trailing `max_threads=2`) + +#### Scenario: Trailing SETTINGS on a top-level wrapped chain attaches to the head's `OuterSettings` +- **WHEN** `(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads=1` is parsed +- **THEN** the outer `*SelectQuery.HasParen == true` AND `outer.OuterSettings` is non-nil AND `outer.Union` is non-nil AND `outer.Union.HasParen == false` AND `outer.Union.OuterSettings == nil` + +#### Scenario: No-parens trailing SETTINGS continues to attach to the inner `Settings` +- **WHEN** `SELECT 1 UNION ALL SELECT 2 SETTINGS max_threads=1` is parsed +- **THEN** `outer.Union` is non-nil AND `outer.Union.Settings` is non-nil AND `outer.Union.HasParen == false` AND `outer.Union.OuterSettings == nil` AND `outer.Settings == nil` + +### Requirement: `OuterSettings` non-nil SHALL imply `HasParen` true (per-node invariant) + +For every `*SelectQuery` produced by `ParseStmts`, the invariant `OuterSettings != nil ⇒ HasParen == true` SHALL hold. Equivalently: a node with `HasParen == false` MUST have `OuterSettings == nil`. + +#### Scenario: Invariant holds across all four fixtures +- **WHEN** each of `select_with_paren_leg_settings_inside.sql`, `select_with_paren_leg_settings_outside.sql`, `select_with_paren_chain_settings.sql`, `select_with_paren_leg_no_settings.sql` is parsed +- **THEN** every `*SelectQuery` node reachable from the result satisfies `OuterSettings == nil OR HasParen == true` + +### Requirement: Top-level paren-wrapped SELECT statements SHALL parse + +`ParseStmts` SHALL accept SQL whose first non-whitespace token is `(` and whose remainder forms a parenthesised `SelectQuery` (with or without a set-op chain inside, with or without a trailing `SETTINGS` clause). + +#### Scenario: Top-level paren-wrapped chain with trailing SETTINGS parses +- **WHEN** `(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads=1` is parsed by `ParseStmts` +- **THEN** no error is returned AND the resulting statement is a `*SelectQuery` with `HasParen == true` AND `OuterSettings` non-nil + +#### Scenario: Bare top-level paren-wrapped SELECT parses +- **WHEN** `(SELECT 1)` is parsed by `ParseStmts` +- **THEN** no error is returned AND the resulting statement is a `*SelectQuery` with `HasParen == true` AND `OuterSettings == nil` AND `Settings == nil` AND no set-op pointer populated + +### Requirement: `Format` SHALL preserve parens and emit `OuterSettings` + +`Format(stmt)` over a `*SelectQuery` SHALL: + +- Emit `(` before the SELECT body and `)` after the (optional) set-op chain when `HasParen == true`. +- Emit the `OuterSettings` clause after the closing `)` when `OuterSettings != nil`. +- Emit no parens and no `OuterSettings` when `HasParen == false` (the invariant guarantees `OuterSettings == nil` in that case). + +The existing emission of an inner `Settings` clause (when non-nil) SHALL be unchanged in all cases. + +#### Scenario: Paren-wrapped leg with inside-SETTINGS round-trips with parens preserved +- **WHEN** `SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads = 1)` is parsed and re-formatted +- **THEN** the formatted output contains `UNION ALL (SELECT 2 SETTINGS max_threads=1)` AND does NOT collapse to `UNION ALL SELECT 2 SETTINGS max_threads=1` + +#### Scenario: Paren-wrapped leg with outside-SETTINGS round-trips with parens preserved and SETTINGS after `)` +- **WHEN** `SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads = 1` is parsed and re-formatted +- **THEN** the formatted output contains `UNION ALL (SELECT 2) SETTINGS max_threads=1` + +#### Scenario: Top-level wrapped chain with trailing SETTINGS round-trips identically +- **WHEN** `(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads = 1` is parsed and re-formatted +- **THEN** the formatted output starts with `(` AND contains `UNION ALL` AND ends with `) SETTINGS max_threads=1` + +#### Scenario: Round-trip is a fixed point for every fixture +- **WHEN** each of the four `.sql` fixtures is parsed, formatted, re-parsed, and re-formatted +- **THEN** the second formatting result is byte-identical to the first (the canonical `validFormatSQL` property) + +### Requirement: `Accept` and `Walk` SHALL traverse `OuterSettings` + +`(*SelectQuery).Accept(visitor)` and `Walk(node, fn)` SHALL traverse the `OuterSettings` subtree (when non-nil) after the set-op chain (`Union`, `Except`, `Intersect`) is traversed and before the outer node's visit callback fires. The lexical order — body → set-op chain → trailing SETTINGS → outer node — SHALL be preserved. + +#### Scenario: Visitor and walker see OuterSettings when present +- **WHEN** an `ASTVisitor` traverses a `*SelectQuery` whose `OuterSettings` is non-nil, or `Walk(outer, fn)` is invoked on it +- **THEN** at least one visit / `fn` invocation hits the `OuterSettings` subtree before the outer `*SelectQuery` itself is visited + +#### Scenario: Traversal is unchanged when OuterSettings is nil +- **WHEN** an `ASTVisitor` traverses a `*SelectQuery` whose `OuterSettings` is nil, or `Walk` is invoked on it +- **THEN** no additional `OuterSettings` visit / `fn` invocation occurs beyond what was triggered by the existing `Settings` traversal + +### Requirement: Pre-existing format and beautify goldens SHALL remain byte-identical + +Adding the two new fields SHALL NOT cause any pre-existing `.sql` fixture's `format/` or `format/beautify/` golden to drift. The load-bearing invariant that makes this possible — parens consumed by any wrapper (`SubQuery`, CTE binding) do NOT set the inner `SelectQuery.HasParen` — is verified by running `TestParser_Format` and `TestParser_FormatBeautify` without `-update` across the full fixture corpus. + +#### Scenario: All pre-existing format and beautify goldens pass without -update +- **WHEN** `TestParser_Format` and `TestParser_FormatBeautify` are run against every pre-existing fixture +- **THEN** every golden file matches byte-for-byte without `-update` + +### Requirement: Pre-existing JSON goldens SHALL gain exactly two added lines per `SelectQuery` rendering + +Every pre-existing JSON golden whose AST contains a `SelectQuery` SHALL gain exactly two added lines at each `SelectQuery` rendering: `"HasParen": false,` and `"OuterSettings": null,`. No line SHALL be removed; no other field SHALL move position. + +#### Scenario: Non-paren JSON golden diff is mechanical +- **WHEN** `TestParser_ParseStatements/select_expr.sql` (any small SELECT golden without parser-consumed parens) is regenerated +- **THEN** the diff against the pre-change golden at each `SelectQuery` rendering consists of exactly two added lines (`"HasParen": false,` and `"OuterSettings": null,`) and no other change + +#### Scenario: Set-op JSON golden gains the same two lines per leg +- **WHEN** `TestParser_ParseStatements/select_with_union_settings.sql` is regenerated +- **THEN** each of the two `SelectQuery` renderings (outer and inner via `Union`) gains the same two added lines AND the existing `Settings`-populated subtree on the inner is unchanged + +### Requirement: Four `.sql` fixtures SHALL exercise the four placements end-to-end + +Four `.sql` fixtures SHALL exist under `parser/testdata/query/`, each carrying its three goldens (`output/`, `format/`, `format/beautify/`): + +- `select_with_paren_leg_settings_inside.sql` — `SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads = 1)`. +- `select_with_paren_leg_settings_outside.sql` — `SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads = 1`. +- `select_with_paren_chain_settings.sql` — `(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads = 1`. +- `select_with_paren_leg_no_settings.sql` — `SELECT 1 UNION ALL (SELECT 2)`. + +#### Scenario: All four fixtures flow through all three goldens +- **WHEN** the four fixtures with their corresponding goldens under `output/`, `format/`, and `format/beautify/` are run +- **THEN** `TestParser_ParseStatements`, `TestParser_Format`, and `TestParser_FormatBeautify` pass without `-update` + +#### Scenario: The two inside-vs-outside fixtures are byte-distinct in the JSON golden +- **WHEN** `select_with_paren_leg_settings_inside.sql.golden.json` and `select_with_paren_leg_settings_outside.sql.golden.json` are compared +- **THEN** they differ in the inner-leg's `Settings` vs `OuterSettings` field placement + +### Requirement: Inline tests SHALL assert the disambiguation contract + +`TestParser_With_ChainSettingsDisambiguation` in `parser/parser_test.go` SHALL parse each of the four fixtures plus the "both placements coexist" SQL (`SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads=1) SETTINGS max_threads=2`) and assert the expected `HasParen` / `Settings` / `OuterSettings` field values on the inner and outer `*SelectQuery`. The test SHALL include at least one assertion that the per-node invariant holds on a no-parens fixture. + +#### Scenario: All disambiguation forms pass the inline test +- **WHEN** `TestParser_With_ChainSettingsDisambiguation` is executed +- **THEN** every SQL string in the test passes `require.NoError(t, err)` after `ParseStmts` AND every per-input field-shape assertion holds + +### Requirement: Pre-existing parser, AST, formatter, walker, and unrelated golden behaviour SHALL be preserved + +The capability SHALL be additive on the exported AST: no pre-existing `SelectQuery` field renamed, removed, or reordered; no visitor method introduced or renamed; no `SubQuery` behaviour changed; no `omitempty` or `-` JSON tag added to any field. Any pre-existing parse-error contract that is asserted only by `require.Error` (not by error-message content) SHALL continue to pass. + +#### Scenario: TestParser_InvalidSyntax keeps passing +- **WHEN** `TestParser_InvalidSyntax` is run +- **THEN** every input that errors continues to error (note: the specific error *message* for some leading-`(` invalid inputs may change because the dispatch path is different, but `require.Error` is the only assertion) + +#### Scenario: SubQuery-wrapped SELECTs preserve their pre-change goldens +- **WHEN** any fixture whose AST contains a `*SubQuery` wrapping a `*SelectQuery` is run through `TestParser_Format` and `TestParser_FormatBeautify` +- **THEN** the format and beautify goldens match byte-for-byte without `-update`, confirming that `SubQuery.HasParen` continues to drive paren emission for subquery contexts and the inner `SelectQuery.HasParen` stays false diff --git a/parser/ast.go b/parser/ast.go index 77f5571d..22cc2d02 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -5135,6 +5135,14 @@ type SelectQuery struct { LimitBy *LimitByClause Limit *LimitClause Settings *SettingsClause + // HasParen is true when parseSelectQuery itself consumed the wrapping + // parens around this SelectQuery (vs. the parens being consumed by an + // enclosing parseSubQuery wrapper). + HasParen bool + // OuterSettings is the SETTINGS clause that appears AFTER the closing `)` + // of a paren-wrapped SelectQuery. Distinct from Settings, which holds + // SETTINGS inside the SELECT body. Non-nil only when HasParen is true. + OuterSettings *SettingsClause Format *FormatClause Union *SelectQuery UnionMode UnionMode @@ -5242,6 +5250,11 @@ func (s *SelectQuery) Accept(visitor ASTVisitor) error { return err } } + if s.OuterSettings != nil { + if err := s.OuterSettings.Accept(visitor); err != nil { + return err + } + } return visitor.VisitSelectQuery(s) } diff --git a/parser/format.go b/parser/format.go index 896b9041..32951817 100644 --- a/parser/format.go +++ b/parser/format.go @@ -2257,6 +2257,9 @@ func (s *SelectItem) FormatSQL(formatter *Formatter) { } func (s *SelectQuery) FormatSQL(formatter *Formatter) { + if s.HasParen { + formatter.WriteByte('(') + } if s.With != nil { formatter.WriteString("WITH") formatter.Indent() @@ -2376,6 +2379,13 @@ func (s *SelectQuery) FormatSQL(formatter *Formatter) { formatter.Break() formatter.WriteExpr(s.Intersect) } + if s.HasParen { + formatter.WriteByte(')') + if s.OuterSettings != nil { + formatter.Break() + formatter.WriteExpr(s.OuterSettings) + } + } } func (s *SetStmt) FormatSQL(formatter *Formatter) { diff --git a/parser/parser_query.go b/parser/parser_query.go index 63dad82c..00913437 100644 --- a/parser/parser_query.go +++ b/parser/parser_query.go @@ -1025,6 +1025,14 @@ func (p *Parser) parseSelectQuery(_ Pos) (*SelectQuery, error) { if err := p.expectTokenKind(TokenKindRParen); err != nil { return nil, err } + selectStmt.HasParen = true + outerSettings, err := p.tryParseSettingsClause(p.Pos()) + if err != nil { + return nil, err + } + if outerSettings != nil { + selectStmt.OuterSettings = outerSettings + } } return selectStmt, nil } @@ -1196,10 +1204,18 @@ func (p *Parser) parseCTEStmt(pos Pos) (*CTEStmt, error) { return nil, err } if p.matchTokenKind(TokenKindLParen) { + // Consume the wrapping parens at this layer (mirrors parseSubQuery), + // so the inner SelectQuery.HasParen stays false — the CTE wrapper + // owns the parens, not the SELECT itself. Keeps every CTE-body's + // format/beautify golden byte-identical post-change. + _ = p.lexer.consumeToken() selectQuery, err := p.parseSelectQuery(p.Pos()) if err != nil { return nil, err } + if err := p.expectTokenKind(TokenKindRParen); err != nil { + return nil, err + } return &CTEStmt{ CTEPos: pos, Expr: expr, diff --git a/parser/parser_table.go b/parser/parser_table.go index 931b10f2..bb173058 100644 --- a/parser/parser_table.go +++ b/parser/parser_table.go @@ -1434,7 +1434,7 @@ func (p *Parser) parseStmt(pos Pos) (Expr, error) { p.matchKeyword(KeywordTruncate), p.matchKeyword(KeywordRename): expr, err = p.parseDDL(pos) - case p.matchKeyword(KeywordSelect), p.matchKeyword(KeywordWith): + case p.matchKeyword(KeywordSelect), p.matchKeyword(KeywordWith), p.matchTokenKind(TokenKindLParen): expr, err = p.parseSelectQuery(pos) case p.matchKeyword(KeywordDelete): expr, err = p.parseDeleteClause(pos) diff --git a/parser/parser_test.go b/parser/parser_test.go index 97dc1b94..6cd20870 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -331,6 +331,115 @@ func TestParser_With_SetOperators(t *testing.T) { } } +// Disambiguates the four placements of SETTINGS around a paren-wrapped set-op +// leg (inside parens, outside parens, top-level wrapped chain, both coexist), +// plus the no-SETTINGS round-trip case. Locks in the per-node invariant +// (OuterSettings != nil ⇒ HasParen == true) and the no-parens fallback. +func TestParser_With_ChainSettingsDisambiguation(t *testing.T) { + type expect struct { + // outer = head SelectQuery returned by ParseStmts. + outerHasParen bool + outerHasSettings bool + outerHasOuterSetts bool + // inner = outer.Union when non-nil (all five SQLs use UNION ALL). + // nil if the SQL has no set-op chain (the top-level wrapped chain case + // still has Union; only future bare top-level SQLs would not). + innerHasParen bool + innerHasSettings bool + innerHasOuterSetts bool + } + cases := []struct { + name string + sql string + want expect + }{ + { + name: "inside parens — attaches to inner Settings", + sql: "SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads=1)", + want: expect{ + innerHasParen: true, + innerHasSettings: true, + }, + }, + { + name: "outside parens — attaches to inner OuterSettings", + sql: "SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads=1", + want: expect{ + innerHasParen: true, + innerHasOuterSetts: true, + }, + }, + { + name: "top-level wrapped chain — attaches to outer OuterSettings", + sql: "(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads=1", + want: expect{ + outerHasParen: true, + outerHasOuterSetts: true, + }, + }, + { + name: "parens preserved, no SETTINGS", + sql: "SELECT 1 UNION ALL (SELECT 2)", + want: expect{ + innerHasParen: true, + }, + }, + { + name: "both placements coexist on the same leg", + sql: "SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads=1) SETTINGS max_threads=2", + want: expect{ + innerHasParen: true, + innerHasSettings: true, + innerHasOuterSetts: true, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + stmts, err := NewParser(tc.sql).ParseStmts() + require.NoError(t, err, "parse failed for: %s", tc.sql) + require.Len(t, stmts, 1, "expected exactly one statement") + outer, ok := stmts[0].(*SelectQuery) + require.True(t, ok, "expected *SelectQuery, got %T", stmts[0]) + + require.Equal(t, tc.want.outerHasParen, outer.HasParen, "outer.HasParen") + require.Equal(t, tc.want.outerHasSettings, outer.Settings != nil, "outer.Settings non-nil") + require.Equal(t, tc.want.outerHasOuterSetts, outer.OuterSettings != nil, "outer.OuterSettings non-nil") + + require.NotNil(t, outer.Union, "all five SQLs have a UNION leg") + inner := outer.Union + require.Equal(t, tc.want.innerHasParen, inner.HasParen, "inner.HasParen") + require.Equal(t, tc.want.innerHasSettings, inner.Settings != nil, "inner.Settings non-nil") + require.Equal(t, tc.want.innerHasOuterSetts, inner.OuterSettings != nil, "inner.OuterSettings non-nil") + + // Per-node invariant: OuterSettings != nil ⇒ HasParen == true. + if outer.OuterSettings != nil { + require.True(t, outer.HasParen, "invariant violated on outer: OuterSettings non-nil but HasParen false") + } + if inner.OuterSettings != nil { + require.True(t, inner.HasParen, "invariant violated on inner: OuterSettings non-nil but HasParen false") + } + }) + } + + // Bare no-parens fallback: SETTINGS lands on inner.Settings, not OuterSettings. + t.Run("no-parens trailing SETTINGS stays on inner Settings", func(t *testing.T) { + stmts, err := NewParser("SELECT 1 UNION ALL SELECT 2 SETTINGS max_threads=1").ParseStmts() + require.NoError(t, err) + outer, ok := stmts[0].(*SelectQuery) + if !ok { + require.Failf(t, "Type coarse fail.", "expected SelectQuery, got %T", stmts[0]) + } + require.False(t, outer.HasParen) + require.Nil(t, outer.Settings) + require.Nil(t, outer.OuterSettings) + require.NotNil(t, outer.Union) + require.False(t, outer.Union.HasParen) + require.NotNil(t, outer.Union.Settings) + require.Nil(t, outer.Union.OuterSettings) + }) +} + // Regression guard against the fork's deletion of the inline EXTRACT case from // parseColumnExpr. Both the function-call form (extract(col, regex)) and the // SQL special form (EXTRACT(unit FROM expr)) must remain parseable. diff --git a/parser/testdata/basic/output/quantile_functions.sql.golden.json b/parser/testdata/basic/output/quantile_functions.sql.golden.json index a84b0613..d1bbdc4f 100644 --- a/parser/testdata/basic/output/quantile_functions.sql.golden.json +++ b/parser/testdata/basic/output/quantile_functions.sql.golden.json @@ -118,6 +118,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/bug_001.sql.golden.json b/parser/testdata/ddl/output/bug_001.sql.golden.json index d1616a6a..81345b70 100644 --- a/parser/testdata/ddl/output/bug_001.sql.golden.json +++ b/parser/testdata/ddl/output/bug_001.sql.golden.json @@ -567,6 +567,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_live_view_basic.sql.golden.json b/parser/testdata/ddl/output/create_live_view_basic.sql.golden.json index fc0326c4..ed232c2f 100644 --- a/parser/testdata/ddl/output/create_live_view_basic.sql.golden.json +++ b/parser/testdata/ddl/output/create_live_view_basic.sql.golden.json @@ -129,6 +129,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_materialized_view_basic.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_basic.sql.golden.json index 2ac5ebae..7d7cc186 100644 --- a/parser/testdata/ddl/output/create_materialized_view_basic.sql.golden.json +++ b/parser/testdata/ddl/output/create_materialized_view_basic.sql.golden.json @@ -534,6 +534,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_materialized_view_with_comment_before_as.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_with_comment_before_as.sql.golden.json index 3be7be9a..22687c58 100644 --- a/parser/testdata/ddl/output/create_materialized_view_with_comment_before_as.sql.golden.json +++ b/parser/testdata/ddl/output/create_materialized_view_with_comment_before_as.sql.golden.json @@ -241,6 +241,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_materialized_view_with_definer.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_with_definer.sql.golden.json index 4f7bf4cf..2a7d5c6d 100644 --- a/parser/testdata/ddl/output/create_materialized_view_with_definer.sql.golden.json +++ b/parser/testdata/ddl/output/create_materialized_view_with_definer.sql.golden.json @@ -352,6 +352,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_materialized_view_with_empty_table_schema.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_with_empty_table_schema.sql.golden.json index e078bd13..efa9a214 100644 --- a/parser/testdata/ddl/output/create_materialized_view_with_empty_table_schema.sql.golden.json +++ b/parser/testdata/ddl/output/create_materialized_view_with_empty_table_schema.sql.golden.json @@ -498,6 +498,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -551,6 +553,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_materialized_view_with_gcs.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_with_gcs.sql.golden.json index e42132c1..32489d35 100644 --- a/parser/testdata/ddl/output/create_materialized_view_with_gcs.sql.golden.json +++ b/parser/testdata/ddl/output/create_materialized_view_with_gcs.sql.golden.json @@ -141,6 +141,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_materialized_view_with_refresh.sql.golden.json b/parser/testdata/ddl/output/create_materialized_view_with_refresh.sql.golden.json index 2974d0e1..ad9550bc 100644 --- a/parser/testdata/ddl/output/create_materialized_view_with_refresh.sql.golden.json +++ b/parser/testdata/ddl/output/create_materialized_view_with_refresh.sql.golden.json @@ -207,6 +207,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_mv_with_not_op.sql.golden.json b/parser/testdata/ddl/output/create_mv_with_not_op.sql.golden.json index e4e3e4f6..faba7b21 100644 --- a/parser/testdata/ddl/output/create_mv_with_not_op.sql.golden.json +++ b/parser/testdata/ddl/output/create_mv_with_not_op.sql.golden.json @@ -605,6 +605,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_mv_with_order_by.sql.golden.json b/parser/testdata/ddl/output/create_mv_with_order_by.sql.golden.json index b84b0978..a8ced155 100644 --- a/parser/testdata/ddl/output/create_mv_with_order_by.sql.golden.json +++ b/parser/testdata/ddl/output/create_mv_with_order_by.sql.golden.json @@ -152,6 +152,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -286,6 +288,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_or_replace.sql.golden.json b/parser/testdata/ddl/output/create_or_replace.sql.golden.json index 68c172b1..208c52ea 100644 --- a/parser/testdata/ddl/output/create_or_replace.sql.golden.json +++ b/parser/testdata/ddl/output/create_or_replace.sql.golden.json @@ -474,6 +474,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_view_basic.sql.golden.json b/parser/testdata/ddl/output/create_view_basic.sql.golden.json index 73e5b458..df62af73 100644 --- a/parser/testdata/ddl/output/create_view_basic.sql.golden.json +++ b/parser/testdata/ddl/output/create_view_basic.sql.golden.json @@ -148,6 +148,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_view_on_cluster_with_uuid.sql.golden.json b/parser/testdata/ddl/output/create_view_on_cluster_with_uuid.sql.golden.json index cbfd7974..32c34455 100644 --- a/parser/testdata/ddl/output/create_view_on_cluster_with_uuid.sql.golden.json +++ b/parser/testdata/ddl/output/create_view_on_cluster_with_uuid.sql.golden.json @@ -99,6 +99,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/create_view_with_comment.sql.golden.json b/parser/testdata/ddl/output/create_view_with_comment.sql.golden.json index 3c81af7e..63a33371 100644 --- a/parser/testdata/ddl/output/create_view_with_comment.sql.golden.json +++ b/parser/testdata/ddl/output/create_view_with_comment.sql.golden.json @@ -162,6 +162,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/ddl/output/describe_subquery.sql.golden.json b/parser/testdata/ddl/output/describe_subquery.sql.golden.json index 92dd7ff5..7bd88430 100644 --- a/parser/testdata/ddl/output/describe_subquery.sql.golden.json +++ b/parser/testdata/ddl/output/describe_subquery.sql.golden.json @@ -59,6 +59,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/dml/output/alter_table_modify_query.sql.golden.json b/parser/testdata/dml/output/alter_table_modify_query.sql.golden.json index 52649a33..909d7d07 100644 --- a/parser/testdata/dml/output/alter_table_modify_query.sql.golden.json +++ b/parser/testdata/dml/output/alter_table_modify_query.sql.golden.json @@ -115,6 +115,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/dml/output/insert_select_without_from.sql.golden.json b/parser/testdata/dml/output/insert_select_without_from.sql.golden.json index 4961e1d3..220e4df9 100644 --- a/parser/testdata/dml/output/insert_select_without_from.sql.golden.json +++ b/parser/testdata/dml/output/insert_select_without_from.sql.golden.json @@ -77,6 +77,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/dml/output/insert_with_select.sql.golden.json b/parser/testdata/dml/output/insert_with_select.sql.golden.json index 1e3e7f6a..106637ed 100644 --- a/parser/testdata/dml/output/insert_with_select.sql.golden.json +++ b/parser/testdata/dml/output/insert_with_select.sql.golden.json @@ -106,6 +106,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/format/beautify/select_with_paren_chain_settings.sql b/parser/testdata/query/format/beautify/select_with_paren_chain_settings.sql new file mode 100644 index 00000000..2be5a997 --- /dev/null +++ b/parser/testdata/query/format/beautify/select_with_paren_chain_settings.sql @@ -0,0 +1,12 @@ +-- Origin SQL: +(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads = 1 + + +-- Beautify SQL: +(SELECT + 1 +UNION ALL +SELECT + 2) +SETTINGS + max_threads=1; diff --git a/parser/testdata/query/format/beautify/select_with_paren_leg_no_settings.sql b/parser/testdata/query/format/beautify/select_with_paren_leg_no_settings.sql new file mode 100644 index 00000000..6ebe1c8b --- /dev/null +++ b/parser/testdata/query/format/beautify/select_with_paren_leg_no_settings.sql @@ -0,0 +1,10 @@ +-- Origin SQL: +SELECT 1 UNION ALL (SELECT 2) + + +-- Beautify SQL: +SELECT + 1 +UNION ALL +(SELECT + 2); diff --git a/parser/testdata/query/format/beautify/select_with_paren_leg_settings_inside.sql b/parser/testdata/query/format/beautify/select_with_paren_leg_settings_inside.sql new file mode 100644 index 00000000..b078ba0b --- /dev/null +++ b/parser/testdata/query/format/beautify/select_with_paren_leg_settings_inside.sql @@ -0,0 +1,12 @@ +-- Origin SQL: +SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads = 1) + + +-- Beautify SQL: +SELECT + 1 +UNION ALL +(SELECT + 2 +SETTINGS + max_threads=1); diff --git a/parser/testdata/query/format/beautify/select_with_paren_leg_settings_outside.sql b/parser/testdata/query/format/beautify/select_with_paren_leg_settings_outside.sql new file mode 100644 index 00000000..618684cc --- /dev/null +++ b/parser/testdata/query/format/beautify/select_with_paren_leg_settings_outside.sql @@ -0,0 +1,12 @@ +-- Origin SQL: +SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads = 1 + + +-- Beautify SQL: +SELECT + 1 +UNION ALL +(SELECT + 2) +SETTINGS + max_threads=1; diff --git a/parser/testdata/query/format/select_with_paren_chain_settings.sql b/parser/testdata/query/format/select_with_paren_chain_settings.sql new file mode 100644 index 00000000..26ab0f49 --- /dev/null +++ b/parser/testdata/query/format/select_with_paren_chain_settings.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads = 1 + + +-- Format SQL: +(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads=1; diff --git a/parser/testdata/query/format/select_with_paren_leg_no_settings.sql b/parser/testdata/query/format/select_with_paren_leg_no_settings.sql new file mode 100644 index 00000000..3ee0120e --- /dev/null +++ b/parser/testdata/query/format/select_with_paren_leg_no_settings.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +SELECT 1 UNION ALL (SELECT 2) + + +-- Format SQL: +SELECT 1 UNION ALL (SELECT 2); diff --git a/parser/testdata/query/format/select_with_paren_leg_settings_inside.sql b/parser/testdata/query/format/select_with_paren_leg_settings_inside.sql new file mode 100644 index 00000000..0a96cd7c --- /dev/null +++ b/parser/testdata/query/format/select_with_paren_leg_settings_inside.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads = 1) + + +-- Format SQL: +SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads=1); diff --git a/parser/testdata/query/format/select_with_paren_leg_settings_outside.sql b/parser/testdata/query/format/select_with_paren_leg_settings_outside.sql new file mode 100644 index 00000000..14fe1c61 --- /dev/null +++ b/parser/testdata/query/format/select_with_paren_leg_settings_outside.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads = 1 + + +-- Format SQL: +SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads=1; diff --git a/parser/testdata/query/output/access_tuple_with_dot.sql.golden.json b/parser/testdata/query/output/access_tuple_with_dot.sql.golden.json index 9014b8e8..1f5e82fb 100644 --- a/parser/testdata/query/output/access_tuple_with_dot.sql.golden.json +++ b/parser/testdata/query/output/access_tuple_with_dot.sql.golden.json @@ -86,6 +86,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -638,6 +640,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/create_window_view.sql.golden.json b/parser/testdata/query/output/create_window_view.sql.golden.json index 824fb167..015d707d 100644 --- a/parser/testdata/query/output/create_window_view.sql.golden.json +++ b/parser/testdata/query/output/create_window_view.sql.golden.json @@ -211,6 +211,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/query_with_expr_compare.sql.golden.json b/parser/testdata/query/output/query_with_expr_compare.sql.golden.json index a23277ef..ba7fa440 100644 --- a/parser/testdata/query/output/query_with_expr_compare.sql.golden.json +++ b/parser/testdata/query/output/query_with_expr_compare.sql.golden.json @@ -153,6 +153,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -297,6 +299,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_case_multiple_when.sql.golden.json b/parser/testdata/query/output/select_case_multiple_when.sql.golden.json index eb7a50f7..8e21a80f 100644 --- a/parser/testdata/query/output/select_case_multiple_when.sql.golden.json +++ b/parser/testdata/query/output/select_case_multiple_when.sql.golden.json @@ -145,6 +145,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_case_when_exists.sql.golden.json b/parser/testdata/query/output/select_case_when_exists.sql.golden.json index 117add32..4163f738 100644 --- a/parser/testdata/query/output/select_case_when_exists.sql.golden.json +++ b/parser/testdata/query/output/select_case_when_exists.sql.golden.json @@ -112,6 +112,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -203,6 +205,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_case_when_regexp.sql.golden.json b/parser/testdata/query/output/select_case_when_regexp.sql.golden.json index 60411b23..6a03cc49 100644 --- a/parser/testdata/query/output/select_case_when_regexp.sql.golden.json +++ b/parser/testdata/query/output/select_case_when_regexp.sql.golden.json @@ -115,6 +115,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_cast.sql.golden.json b/parser/testdata/query/output/select_cast.sql.golden.json index 066b9b7d..4e94d82e 100644 --- a/parser/testdata/query/output/select_cast.sql.golden.json +++ b/parser/testdata/query/output/select_cast.sql.golden.json @@ -47,6 +47,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -100,6 +102,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -163,6 +167,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -217,6 +223,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_column_alias_string.sql.golden.json b/parser/testdata/query/output/select_column_alias_string.sql.golden.json index 8b42988c..53727fae 100644 --- a/parser/testdata/query/output/select_column_alias_string.sql.golden.json +++ b/parser/testdata/query/output/select_column_alias_string.sql.golden.json @@ -33,6 +33,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -91,6 +93,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_concat_expr.sql.golden.json b/parser/testdata/query/output/select_concat_expr.sql.golden.json index 01e9ef0c..b81e50b8 100644 --- a/parser/testdata/query/output/select_concat_expr.sql.golden.json +++ b/parser/testdata/query/output/select_concat_expr.sql.golden.json @@ -38,6 +38,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -95,6 +97,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -163,6 +167,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_except_bare_ident.sql.golden.json b/parser/testdata/query/output/select_except_bare_ident.sql.golden.json index f35bb3bf..f7863a17 100644 --- a/parser/testdata/query/output/select_except_bare_ident.sql.golden.json +++ b/parser/testdata/query/output/select_except_bare_ident.sql.golden.json @@ -78,6 +78,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_except_mixed_modifiers.sql.golden.json b/parser/testdata/query/output/select_except_mixed_modifiers.sql.golden.json index bf642d00..b862fe17 100644 --- a/parser/testdata/query/output/select_except_mixed_modifiers.sql.golden.json +++ b/parser/testdata/query/output/select_except_mixed_modifiers.sql.golden.json @@ -152,6 +152,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_expr.sql.golden.json b/parser/testdata/query/output/select_expr.sql.golden.json index f1e40332..3baf6ec9 100644 --- a/parser/testdata/query/output/select_expr.sql.golden.json +++ b/parser/testdata/query/output/select_expr.sql.golden.json @@ -40,6 +40,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_extract_with_regex.sql.golden.json b/parser/testdata/query/output/select_extract_with_regex.sql.golden.json index 7bad66f4..a7c29bef 100644 --- a/parser/testdata/query/output/select_extract_with_regex.sql.golden.json +++ b/parser/testdata/query/output/select_extract_with_regex.sql.golden.json @@ -445,6 +445,8 @@ "Offset": null }, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_item_with_modifiers.sql.golden.json b/parser/testdata/query/output/select_item_with_modifiers.sql.golden.json index 7a0824de..c233306b 100644 --- a/parser/testdata/query/output/select_item_with_modifiers.sql.golden.json +++ b/parser/testdata/query/output/select_item_with_modifiers.sql.golden.json @@ -86,6 +86,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -192,6 +194,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -356,6 +360,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_json_type.sql.golden.json b/parser/testdata/query/output/select_json_type.sql.golden.json index 09eeda48..fcfc6bab 100644 --- a/parser/testdata/query/output/select_json_type.sql.golden.json +++ b/parser/testdata/query/output/select_json_type.sql.golden.json @@ -87,6 +87,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -155,6 +157,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -208,6 +212,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -271,6 +277,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -340,6 +348,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -415,6 +425,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_keyword_alias_no_as.sql.golden.json b/parser/testdata/query/output/select_keyword_alias_no_as.sql.golden.json index 257cb20b..360dac39 100644 --- a/parser/testdata/query/output/select_keyword_alias_no_as.sql.golden.json +++ b/parser/testdata/query/output/select_keyword_alias_no_as.sql.golden.json @@ -55,6 +55,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_not_regexp.sql.golden.json b/parser/testdata/query/output/select_not_regexp.sql.golden.json index d7896de8..12643a96 100644 --- a/parser/testdata/query/output/select_not_regexp.sql.golden.json +++ b/parser/testdata/query/output/select_not_regexp.sql.golden.json @@ -79,6 +79,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_order_by_timestamp.sql.golden.json b/parser/testdata/query/output/select_order_by_timestamp.sql.golden.json index 61ddef41..68696aa1 100644 --- a/parser/testdata/query/output/select_order_by_timestamp.sql.golden.json +++ b/parser/testdata/query/output/select_order_by_timestamp.sql.golden.json @@ -69,6 +69,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_order_by_with_fill_basic.sql.golden.json b/parser/testdata/query/output/select_order_by_with_fill_basic.sql.golden.json index d37a63a1..5bb5f3ee 100644 --- a/parser/testdata/query/output/select_order_by_with_fill_basic.sql.golden.json +++ b/parser/testdata/query/output/select_order_by_with_fill_basic.sql.golden.json @@ -183,6 +183,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -233,6 +235,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_order_by_with_fill_from_to.sql.golden.json b/parser/testdata/query/output/select_order_by_with_fill_from_to.sql.golden.json index 9f6a9d23..f62ae52c 100644 --- a/parser/testdata/query/output/select_order_by_with_fill_from_to.sql.golden.json +++ b/parser/testdata/query/output/select_order_by_with_fill_from_to.sql.golden.json @@ -183,6 +183,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -248,6 +250,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_order_by_with_fill_interpolate.sql.golden.json b/parser/testdata/query/output/select_order_by_with_fill_interpolate.sql.golden.json index 3f9f3a26..1fd8076a 100644 --- a/parser/testdata/query/output/select_order_by_with_fill_interpolate.sql.golden.json +++ b/parser/testdata/query/output/select_order_by_with_fill_interpolate.sql.golden.json @@ -208,6 +208,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -303,6 +305,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_order_by_with_fill_interpolate_no_columns.sql.golden.json b/parser/testdata/query/output/select_order_by_with_fill_interpolate_no_columns.sql.golden.json index cd2fe9a3..08cd84c2 100644 --- a/parser/testdata/query/output/select_order_by_with_fill_interpolate_no_columns.sql.golden.json +++ b/parser/testdata/query/output/select_order_by_with_fill_interpolate_no_columns.sql.golden.json @@ -184,6 +184,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -253,6 +255,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_order_by_with_fill_staleness.sql.golden.json b/parser/testdata/query/output/select_order_by_with_fill_staleness.sql.golden.json index c2d57101..063c7162 100644 --- a/parser/testdata/query/output/select_order_by_with_fill_staleness.sql.golden.json +++ b/parser/testdata/query/output/select_order_by_with_fill_staleness.sql.golden.json @@ -181,6 +181,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_order_by_with_fill_step.sql.golden.json b/parser/testdata/query/output/select_order_by_with_fill_step.sql.golden.json index aa1f454b..c2a4770c 100644 --- a/parser/testdata/query/output/select_order_by_with_fill_step.sql.golden.json +++ b/parser/testdata/query/output/select_order_by_with_fill_step.sql.golden.json @@ -162,6 +162,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -226,6 +228,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_regexp.sql.golden.json b/parser/testdata/query/output/select_regexp.sql.golden.json index 8faf6693..6c4f3030 100644 --- a/parser/testdata/query/output/select_regexp.sql.golden.json +++ b/parser/testdata/query/output/select_regexp.sql.golden.json @@ -79,6 +79,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_simple.sql.golden.json b/parser/testdata/query/output/select_simple.sql.golden.json index 91a60ce2..c9673270 100644 --- a/parser/testdata/query/output/select_simple.sql.golden.json +++ b/parser/testdata/query/output/select_simple.sql.golden.json @@ -430,6 +430,8 @@ }, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_simple_field_alias.sql.golden.json b/parser/testdata/query/output/select_simple_field_alias.sql.golden.json index 5ee6bebd..066c2b89 100644 --- a/parser/testdata/query/output/select_simple_field_alias.sql.golden.json +++ b/parser/testdata/query/output/select_simple_field_alias.sql.golden.json @@ -81,6 +81,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_simple_with_bracket.sql.golden.json b/parser/testdata/query/output/select_simple_with_bracket.sql.golden.json index 234c86d5..422bfd0a 100644 --- a/parser/testdata/query/output/select_simple_with_bracket.sql.golden.json +++ b/parser/testdata/query/output/select_simple_with_bracket.sql.golden.json @@ -181,6 +181,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_simple_with_cte_with_column_aliases.sql.golden.json b/parser/testdata/query/output/select_simple_with_cte_with_column_aliases.sql.golden.json index 9a358ce9..602a6759 100644 --- a/parser/testdata/query/output/select_simple_with_cte_with_column_aliases.sql.golden.json +++ b/parser/testdata/query/output/select_simple_with_cte_with_column_aliases.sql.golden.json @@ -127,6 +127,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -221,6 +223,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_simple_with_group_by_with_cube_totals.sql.golden.json b/parser/testdata/query/output/select_simple_with_group_by_with_cube_totals.sql.golden.json index 44239cf6..eab59b72 100644 --- a/parser/testdata/query/output/select_simple_with_group_by_with_cube_totals.sql.golden.json +++ b/parser/testdata/query/output/select_simple_with_group_by_with_cube_totals.sql.golden.json @@ -130,6 +130,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_simple_with_is_not_null.sql.golden.json b/parser/testdata/query/output/select_simple_with_is_not_null.sql.golden.json index 2f6d1ad5..70b1306c 100644 --- a/parser/testdata/query/output/select_simple_with_is_not_null.sql.golden.json +++ b/parser/testdata/query/output/select_simple_with_is_not_null.sql.golden.json @@ -219,6 +219,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_simple_with_is_null.sql.golden.json b/parser/testdata/query/output/select_simple_with_is_null.sql.golden.json index bb0dff9f..a2ddbd02 100644 --- a/parser/testdata/query/output/select_simple_with_is_null.sql.golden.json +++ b/parser/testdata/query/output/select_simple_with_is_null.sql.golden.json @@ -205,6 +205,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_simple_with_limit.sql.golden.json b/parser/testdata/query/output/select_simple_with_limit.sql.golden.json index 416b1293..e9f53ce3 100644 --- a/parser/testdata/query/output/select_simple_with_limit.sql.golden.json +++ b/parser/testdata/query/output/select_simple_with_limit.sql.golden.json @@ -38,6 +38,8 @@ "Offset": null }, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -90,6 +92,8 @@ } }, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -137,6 +141,8 @@ } }, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_simple_with_top_clause.sql.golden.json b/parser/testdata/query/output/select_simple_with_top_clause.sql.golden.json index 6216ac6f..0895845d 100644 --- a/parser/testdata/query/output/select_simple_with_top_clause.sql.golden.json +++ b/parser/testdata/query/output/select_simple_with_top_clause.sql.golden.json @@ -61,6 +61,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_simple_with_with_clause.sql.golden.json b/parser/testdata/query/output/select_simple_with_with_clause.sql.golden.json index 846944ca..ee2dcc01 100644 --- a/parser/testdata/query/output/select_simple_with_with_clause.sql.golden.json +++ b/parser/testdata/query/output/select_simple_with_with_clause.sql.golden.json @@ -66,6 +66,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -135,6 +137,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -294,6 +298,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_table_alias_without_keyword.sql.golden.json b/parser/testdata/query/output/select_table_alias_without_keyword.sql.golden.json index 0c10d9ad..dec19c21 100644 --- a/parser/testdata/query/output/select_table_alias_without_keyword.sql.golden.json +++ b/parser/testdata/query/output/select_table_alias_without_keyword.sql.golden.json @@ -162,6 +162,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_table_function_with_query.sql.golden.json b/parser/testdata/query/output/select_table_function_with_query.sql.golden.json index e47ad3c3..631c4bae 100644 --- a/parser/testdata/query/output/select_table_function_with_query.sql.golden.json +++ b/parser/testdata/query/output/select_table_function_with_query.sql.golden.json @@ -50,6 +50,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -165,6 +167,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -206,6 +210,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_when_condition.sql.golden.json b/parser/testdata/query/output/select_when_condition.sql.golden.json index 29c8a8d1..6859a4fb 100644 --- a/parser/testdata/query/output/select_when_condition.sql.golden.json +++ b/parser/testdata/query/output/select_when_condition.sql.golden.json @@ -53,6 +53,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_window_comprehensive.sql.golden.json b/parser/testdata/query/output/select_window_comprehensive.sql.golden.json index 1e0f2733..cc027edb 100644 --- a/parser/testdata/query/output/select_window_comprehensive.sql.golden.json +++ b/parser/testdata/query/output/select_window_comprehensive.sql.golden.json @@ -2253,6 +2253,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_window_cte.sql.golden.json b/parser/testdata/query/output/select_window_cte.sql.golden.json index c8b60c2b..c3a12df3 100644 --- a/parser/testdata/query/output/select_window_cte.sql.golden.json +++ b/parser/testdata/query/output/select_window_cte.sql.golden.json @@ -193,6 +193,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -357,6 +359,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -609,6 +613,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_window_keyword_name_in_parens.sql.golden.json b/parser/testdata/query/output/select_window_keyword_name_in_parens.sql.golden.json index 9db23557..60d59310 100644 --- a/parser/testdata/query/output/select_window_keyword_name_in_parens.sql.golden.json +++ b/parser/testdata/query/output/select_window_keyword_name_in_parens.sql.golden.json @@ -153,6 +153,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_window_named_in_parens.sql.golden.json b/parser/testdata/query/output/select_window_named_in_parens.sql.golden.json index 5e933b4f..9df41418 100644 --- a/parser/testdata/query/output/select_window_named_in_parens.sql.golden.json +++ b/parser/testdata/query/output/select_window_named_in_parens.sql.golden.json @@ -153,6 +153,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_window_named_reference_extensions.sql.golden.json b/parser/testdata/query/output/select_window_named_reference_extensions.sql.golden.json index 9fe9b057..bfc49134 100644 --- a/parser/testdata/query/output/select_window_named_reference_extensions.sql.golden.json +++ b/parser/testdata/query/output/select_window_named_reference_extensions.sql.golden.json @@ -284,6 +284,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_window_params.sql.golden.json b/parser/testdata/query/output/select_window_params.sql.golden.json index e77c94e1..cc3640c8 100644 --- a/parser/testdata/query/output/select_window_params.sql.golden.json +++ b/parser/testdata/query/output/select_window_params.sql.golden.json @@ -498,6 +498,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_bare_union.sql.golden.json b/parser/testdata/query/output/select_with_bare_union.sql.golden.json index 0f0b79e2..4ad3ccc6 100644 --- a/parser/testdata/query/output/select_with_bare_union.sql.golden.json +++ b/parser/testdata/query/output/select_with_bare_union.sql.golden.json @@ -34,6 +34,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": { "SelectPos": 20, @@ -70,6 +72,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_distinct.sql.golden.json b/parser/testdata/query/output/select_with_distinct.sql.golden.json index 1b41a642..147987e2 100644 --- a/parser/testdata/query/output/select_with_distinct.sql.golden.json +++ b/parser/testdata/query/output/select_with_distinct.sql.golden.json @@ -89,6 +89,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_distinct_keyword.sql.golden.json b/parser/testdata/query/output/select_with_distinct_keyword.sql.golden.json index fea865f4..49bdd683 100644 --- a/parser/testdata/query/output/select_with_distinct_keyword.sql.golden.json +++ b/parser/testdata/query/output/select_with_distinct_keyword.sql.golden.json @@ -51,6 +51,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_distinct_on_dotted_columns.sql.golden.json b/parser/testdata/query/output/select_with_distinct_on_dotted_columns.sql.golden.json index babf4e94..5784ba02 100644 --- a/parser/testdata/query/output/select_with_distinct_on_dotted_columns.sql.golden.json +++ b/parser/testdata/query/output/select_with_distinct_on_dotted_columns.sql.golden.json @@ -143,6 +143,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_distinct_on_keyword.sql.golden.json b/parser/testdata/query/output/select_with_distinct_on_keyword.sql.golden.json index f3ea3145..c3ebbd69 100644 --- a/parser/testdata/query/output/select_with_distinct_on_keyword.sql.golden.json +++ b/parser/testdata/query/output/select_with_distinct_on_keyword.sql.golden.json @@ -74,6 +74,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_except_all.sql.golden.json b/parser/testdata/query/output/select_with_except_all.sql.golden.json index 1bc6f79d..f71a284c 100644 --- a/parser/testdata/query/output/select_with_except_all.sql.golden.json +++ b/parser/testdata/query/output/select_with_except_all.sql.golden.json @@ -34,6 +34,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -72,6 +74,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_except_distinct.sql.golden.json b/parser/testdata/query/output/select_with_except_distinct.sql.golden.json index c952c765..3691cab9 100644 --- a/parser/testdata/query/output/select_with_except_distinct.sql.golden.json +++ b/parser/testdata/query/output/select_with_except_distinct.sql.golden.json @@ -34,6 +34,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -72,6 +74,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_group_by.sql.golden.json b/parser/testdata/query/output/select_with_group_by.sql.golden.json index b25f1565..c80923e8 100644 --- a/parser/testdata/query/output/select_with_group_by.sql.golden.json +++ b/parser/testdata/query/output/select_with_group_by.sql.golden.json @@ -213,6 +213,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -329,6 +331,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_intersect.sql.golden.json b/parser/testdata/query/output/select_with_intersect.sql.golden.json index 63d04044..86d6f017 100644 --- a/parser/testdata/query/output/select_with_intersect.sql.golden.json +++ b/parser/testdata/query/output/select_with_intersect.sql.golden.json @@ -34,6 +34,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -74,6 +76,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_intersect_modifiers.sql.golden.json b/parser/testdata/query/output/select_with_intersect_modifiers.sql.golden.json index 7ee05644..b2979d49 100644 --- a/parser/testdata/query/output/select_with_intersect_modifiers.sql.golden.json +++ b/parser/testdata/query/output/select_with_intersect_modifiers.sql.golden.json @@ -34,6 +34,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -74,6 +76,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -114,6 +118,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_join_only.sql.golden.json b/parser/testdata/query/output/select_with_join_only.sql.golden.json index a437143b..fd81f9a5 100644 --- a/parser/testdata/query/output/select_with_join_only.sql.golden.json +++ b/parser/testdata/query/output/select_with_join_only.sql.golden.json @@ -102,6 +102,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_keyword_in_group_by.sql.golden.json b/parser/testdata/query/output/select_with_keyword_in_group_by.sql.golden.json index 85de40ab..424efe57 100644 --- a/parser/testdata/query/output/select_with_keyword_in_group_by.sql.golden.json +++ b/parser/testdata/query/output/select_with_keyword_in_group_by.sql.golden.json @@ -223,6 +223,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_keyword_placeholder.sql.golden.json b/parser/testdata/query/output/select_with_keyword_placeholder.sql.golden.json index 3858050e..98c6687b 100644 --- a/parser/testdata/query/output/select_with_keyword_placeholder.sql.golden.json +++ b/parser/testdata/query/output/select_with_keyword_placeholder.sql.golden.json @@ -41,6 +41,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -114,6 +116,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_left_join.sql.golden.json b/parser/testdata/query/output/select_with_left_join.sql.golden.json index 0ef9e9d6..c9e4867c 100644 --- a/parser/testdata/query/output/select_with_left_join.sql.golden.json +++ b/parser/testdata/query/output/select_with_left_join.sql.golden.json @@ -49,6 +49,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -101,6 +103,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -212,6 +216,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_literal_table_name.sql.golden.json b/parser/testdata/query/output/select_with_literal_table_name.sql.golden.json index 6227f472..ef58b441 100644 --- a/parser/testdata/query/output/select_with_literal_table_name.sql.golden.json +++ b/parser/testdata/query/output/select_with_literal_table_name.sql.golden.json @@ -65,6 +65,8 @@ "Offset": null }, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_multi_array_and_inner_join.sql.golden.json b/parser/testdata/query/output/select_with_multi_array_and_inner_join.sql.golden.json index b14e567f..73ab6751 100644 --- a/parser/testdata/query/output/select_with_multi_array_and_inner_join.sql.golden.json +++ b/parser/testdata/query/output/select_with_multi_array_and_inner_join.sql.golden.json @@ -457,6 +457,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_multi_array_join.sql.golden.json b/parser/testdata/query/output/select_with_multi_array_join.sql.golden.json index 0828b427..6ee60041 100644 --- a/parser/testdata/query/output/select_with_multi_array_join.sql.golden.json +++ b/parser/testdata/query/output/select_with_multi_array_join.sql.golden.json @@ -242,6 +242,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_multi_except.sql.golden.json b/parser/testdata/query/output/select_with_multi_except.sql.golden.json index 9da11f74..81c16833 100644 --- a/parser/testdata/query/output/select_with_multi_except.sql.golden.json +++ b/parser/testdata/query/output/select_with_multi_except.sql.golden.json @@ -68,6 +68,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -140,6 +142,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -212,6 +216,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_multi_join.sql.golden.json b/parser/testdata/query/output/select_with_multi_join.sql.golden.json index a5ac9249..0232d97f 100644 --- a/parser/testdata/query/output/select_with_multi_join.sql.golden.json +++ b/parser/testdata/query/output/select_with_multi_join.sql.golden.json @@ -48,6 +48,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -99,6 +101,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -150,6 +154,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -424,6 +430,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_multi_line_comment.sql.golden.json b/parser/testdata/query/output/select_with_multi_line_comment.sql.golden.json index 49dfde4b..cf42489d 100644 --- a/parser/testdata/query/output/select_with_multi_line_comment.sql.golden.json +++ b/parser/testdata/query/output/select_with_multi_line_comment.sql.golden.json @@ -51,6 +51,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_multi_union.sql.golden.json b/parser/testdata/query/output/select_with_multi_union.sql.golden.json index 07eaf33f..26f25c22 100644 --- a/parser/testdata/query/output/select_with_multi_union.sql.golden.json +++ b/parser/testdata/query/output/select_with_multi_union.sql.golden.json @@ -34,6 +34,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": { "SelectPos": 25, @@ -70,6 +72,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": { "SelectPos": 50, @@ -106,6 +110,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_multi_union_distinct.sql.golden.json b/parser/testdata/query/output/select_with_multi_union_distinct.sql.golden.json index fd3cdfc6..0fce2c2c 100644 --- a/parser/testdata/query/output/select_with_multi_union_distinct.sql.golden.json +++ b/parser/testdata/query/output/select_with_multi_union_distinct.sql.golden.json @@ -34,6 +34,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": { "SelectPos": 30, @@ -70,6 +72,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": { "SelectPos": 60, @@ -106,6 +110,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_number_field.sql.golden.json b/parser/testdata/query/output/select_with_number_field.sql.golden.json index cded196e..02d708c3 100644 --- a/parser/testdata/query/output/select_with_number_field.sql.golden.json +++ b/parser/testdata/query/output/select_with_number_field.sql.golden.json @@ -124,6 +124,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_paren_chain_settings.sql.golden.json b/parser/testdata/query/output/select_with_paren_chain_settings.sql.golden.json new file mode 100644 index 00000000..75187f6c --- /dev/null +++ b/parser/testdata/query/output/select_with_paren_chain_settings.sql.golden.json @@ -0,0 +1,101 @@ +[ + { + "SelectPos": 1, + "StatementEnd": 9, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 8, + "NumEnd": 9, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "HasParen": true, + "OuterSettings": { + "SettingsPos": 30, + "ListEnd": 54, + "Items": [ + { + "SettingsPos": 39, + "Name": { + "Name": "max_threads", + "QuoteType": 1, + "NamePos": 39, + "NameEnd": 50 + }, + "Expr": { + "NumPos": 53, + "NumEnd": 54, + "Literal": "1", + "Base": 10 + } + } + ] + }, + "Format": null, + "Union": { + "SelectPos": 20, + "StatementEnd": 28, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 27, + "NumEnd": 28, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "HasParen": false, + "OuterSettings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + }, + "UnionMode": "ALL", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + } +] \ No newline at end of file diff --git a/parser/testdata/query/output/select_with_paren_leg_no_settings.sql.golden.json b/parser/testdata/query/output/select_with_paren_leg_no_settings.sql.golden.json new file mode 100644 index 00000000..6de591a2 --- /dev/null +++ b/parser/testdata/query/output/select_with_paren_leg_no_settings.sql.golden.json @@ -0,0 +1,81 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 8, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 7, + "NumEnd": 8, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "HasParen": false, + "OuterSettings": null, + "Format": null, + "Union": { + "SelectPos": 20, + "StatementEnd": 28, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 27, + "NumEnd": 28, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "HasParen": true, + "OuterSettings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + }, + "UnionMode": "ALL", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + } +] \ No newline at end of file diff --git a/parser/testdata/query/output/select_with_paren_leg_settings_inside.sql.golden.json b/parser/testdata/query/output/select_with_paren_leg_settings_inside.sql.golden.json new file mode 100644 index 00000000..e5656961 --- /dev/null +++ b/parser/testdata/query/output/select_with_paren_leg_settings_inside.sql.golden.json @@ -0,0 +1,101 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 8, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 7, + "NumEnd": 8, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "HasParen": false, + "OuterSettings": null, + "Format": null, + "Union": { + "SelectPos": 20, + "StatementEnd": 53, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 27, + "NumEnd": 28, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": { + "SettingsPos": 29, + "ListEnd": 53, + "Items": [ + { + "SettingsPos": 38, + "Name": { + "Name": "max_threads", + "QuoteType": 1, + "NamePos": 38, + "NameEnd": 49 + }, + "Expr": { + "NumPos": 52, + "NumEnd": 53, + "Literal": "1", + "Base": 10 + } + } + ] + }, + "HasParen": true, + "OuterSettings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + }, + "UnionMode": "ALL", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + } +] \ No newline at end of file diff --git a/parser/testdata/query/output/select_with_paren_leg_settings_outside.sql.golden.json b/parser/testdata/query/output/select_with_paren_leg_settings_outside.sql.golden.json new file mode 100644 index 00000000..6596fdf8 --- /dev/null +++ b/parser/testdata/query/output/select_with_paren_leg_settings_outside.sql.golden.json @@ -0,0 +1,101 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 8, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 7, + "NumEnd": 8, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "HasParen": false, + "OuterSettings": null, + "Format": null, + "Union": { + "SelectPos": 20, + "StatementEnd": 28, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 27, + "NumEnd": 28, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": null + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "HasParen": true, + "OuterSettings": { + "SettingsPos": 30, + "ListEnd": 54, + "Items": [ + { + "SettingsPos": 39, + "Name": { + "Name": "max_threads", + "QuoteType": 1, + "NamePos": 39, + "NameEnd": 50 + }, + "Expr": { + "NumPos": 53, + "NumEnd": 54, + "Literal": "1", + "Base": 10 + } + } + ] + }, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + }, + "UnionMode": "ALL", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + } +] \ No newline at end of file diff --git a/parser/testdata/query/output/select_with_placeholder.sql.golden.json b/parser/testdata/query/output/select_with_placeholder.sql.golden.json index 154ba845..2e7215b0 100644 --- a/parser/testdata/query/output/select_with_placeholder.sql.golden.json +++ b/parser/testdata/query/output/select_with_placeholder.sql.golden.json @@ -69,6 +69,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_query_parameter.sql.golden.json b/parser/testdata/query/output/select_with_query_parameter.sql.golden.json index 74d87fd9..9677fc8f 100644 --- a/parser/testdata/query/output/select_with_query_parameter.sql.golden.json +++ b/parser/testdata/query/output/select_with_query_parameter.sql.golden.json @@ -305,6 +305,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -396,6 +398,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_settings_additional_table_filters.sql.golden.json b/parser/testdata/query/output/select_with_settings_additional_table_filters.sql.golden.json index e7cfc697..dcbfdfe5 100644 --- a/parser/testdata/query/output/select_with_settings_additional_table_filters.sql.golden.json +++ b/parser/testdata/query/output/select_with_settings_additional_table_filters.sql.golden.json @@ -83,6 +83,8 @@ } ] }, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -175,6 +177,8 @@ } ] }, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -267,6 +271,8 @@ } ] }, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -359,6 +365,8 @@ } ] }, + "HasParen": false, + "OuterSettings": null, "Format": { "FormatPos": 404, "Format": { @@ -492,6 +500,8 @@ "Offset": null }, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -587,6 +597,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -725,6 +737,8 @@ } ] }, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_single_quote_table.sql.golden.json b/parser/testdata/query/output/select_with_single_quote_table.sql.golden.json index 2f06c0bb..5e5c7936 100644 --- a/parser/testdata/query/output/select_with_single_quote_table.sql.golden.json +++ b/parser/testdata/query/output/select_with_single_quote_table.sql.golden.json @@ -51,6 +51,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_string_expr.sql.golden.json b/parser/testdata/query/output/select_with_string_expr.sql.golden.json index 043c7588..a83b4f8d 100644 --- a/parser/testdata/query/output/select_with_string_expr.sql.golden.json +++ b/parser/testdata/query/output/select_with_string_expr.sql.golden.json @@ -49,6 +49,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -108,6 +110,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_union_distinct.sql.golden.json b/parser/testdata/query/output/select_with_union_distinct.sql.golden.json index 971eca04..ff4d6b83 100644 --- a/parser/testdata/query/output/select_with_union_distinct.sql.golden.json +++ b/parser/testdata/query/output/select_with_union_distinct.sql.golden.json @@ -56,6 +56,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": { "SelectPos": 59, @@ -114,6 +116,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": { "FormatPos": 110, "Format": { diff --git a/parser/testdata/query/output/select_with_union_settings.sql.golden.json b/parser/testdata/query/output/select_with_union_settings.sql.golden.json index 5fcdef22..58909a8c 100644 --- a/parser/testdata/query/output/select_with_union_settings.sql.golden.json +++ b/parser/testdata/query/output/select_with_union_settings.sql.golden.json @@ -54,6 +54,8 @@ } ] }, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": { "SelectPos": 45, @@ -110,6 +112,8 @@ } ] }, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_variable.sql.golden.json b/parser/testdata/query/output/select_with_variable.sql.golden.json index 23c6a268..87f9661e 100644 --- a/parser/testdata/query/output/select_with_variable.sql.golden.json +++ b/parser/testdata/query/output/select_with_variable.sql.golden.json @@ -49,6 +49,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -108,6 +110,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_with_window_function.sql.golden.json b/parser/testdata/query/output/select_with_window_function.sql.golden.json index 195fc43f..0a9bea6d 100644 --- a/parser/testdata/query/output/select_with_window_function.sql.golden.json +++ b/parser/testdata/query/output/select_with_window_function.sql.golden.json @@ -380,6 +380,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/output/select_without_from_where.sql.golden.json b/parser/testdata/query/output/select_without_from_where.sql.golden.json index 45bdcbed..d47f6b6f 100644 --- a/parser/testdata/query/output/select_without_from_where.sql.golden.json +++ b/parser/testdata/query/output/select_without_from_where.sql.golden.json @@ -48,6 +48,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", @@ -129,6 +131,8 @@ "LimitBy": null, "Limit": null, "Settings": null, + "HasParen": false, + "OuterSettings": null, "Format": null, "Union": null, "UnionMode": "", diff --git a/parser/testdata/query/select_with_paren_chain_settings.sql b/parser/testdata/query/select_with_paren_chain_settings.sql new file mode 100644 index 00000000..9c2c67b5 --- /dev/null +++ b/parser/testdata/query/select_with_paren_chain_settings.sql @@ -0,0 +1 @@ +(SELECT 1 UNION ALL SELECT 2) SETTINGS max_threads = 1 diff --git a/parser/testdata/query/select_with_paren_leg_no_settings.sql b/parser/testdata/query/select_with_paren_leg_no_settings.sql new file mode 100644 index 00000000..ff3cfa60 --- /dev/null +++ b/parser/testdata/query/select_with_paren_leg_no_settings.sql @@ -0,0 +1 @@ +SELECT 1 UNION ALL (SELECT 2) diff --git a/parser/testdata/query/select_with_paren_leg_settings_inside.sql b/parser/testdata/query/select_with_paren_leg_settings_inside.sql new file mode 100644 index 00000000..15bc7c10 --- /dev/null +++ b/parser/testdata/query/select_with_paren_leg_settings_inside.sql @@ -0,0 +1 @@ +SELECT 1 UNION ALL (SELECT 2 SETTINGS max_threads = 1) diff --git a/parser/testdata/query/select_with_paren_leg_settings_outside.sql b/parser/testdata/query/select_with_paren_leg_settings_outside.sql new file mode 100644 index 00000000..c4be2f16 --- /dev/null +++ b/parser/testdata/query/select_with_paren_leg_settings_outside.sql @@ -0,0 +1 @@ +SELECT 1 UNION ALL (SELECT 2) SETTINGS max_threads = 1 diff --git a/parser/walk.go b/parser/walk.go index af3bc4a5..3fef069d 100644 --- a/parser/walk.go +++ b/parser/walk.go @@ -72,6 +72,9 @@ func Walk(node Expr, fn WalkFunc) bool { if !Walk(n.Intersect, fn) { return false } + if !Walk(n.OuterSettings, fn) { + return false + } if !Walk(n.Format, fn) { return false }