From 06efabd78c3294c95c260d66de22e82a53f37b64 Mon Sep 17 00:00:00 2001 From: Ostap Demkovych Date: Tue, 2 Jun 2026 19:59:17 +0300 Subject: [PATCH] Parse the full 3x3 set-operator matrix (UNION/EXCEPT/INTERSECT x bare/ALL/DISTINCT) ClickHouse SQL supports three set operators between SELECT queries, each with an optional ALL or DISTINCT modifier (nine surface forms total). The parser only handled five: UNION ALL, UNION DISTINCT, and bare EXCEPT. Bare UNION errored, EXCEPT ALL/DISTINCT errored, and INTERSECT wasn't a keyword. Refactor SelectQuery from per-mode pointer fields to one pointer + a typed mode discriminator per operator, mirroring the existing OrderDirection precedent: - REMOVE UnionAll *SelectQuery and UnionDistinct *SelectQuery. - ADD Union *SelectQuery + UnionMode UnionMode, Except *SelectQuery + ExceptMode ExceptMode (Except itself kept), Intersect *SelectQuery + IntersectMode IntersectMode. - New typed aliases UnionMode/ExceptMode/IntersectMode each with *None/*All/*Distinct constants. Walker, Accept, formatter, and parseSelectQuery migrate together. The formatter becomes three parallel arms with an inner switch on the mode. parseSelectQuery shares a consumeOptionalSetOpModifier helper across all three operator branches and drops the prior "expected ALL or DISTINCT" error on bare UNION. KeywordIntersect is added alphabetically to keyword.go. Bare EXCEPT and INTERSECT between top-level SELECTs surface two latent ambiguities with the in-tree SELECT-modifier feature (add-select-except-bare-ident) that did not exist when only EXCEPT-after- FROM was supported: - parseSelectItem's modifier loop greedily consumed `EXCEPT SELECT` as a column modifier because matchTokenKind(TokenKindIdent) treats keywords as identifiers. Guard with peekTokenKind so EXCEPT is only treated as a modifier when followed by `(` (parens form) or a true ident token (bare-ident form). - INTERSECT-as-bare-alias is now possible since INTERSECT is a keyword; add it to isSelectItemTerminatorKeyword alongside UNION and EXCEPT. Also restore parseExceptExpr in parser_column.go: the prior HOQ commit removed its definition while leaving the call site, which left the branch in a non-compiling state. The restored function is identical to its pre-HOQ form from ac97d1d. 90 pre-existing JSON goldens regenerate with the documented per-rendering diff (UnionAll/UnionDistinct lines removed; Union/UnionMode/ExceptMode/ Intersect/IntersectMode lines added; populated subtrees migrate to the new field name for the four set-op-populated fixtures). All parser/testdata/**/format/** and format/beautify/** goldens for pre-existing set-op fixtures remain byte-identical. Six new fixtures cover the newly-unlocked surface forms, each with parse/format/beautify goldens: - select_with_bare_union.sql - select_with_union_settings.sql (per-leg SETTINGS + bare UNION) - select_with_except_all.sql - select_with_except_distinct.sql - select_with_intersect.sql - select_with_intersect_modifiers.sql (chained ALL + DISTINCT) Tracked as openspec change add-set-operator-modes, archived under openspec/changes/archive/2026-06-02-add-set-operator-modes/. Canonical contract at openspec/specs/set-operator-modes/spec.md. Out of scope: mixed-operator precedence. The parser stays right- recursive, so `a UNION ALL b INTERSECT c` happens to associate correctly by luck but `a INTERSECT b UNION ALL c` mis-associates. ClickHouse's "INTERSECT binds tighter than UNION/EXCEPT; UNION/EXCEPT are left-to- right at equal precedence" rule is not enforced here. A follow-up change will address this; see Decision 8 in design.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yaml | 2 +- .golangci.yml | 829 ++++++------------ .../design.md | 252 ++++++ .../proposal.md | 85 ++ .../specs/set-operator-modes/spec.md | 233 +++++ .../tasks.md | 251 ++++++ openspec/specs/set-operator-modes/spec.md | 237 +++++ parser/ast.go | 43 +- parser/format.go | 39 +- parser/keyword.go | 2 + parser/parser_column.go | 4 +- parser/parser_query.go | 46 +- parser/parser_test.go | 23 + .../output/quantile_functions.sql.golden.json | 9 +- .../ddl/output/bug_001.sql.golden.json | 9 +- .../create_live_view_basic.sql.golden.json | 9 +- ...te_materialized_view_basic.sql.golden.json | 9 +- ...iew_with_comment_before_as.sql.golden.json | 9 +- ...rialized_view_with_definer.sql.golden.json | 9 +- ...ew_with_empty_table_schema.sql.golden.json | 18 +- ...materialized_view_with_gcs.sql.golden.json | 9 +- ...rialized_view_with_refresh.sql.golden.json | 9 +- .../create_mv_with_not_op.sql.golden.json | 9 +- .../create_mv_with_order_by.sql.golden.json | 18 +- .../output/create_or_replace.sql.golden.json | 9 +- .../output/create_view_basic.sql.golden.json | 9 +- ..._view_on_cluster_with_uuid.sql.golden.json | 9 +- .../create_view_with_comment.sql.golden.json | 9 +- .../output/describe_subquery.sql.golden.json | 9 +- .../alter_table_modify_query.sql.golden.json | 9 +- ...insert_select_without_from.sql.golden.json | 9 +- .../output/insert_with_select.sql.golden.json | 9 +- .../beautify/select_with_bare_union.sql | 10 + .../beautify/select_with_except_all.sql | 10 + .../beautify/select_with_except_distinct.sql | 10 + .../format/beautify/select_with_intersect.sql | 10 + .../select_with_intersect_modifiers.sql | 13 + .../beautify/select_with_union_settings.sql | 14 + .../query/format/select_with_bare_union.sql | 6 + .../query/format/select_with_except_all.sql | 6 + .../format/select_with_except_distinct.sql | 6 + .../query/format/select_with_intersect.sql | 6 + .../select_with_intersect_modifiers.sql | 6 + .../format/select_with_union_settings.sql | 6 + .../access_tuple_with_dot.sql.golden.json | 18 +- .../output/create_window_view.sql.golden.json | 9 +- .../query_with_expr_compare.sql.golden.json | 18 +- .../select_case_multiple_when.sql.golden.json | 9 +- .../select_case_when_exists.sql.golden.json | 18 +- .../select_case_when_regexp.sql.golden.json | 9 +- .../query/output/select_cast.sql.golden.json | 36 +- ...select_column_alias_string.sql.golden.json | 18 +- .../output/select_concat_expr.sql.golden.json | 27 +- .../select_except_bare_ident.sql.golden.json | 9 +- ...ect_except_mixed_modifiers.sql.golden.json | 9 +- .../query/output/select_expr.sql.golden.json | 9 +- .../select_extract_with_regex.sql.golden.json | 9 +- ...select_item_with_modifiers.sql.golden.json | 27 +- .../output/select_json_type.sql.golden.json | 54 +- ...select_keyword_alias_no_as.sql.golden.json | 9 +- .../output/select_not_regexp.sql.golden.json | 9 +- .../select_order_by_timestamp.sql.golden.json | 9 +- ...t_order_by_with_fill_basic.sql.golden.json | 18 +- ...order_by_with_fill_from_to.sql.golden.json | 18 +- ...r_by_with_fill_interpolate.sql.golden.json | 18 +- ...ill_interpolate_no_columns.sql.golden.json | 18 +- ...der_by_with_fill_staleness.sql.golden.json | 9 +- ...ct_order_by_with_fill_step.sql.golden.json | 18 +- .../output/select_regexp.sql.golden.json | 9 +- .../output/select_simple.sql.golden.json | 9 +- .../select_simple_field_alias.sql.golden.json | 9 +- ...select_simple_with_bracket.sql.golden.json | 9 +- ...th_cte_with_column_aliases.sql.golden.json | 18 +- ..._group_by_with_cube_totals.sql.golden.json | 9 +- ...ct_simple_with_is_not_null.sql.golden.json | 9 +- ...select_simple_with_is_null.sql.golden.json | 9 +- .../select_simple_with_limit.sql.golden.json | 27 +- ...ect_simple_with_top_clause.sql.golden.json | 9 +- ...ct_simple_with_with_clause.sql.golden.json | 27 +- ...able_alias_without_keyword.sql.golden.json | 9 +- ..._table_function_with_query.sql.golden.json | 27 +- .../select_when_condition.sql.golden.json | 9 +- ...elect_window_comprehensive.sql.golden.json | 9 +- .../output/select_window_cte.sql.golden.json | 27 +- ...dow_keyword_name_in_parens.sql.golden.json | 9 +- ...ect_window_named_in_parens.sql.golden.json | 9 +- ...named_reference_extensions.sql.golden.json | 9 +- .../select_window_params.sql.golden.json | 9 +- .../select_with_bare_union.sql.golden.json | 87 ++ .../select_with_distinct.sql.golden.json | 9 +- ...lect_with_distinct_keyword.sql.golden.json | 9 +- ...distinct_on_dotted_columns.sql.golden.json | 9 +- ...t_with_distinct_on_keyword.sql.golden.json | 9 +- .../select_with_except_all.sql.golden.json | 87 ++ ...elect_with_except_distinct.sql.golden.json | 87 ++ .../select_with_group_by.sql.golden.json | 18 +- .../select_with_intersect.sql.golden.json | 87 ++ ...t_with_intersect_modifiers.sql.golden.json | 129 +++ .../select_with_join_only.sql.golden.json | 9 +- ...t_with_keyword_in_group_by.sql.golden.json | 9 +- ...t_with_keyword_placeholder.sql.golden.json | 18 +- .../select_with_left_join.sql.golden.json | 27 +- ...ct_with_literal_table_name.sql.golden.json | 9 +- ...multi_array_and_inner_join.sql.golden.json | 9 +- ...lect_with_multi_array_join.sql.golden.json | 9 +- .../select_with_multi_except.sql.golden.json | 27 +- .../select_with_multi_join.sql.golden.json | 36 +- ...ct_with_multi_line_comment.sql.golden.json | 9 +- .../select_with_multi_union.sql.golden.json | 27 +- ..._with_multi_union_distinct.sql.golden.json | 27 +- .../select_with_number_field.sql.golden.json | 9 +- .../select_with_placeholder.sql.golden.json | 9 +- ...elect_with_query_parameter.sql.golden.json | 18 +- ...s_additional_table_filters.sql.golden.json | 63 +- ...ct_with_single_quote_table.sql.golden.json | 9 +- .../select_with_string_expr.sql.golden.json | 18 +- ...select_with_union_distinct.sql.golden.json | 18 +- ...select_with_union_settings.sql.golden.json | 127 +++ .../select_with_variable.sql.golden.json | 18 +- ...elect_with_window_function.sql.golden.json | 9 +- .../select_without_from_where.sql.golden.json | 18 +- .../testdata/query/select_with_bare_union.sql | 1 + .../testdata/query/select_with_except_all.sql | 1 + .../query/select_with_except_distinct.sql | 1 + .../testdata/query/select_with_intersect.sql | 1 + .../query/select_with_intersect_modifiers.sql | 1 + .../query/select_with_union_settings.sql | 1 + parser/walk.go | 7 +- 128 files changed, 3053 insertions(+), 1027 deletions(-) create mode 100644 openspec/changes/archive/2026-06-02-add-set-operator-modes/design.md create mode 100644 openspec/changes/archive/2026-06-02-add-set-operator-modes/proposal.md create mode 100644 openspec/changes/archive/2026-06-02-add-set-operator-modes/specs/set-operator-modes/spec.md create mode 100644 openspec/changes/archive/2026-06-02-add-set-operator-modes/tasks.md create mode 100644 openspec/specs/set-operator-modes/spec.md create mode 100644 parser/testdata/query/format/beautify/select_with_bare_union.sql create mode 100644 parser/testdata/query/format/beautify/select_with_except_all.sql create mode 100644 parser/testdata/query/format/beautify/select_with_except_distinct.sql create mode 100644 parser/testdata/query/format/beautify/select_with_intersect.sql create mode 100644 parser/testdata/query/format/beautify/select_with_intersect_modifiers.sql create mode 100644 parser/testdata/query/format/beautify/select_with_union_settings.sql create mode 100644 parser/testdata/query/format/select_with_bare_union.sql create mode 100644 parser/testdata/query/format/select_with_except_all.sql create mode 100644 parser/testdata/query/format/select_with_except_distinct.sql create mode 100644 parser/testdata/query/format/select_with_intersect.sql create mode 100644 parser/testdata/query/format/select_with_intersect_modifiers.sql create mode 100644 parser/testdata/query/format/select_with_union_settings.sql create mode 100644 parser/testdata/query/output/select_with_bare_union.sql.golden.json create mode 100644 parser/testdata/query/output/select_with_except_all.sql.golden.json create mode 100644 parser/testdata/query/output/select_with_except_distinct.sql.golden.json create mode 100644 parser/testdata/query/output/select_with_intersect.sql.golden.json create mode 100644 parser/testdata/query/output/select_with_intersect_modifiers.sql.golden.json create mode 100644 parser/testdata/query/output/select_with_union_settings.sql.golden.json create mode 100644 parser/testdata/query/select_with_bare_union.sql create mode 100644 parser/testdata/query/select_with_except_all.sql create mode 100644 parser/testdata/query/select_with_except_distinct.sql create mode 100644 parser/testdata/query/select_with_intersect.sql create mode 100644 parser/testdata/query/select_with_intersect_modifiers.sql create mode 100644 parser/testdata/query/select_with_union_settings.sql diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 03ce33ad..a4f5d22b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,7 +34,7 @@ jobs: run: | export GOPATH=$HOME/go export PATH=$PATH:$GOPATH/bin - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.64.8 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.7.2 go mod tidy make lint diff --git a/.golangci.yml b/.golangci.yml index 896a8676..0c2c06b8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,578 +1,305 @@ - -# This file contains all available configuration options -# with their default values. - -# options for analysis running +version: "2" run: - # default concurrency is a available CPU number concurrency: 4 - - # timeout for analysis, e.g. 30s, 5m, default is 1m - timeout: 5m - - # exit code when at least one issue was found, default is 1 issues-exit-code: 1 - - # include test files or not, default is true tests: true - - # list of build tags, all linters use it. Default is empty list. - build-tags: - - # which dirs to skip: issues from them won't be reported; - # can use regexp here: generated.*, regexp is applied on full path; - # default value is empty list, but default dirs are skipped independently - # from this option's value (see skip-dirs-use-default). - skip-dirs: - - # default is true. Enables skipping of directories: - # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ - skip-dirs-use-default: true - - # which files to skip: they will be analyzed, but issues from them - # won't be reported. Default value is empty list, but there is - # no need to include all autogenerated files, we confidently recognize - # autogenerated files. If it's not please let us know. - skip-files: - -# output configuration options output: - # colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions - # default is "colored-line-number" - format: colored-line-number - - # print lines of code with issue, default is true - print-issued-lines: true - - # print linter name in the end of issue text, default is true - print-linter-name: true - - # make issues output unique by line, default is true - uniq-by-line: true - - # add a prefix to the output file references; default is no prefix path-prefix: "" - - # sorts results by: filepath, line and column - sort-results: false - - -# all available settings of specific linters -linters-settings: - dogsled: - # checks assignments with too many blank identifiers; default is 2 - max-blank-identifiers: 2 - - dupl: - # tokens count to trigger issue, 150 by default - threshold: 100 - - errcheck: - # default is false: such cases aren't reported by default. - check-type-assertions: false - - # default is false: such cases aren't reported by default. - check-blank: false - - # list of functions to exclude from checking, where each entry is a single function to exclude. - # see https://github.com/kisielk/errcheck#excluding-functions for details - exclude-functions: - - errchkjson: - # with check-error-free-encoding set to true, errchkjson does warn about errors - # from json encoding functions that are safe to be ignored, - # because they are not possible to happen (default false) - # - # if check-error-free-encoding is set to true and errcheck linter is enabled, - # it is recommended to add the following exceptions to prevent from false positives: - # - # linters-settings: - # errcheck: - # exclude-functions: - # - encoding/json.Marshal - # - encoding/json.MarshalIndent - # - (*encoding/json.Encoder).Encode - check-error-free-encoding: false - # if report-no-exported is true, encoding a struct without exported fields is reported as issue (default false) - report-no-exported: false - - errorlint: - # Check whether fmt.Errorf uses the %w verb for formatting errors. See the readme for caveats - errorf: true - # Check for plain type assertions and type switches - asserts: true - # Check for plain error comparisons - comparison: true - - funlen: - lines: 100 - statements: 60 - - gocognit: - # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 20 - - goconst: - # minimal length of string constant, 3 by default - min-len: 3 - # minimum occurrences of constant string count to trigger issue, 3 by default - min-occurrences: 3 - # ignore test files, false by default - ignore-tests: false - # look for existing constants matching the values, true by default - match-constant: true - # search also for duplicated numbers, false by default - numbers: false - # minimum value, only works with goconst.numbers, 3 by default - min: 3 - # maximum value, only works with goconst.numbers, 3 by default - max: 3 - # ignore when constant is not used as function argument, true by default - ignore-calls: true - - gocritic: - # Which checks should be enabled; can't be combined with 'disabled-checks'; - # See https://go-critic.github.io/overview#checks-overview - # By default list of stable checks is used. - enabled-checks: - - # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". - enabled-tags: - - performance - - # Settings passed to gocritic. - # The settings key is the name of a supported gocritic checker. - # The list of supported checkers can be find in https://go-critic.github.io/overview. - settings: - captLocal: # must be valid enabled check name - # whether to restrict checker to params only (default true) - paramsOnly: true - elseif: - # whether to skip balanced if-else pairs (default true) - skipBalanced: true - hugeParam: - # size in bytes that makes the warning trigger (default 80) - sizeThreshold: 80 - rangeExprCopy: - # size in bytes that makes the warning trigger (default 512) - sizeThreshold: 512 - # whether to check test functions (default true) - skipTestFuncs: true - rangeValCopy: - # size in bytes that makes the warning trigger (default 128) - sizeThreshold: 128 - # whether to check test functions (default true) - skipTestFuncs: true - underef: - # whether to skip (*x).method() calls where x is a pointer receiver (default true) - skipRecvDeref: true - - gocyclo: - # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 10 - - godot: - scope: declarations - # list of regexps for excluding particular comment lines from check - exclude: - # example: exclude comments which contain numbers - # - '[0-9]+' - # check that each sentence starts with a capital letter - capital: false - - godox: - # report any comments starting with keywords, this is useful for TODO or FIXME comments that - # might be left in the code accidentally and should be resolved before merging - keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting - - gofmt: - simplify: true - - goimports: - # put imports beginning with prefix after 3rd-party packages; - # it's a comma-separated list of prefixes - local-prefixes: - - golint: - # minimal confidence for issues, default is 0.8 - min-confidence: 0.8 - - gomnd: - settings: - mnd: - # the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description. - checks: argument,case,condition,operation,return,assign - # ignored-numbers: 1000 - # ignored-files: magic_.*.go - # ignored-functions: math.* - - gomoddirectives: - replace-local: false - replace-allow-list: - retract-allow-no-explanation: false - exclude-forbidden: false - - gomodguard: - allowed: - modules: # List of allowed modules - # - gopkg.in/yaml.v2 - domains: # List of allowed module domains - # - golang.org - blocked: - modules: # List of blocked modules - # - github.com/uudashr/go-module: # Blocked module - # recommendations: # Recommended modules that should be used instead (Optional) - # - golang.org/x/mod - versions: # List of blocked module version constraints - # - github.com/mitchellh/go-homedir: # Blocked module with version constraint - # version: "< 1.1.0" # Version constraint, see https://github.com/Masterminds/semver#basic-comparisons - # reason: "testing if blocked version constraint works." # Reason why the version constraint exists. (Optional) - local_replace_directives: false # Set to true to raise lint issues for packages that are loaded from a local path via replace directive - - gosec: - # To select a subset of rules to run. - # Available rules: https://github.com/securego/gosec#available-rules - includes: - - G401 - - G306 - - G101 - # To specify a set of rules to explicitly exclude. - # Available rules: https://github.com/securego/gosec#available-rules - excludes: - - G204 - # Exclude generated files - exclude-generated: true - # Filter out the issues with a lower severity than the given value. Valid options are: low, medium, high. - severity: "low" - # Filter out the issues with a lower confidence than the given value. Valid options are: low, medium, high. - confidence: "low" - # To specify the configuration of rules. - # The configuration of rules is not fully documented by gosec: - # https://github.com/securego/gosec#configuration - # https://github.com/securego/gosec/blob/569328eade2ccbad4ce2d0f21ee158ab5356a5cf/rules/rulelist.go#L60-L102 - config: - G306: "0600" - G101: - pattern: "(?i)example" - ignore_entropy: false - entropy_threshold: "80.0" - per_char_threshold: "3.0" - truncate: "32" - - gosimple: - # Select the Go version to target. The default is '1.13'. - go: "1.15" - # https://staticcheck.io/docs/options#checks - checks: [ "all" ] - - govet: - # report about shadowed variables - check-shadowing: false - - # settings per analyzer - settings: - printf: - funcs: - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf - - # enable or disable analyzers by name - enable: - # - atomicalign - enable-all: true - disable: - - shadow - - fieldalignment - disable-all: false - - depguard: - list-type: blacklist - include-go-root: false - packages: - # - github.com/sirupsen/logrus - packages-with-error-message: - # specify an error message to output when a blacklisted package is used - # - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" - - ifshort: - # Maximum length of variable declaration measured in number of lines, after which linter won't suggest using short syntax. - # Has higher priority than max-decl-chars. - max-decl-lines: 1 - # Maximum length of variable declaration measured in number of characters, after which linter won't suggest using short syntax. - max-decl-chars: 30 - - lll: - # max line length, lines longer will be reported. Default is 120. - # '\t' is counted as 1 character by default, and can be changed with the tab-width option - line-length: 160 - # tab width in spaces. Default to 1. - tab-width: 4 - - makezero: - # Allow only slices initialized with a length of zero. Default is false. - always: false - - maligned: - # print struct with more effective memory layout or not, false by default - suggest-new: true - - misspell: - # Correct spellings using locale preferences for US or UK. - # Default is to use a neutral variety of English. - # Setting locale to US will correct the British spelling of 'colour' to 'color'. - locale: US - ignore-words: - - automizely - - nakedret: - # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 - max-func-lines: 30 - - nestif: - # minimal complexity of if statements to report, 5 by default - min-complexity: 5 - - nlreturn: - # size of the block (including return statement that is still "OK") - # so no return split required. - block-size: 1 - - nolintlint: - # Disable to ensure that all nolint directives actually have an effect. Default is false. - allow-unused: false - # Disable to ensure that nolint directives don't have a leading space. Default is true. - allow-leading-space: true - # Exclude following linters from requiring an explanation. Default is []. - allow-no-explanation: [ ] - # Enable to require an explanation of nonzero length after each nolint directive. Default is false. - require-explanation: false - # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. - require-specific: false - - prealloc: - # XXX: we don't recommend using this linter before doing performance profiling. - # For most programs usage of prealloc will be a premature optimization. - - # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. - # True by default. - simple: true - range-loops: true # Report preallocation suggestions on range loops, true by default - for-loops: false # Report preallocation suggestions on for loops, false by default - - promlinter: - # Promlinter cannot infer all metrics name in static analysis. - # Enable strict mode will also include the errors caused by failing to parse the args. - strict: false - # Please refer to https://github.com/yeya24/promlinter#usage for detailed usage. - disabled-linters: - # - "Help" - # - "MetricUnits" - # - "Counter" - # - "HistogramSummaryReserved" - # - "MetricTypeInName" - # - "ReservedChars" - # - "CamelCase" - # - "lintUnitAbbreviations" - - predeclared: - # comma-separated list of predeclared identifiers to not report on - ignore: "" - # include method names and field names (i.e., qualified names) in checks - q: false - - rowserrcheck: - packages: - - github.com/jmoiron/sqlx - - revive: - # see https://github.com/mgechev/revive#available-rules for details. - ignore-generated-header: true - severity: warning - rules: - - name: indent-error-flow - severity: warning - - staticcheck: - # Select the Go version to target. The default is '1.13'. - go: "1.15" - # https://staticcheck.io/docs/options#checks - checks: [ "all" ] - - stylecheck: - # Select the Go version to target. The default is '1.13'. - go: "1.15" - # https://staticcheck.io/docs/options#checks - checks: [ "all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022" ] - # https://staticcheck.io/docs/options#dot_import_whitelist - dot-import-whitelist: - # https://staticcheck.io/docs/options#initialisms - initialisms: [ "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS" ] - # https://staticcheck.io/docs/options#http_status_code_whitelist - http-status-code-whitelist: [] - - tagliatelle: - # check the struck tag name case - case: - # use the struct field name to check the name of the struct tag - use-field-name: true - rules: - # any struct tag type can be used. - json: snake - - testpackage: - # regexp pattern to skip files - skip-regexp: (export|internal)_test\.go - - unparam: - # Inspect exported functions, default is false. Set to true if no external program/library imports your code. - # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find external interfaces. All text editor integrations - # with golangci-lint call it on a directory with the changed file. - check-exported: false - - unused: - # Select the Go version to target. The default is '1.13'. - go: "1.15" - - whitespace: - multi-if: false # Enforces newlines (or comments) after every multi-line if statement - multi-func: false # Enforces newlines (or comments) after every multi-line function signature - - wsl: - allow-assign-and-anything: false - allow-assign-and-call: true - allow-cuddle-declarations: false - allow-multiline-assign: true - allow-separated-leading-comment: false - allow-trailing-comment: false - force-case-trailing-whitespace: 0 - force-err-cuddling: false - force-short-decl-cuddling: false - strict-append: true - linters: enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - contextcheck - dogsled + - durationcheck + - errchkjson + - errorlint + - exhaustive - forcetypeassert - funlen + - gocheckcompilerdirectives + - gochecksumtype - goconst - - govet - - gofmt - - goimports + - gosec + - gosmopolitan - lll - - megacheck + - loggercheck + - makezero - misspell + - musttag + - nilerr + - nilnesserr + - noctx + - protogetter + - reassign + - recvcheck - revive + - rowserrcheck + - spancheck + - sqlclosecheck + - testifylint + - unparam + - zerologlint disable: - - maligned - - prealloc - - scopelint - nilnil - disable-all: false - presets: - - bugs - - unused - - sql - fast: false - + - prealloc + settings: + dogsled: + max-blank-identifiers: 2 + dupl: + threshold: 100 + errcheck: + check-type-assertions: false + check-blank: false + errchkjson: + check-error-free-encoding: false + report-no-exported: false + errorlint: + errorf: true + asserts: true + comparison: true + funlen: + lines: 100 + statements: 60 + gocognit: + min-complexity: 20 + goconst: + match-constant: true + min-len: 3 + min-occurrences: 3 + numbers: false + min: 3 + max: 3 + ignore-calls: true + gocritic: + enabled-tags: + - performance + settings: + captLocal: + paramsOnly: true + elseif: + skipBalanced: true + hugeParam: + sizeThreshold: 80 + rangeExprCopy: + sizeThreshold: 512 + skipTestFuncs: true + rangeValCopy: + sizeThreshold: 128 + skipTestFuncs: true + underef: + skipRecvDeref: true + gocyclo: + min-complexity: 10 + godot: + scope: declarations + capital: false + gomoddirectives: + replace-local: false + exclude-forbidden: false + retract-allow-no-explanation: false + gomodguard: + blocked: + local-replace-directives: false + gosec: + includes: + - G401 + - G306 + - G101 + excludes: + - G204 + severity: low + confidence: low + config: + G101: + entropy_threshold: "80.0" + ignore_entropy: false + pattern: (?i)example + per_char_threshold: "3.0" + truncate: "32" + G306: "0600" + govet: + disable: + - shadow + - fieldalignment + enable-all: true + disable-all: false + settings: + printf: + funcs: + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + lll: + line-length: 160 + tab-width: 4 + makezero: + always: false + misspell: + locale: US + ignore-rules: + - automizely + nakedret: + max-func-lines: 30 + nestif: + min-complexity: 5 + nlreturn: + block-size: 1 + nolintlint: + require-explanation: false + require-specific: false + allow-unused: false + prealloc: + simple: true + range-loops: true + for-loops: false + predeclared: + qualified-name: false + promlinter: + strict: false + revive: + severity: warning + rules: + - name: indent-error-flow + severity: warning + rowserrcheck: + packages: + - github.com/jmoiron/sqlx + staticcheck: + checks: + - all + - -ST1000 + - -ST1003 + - -ST1016 + - -ST1020 + - -ST1021 + - -ST1022 + initialisms: + - ACL + - API + - ASCII + - CPU + - CSS + - DNS + - EOF + - GUID + - HTML + - HTTP + - HTTPS + - ID + - IP + - JSON + - QPS + - RAM + - RPC + - SLA + - SMTP + - SQL + - SSH + - TCP + - TLS + - TTL + - UDP + - UI + - GID + - UID + - UUID + - URI + - URL + - UTF8 + - VM + - XML + - XMPP + - XSRF + - XSS + tagliatelle: + case: + rules: + json: snake + use-field-name: true + testpackage: + skip-regexp: (export|internal)_test\.go + unparam: + check-exported: false + whitespace: + multi-if: false + multi-func: false + wsl: + strict-append: true + allow-assign-and-call: true + allow-assign-and-anything: false + allow-multiline-assign: true + force-case-trailing-whitespace: 0 + allow-trailing-comment: false + allow-separated-leading-comment: false + allow-cuddle-declarations: false + force-err-cuddling: false + force-short-decl-cuddling: false + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - cyclop + - dupl + - errcheck + - funlen + - gocognit + - goconst + - gocritic + - gocyclo + - gosec + - lll + - thelper + - wrapcheck + path: _test\.go + - linters: + - gosec + path: internal/hmac/ + text: weak cryptographic primitive + - linters: + - staticcheck + text: 'SA9003:' + - linters: + - lll + source: '^//go:generate ' + - path: (.+)\.go$ + text: G404 + - path: (.+)\.go$ + text: SA1029 + paths: + - third_party$ + - builtin$ + - examples$ issues: - # List of regexps of issue texts to exclude, empty list by default. - # But independently from this option we use default exclude patterns, - exclude: - - G404 - - SA1029 - - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - # Exclude some linters from running on tests files. - - path: _test\.go - linters: - - cyclop - - dupl - - errcheck - - funlen - - gocognit - - goconst - - gocritic - - gocyclo - - gosec - - lll - - thelper - - wrapcheck - - # Exclude known linters from partially hard-vendored code, - # which is impossible to exclude via "nolint" comments. - - path: internal/hmac/ - text: "weak cryptographic primitive" - linters: - - gosec - - # Exclude some staticcheck messages - - linters: - - staticcheck - text: "SA9003:" - - # Exclude lll issues for long lines with go:generate - - linters: - - lll - source: "^//go:generate " - - # Default value for this option is true. - exclude-use-default: true - - # The default value is false. If set to true exclude and exclude-rules - # regular expressions become case sensitive. - exclude-case-sensitive: false - - # The list of ids of default excludes to include or disable. By default it's empty. - include: - # - EXC0002 # disable excluding of issues about comments from golint - - # Maximum issues count per one linter. Set to 0 to disable. Default is 50. max-issues-per-linter: 0 - - # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. max-same-issues: 0 - - # Show only new issues: if there are unstaged changes or untracked files, - # only those changes are analyzed, else only changes in HEAD~ are analyzed. - # It's a super-useful option for integration of golangci-lint into existing - # large codebase. It's not practical to fix all existing issues at the moment - # of integration: much better don't allow issues in new code. - # Default is false. - new: true - new-from-rev: "" - - # Show only new issues created in git patch with set file path. - new-from-patch: - - # Fix found issues (if it's supported by the linter) + new: true fix: true - severity: - # Default value is empty string. - # Set the default severity for issues. If severity rules are defined and the issues - # do not match or no severity is provided to the rule this will be the default - # severity applied. Severities should match the supported severity names of the - # selected out format. - # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity - # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity - # - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message - default-severity: error - - # The default value is false. - # If set to true severity-rules regular expressions become case sensitive. - case-sensitive: false - - # Default value is empty list. - # When a list of severity rules are provided, severity information will be added to lint - # issues. Severity rules have the same filtering capability as exclude rules except you - # are allowed to specify one matcher per severity rule. - # Only affects out formats that support setting severity information. + default: error rules: - linters: - dupl severity: info +formatters: + enable: + - gofmt + - goimports + settings: + gofmt: + simplify: true + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/openspec/changes/archive/2026-06-02-add-set-operator-modes/design.md b/openspec/changes/archive/2026-06-02-add-set-operator-modes/design.md new file mode 100644 index 00000000..4c53fdb8 --- /dev/null +++ b/openspec/changes/archive/2026-06-02-add-set-operator-modes/design.md @@ -0,0 +1,252 @@ +## Context + +`SelectQuery` in `parser/ast.go` (line ~5129) carries optional set-operation slots that recurse into another `SelectQuery`. Today there are three: `UnionAll`, `UnionDistinct`, and `Except`. The parser populates exactly one per node, and the formatter (`parser/format.go` line ~2342) renders exactly one per node via an `if … else if …` chain. + +`parseSelectQuery` in `parser/parser_query.go` (line ~998) drives this. After parsing a leading `parseSelectStmt`, it inspects the next keyword: +- `UNION` followed by `ALL` → recurse, store in `UnionAll`. +- `UNION` followed by `DISTINCT` → recurse, store in `UnionDistinct`. +- `UNION` followed by anything else → **error**: `"expected ALL or DISTINCT, got "`. +- `EXCEPT` → recurse, store in `Except`. (No modifier handling — `EXCEPT ALL` and `EXCEPT DISTINCT` are rejected at the recursive call level.) +- `INTERSECT` → not handled at all; `INTERSECT` is not a recognised keyword in `parser/keyword.go`, so it would be lexed as an identifier and never reach this switch. + +This change fills out the matrix: all three operators accept the bare form and the two modifiers, all three are encoded uniformly on `SelectQuery`, all three follow the same parser/formatter shape. + +`parser/ast.go` line 3 already establishes the precedent for a typed string alias with an empty-string sentinel and uppercase keyword values: +```go +type OrderDirection string +const ( + OrderDirectionNone OrderDirection = "" + OrderDirectionAsc OrderDirection = "ASC" + OrderDirectionDesc OrderDirection = "DESC" +) +``` +`OrderByExpr.Direction` is `OrderDirection`, populated explicitly by the parser, consulted by the formatter via `if o.Direction != OrderDirectionNone { … }` (`parser/format.go:2046`). The three new mode types (`UnionMode`, `ExceptMode`, `IntersectMode`) mirror this shape exactly. + +The same `SelectQuery` already handles a leading `SETTINGS` clause via `parseSelectStmt`'s call to `tryParseSettingsClause`, so each leg of a set-op chain already parses its own SETTINGS independently. This change does not touch that path; it merely needs test coverage that proves the SETTINGS + set-op combination works. + +## Goals / Non-Goals + +**Goals:** +- Replace the two-pointer (`UnionAll`, `UnionDistinct`) UNION representation with a single-pointer + discriminator (`Union`, `UnionMode`). +- Add a companion `ExceptMode` discriminator to the existing `Except` field, and accept `EXCEPT ALL` / `EXCEPT DISTINCT` in the parser. +- Introduce `Intersect *SelectQuery` + `IntersectMode IntersectMode` and the supporting `KeywordIntersect`, accepting `INTERSECT`, `INTERSECT ALL`, and `INTERSECT DISTINCT`. +- Round-trip every surface form through the formatter unchanged: the same SQL text comes back out with the same modifier (or its absence). +- `SETTINGS` on either or both legs of any set-op continues to parse, format, and beautify correctly. Per-leg SETTINGS is the only supported placement. +- Inline and golden test coverage for all nine surface forms and for SETTINGS-with-set-op combinations. + +**Non-Goals:** +- Multi-operator precedence (mixed `UNION` + `INTERSECT` chains, mixed `UNION` + `EXCEPT` chains). The current right-recursive model can't represent these correctly per ClickHouse's precedence rules; see Decision 8. +- Validating the `*_default_mode` settings, or rejecting bare forms when no permissive setting is in scope. ClickHouse diagnoses these at runtime; the parser stays syntactically permissive. +- Adding `omitempty` JSON tags to retroactively suppress the new fields' `null`/`""` rendering. The repo's convention is explicit rendering (see Decision 5 below). +- A unified `SetOp *SetOpClause { Operator, Mode, Right }` field that replaces all three pointer-pairs with one. Rejected for this scope; see Decision 1. +- A shared `SetOpMode` type used by all three operators. Rejected for this scope; see Decision 3. + +## Decisions + +### Decision 1: Three pointer-pair fields, NOT a single discriminated `SetOp` field + +Each operator gets its own pointer (`Union`/`Except`/`Intersect`) plus its own mode discriminator (`UnionMode`/`ExceptMode`/`IntersectMode`). The per-node invariant — "at most one set-op pointer is non-nil" — is preserved by convention (parser logic) rather than by the type system. + +**Why:** Matches the existing AST style (optional pointers per concept, e.g. `Where`/`Prewhere`/`Having`, `Top`/`Limit`, `With`/`Format`). Each operator is name-addressable directly from consumer code, with no need to switch on a `.Operator` string. The visitor protocol is unchanged. The JSON dump renders each operator as its own field, which makes the AST snapshot more navigable than a single `SetOp` envelope would. + +**Alternative considered:** Replace all three set-op slots with `SetOp *SetOpClause { Operator string, Mode string, Right *SelectQuery }`. **Rejected.** Operationally cleaner in some ways (one field, one traversal, one format arm), but the downstream cost is higher: every consumer that today reads `Union`/`Except` directly would need to switch on `SetOp.Operator`; the JSON dump becomes less self-describing; the migration is structural rather than additive. Adopting the three-pair shape first and revisiting consolidation later if patterns warrant it is the lower-risk move. + +### Decision 2: Model each mode after `OrderDirection` + +`OrderDirection` (`parser/ast.go:3-9`) is the in-repo precedent. The three new types follow it exactly: + +```go +type UnionMode string +const ( + UnionModeNone UnionMode = "" + UnionModeAll UnionMode = "ALL" + UnionModeDistinct UnionMode = "DISTINCT" +) + +type ExceptMode string +const ( + ExceptModeNone ExceptMode = "" + ExceptModeAll ExceptMode = "ALL" + ExceptModeDistinct ExceptMode = "DISTINCT" +) + +type IntersectMode string +const ( + IntersectModeNone IntersectMode = "" + IntersectModeAll IntersectMode = "ALL" + IntersectModeDistinct IntersectMode = "DISTINCT" +) +``` + +**Invariant.** For each operator, when the pointer is nil, the mode is the zero value (`*ModeNone`) and is not meaningful. When the pointer is non-nil, the mode is exactly one of the three constants. The overload of `*ModeNone` between "no operator" (pointer == nil) and "operator present, no modifier" (pointer != nil, mode == None) mirrors `OrderDirectionNone`'s overload between "no direction at all" and the default direction; the companion pointer being nil is the disambiguator. + +### Decision 3: Three separate mode types, NOT a single shared `SetOpMode` + +Each operator gets its own typed alias even though all three have identical string values (`""`/`"ALL"`/`"DISTINCT"`). + +**Why:** Matches `OrderDirection`'s scope-specific naming (it isn't called `Direction`; it's specifically for ordering). Surface area is small enough that the duplication cost is negligible — three eight-line blocks, all alphabetically grouped at the top of `parser/ast.go`. Type safety: `UnionMode("ALL")` and `ExceptMode("ALL")` aren't interchangeable at call sites, which makes accidental cross-assignment a compile error. + +**Alternative considered:** A single `type SetOpMode string` with `SetOpModeNone/All/Distinct`, reused across all three operator pairs. **Rejected for this scope.** It would consolidate the eight-line declaration trio to one, save a small amount of typing, and reflect that the three modes ARE semantically identical concepts. But it sacrifices scope-specific naming (which `OrderDirection` chose explicitly) and offers no actual functional advantage — the values are constants known at compile time. If consolidation proves worthwhile later (e.g. if a helper function genuinely benefits from being mode-type-polymorphic), the rename is a trivial mechanical refactor. + +### Decision 4: Parser uses a shared modifier-consumption helper + +The three operator branches in `parseSelectQuery` share the same modifier-parsing logic (try `ALL`, else try `DISTINCT`, else none). A small helper avoids triplicating it: + +```go +func (p *Parser) consumeOptionalSetOpModifier() string { + switch { + case p.tryConsumeKeywords(KeywordAll): + return "ALL" + case p.tryConsumeKeywords(KeywordDistinct): + return "DISTINCT" + default: + return "" + } +} +``` + +Each operator branch then reads the helper's string return and wraps it in its own typed alias at the assignment site: +```go +case p.tryConsumeKeywords(KeywordUnion): + mode := UnionMode(p.consumeOptionalSetOpModifier()) + next, err := p.parseSelectQuery(p.Pos()) + if err != nil { + return nil, err + } + selectStmt.Union = next + selectStmt.UnionMode = mode +``` + +The string-typed helper return is the bridge between Decision 3's three separate types and the shared parsing logic. + +### Decision 5: Accept the JSON-golden regen as a controlled, mechanical edit + +Adding three new fields and removing two old ones changes every JSON-rendered `SelectQuery` object. Per occurrence, the diff is: +- REMOVE: `"UnionAll": null,` and `"UnionDistinct": null,` (2 lines). +- ADD: `"Union": null,`, `"UnionMode": "",`, `"ExceptMode": "",`, `"Intersect": null,`, `"IntersectMode": ""` (5 lines). +- KEEP: `"Except": null,` (or its populated counterpart) — this line was already present. + +Net per occurrence: +3 lines. Across 90 fixtures with on average 1–3 `SelectQuery` renderings each, the total addition is in the low hundreds of lines. + +For the four set-op-populated fixtures (`select_with_union_distinct.sql`, `select_with_multi_union.sql`, `select_with_multi_union_distinct.sql`, `select_with_multi_except.sql`), the populated subtree additionally migrates: UNION fixtures' subtree moves from `UnionAll`/`UnionDistinct` to `Union` and gains `UnionMode: "ALL"`/`"DISTINCT"`; the EXCEPT fixture's `Except` subtree stays in place and gains `ExceptMode: ""` at the populated node. + +**Why accept this:** The repo convention is explicit JSON rendering of every AST field, including nils (see archived `add-describe-settings-clause` Decision 4). Adding `omitempty` would deviate from that convention and bury the discriminator fields in the JSON dump precisely when the AST snapshot is the source of truth for understanding parse output. + +**Workflow:** Land the AST + parser + formatter + walk + keyword changes together — the build won't succeed until they're aligned. Then run `TestParser_ParseStatements -update` once. Then `git diff --stat` to confirm the changed-files set is exactly the ~90 JSON goldens plus the 6 new fixtures (+ 18 new goldens). Then targeted spot-checks: one UNION fixture (subtree migration), one EXCEPT fixture (ExceptMode added), one non-set-op fixture (pure rename/addition). Commit. + +### Decision 6: Formatter uses three parallel arms, each with an inner switch + +The chain in `SelectQuery.FormatSQL` (`parser/format.go:2342-2357`) becomes: +```go +if s.Union != nil { + formatter.Break() + switch s.UnionMode { + case UnionModeAll: + formatter.WriteString("UNION ALL") + case UnionModeDistinct: + formatter.WriteString("UNION DISTINCT") + default: + formatter.WriteString("UNION") + } + formatter.Break() + formatter.WriteExpr(s.Union) +} else if s.Except != nil { + formatter.Break() + switch s.ExceptMode { + case ExceptModeAll: + formatter.WriteString("EXCEPT ALL") + case ExceptModeDistinct: + formatter.WriteString("EXCEPT DISTINCT") + default: + formatter.WriteString("EXCEPT") + } + formatter.Break() + formatter.WriteExpr(s.Except) +} else if s.Intersect != nil { + formatter.Break() + switch s.IntersectMode { + case IntersectModeAll: + formatter.WriteString("INTERSECT ALL") + case IntersectModeDistinct: + formatter.WriteString("INTERSECT DISTINCT") + default: + formatter.WriteString("INTERSECT") + } + formatter.Break() + formatter.WriteExpr(s.Intersect) +} +``` + +The three arms are visibly parallel. A more aggressive consolidation (e.g. extracting `formatSetOp(formatter, op string, mode string, right *SelectQuery)`) would reduce duplication but obscure the per-operator structure that JSON-golden reviewers will look for. The three-arm shape stays. + +### Decision 7: Drop the "expected ALL or DISTINCT" error path on UNION; ensure bare EXCEPT and bare INTERSECT do not regress + +Today's parser explicitly enforces that `UNION` is followed by one of two keywords. After this change, the absence of `ALL`/`DISTINCT` simply means "bare form" — assign `UnionModeNone`, recurse, store in `Union`. The same logic applies to EXCEPT and INTERSECT. + +**Implication for error messages.** A SQL like `SELECT 1 UNION` followed immediately by `;` or EOF — which today errors with "expected ALL or DISTINCT, got " — will after this change error one level deeper, inside the recursive `parseSelectQuery`, with "expected SELECT, WITH or (, got ". The error still happens; the message just changes. `TestParser_InvalidSyntax` was spot-checked: it asserts only `require.Error(t, err, …)` with no message-content pinning, so this is safe. + +**Risk for EXCEPT regression.** Today's `EXCEPT` branch already accepts the bare form (which is the only form it supports). After this change, the branch accepts bare/`ALL`/`DISTINCT`. Bare EXCEPT MUST continue to parse identically — the existing `select_with_multi_except.sql` fixture is the regression guard. The new branch, structurally, is `KeywordExcept → optional modifier consumption → recurse → assign`, where "optional modifier consumption" returns `""` for bare. The behaviour for bare EXCEPT is unchanged; only the `ExceptMode` field is new (and is `ExceptModeNone == ""` for bare cases). + +### Decision 8: Mixed-operator precedence is NOT addressed by this change + +ClickHouse's documented set-operator precedence is: +- `INTERSECT` binds tighter than `UNION` and `EXCEPT`. +- `UNION` and `EXCEPT` have equal precedence and are evaluated left-to-right. + +The current parser uses right-recursion: `a UNION b UNION c` is parsed as `SelectQuery{a, Union: SelectQuery{b, Union: SelectQuery{c}}}` — right-associative. For same-modifier `UNION ALL` chains this is harmless (the operator is associative). For mixed-operator chains the right-recursion breaks ClickHouse's semantics: + +| SQL | ClickHouse precedence | Right-recursion (this change) | Correct? | +| -------------------------------- | ------------------------ | ----------------------------- | -------- | +| `a UNION ALL b INTERSECT c` | `a UNION ALL (b ∩ c)` | `a UNION ALL (b ∩ c)` | ✅ (by luck) | +| `a INTERSECT b UNION ALL c` | `(a ∩ b) UNION ALL c` | `a INTERSECT (b UNION ALL c)` | ❌ | +| `a UNION ALL b EXCEPT c` | `(a UNION ALL b) EXCEPT c` | `a UNION ALL (b EXCEPT c)` | ❌ | +| `a EXCEPT b UNION ALL c` | `(a EXCEPT b) UNION ALL c` | `a EXCEPT (b UNION ALL c)` | ❌ | + +This is a pre-existing limitation (the EXCEPT/UNION mis-association already exists today). Adding INTERSECT does not fix it; in fact INTERSECT adds new wrong-precedence cases too. + +**Decision: ship the additive feature work without fixing precedence.** Fixing precedence requires either: +1. A left-associative chain refactor: replace the per-`SelectQuery` pointer fields with an ordered slice of `(operator, mode, query)` entries, processed left-to-right with a per-operator precedence lookup. +2. A precedence-climbing rewrite of `parseSelectQuery`: introduce a precedence-aware loop where `INTERSECT` is parsed at higher precedence than `UNION`/`EXCEPT`. + +Both are structural changes that warrant their own design and their own JSON-golden regen. Bundling either into this change would multiply the migration surface without obvious benefit — and the present (broken) right-recursion is what every existing fixture and every consumer expects, so changing it under the hood would be a more invasive break than the field rename. + +**What this change DOES guarantee:** every same-operator-same-mode chain (e.g. `a UNION ALL b UNION ALL c`, `a INTERSECT b INTERSECT c`) is associatively unambiguous and round-trips correctly. Mixed-operator/mixed-mode chains MAY mis-associate per the table above. + +**What it does NOT guarantee:** correctness of any expression whose meaning depends on cross-operator precedence. + +This limitation MUST be called out in the proposal's "Impact" section so future readers know which cases were knowingly left for later. A follow-up change is anticipated to address precedence; its design will be informed by usage patterns observed after this change ships. + +### Decision 9: SETTINGS placement — per-leg only + +ClickHouse parses a trailing `SETTINGS` clause as belonging to the immediately preceding SELECT, not to the set-op as a whole. The existing `parseSelectStmt` already handles this correctly: each call parses its own optional SETTINGS before returning, and `parseSelectQuery`'s set-op recursion fires only on `UNION`/`EXCEPT`/`INTERSECT`, not on `SETTINGS`. So `SELECT 1 SETTINGS x=1 INTERSECT SELECT 2 SETTINGS y=2` produces two `SelectQuery` nodes each with their own `Settings` populated — no parser change needed for the SETTINGS path. The tests in this change verify this end-to-end for UNION and INTERSECT (EXCEPT-with-SETTINGS is symmetric and not separately fixtured). + +### Decision 10: Test coverage spans 11 inline cases and 6 golden fixtures + +Inline tests give a fast, readable per-form signal. Golden fixtures lock in the exact parse + format + beautify shape, which is what regression-protects round-tripping. Both layers are added: + +- Inline (`TestParser_With_SetOperators`): 11 SQLs covering all 9 cells of the {UNION/EXCEPT/INTERSECT} × {bare/ALL/DISTINCT} matrix plus 2 SETTINGS combinations. Parse-only — `ParseStmts` must succeed. +- Golden fixtures (focused on the newly-unlocked surface forms, plus the SETTINGS combination the user explicitly requested): + - `select_with_bare_union.sql` — bare UNION (newly accepted). + - `select_with_union_settings.sql` — bare UNION with per-leg SETTINGS (the use case observability tooling cares about most). + - `select_with_except_all.sql` — EXCEPT ALL (newly accepted). + - `select_with_except_distinct.sql` — EXCEPT DISTINCT (newly accepted). + - `select_with_intersect.sql` — bare INTERSECT (newly accepted; covers the new keyword path). + - `select_with_intersect_modifiers.sql` — chained `INTERSECT ALL` + `INTERSECT DISTINCT` (covers both INTERSECT modifiers in one fixture, also locks in same-operator chaining for INTERSECT). + +Existing fixtures (`select_with_union_distinct.sql`, `select_with_multi_union.sql`, `select_with_multi_union_distinct.sql`, `select_with_multi_except.sql`) continue to cover their respective forms — their JSON goldens are regenerated, their format/beautify goldens stay byte-identical. + +## Risks / Trade-offs + +- **Risk: breaking AST API change.** Any external consumer that pattern-matches `UnionAll` or `UnionDistinct` will fail to compile after this change. **Mitigation:** verified by grep — no consumer outside `parser/` exists in this repo. Internal call sites are migrated in lockstep within the same commit. External consumers (out-of-tree) need to be flagged in release notes. +- **Risk: the JSON regen accidentally masks an unrelated drift.** **Mitigation:** Decision 5's workflow — `-update`, then `git diff --stat` to confirm the changed-files set is exactly the expected ~90 JSON goldens + the 6 new fixtures, then targeted spot-checks on three representative goldens (UNION subtree migration, EXCEPT mode addition, non-set-op pure rename/addition). +- **Risk: error-message change for invalid SQL after `UNION`/`EXCEPT`/`INTERSECT`.** Today `SELECT 1 UNION ` errors with "expected ALL or DISTINCT"; after this change it errors with "expected SELECT, WITH or (". **Mitigation:** Decision 7 — `TestParser_InvalidSyntax` uses `require.Error` without message assertions. +- **Risk: bare `EXCEPT` regression.** Today's bare EXCEPT parses successfully; the new shape must preserve that. **Mitigation:** the existing `select_with_multi_except.sql` golden is the regression guard; the new branch is structurally a superset of the old branch's behaviour for bare-mode input. +- **Risk: shadowing of `INTERSECT` as an identifier.** Adding `KeywordIntersect = "INTERSECT"` to the keyword list makes it a reserved word; any pre-existing test or fixture that used `INTERSECT` as a column or table name would break. **Mitigation:** verified by grep (`grep -rn -i intersect parser/testdata/`) — no fixture uses `INTERSECT` as an identifier in this repo. If any future fixture needs to use it as an identifier, the fix is to backtick-quote it. +- **Trade-off: mixed-operator precedence is knowingly left broken.** See Decision 8. A follow-up change will address it. +- **Trade-off: three near-duplicate type declarations and three near-duplicate format arms.** See Decisions 3 and 6. Consolidation is reversible; over-consolidating now is not. + +## Migration Plan + +Single commit, no external dependencies, no data or config involvement. The keyword addition, AST refactor + additions, walk update, formatter rewrite, parser rewrite, JSON-golden regen, and new test inputs all land together — the build won't compile in any intermediate state, so staging the work in sub-commits would not be ergonomic. Rollback is `git revert`. The ~90 JSON goldens shift by a structured field-rename + three-field-addition pattern (zero-line delta on the rename, +3 lines on the addition per `SelectQuery` rendering) as part of the commit; the four set-op-populated fixtures additionally migrate or gain populated subtrees; the format/beautify goldens for pre-existing fixtures remain byte-identical; six new fixtures and their eighteen new goldens are committed alongside. + +After this change ships, the anticipated follow-up is precedence correctness for mixed-operator chains (Decision 8). That follow-up's design will likely require revisiting the right-recursive pointer model itself. diff --git a/openspec/changes/archive/2026-06-02-add-set-operator-modes/proposal.md b/openspec/changes/archive/2026-06-02-add-set-operator-modes/proposal.md new file mode 100644 index 00000000..cb9f6f5a --- /dev/null +++ b/openspec/changes/archive/2026-06-02-add-set-operator-modes/proposal.md @@ -0,0 +1,85 @@ +## Why + +ClickHouse SQL supports three set operators between SELECT queries: `UNION`, `EXCEPT`, and `INTERSECT`. Each accepts an optional `ALL` or `DISTINCT` modifier; without a modifier (the "bare" form) ClickHouse resolves the semantics at execution time via the `union_default_mode` / `except_default_mode` / `intersect_default_mode` settings. The matrix is nine surface forms — three operators × three modifier modes (bare, `ALL`, `DISTINCT`). + +This parser today supports five of those nine: + +| Operator | bare | ALL | DISTINCT | +| ----------- | ------------- | ------------------ | ------------------ | +| `UNION` | ❌ error | ✅ `UnionAll` | ✅ `UnionDistinct` | +| `EXCEPT` | ✅ `Except` | ❌ error | ❌ error | +| `INTERSECT` | ❌ unknown keyword | ❌ unknown keyword | ❌ unknown keyword | + +The current AST encodes each supported form as its own optional pointer field on `SelectQuery` (`UnionAll`, `UnionDistinct`, `Except`). Filling in the remaining four forms by adding four more pointer fields would balloon the field set to seven near-identical optional pointers on a single struct — the wrong direction. The set-operator-modifier dimension is naturally a discriminator, not a pointer-per-value. + +This change refactors all three operators to a uniform shape: one pointer (`Union` / `Except` / `Intersect`) for the right-hand side, paired with a typed discriminator (`UnionMode` / `ExceptMode` / `IntersectMode`) for the modifier. The shape mirrors the existing `OrderDirection` precedent in `parser/ast.go:3-9` (typed string alias, empty-string sentinel for "absent", uppercase keyword values for the explicit cases). The parser, walker, and formatter are migrated together, and the four missing surface forms are unlocked along the way. + +## What Changes + +**Refactor (AST-shape change):** +- Three new typed aliases are added to `parser/ast.go` alongside `OrderDirection`: + - `type UnionMode string` with `UnionModeNone = ""`, `UnionModeAll = "ALL"`, `UnionModeDistinct = "DISTINCT"`. + - `type ExceptMode string` with `ExceptModeNone = ""`, `ExceptModeAll = "ALL"`, `ExceptModeDistinct = "DISTINCT"`. + - `type IntersectMode string` with `IntersectModeNone = ""`, `IntersectModeAll = "ALL"`, `IntersectModeDistinct = "DISTINCT"`. +- The `SelectQuery` struct (`parser/ast.go` ~line 5129): + - **REMOVES** the existing fields `UnionAll *SelectQuery` and `UnionDistinct *SelectQuery`. + - **ADDS** `Union *SelectQuery` and `UnionMode UnionMode` (in the same struct-position the removed fields occupied). + - **KEEPS** the existing `Except *SelectQuery` field, and **ADDS** a companion `ExceptMode ExceptMode` immediately after it. + - **ADDS** two new fields at the end of the set-op block: `Intersect *SelectQuery` and `IntersectMode IntersectMode`. +- Per-node invariant (unchanged in spirit): at most one of `Union`, `Except`, `Intersect` is non-nil. When a pointer is nil, its companion `*Mode` is the zero value and not meaningful. When a pointer is non-nil, its companion is one of the three constants. +- `SelectQuery.Accept()` and `Walk()` collapse the prior pair of UNION traversal blocks into one, keep one EXCEPT traversal block (mode-agnostic — the recursion target is unchanged), and gain one new INTERSECT traversal block. +- `SelectQuery.FormatSQL()` in `parser/format.go` becomes a three-arm chain (`if s.Union != nil { … } else if s.Except != nil { … } else if s.Intersect != nil { … }`), each arm using an inner `switch` on the mode discriminator to choose the emitted keyword sequence (`UNION` / `UNION ALL` / `UNION DISTINCT`, analogous for the other two). + +**New keyword:** +- `KeywordIntersect = "INTERSECT"` is added to `parser/keyword.go` in both the constant block and the keyword slice (alphabetically between `Interpolate` and `Into`). + +**Parser feature changes in `parser/parser_query.go`:** +- The `parseSelectQuery` switch is rewritten so that each of `UNION` / `EXCEPT` / `INTERSECT` follows the same shape: + 1. Consume the operator keyword. + 2. Optionally consume `ALL` or `DISTINCT`; default to the `*ModeNone` sentinel. + 3. Recurse via `parseSelectQuery` into the right-hand side. + 4. Store the result and the mode on the parent `SelectQuery`. +- The existing "expected ALL or DISTINCT" error on bare `UNION` is gone; the bare form is now accepted for all three operators. + +**Out of scope:** +- Multi-operator precedence (e.g. `a UNION ALL b INTERSECT c` per ClickHouse's "INTERSECT binds tighter than UNION/EXCEPT" rule, or `a EXCEPT b UNION c` per left-to-right evaluation). The right-recursive pointer chain inherited from today's parser already mis-associates mixed-operator chains; this change does NOT fix that. It is a separate structural concern that requires either a left-associative chain refactor or a precedence-climbing rewrite of `parseSelectQuery`. See Decision 8 in `design.md`. +- Adding `omitempty` JSON tags. The repo's convention is explicit rendering (see the archived `add-describe-settings-clause` change's Decision 4). +- A new visitor method. `VisitSelectQuery` remains the only hook; recursion into UNION/EXCEPT/INTERSECT right-hand sides invokes it on the child. + +## Capabilities + +### New Capabilities +- `set-operator-modes`: Parse and format the full nine-cell matrix of `{UNION, EXCEPT, INTERSECT} × {bare, ALL, DISTINCT}` between SELECT queries. Each operator is represented on `SelectQuery` as one optional pointer to the right-hand side plus a typed mode discriminator. + +### Modified Capabilities + + +## Impact + +- **Code touched**: three new typed-alias declarations (with constants) at the top of `parser/ast.go`; field rename + additions on `SelectQuery`; collapsed/extended `Accept` block in `parser/ast.go`; collapsed/extended `Walk` block in `parser/walk.go`; rewritten set-op chain in `parser/format.go`; rewritten `parseSelectQuery` set-op switch in `parser/parser_query.go`; one new `KeywordIntersect` constant + slice entry in `parser/keyword.go`. **Breaking AST API change**: consumers that pattern-match `UnionAll` or `UnionDistinct` must migrate. +- **Internal call sites that must migrate (verified by grep across `parser/`)**: `parser/ast.go` (Accept), `parser/walk.go` (Walk), `parser/format.go` (FormatSQL), `parser/parser_query.go` (parseSelectQuery). No test file references either removed field directly. No file outside `parser/` references them. +- **JSON-golden footprint**: every JSON golden under `parser/testdata/**/output/*.sql.golden.json` that today renders `"UnionAll": null` (90 files) will change. Per `SelectQuery` rendering, the diff is: + - REMOVE: `"UnionAll": null,` and `"UnionDistinct": null,` (2 lines). + - ADD: `"Union": null,`, `"UnionMode": "",`, `"ExceptMode": "",`, `"Intersect": null,`, `"IntersectMode": ""` (5 lines). + - The pre-existing `"Except": null,` line stays. + Net delta: +3 lines per `SelectQuery` rendering. The four UNION/EXCEPT-using fixtures (`select_with_union_distinct.sql`, `select_with_multi_union.sql`, `select_with_multi_union_distinct.sql`, `select_with_multi_except.sql`) additionally have their populated subtree migrate to the new field name (UNION fixtures) or pick up a `"ExceptMode": ""` line at the populated EXCEPT node. +- **Format-golden footprint**: `parser/testdata/**/format/**` and `parser/testdata/**/format/beautify/**` goldens MUST remain byte-identical for every existing fixture. The formatter still emits exactly `UNION ALL`, `UNION DISTINCT`, and `EXCEPT` for the corresponding modes; only the source-of-truth fields have moved. +- **New inline test in `parser/parser_test.go`** — `TestParser_With_SetOperators` — exercises at minimum 11 SQLs spanning the full 3×3 matrix plus three SETTINGS combinations: + - `SELECT 1 UNION SELECT 2`, `SELECT 1 UNION ALL SELECT 2`, `SELECT 1 UNION DISTINCT SELECT 2` + - `SELECT 1 EXCEPT SELECT 2`, `SELECT 1 EXCEPT ALL SELECT 2`, `SELECT 1 EXCEPT DISTINCT SELECT 2` + - `SELECT 1 INTERSECT SELECT 2`, `SELECT 1 INTERSECT ALL SELECT 2`, `SELECT 1 INTERSECT DISTINCT SELECT 2` + - `SELECT 1 SETTINGS max_threads=1 UNION SELECT 2 SETTINGS max_threads=2` + - `SELECT 1 INTERSECT ALL SELECT 2 SETTINGS max_threads=2` + Five of these currently FAIL (the four formerly-unsupported surface forms plus the bare-UNION-with-SETTINGS combo); after this change all 11 PASS. +- **New `.sql` golden fixtures** under `parser/testdata/query/`, focused on the newly-unlocked surface forms. Six fixtures × 3 goldens each = 6 inputs + 18 new goldens: + - `select_with_bare_union.sql` — `SELECT 1 AS v UNION SELECT 2 AS v`. + - `select_with_union_settings.sql` — `SELECT 1 AS v SETTINGS max_threads = 1 UNION SELECT 2 AS v SETTINGS max_threads = 2`. + - `select_with_except_all.sql` — `SELECT 1 AS v EXCEPT ALL SELECT 2 AS v`. + - `select_with_except_distinct.sql` — `SELECT 1 AS v EXCEPT DISTINCT SELECT 2 AS v`. + - `select_with_intersect.sql` — `SELECT 1 AS v INTERSECT SELECT 2 AS v`. + - `select_with_intersect_modifiers.sql` — `SELECT 1 AS v INTERSECT ALL SELECT 2 AS v INTERSECT DISTINCT SELECT 3 AS v` (covers chained INTERSECT with both modifiers in one fixture). +- **No dependencies** added, no runtime semantics changed for SQL that previously parsed. +- **Behavioural compatibility with `*_default_mode` settings**: the parser stays syntactically permissive — it accepts the bare form regardless of any setting. ClickHouse rejects bare forms at execution time when the corresponding default-mode setting is empty; this change does not attempt to mirror that runtime behaviour. The parser's job is shape recognition, not semantic validation. +- **Known limitation — mixed-operator precedence is NOT fixed by this change**: the existing right-recursive pointer chain mis-associates mixed-operator chains by ClickHouse's precedence rules (INTERSECT binds tighter than UNION/EXCEPT; UNION/EXCEPT are left-to-right at equal precedence). After this change, `a UNION ALL b INTERSECT c` parses as `a UNION ALL (b INTERSECT c)` (correct by luck of right-recursion matching INTERSECT's higher precedence), but `a INTERSECT b UNION ALL c` parses as `a INTERSECT (b UNION ALL c)` (wrong — should be `(a INTERSECT b) UNION ALL c`). See `design.md` Decision 8. diff --git a/openspec/changes/archive/2026-06-02-add-set-operator-modes/specs/set-operator-modes/spec.md b/openspec/changes/archive/2026-06-02-add-set-operator-modes/specs/set-operator-modes/spec.md new file mode 100644 index 00000000..13816f00 --- /dev/null +++ b/openspec/changes/archive/2026-06-02-add-set-operator-modes/specs/set-operator-modes/spec.md @@ -0,0 +1,233 @@ +## ADDED Requirements + +### Requirement: AST SHALL model UNION, EXCEPT, and INTERSECT as three pointer-pairs of `, Mode` + +Three new typed aliases SHALL be added to `parser/ast.go`, each with three constants matching the `OrderDirection` precedent: + +```go +type UnionMode string +const ( + UnionModeNone UnionMode = "" + UnionModeAll UnionMode = "ALL" + UnionModeDistinct UnionMode = "DISTINCT" +) + +type ExceptMode string +const ( + ExceptModeNone ExceptMode = "" + ExceptModeAll ExceptMode = "ALL" + ExceptModeDistinct ExceptMode = "DISTINCT" +) + +type IntersectMode string +const ( + IntersectModeNone IntersectMode = "" + IntersectModeAll IntersectMode = "ALL" + IntersectModeDistinct IntersectMode = "DISTINCT" +) +``` + +The `SelectQuery` struct SHALL expose its set-operator right-hand side via three optional pointer-pairs: +- `Union *SelectQuery` + `UnionMode UnionMode` +- `Except *SelectQuery` + `ExceptMode ExceptMode` +- `Intersect *SelectQuery` + `IntersectMode IntersectMode` + +The legacy fields `UnionAll *SelectQuery` and `UnionDistinct *SelectQuery` SHALL be removed in the same change. The existing `Except *SelectQuery` field SHALL be retained and gain its companion `ExceptMode`. The new `Intersect *SelectQuery` and `IntersectMode IntersectMode` SHALL be added at the end of the set-op block. + +**Per-node invariant.** For each operator pair, when the pointer is nil the mode SHALL be the zero value (`*ModeNone`) and MUST NOT be depended on. When the pointer is non-nil the mode SHALL be exactly one of the three constants for that operator. At most one of `Union`, `Except`, `Intersect` SHALL be non-nil on any given `SelectQuery` node. + +#### Scenario: Three mode types exist with documented values +- **WHEN** a Go consumer imports the `parser` package after this change +- **THEN** `parser.UnionMode`, `parser.ExceptMode`, and `parser.IntersectMode` are each a string-typed alias AND each has exported `*None` (value `""`), `*All` (value `"ALL"`), `*Distinct` (value `"DISTINCT"`) constants of the corresponding type + +#### Scenario: SelectQuery exposes the new fields and not the old ones +- **WHEN** a Go consumer reflects on `parser.SelectQuery` after this change +- **THEN** the struct has fields `Union *SelectQuery`, `UnionMode UnionMode`, `Except *SelectQuery`, `ExceptMode ExceptMode`, `Intersect *SelectQuery`, `IntersectMode IntersectMode` AND does NOT have fields named `UnionAll` or `UnionDistinct` + +### Requirement: `INTERSECT` SHALL be a recognised keyword + +`parser/keyword.go` SHALL declare a new constant `KeywordIntersect = "INTERSECT"` and include it in the keyword-recognition slice. The keyword SHALL be ordered alphabetically among existing entries (between `KeywordInterpolate` and `KeywordInto`). + +#### Scenario: INTERSECT is recognised as a keyword token +- **WHEN** the lexer scans the literal text `INTERSECT` in a position where a keyword can appear +- **THEN** the resulting token matches `KeywordIntersect` AND the lexer does NOT treat the text as an identifier + +### Requirement: Parser SHALL accept all nine surface forms + +`parseSelectQuery` SHALL recognise each of `UNION`, `EXCEPT`, and `INTERSECT` followed optionally by `ALL` or `DISTINCT`. In all nine cases it SHALL recurse into `parseSelectQuery` for the right-hand side and store the result in the corresponding pointer field on the parent `SelectQuery`, setting the corresponding mode field to one of `*ModeNone` (bare), `*ModeAll`, or `*ModeDistinct`. + +#### Scenario: All three UNION forms populate Union with the correct mode +- **WHEN** `SELECT 1 UNION SELECT 2`, `SELECT 1 UNION ALL SELECT 2`, and `SELECT 1 UNION DISTINCT SELECT 2` are parsed +- **THEN** each `ParseStmts` returns no error AND each outer `*SelectQuery` has its `Union` field non-nil AND `UnionMode` equal to `UnionModeNone`, `UnionModeAll`, `UnionModeDistinct` respectively AND its `Except` and `Intersect` fields are nil + +#### Scenario: All three EXCEPT forms populate Except with the correct mode +- **WHEN** `SELECT 1 EXCEPT SELECT 2`, `SELECT 1 EXCEPT ALL SELECT 2`, and `SELECT 1 EXCEPT DISTINCT SELECT 2` are parsed +- **THEN** each `ParseStmts` returns no error AND each outer `*SelectQuery` has its `Except` field non-nil AND `ExceptMode` equal to `ExceptModeNone`, `ExceptModeAll`, `ExceptModeDistinct` respectively AND its `Union` and `Intersect` fields are nil + +#### Scenario: All three INTERSECT forms populate Intersect with the correct mode +- **WHEN** `SELECT 1 INTERSECT SELECT 2`, `SELECT 1 INTERSECT ALL SELECT 2`, and `SELECT 1 INTERSECT DISTINCT SELECT 2` are parsed +- **THEN** each `ParseStmts` returns no error AND each outer `*SelectQuery` has its `Intersect` field non-nil AND `IntersectMode` equal to `IntersectModeNone`, `IntersectModeAll`, `IntersectModeDistinct` respectively AND its `Union` and `Except` fields are nil + +#### Scenario: Bare UNION combined with per-leg SETTINGS +- **WHEN** `SELECT 1 SETTINGS max_threads=1 UNION SELECT 2 SETTINGS max_threads=2` is parsed +- **THEN** `ParseStmts` returns no error AND the outer `*SelectQuery` has both its `Settings` and `Union` non-nil AND `outer.UnionMode == UnionModeNone` AND the inner `*SelectQuery` (`outer.Union`) also has its `Settings` non-nil + +#### Scenario: INTERSECT ALL with trailing SETTINGS on the right leg +- **WHEN** `SELECT 1 INTERSECT ALL SELECT 2 SETTINGS max_threads=2` is parsed +- **THEN** `ParseStmts` returns no error AND the outer `*SelectQuery` has `Intersect` non-nil AND `outer.IntersectMode == IntersectModeAll` AND `outer.Intersect.Settings` is non-nil AND `outer.Settings` is nil + +#### Scenario: Bare EXCEPT continues to parse unchanged +- **WHEN** `SELECT number FROM numbers(1, 10) EXCEPT SELECT number FROM numbers(3, 6) EXCEPT SELECT number FROM numbers(8, 9)` (the existing `select_with_multi_except.sql` fixture) is parsed +- **THEN** `ParseStmts` returns no error AND the result has `Except` populated all the way down the chain with each level's `ExceptMode == ExceptModeNone` (bare form preserved) + +### Requirement: `SelectQuery.Accept()` SHALL traverse the new field shape + +`(*SelectQuery).Accept(visitor)` SHALL contain exactly three set-op traversal blocks, in order — `Union`, `Except`, `Intersect` — each gated on the corresponding pointer being non-nil. The function SHALL NOT reference `UnionAll` or `UnionDistinct`. + +#### Scenario: Visitor sees the UNION RHS regardless of mode +- **WHEN** a visitor traverses a `SelectQuery` whose `Union` is populated (any `UnionMode` value) +- **THEN** `VisitSelectQuery` is invoked on both the outer node and the RHS node (RHS as a child of outer) in pre-order traversal + +#### Scenario: Visitor sees the INTERSECT RHS +- **WHEN** a visitor traverses a `SelectQuery` whose `Intersect` is populated +- **THEN** `VisitSelectQuery` is invoked on the RHS node referenced by `outer.Intersect` as a child of the outer node + +#### Scenario: Visitor traversal skips empty set-op slots +- **WHEN** a visitor traverses a `SelectQuery` whose `Union`, `Except`, and `Intersect` are all nil +- **THEN** no traversal happens for any of the three slots; the rest of the traversal (`With`, `Top`, `SelectItems`, …, `Format`) is unchanged + +### Requirement: `Walk()` SHALL traverse the new field shape + +The `SelectQuery` case in `parser/walk.go`'s `Walk` function SHALL contain exactly one `Walk(n.Union, fn)` call, one `Walk(n.Except, fn)` call, and one `Walk(n.Intersect, fn)` call, in that order. It SHALL NOT reference `UnionAll` or `UnionDistinct`. + +#### Scenario: Walk visits each populated set-op RHS +- **WHEN** `Walk(outer, fn)` is invoked on a `SelectQuery` whose any one of `Union`, `Except`, `Intersect` is non-nil +- **THEN** `fn` is invoked at least once on the corresponding RHS subtree + +### Requirement: Formatter SHALL emit the correct keyword sequence for each operator and mode + +`SelectQuery.FormatSQL` SHALL contain three parallel set-op arms — `if s.Union != nil { … } else if s.Except != nil { … } else if s.Intersect != nil { … }` — each using an inner `switch` on its mode discriminator to select: +- `UNION ALL` / `UNION DISTINCT` / `UNION` for the Union arm +- `EXCEPT ALL` / `EXCEPT DISTINCT` / `EXCEPT` for the Except arm +- `INTERSECT ALL` / `INTERSECT DISTINCT` / `INTERSECT` for the Intersect arm + +Each arm SHALL use the same `Break` / `WriteString` / `Break` / `WriteExpr` shape used by the existing set-op formatter, so beautified output places the operator on its own line. + +#### Scenario: UNION ALL formats as `UNION ALL` +- **WHEN** a `SelectQuery` with `Union != nil` and `UnionMode == UnionModeAll` is formatted +- **THEN** the output contains the substring `UNION ALL` AND does NOT contain `UNION DISTINCT` + +#### Scenario: Bare UNION round-trips as `UNION` +- **WHEN** `SELECT 1 AS v UNION SELECT 2 AS v` is parsed and formatted +- **THEN** the output contains the substring `UNION` AND does NOT contain `UNION ALL` or `UNION DISTINCT` + +#### Scenario: EXCEPT ALL and EXCEPT DISTINCT round-trip with their modifiers +- **WHEN** `SELECT 1 AS v EXCEPT ALL SELECT 2 AS v` and `SELECT 1 AS v EXCEPT DISTINCT SELECT 2 AS v` are parsed and formatted +- **THEN** the outputs contain the substrings `EXCEPT ALL` and `EXCEPT DISTINCT` respectively (and not the other variant) + +#### Scenario: Bare EXCEPT continues to round-trip as `EXCEPT` +- **WHEN** the existing `select_with_multi_except.sql` is formatted +- **THEN** the output is byte-identical to its pre-change format golden — each `EXCEPT` keyword appears with no `ALL`/`DISTINCT` modifier + +#### Scenario: All three INTERSECT forms round-trip with the correct keyword sequence +- **WHEN** `SELECT 1 AS v INTERSECT SELECT 2 AS v`, `SELECT 1 AS v INTERSECT ALL SELECT 2 AS v`, and `SELECT 1 AS v INTERSECT DISTINCT SELECT 2 AS v` are parsed and formatted +- **THEN** the outputs contain `INTERSECT`, `INTERSECT ALL`, `INTERSECT DISTINCT` respectively (each exclusive of the other two) + +#### Scenario: Beautified output places each operator on its own line +- **WHEN** any of the six new golden fixtures is beautified +- **THEN** the beautified output contains a line whose trimmed contents are exactly the operator's emitted keyword sequence (`UNION`, `UNION ALL`, `EXCEPT ALL`, `EXCEPT DISTINCT`, `INTERSECT`, `INTERSECT ALL`, `INTERSECT DISTINCT`) + +#### Scenario: Existing format goldens unchanged for all four pre-existing set-op fixtures +- **WHEN** `TestParser_Format` and `TestParser_FormatBeautify` are run after this change against `select_with_union_distinct.sql`, `select_with_multi_union.sql`, `select_with_multi_union_distinct.sql`, and `select_with_multi_except.sql` +- **THEN** all eight golden files (four format, four beautify) match byte-for-byte without `-update` + +### Requirement: New `.sql` fixtures SHALL exercise the newly-unlocked surface forms end-to-end + +Six `.sql` fixtures SHALL be added under `parser/testdata/query/`. Each SHALL be exercised by `TestParser_ParseStatements`, `TestParser_Format`, and `TestParser_FormatBeautify`, with corresponding golden files committed under `output/`, `format/`, and `format/beautify/`: + +- `select_with_bare_union.sql` — `SELECT 1 AS v UNION SELECT 2 AS v` +- `select_with_union_settings.sql` — `SELECT 1 AS v SETTINGS max_threads = 1 UNION SELECT 2 AS v SETTINGS max_threads = 2` +- `select_with_except_all.sql` — `SELECT 1 AS v EXCEPT ALL SELECT 2 AS v` +- `select_with_except_distinct.sql` — `SELECT 1 AS v EXCEPT DISTINCT SELECT 2 AS v` +- `select_with_intersect.sql` — `SELECT 1 AS v INTERSECT SELECT 2 AS v` +- `select_with_intersect_modifiers.sql` — `SELECT 1 AS v INTERSECT ALL SELECT 2 AS v INTERSECT DISTINCT SELECT 3 AS v` + +#### Scenario: All six new fixtures flow through all three goldens +- **WHEN** the six fixtures listed above are added with their corresponding goldens under `output/`, `format/`, and `format/beautify/` +- **THEN** `go test ./parser/... -run 'TestParser_ParseStatements|TestParser_Format|TestParser_FormatBeautify' -count=1` passes without `-update` + +#### Scenario: The SETTINGS+UNION JSON golden shows per-leg SETTINGS +- **WHEN** `select_with_union_settings.sql.golden.json` is generated +- **THEN** the outer `*SelectQuery` has both `Settings` and `Union` non-nil, AND `outer.Union.Settings` is also non-nil + +### Requirement: Inline tests SHALL cover all nine surface forms and SETTINGS combinations + +A new test function `TestParser_With_SetOperators` SHALL be added to `parser/parser_test.go`, exercising at least these 11 SQL strings: +- bare/`ALL`/`DISTINCT` UNION (3 SQLs) +- bare/`ALL`/`DISTINCT` EXCEPT (3 SQLs) +- bare/`ALL`/`DISTINCT` INTERSECT (3 SQLs) +- bare UNION with per-leg SETTINGS (1 SQL) +- INTERSECT ALL with trailing SETTINGS on the right leg (1 SQL) + +Each SQL SHALL parse without error. + +#### Scenario: All nine matrix cells and both SETTINGS combinations parse +- **WHEN** `TestParser_With_SetOperators` is executed against the post-change parser +- **THEN** every SQL string in the test passes `require.NoError(t, err)` after `ParseStmts` + +### Requirement: Pre-existing JSON goldens SHALL be regenerated by a defined per-occurrence diff + +Every JSON golden under `parser/testdata/**/output/*.sql.golden.json` that today renders `"UnionAll": null` (90 files) SHALL be regenerated as part of this change. For each `SelectQuery` rendering inside such a file: +- The lines `"UnionAll": null,` and `"UnionDistinct": null,` SHALL be removed. +- The lines `"Union": null,`, `"UnionMode": "",`, `"ExceptMode": "",`, `"Intersect": null,`, and `"IntersectMode": ""` SHALL be added in the appropriate struct positions. +- The pre-existing `"Except": null,` line (or its populated counterpart) SHALL remain. + +For the four set-op-populated fixtures (`select_with_union_distinct.sql`, `select_with_multi_union.sql`, `select_with_multi_union_distinct.sql`, `select_with_multi_except.sql`): +- The UNION fixtures' previously-populated `"UnionAll": { … }` or `"UnionDistinct": { … }` subtree SHALL appear under the new `"Union"` key with byte-identical inner contents (modulo the same renames applied recursively to nested SelectQuery objects), and `"UnionMode"` SHALL render as `"ALL"` or `"DISTINCT"` at the SelectQuery node that owns the populated subtree. +- The EXCEPT fixture's `"Except": { … }` subtree SHALL appear at the same key with the same inner contents, and `"ExceptMode": ""` SHALL render at every populated EXCEPT node. + +The format and beautify goldens for the four set-op fixtures SHALL remain byte-identical. + +#### Scenario: Non-set-op JSON golden experiences a structured rename + addition +- **WHEN** `TestParser_ParseStatements/select_expr.sql` (any small SELECT golden without UNION/EXCEPT/INTERSECT) is run against the post-change parser and the golden is regenerated +- **THEN** at each SelectQuery rendering the diff against the pre-change golden consists of exactly two removed lines (`"UnionAll": null,` and `"UnionDistinct": null,`) and exactly five added lines (`"Union": null,`, `"UnionMode": "",`, `"ExceptMode": "",`, `"Intersect": null,`, `"IntersectMode": ""`) with no positional movement of other fields + +#### Scenario: UNION JSON golden migrates the populated subtree +- **WHEN** `TestParser_ParseStatements/select_with_union_distinct.sql` is run against the post-change parser and the golden is regenerated +- **THEN** the previously-populated `"UnionDistinct": { … }` subtree appears under `"Union"` AND `"UnionMode": "DISTINCT"` appears at the outer SelectQuery AND no other AST field has shifted + +#### Scenario: EXCEPT JSON golden gains the mode discriminator +- **WHEN** `TestParser_ParseStatements/select_with_multi_except.sql` is run against the post-change parser and the golden is regenerated +- **THEN** the `"Except": { … }` subtree stays at the same field AND `"ExceptMode": ""` appears at every populated EXCEPT node AND no other AST field has shifted (besides the per-SelectQuery rename + additions also applied here) + +#### Scenario: Pre-existing set-op format goldens unchanged +- **WHEN** `TestParser_Format` and `TestParser_FormatBeautify` are run after this change against `select_with_union_distinct.sql`, `select_with_multi_union.sql`, `select_with_multi_union_distinct.sql`, and `select_with_multi_except.sql` +- **THEN** all eight golden files (four format, four beautify) match byte-for-byte without `-update` + +### Requirement: Existing parser, lexer, and unrelated golden behaviour SHALL be preserved + +This change SHALL NOT alter the lexer (beyond the `KeywordIntersect` addition), SHALL NOT introduce or rename any visitor method, SHALL NOT modify `parseSelectStmt` or any helper involved in parsing optional clauses (`tryParseSettingsClause`, etc.), SHALL NOT add `omitempty` or `-` JSON tags to any existing or new field, and SHALL NOT cause any golden-file fixture whose AST does not contain a `SelectQuery` to drift. + +#### Scenario: Non-SelectQuery JSON goldens unchanged +- **WHEN** the full golden suite is run after this change +- **THEN** every JSON golden whose rendered AST does not contain a `SelectQuery` (e.g. DDL-only fixtures, ALTER fixtures) matches byte-for-byte without `-update` + +#### Scenario: TestParser_InvalidSyntax unchanged +- **WHEN** `TestParser_InvalidSyntax` is run after this change +- **THEN** the test passes with the same set of error inputs that pass today (note: the error *message* for `SELECT 1 UNION `-style inputs may change from "expected ALL or DISTINCT" to "expected SELECT, WITH or (", but `require.Error` is the only assertion, so this remains a passing test) + +#### Scenario: No existing fixture uses INTERSECT as an identifier +- **WHEN** the keyword `KeywordIntersect = "INTERSECT"` is added to `parser/keyword.go` +- **THEN** no pre-existing fixture under `parser/testdata/` parses differently because `INTERSECT` is now reserved (verified by grep before implementation; if a future fixture needs to use it as an identifier, the fix is backtick-quoting) + +### Requirement: Mixed-operator precedence is explicitly NOT a guarantee of this change + +This change SHALL NOT modify the right-recursive parsing model that `parseSelectQuery` inherits from today's implementation. Mixed-operator chains such as `a INTERSECT b UNION c` or `a UNION ALL b EXCEPT c` MAY parse with associativity that does not match ClickHouse's documented precedence rules (INTERSECT binds tighter than UNION/EXCEPT; UNION/EXCEPT are left-to-right at equal precedence). Fixing this is anticipated as a separate future change. + +#### Scenario: Same-operator chains are unambiguous +- **WHEN** `SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3`, `SELECT 1 INTERSECT ALL SELECT 2 INTERSECT DISTINCT SELECT 3`, or any same-operator multi-leg chain is parsed +- **THEN** each level of the chain has the same operator pointer populated with the correct per-level mode, and round-trip through the formatter yields the same operator-and-modifier sequence + +#### Scenario: Mixed-operator chains are NOT asserted to match ClickHouse precedence +- **WHEN** `SELECT 1 INTERSECT SELECT 2 UNION ALL SELECT 3` is parsed +- **THEN** `ParseStmts` returns no error (the SQL is syntactically accepted) but this change does NOT assert that the resulting AST corresponds to ClickHouse's `(SELECT 1 INTERSECT SELECT 2) UNION ALL SELECT 3` semantics; the right-recursive parse produces `SELECT 1 INTERSECT (SELECT 2 UNION ALL SELECT 3)`, and consumers MUST NOT rely on cross-operator precedence being correct until a follow-up change addresses it diff --git a/openspec/changes/archive/2026-06-02-add-set-operator-modes/tasks.md b/openspec/changes/archive/2026-06-02-add-set-operator-modes/tasks.md new file mode 100644 index 00000000..23d364d8 --- /dev/null +++ b/openspec/changes/archive/2026-06-02-add-set-operator-modes/tasks.md @@ -0,0 +1,251 @@ +## 1. Baseline + +- [x] 1.1 Add the new inline test `TestParser_With_SetOperators` to `parser/parser_test.go` (alongside the other `TestParser_With_*` helpers). Cover 11 SQLs spanning the full 3×3 matrix and the SETTINGS combinations: + - `SELECT 1 UNION SELECT 2` + - `SELECT 1 UNION ALL SELECT 2` + - `SELECT 1 UNION DISTINCT SELECT 2` + - `SELECT 1 EXCEPT SELECT 2` + - `SELECT 1 EXCEPT ALL SELECT 2` + - `SELECT 1 EXCEPT DISTINCT SELECT 2` + - `SELECT 1 INTERSECT SELECT 2` + - `SELECT 1 INTERSECT ALL SELECT 2` + - `SELECT 1 INTERSECT DISTINCT SELECT 2` + - `SELECT 1 SETTINGS max_threads=1 UNION SELECT 2 SETTINGS max_threads=2` + - `SELECT 1 INTERSECT ALL SELECT 2 SETTINGS max_threads=2` +- [x] 1.2 Confirm the currently-unsupported cases (bare UNION, EXCEPT ALL/DISTINCT, all three INTERSECT forms, the bare-UNION + SETTINGS combo) FAIL: `go test ./parser/... -run 'TestParser_With_SetOperators' -v -count=1`. Expected starting state: 5 SQLs PASS (UNION ALL/DISTINCT, bare EXCEPT, INTERSECT-ALL-with-trailing-SETTINGS errors at INTERSECT, the 11th case…) — wait, INTERSECT isn't yet a keyword so those will fail at the lexer-or-after-SELECT stage too. Treat the expected starting state as: only UNION ALL, UNION DISTINCT, and bare EXCEPT pass. Everything else FAILs. +- [x] 1.3 Confirm `INTERSECT` is not yet a recognised keyword: `grep -nE 'KeywordIntersect|"INTERSECT"' parser/keyword.go`. Expected: no matches. +- [x] 1.4 Confirm no current fixture uses `INTERSECT` as an identifier (would conflict with the new keyword): `grep -rn -iE '\bintersect\b' parser/testdata/ | grep -v _binary`. Expected: no fixture references INTERSECT (it has not been a keyword and isn't expected to appear). +- [x] 1.5 Capture the full test baseline: `go test ./parser/... -count=1 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.6 Snapshot three representative pre-change JSON goldens for spot-checking: + - `cp parser/testdata/query/output/select_with_union_distinct.sql.golden.json /tmp/select_with_union_distinct.before.json` — UNION fixture (subtree migration). + - `cp parser/testdata/query/output/select_with_multi_except.sql.golden.json /tmp/select_with_multi_except.before.json` — EXCEPT fixture (gains `ExceptMode`). + - `cp parser/testdata/query/output/select_expr.sql.golden.json /tmp/select_expr.before.json` — non-set-op fixture (pure rename + addition). + All three deleted in task 9.x. +- [x] 1.7 Confirm no source consumers exist outside `parser/`: `grep -rn -E 'UnionAll|UnionDistinct' . --include='*.go' | grep -v /parser/`. Expected: empty. If anything appears, STOP and add explicit migration tasks for those call sites. + +## 2. Keyword addition in `parser/keyword.go` + +- [x] 2.1 Add `KeywordIntersect = "INTERSECT"` to the constant declaration block, alphabetically between `KeywordInterpolate` and `KeywordInto` (around lines 115–116). +- [x] 2.2 Add `KeywordIntersect,` to the keyword slice, alphabetically between `KeywordInterpolate` and `KeywordInto` (around lines 374–375). +- [x] 2.3 `go build ./parser/...`. Expected: compiles. + +## 3. AST changes in `parser/ast.go` + +- [x] 3.1 Immediately after the existing `OrderDirection` block (after line 9), add three new typed aliases with their constants. Place all three in one grouped block for readability: + ```go + type UnionMode string + const ( + UnionModeNone UnionMode = "" + UnionModeAll UnionMode = "ALL" + UnionModeDistinct UnionMode = "DISTINCT" + ) + + type ExceptMode string + const ( + ExceptModeNone ExceptMode = "" + ExceptModeAll ExceptMode = "ALL" + ExceptModeDistinct ExceptMode = "DISTINCT" + ) + + type IntersectMode string + const ( + IntersectModeNone IntersectMode = "" + IntersectModeAll IntersectMode = "ALL" + IntersectModeDistinct IntersectMode = "DISTINCT" + ) + ``` +- [x] 3.2 In `type SelectQuery struct { … }` (around line 5129): + - **REMOVE** the fields `UnionAll *SelectQuery` and `UnionDistinct *SelectQuery`. + - **ADD** in their place (same struct position, between `Format` and `Except`): + ```go + Union *SelectQuery + UnionMode UnionMode + ``` + - **ADD** immediately after the existing `Except *SelectQuery` field: + ```go + ExceptMode ExceptMode + Intersect *SelectQuery + IntersectMode IntersectMode + ``` + Final block order: `Union`, `UnionMode`, `Except`, `ExceptMode`, `Intersect`, `IntersectMode`. +- [x] 3.3 In `func (s *SelectQuery) Accept(visitor ASTVisitor) error` (around line 5162): + - **REMOVE** the two traversal blocks for `s.UnionAll` and `s.UnionDistinct` (around lines 5237–5246). + - **REPLACE** with a single block: + ```go + if s.Union != nil { + if err := s.Union.Accept(visitor); err != nil { + return err + } + } + ``` + Position: same as the removed blocks (between the existing `Format` traversal and the existing `Except` traversal). + - The existing `s.Except` traversal block is kept verbatim. + - **ADD** after the existing `s.Except` traversal block: + ```go + if s.Intersect != nil { + if err := s.Intersect.Accept(visitor); err != nil { + return err + } + } + ``` +- [x] 3.4 Do not yet build — `walk.go`, `format.go`, `parser_query.go` still reference the old names. + +## 4. Walk update in `parser/walk.go` + +- [x] 4.1 Locate the `SelectQuery` case in `Walk` (around lines 66–69). + - **REMOVE** the `Walk(n.UnionAll, fn)` and `Walk(n.UnionDistinct, fn)` calls. + - **REPLACE** with a single `if !Walk(n.Union, fn) { return false }` (matching the existing local helper-call shape). + - Preserve the existing `Walk(n.Except, fn)` call verbatim, AND **ADD** a new `if !Walk(n.Intersect, fn) { return false }` immediately after it. +- [x] 4.2 Do not yet build — `format.go` and `parser_query.go` still reference the old fields. + +## 5. Formatter update in `parser/format.go` + +- [x] 5.1 Locate the set-op chain in `SelectQuery.FormatSQL` (around lines 2342–2357). Replace the entire chain (the `if s.UnionAll != nil` / `else if s.UnionDistinct != nil` / `else if s.Except != nil` block) with three parallel arms, one per operator, each using an inner `switch` on the mode: + ```go + if s.Union != nil { + formatter.Break() + switch s.UnionMode { + case UnionModeAll: + formatter.WriteString("UNION ALL") + case UnionModeDistinct: + formatter.WriteString("UNION DISTINCT") + default: // UnionModeNone — bare + formatter.WriteString("UNION") + } + formatter.Break() + formatter.WriteExpr(s.Union) + } else if s.Except != nil { + formatter.Break() + switch s.ExceptMode { + case ExceptModeAll: + formatter.WriteString("EXCEPT ALL") + case ExceptModeDistinct: + formatter.WriteString("EXCEPT DISTINCT") + default: // ExceptModeNone — bare + formatter.WriteString("EXCEPT") + } + formatter.Break() + formatter.WriteExpr(s.Except) + } else if s.Intersect != nil { + formatter.Break() + switch s.IntersectMode { + case IntersectModeAll: + formatter.WriteString("INTERSECT ALL") + case IntersectModeDistinct: + formatter.WriteString("INTERSECT DISTINCT") + default: // IntersectModeNone — bare + formatter.WriteString("INTERSECT") + } + formatter.Break() + formatter.WriteExpr(s.Intersect) + } + ``` +- [x] 5.2 Do not yet build — `parser_query.go` still references the old fields. + +## 6. Parser update in `parser/parser_query.go` + +- [x] 6.1 Add a small private helper near the top of the file (or alongside `parseSelectQuery`): + ```go + func (p *Parser) consumeOptionalSetOpModifier() string { + switch { + case p.tryConsumeKeywords(KeywordAll): + return "ALL" + case p.tryConsumeKeywords(KeywordDistinct): + return "DISTINCT" + default: + return "" + } + } + ``` +- [x] 6.2 Locate the `switch` block inside `parseSelectQuery` (around line 1008–1032). Rewrite the entire UNION arm AND the EXCEPT arm AND add a new INTERSECT arm so all three follow the same shape: + ```go + case p.tryConsumeKeywords(KeywordUnion): + mode := UnionMode(p.consumeOptionalSetOpModifier()) + next, err := p.parseSelectQuery(p.Pos()) + if err != nil { + return nil, err + } + selectStmt.Union = next + selectStmt.UnionMode = mode + case p.tryConsumeKeywords(KeywordExcept): + mode := ExceptMode(p.consumeOptionalSetOpModifier()) + next, err := p.parseSelectQuery(p.Pos()) + if err != nil { + return nil, err + } + selectStmt.Except = next + selectStmt.ExceptMode = mode + case p.tryConsumeKeywords(KeywordIntersect): + mode := IntersectMode(p.consumeOptionalSetOpModifier()) + next, err := p.parseSelectQuery(p.Pos()) + if err != nil { + return nil, err + } + selectStmt.Intersect = next + selectStmt.IntersectMode = mode + ``` + The previous `default: return nil, fmt.Errorf("expected ALL or DISTINCT, ...")` under the UNION arm is gone — its absence is the bare-UNION acceptance. +- [x] 6.3 `go build ./parser/...`. Expected: compiles. If references to `UnionAll`/`UnionDistinct` remain anywhere, the build will surface them — fix and re-run. +- [x] 6.4 `go vet ./parser/...`. Expected: no new warnings (the pre-existing `WriteByte` notice is acceptable). + +## 7. Verify behavioural fix and regenerate JSON goldens + +- [x] 7.1 `go test ./parser/... -run 'TestParser_With_SetOperators' -v -count=1`. Expected: all 11 cases now PASS. +- [x] 7.2 `go test ./parser/... -run 'TestParser_InvalidSyntax' -v -count=1`. Expected: PASS. +- [x] 7.3 `go test ./parser/... -run 'TestParser_ParseStatements' -count=1`. Expected: many failures — every SelectQuery-containing JSON golden references the old field shape. +- [x] 7.4 Regenerate: `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. The count of changed goldens should be approximately 90. +- [x] 7.6 Spot-check the UNION fixture diff: `diff /tmp/select_with_union_distinct.before.json parser/testdata/query/output/select_with_union_distinct.sql.golden.json`. Expected: the populated `UnionDistinct: { … }` subtree is now `Union: { … }` (same subtree contents at the new field name with the same recursive rename), `"UnionAll": null,` is gone, `"UnionDistinct": null,` is gone (or transformed at the inner SelectQuery), `"UnionMode": "DISTINCT"` is present at the outer SelectQuery, `"ExceptMode": ""` lines and `"Intersect": null, "IntersectMode": ""` lines appear at every SelectQuery rendering. No other field movement. +- [x] 7.7 Spot-check the EXCEPT fixture diff: `diff /tmp/select_with_multi_except.before.json parser/testdata/query/output/select_with_multi_except.sql.golden.json`. Expected: the populated `"Except": { … }` subtree stays at the same field, and each SelectQuery rendering gains `"UnionMode": ""`, `"ExceptMode": ""`, `"Intersect": null`, `"IntersectMode": ""` lines; `"UnionAll": null,` and `"UnionDistinct": null,` lines are removed and replaced by `"Union": null,` and `"UnionMode": "",`. No other field movement. +- [x] 7.8 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 lines removed (`"UnionAll": null,` and `"UnionDistinct": null,`) and exactly five lines added (`"Union": null,`, `"UnionMode": "",`, `"ExceptMode": "",`, `"Intersect": null,`, `"IntersectMode": ""`). Net delta +3 lines per rendering. No positional movement of other fields. +- [x] 7.9 Confirm format and beautify goldens did NOT shift for the four existing set-op fixtures: `git diff parser/testdata/query/format/select_with_union_distinct.sql parser/testdata/query/format/beautify/select_with_union_distinct.sql parser/testdata/query/format/select_with_multi_union.sql parser/testdata/query/format/beautify/select_with_multi_union.sql parser/testdata/query/format/select_with_multi_union_distinct.sql parser/testdata/query/format/beautify/select_with_multi_union_distinct.sql parser/testdata/query/format/select_with_multi_except.sql parser/testdata/query/format/beautify/select_with_multi_except.sql`. Expected: empty diff for all eight files. +- [x] 7.10 Run `go test ./parser/... -run 'TestParser_Format|TestParser_FormatBeautify' -count=1`. Expected: PASS — no format/beautify golden should have drifted. + +## 8. Add new `.sql` fixtures and goldens for the newly-unlocked surface forms + +- [x] 8.1 Create `parser/testdata/query/select_with_bare_union.sql` (single line): + ``` + SELECT 1 AS v UNION SELECT 2 AS v + ``` +- [x] 8.2 Create `parser/testdata/query/select_with_union_settings.sql` (single line): + ``` + SELECT 1 AS v SETTINGS max_threads = 1 UNION SELECT 2 AS v SETTINGS max_threads = 2 + ``` +- [x] 8.3 Create `parser/testdata/query/select_with_except_all.sql` (single line): + ``` + SELECT 1 AS v EXCEPT ALL SELECT 2 AS v + ``` +- [x] 8.4 Create `parser/testdata/query/select_with_except_distinct.sql` (single line): + ``` + SELECT 1 AS v EXCEPT DISTINCT SELECT 2 AS v + ``` +- [x] 8.5 Create `parser/testdata/query/select_with_intersect.sql` (single line): + ``` + SELECT 1 AS v INTERSECT SELECT 2 AS v + ``` +- [x] 8.6 Create `parser/testdata/query/select_with_intersect_modifiers.sql` (single line): + ``` + SELECT 1 AS v INTERSECT ALL SELECT 2 AS v INTERSECT DISTINCT SELECT 3 AS v + ``` +- [x] 8.7 Generate JSON goldens for all six new fixtures: `go test ./parser/... -run 'TestParser_ParseStatements/(select_with_bare_union|select_with_union_settings|select_with_except_all|select_with_except_distinct|select_with_intersect|select_with_intersect_modifiers)\.sql$' -count=1 -update`. **Visually inspect each generated JSON**: + - `select_with_bare_union.sql.golden.json` — outer `Union` non-nil, `UnionMode == ""`, `Except` nil, `Intersect` nil. + - `select_with_union_settings.sql.golden.json` — both outer and inner SelectQuery have `Settings` non-nil; outer `Union` non-nil; `UnionMode == ""`. + - `select_with_except_all.sql.golden.json` — outer `Except` non-nil, `ExceptMode == "ALL"`. + - `select_with_except_distinct.sql.golden.json` — outer `Except` non-nil, `ExceptMode == "DISTINCT"`. + - `select_with_intersect.sql.golden.json` — outer `Intersect` non-nil, `IntersectMode == ""`, `Union`/`Except` nil. + - `select_with_intersect_modifiers.sql.golden.json` — outer `Intersect` non-nil with `IntersectMode == "ALL"`, the inner SelectQuery (at `outer.Intersect`) has `Intersect` non-nil with `IntersectMode == "DISTINCT"`. +- [x] 8.8 Generate format goldens: `go test ./parser/... -run 'TestParser_Format/(select_with_bare_union|select_with_union_settings|select_with_except_all|select_with_except_distinct|select_with_intersect|select_with_intersect_modifiers)\.sql$' -count=1 -update`. **Visually inspect each**: + - bare-UNION format: exactly the token sequence `UNION` between the SELECTs — not `UNION ALL`, not `UNION DISTINCT`. + - SETTINGS+UNION format: preserves `SETTINGS max_threads = N` on both legs. + - EXCEPT ALL/DISTINCT: emits exactly `EXCEPT ALL` / `EXCEPT DISTINCT`. + - bare-INTERSECT: emits exactly `INTERSECT` (no modifier). + - chained-INTERSECT-modifiers: emits `INTERSECT ALL` then `INTERSECT DISTINCT` in order. +- [x] 8.9 Generate beautify goldens: `go test ./parser/... -run 'TestParser_FormatBeautify/(select_with_bare_union|select_with_union_settings|select_with_except_all|select_with_except_distinct|select_with_intersect|select_with_intersect_modifiers)\.sql$' -count=1 -update`. **Visually inspect** each beautified file for the same properties (each operator+modifier on its own line per the formatter's `Break` calls). +- [x] 8.10 Re-run all three without `-update`: `go test ./parser/... -run 'TestParser_ParseStatements|TestParser_Format|TestParser_FormatBeautify' -count=1`. All goldens (regenerated + new) must pass. + +## 9. Close out + +- [x] 9.1 `go test ./parser/... -count=1`. Compare against the baseline captured in 1.5: `TestParser_With_SetOperators` flips FAIL → PASS for all formerly-unsupported cases; ~90 pre-existing JSON goldens are regenerated (per-occurrence: two-line removal + five-line addition + one populated-subtree migration for set-op fixtures); six new fixtures × 3 goldens = 18 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 add-set-operator-modes` reports the change as valid. +- [x] 9.4 Delete the temporary snapshots from tasks 1.5/1.6: `rm /tmp/baseline-test-output.txt /tmp/select_with_union_distinct.before.json /tmp/select_with_multi_except.before.json /tmp/select_expr.before.json`. diff --git a/openspec/specs/set-operator-modes/spec.md b/openspec/specs/set-operator-modes/spec.md new file mode 100644 index 00000000..5d3b2484 --- /dev/null +++ b/openspec/specs/set-operator-modes/spec.md @@ -0,0 +1,237 @@ +## Purpose + +Parse and format the full nine-cell matrix of set operators between SELECT queries — `{UNION, EXCEPT, INTERSECT} × {bare, ALL, DISTINCT}` — in the ClickHouse SQL parser. Each operator is represented on `SelectQuery` as one optional pointer to the right-hand side plus a typed mode discriminator (`UnionMode` / `ExceptMode` / `IntersectMode`), mirroring the `OrderDirection` precedent (typed string alias with empty-string sentinel for "absent" and uppercase keyword values for the explicit cases). The shape unlocks the four surface forms the prior parser rejected — bare `UNION`, `EXCEPT ALL`, `EXCEPT DISTINCT`, and all three `INTERSECT` variants — while collapsing the prior pair of UNION pointer fields (`UnionAll`, `UnionDistinct`) into a single discriminated pair. + +## Requirements + +### Requirement: AST SHALL model UNION, EXCEPT, and INTERSECT as three pointer-pairs of `, Mode` + +Three new typed aliases SHALL be added to `parser/ast.go`, each with three constants matching the `OrderDirection` precedent: + +```go +type UnionMode string +const ( + UnionModeNone UnionMode = "" + UnionModeAll UnionMode = "ALL" + UnionModeDistinct UnionMode = "DISTINCT" +) + +type ExceptMode string +const ( + ExceptModeNone ExceptMode = "" + ExceptModeAll ExceptMode = "ALL" + ExceptModeDistinct ExceptMode = "DISTINCT" +) + +type IntersectMode string +const ( + IntersectModeNone IntersectMode = "" + IntersectModeAll IntersectMode = "ALL" + IntersectModeDistinct IntersectMode = "DISTINCT" +) +``` + +The `SelectQuery` struct SHALL expose its set-operator right-hand side via three optional pointer-pairs: +- `Union *SelectQuery` + `UnionMode UnionMode` +- `Except *SelectQuery` + `ExceptMode ExceptMode` +- `Intersect *SelectQuery` + `IntersectMode IntersectMode` + +The legacy fields `UnionAll *SelectQuery` and `UnionDistinct *SelectQuery` SHALL be removed in the same change. The existing `Except *SelectQuery` field SHALL be retained and gain its companion `ExceptMode`. The new `Intersect *SelectQuery` and `IntersectMode IntersectMode` SHALL be added at the end of the set-op block. + +**Per-node invariant.** For each operator pair, when the pointer is nil the mode SHALL be the zero value (`*ModeNone`) and MUST NOT be depended on. When the pointer is non-nil the mode SHALL be exactly one of the three constants for that operator. At most one of `Union`, `Except`, `Intersect` SHALL be non-nil on any given `SelectQuery` node. + +#### Scenario: Three mode types exist with documented values +- **WHEN** a Go consumer imports the `parser` package after this change +- **THEN** `parser.UnionMode`, `parser.ExceptMode`, and `parser.IntersectMode` are each a string-typed alias AND each has exported `*None` (value `""`), `*All` (value `"ALL"`), `*Distinct` (value `"DISTINCT"`) constants of the corresponding type + +#### Scenario: SelectQuery exposes the new fields and not the old ones +- **WHEN** a Go consumer reflects on `parser.SelectQuery` after this change +- **THEN** the struct has fields `Union *SelectQuery`, `UnionMode UnionMode`, `Except *SelectQuery`, `ExceptMode ExceptMode`, `Intersect *SelectQuery`, `IntersectMode IntersectMode` AND does NOT have fields named `UnionAll` or `UnionDistinct` + +### Requirement: `INTERSECT` SHALL be a recognised keyword + +`parser/keyword.go` SHALL declare a new constant `KeywordIntersect = "INTERSECT"` and include it in the keyword-recognition slice. The keyword SHALL be ordered alphabetically among existing entries (between `KeywordInterpolate` and `KeywordInto`). + +#### Scenario: INTERSECT is recognised as a keyword token +- **WHEN** the lexer scans the literal text `INTERSECT` in a position where a keyword can appear +- **THEN** the resulting token matches `KeywordIntersect` AND the lexer does NOT treat the text as an identifier + +### Requirement: Parser SHALL accept all nine surface forms + +`parseSelectQuery` SHALL recognise each of `UNION`, `EXCEPT`, and `INTERSECT` followed optionally by `ALL` or `DISTINCT`. In all nine cases it SHALL recurse into `parseSelectQuery` for the right-hand side and store the result in the corresponding pointer field on the parent `SelectQuery`, setting the corresponding mode field to one of `*ModeNone` (bare), `*ModeAll`, or `*ModeDistinct`. + +#### Scenario: All three UNION forms populate Union with the correct mode +- **WHEN** `SELECT 1 UNION SELECT 2`, `SELECT 1 UNION ALL SELECT 2`, and `SELECT 1 UNION DISTINCT SELECT 2` are parsed +- **THEN** each `ParseStmts` returns no error AND each outer `*SelectQuery` has its `Union` field non-nil AND `UnionMode` equal to `UnionModeNone`, `UnionModeAll`, `UnionModeDistinct` respectively AND its `Except` and `Intersect` fields are nil + +#### Scenario: All three EXCEPT forms populate Except with the correct mode +- **WHEN** `SELECT 1 EXCEPT SELECT 2`, `SELECT 1 EXCEPT ALL SELECT 2`, and `SELECT 1 EXCEPT DISTINCT SELECT 2` are parsed +- **THEN** each `ParseStmts` returns no error AND each outer `*SelectQuery` has its `Except` field non-nil AND `ExceptMode` equal to `ExceptModeNone`, `ExceptModeAll`, `ExceptModeDistinct` respectively AND its `Union` and `Intersect` fields are nil + +#### Scenario: All three INTERSECT forms populate Intersect with the correct mode +- **WHEN** `SELECT 1 INTERSECT SELECT 2`, `SELECT 1 INTERSECT ALL SELECT 2`, and `SELECT 1 INTERSECT DISTINCT SELECT 2` are parsed +- **THEN** each `ParseStmts` returns no error AND each outer `*SelectQuery` has its `Intersect` field non-nil AND `IntersectMode` equal to `IntersectModeNone`, `IntersectModeAll`, `IntersectModeDistinct` respectively AND its `Union` and `Except` fields are nil + +#### Scenario: Bare UNION combined with per-leg SETTINGS +- **WHEN** `SELECT 1 SETTINGS max_threads=1 UNION SELECT 2 SETTINGS max_threads=2` is parsed +- **THEN** `ParseStmts` returns no error AND the outer `*SelectQuery` has both its `Settings` and `Union` non-nil AND `outer.UnionMode == UnionModeNone` AND the inner `*SelectQuery` (`outer.Union`) also has its `Settings` non-nil + +#### Scenario: INTERSECT ALL with trailing SETTINGS on the right leg +- **WHEN** `SELECT 1 INTERSECT ALL SELECT 2 SETTINGS max_threads=2` is parsed +- **THEN** `ParseStmts` returns no error AND the outer `*SelectQuery` has `Intersect` non-nil AND `outer.IntersectMode == IntersectModeAll` AND `outer.Intersect.Settings` is non-nil AND `outer.Settings` is nil + +#### Scenario: Bare EXCEPT continues to parse unchanged +- **WHEN** `SELECT number FROM numbers(1, 10) EXCEPT SELECT number FROM numbers(3, 6) EXCEPT SELECT number FROM numbers(8, 9)` (the existing `select_with_multi_except.sql` fixture) is parsed +- **THEN** `ParseStmts` returns no error AND the result has `Except` populated all the way down the chain with each level's `ExceptMode == ExceptModeNone` (bare form preserved) + +### Requirement: `SelectQuery.Accept()` SHALL traverse the new field shape + +`(*SelectQuery).Accept(visitor)` SHALL contain exactly three set-op traversal blocks, in order — `Union`, `Except`, `Intersect` — each gated on the corresponding pointer being non-nil. The function SHALL NOT reference `UnionAll` or `UnionDistinct`. + +#### Scenario: Visitor sees the UNION RHS regardless of mode +- **WHEN** a visitor traverses a `SelectQuery` whose `Union` is populated (any `UnionMode` value) +- **THEN** `VisitSelectQuery` is invoked on both the outer node and the RHS node (RHS as a child of outer) in pre-order traversal + +#### Scenario: Visitor sees the INTERSECT RHS +- **WHEN** a visitor traverses a `SelectQuery` whose `Intersect` is populated +- **THEN** `VisitSelectQuery` is invoked on the RHS node referenced by `outer.Intersect` as a child of the outer node + +#### Scenario: Visitor traversal skips empty set-op slots +- **WHEN** a visitor traverses a `SelectQuery` whose `Union`, `Except`, and `Intersect` are all nil +- **THEN** no traversal happens for any of the three slots; the rest of the traversal (`With`, `Top`, `SelectItems`, …, `Format`) is unchanged + +### Requirement: `Walk()` SHALL traverse the new field shape + +The `SelectQuery` case in `parser/walk.go`'s `Walk` function SHALL contain exactly one `Walk(n.Union, fn)` call, one `Walk(n.Except, fn)` call, and one `Walk(n.Intersect, fn)` call, in that order. It SHALL NOT reference `UnionAll` or `UnionDistinct`. + +#### Scenario: Walk visits each populated set-op RHS +- **WHEN** `Walk(outer, fn)` is invoked on a `SelectQuery` whose any one of `Union`, `Except`, `Intersect` is non-nil +- **THEN** `fn` is invoked at least once on the corresponding RHS subtree + +### Requirement: Formatter SHALL emit the correct keyword sequence for each operator and mode + +`SelectQuery.FormatSQL` SHALL contain three parallel set-op arms — `if s.Union != nil { … } else if s.Except != nil { … } else if s.Intersect != nil { … }` — each using an inner `switch` on its mode discriminator to select: +- `UNION ALL` / `UNION DISTINCT` / `UNION` for the Union arm +- `EXCEPT ALL` / `EXCEPT DISTINCT` / `EXCEPT` for the Except arm +- `INTERSECT ALL` / `INTERSECT DISTINCT` / `INTERSECT` for the Intersect arm + +Each arm SHALL use the same `Break` / `WriteString` / `Break` / `WriteExpr` shape used by the existing set-op formatter, so beautified output places the operator on its own line. + +#### Scenario: UNION ALL formats as `UNION ALL` +- **WHEN** a `SelectQuery` with `Union != nil` and `UnionMode == UnionModeAll` is formatted +- **THEN** the output contains the substring `UNION ALL` AND does NOT contain `UNION DISTINCT` + +#### Scenario: Bare UNION round-trips as `UNION` +- **WHEN** `SELECT 1 AS v UNION SELECT 2 AS v` is parsed and formatted +- **THEN** the output contains the substring `UNION` AND does NOT contain `UNION ALL` or `UNION DISTINCT` + +#### Scenario: EXCEPT ALL and EXCEPT DISTINCT round-trip with their modifiers +- **WHEN** `SELECT 1 AS v EXCEPT ALL SELECT 2 AS v` and `SELECT 1 AS v EXCEPT DISTINCT SELECT 2 AS v` are parsed and formatted +- **THEN** the outputs contain the substrings `EXCEPT ALL` and `EXCEPT DISTINCT` respectively (and not the other variant) + +#### Scenario: Bare EXCEPT continues to round-trip as `EXCEPT` +- **WHEN** the existing `select_with_multi_except.sql` is formatted +- **THEN** the output is byte-identical to its pre-change format golden — each `EXCEPT` keyword appears with no `ALL`/`DISTINCT` modifier + +#### Scenario: All three INTERSECT forms round-trip with the correct keyword sequence +- **WHEN** `SELECT 1 AS v INTERSECT SELECT 2 AS v`, `SELECT 1 AS v INTERSECT ALL SELECT 2 AS v`, and `SELECT 1 AS v INTERSECT DISTINCT SELECT 2 AS v` are parsed and formatted +- **THEN** the outputs contain `INTERSECT`, `INTERSECT ALL`, `INTERSECT DISTINCT` respectively (each exclusive of the other two) + +#### Scenario: Beautified output places each operator on its own line +- **WHEN** any of the six new golden fixtures is beautified +- **THEN** the beautified output contains a line whose trimmed contents are exactly the operator's emitted keyword sequence (`UNION`, `UNION ALL`, `EXCEPT ALL`, `EXCEPT DISTINCT`, `INTERSECT`, `INTERSECT ALL`, `INTERSECT DISTINCT`) + +#### Scenario: Existing format goldens unchanged for all four pre-existing set-op fixtures +- **WHEN** `TestParser_Format` and `TestParser_FormatBeautify` are run after this change against `select_with_union_distinct.sql`, `select_with_multi_union.sql`, `select_with_multi_union_distinct.sql`, and `select_with_multi_except.sql` +- **THEN** all eight golden files (four format, four beautify) match byte-for-byte without `-update` + +### Requirement: New `.sql` fixtures SHALL exercise the newly-unlocked surface forms end-to-end + +Six `.sql` fixtures SHALL be added under `parser/testdata/query/`. Each SHALL be exercised by `TestParser_ParseStatements`, `TestParser_Format`, and `TestParser_FormatBeautify`, with corresponding golden files committed under `output/`, `format/`, and `format/beautify/`: + +- `select_with_bare_union.sql` — `SELECT 1 AS v UNION SELECT 2 AS v` +- `select_with_union_settings.sql` — `SELECT 1 AS v SETTINGS max_threads = 1 UNION SELECT 2 AS v SETTINGS max_threads = 2` +- `select_with_except_all.sql` — `SELECT 1 AS v EXCEPT ALL SELECT 2 AS v` +- `select_with_except_distinct.sql` — `SELECT 1 AS v EXCEPT DISTINCT SELECT 2 AS v` +- `select_with_intersect.sql` — `SELECT 1 AS v INTERSECT SELECT 2 AS v` +- `select_with_intersect_modifiers.sql` — `SELECT 1 AS v INTERSECT ALL SELECT 2 AS v INTERSECT DISTINCT SELECT 3 AS v` + +#### Scenario: All six new fixtures flow through all three goldens +- **WHEN** the six fixtures listed above are added with their corresponding goldens under `output/`, `format/`, and `format/beautify/` +- **THEN** `go test ./parser/... -run 'TestParser_ParseStatements|TestParser_Format|TestParser_FormatBeautify' -count=1` passes without `-update` + +#### Scenario: The SETTINGS+UNION JSON golden shows per-leg SETTINGS +- **WHEN** `select_with_union_settings.sql.golden.json` is generated +- **THEN** the outer `*SelectQuery` has both `Settings` and `Union` non-nil, AND `outer.Union.Settings` is also non-nil + +### Requirement: Inline tests SHALL cover all nine surface forms and SETTINGS combinations + +A new test function `TestParser_With_SetOperators` SHALL be added to `parser/parser_test.go`, exercising at least these 11 SQL strings: +- bare/`ALL`/`DISTINCT` UNION (3 SQLs) +- bare/`ALL`/`DISTINCT` EXCEPT (3 SQLs) +- bare/`ALL`/`DISTINCT` INTERSECT (3 SQLs) +- bare UNION with per-leg SETTINGS (1 SQL) +- INTERSECT ALL with trailing SETTINGS on the right leg (1 SQL) + +Each SQL SHALL parse without error. + +#### Scenario: All nine matrix cells and both SETTINGS combinations parse +- **WHEN** `TestParser_With_SetOperators` is executed against the post-change parser +- **THEN** every SQL string in the test passes `require.NoError(t, err)` after `ParseStmts` + +### Requirement: Pre-existing JSON goldens SHALL be regenerated by a defined per-occurrence diff + +Every JSON golden under `parser/testdata/**/output/*.sql.golden.json` that today renders `"UnionAll": null` (90 files) SHALL be regenerated as part of this change. For each `SelectQuery` rendering inside such a file: +- The lines `"UnionAll": null,` and `"UnionDistinct": null,` SHALL be removed. +- The lines `"Union": null,`, `"UnionMode": "",`, `"ExceptMode": "",`, `"Intersect": null,`, and `"IntersectMode": ""` SHALL be added in the appropriate struct positions. +- The pre-existing `"Except": null,` line (or its populated counterpart) SHALL remain. + +For the four set-op-populated fixtures (`select_with_union_distinct.sql`, `select_with_multi_union.sql`, `select_with_multi_union_distinct.sql`, `select_with_multi_except.sql`): +- The UNION fixtures' previously-populated `"UnionAll": { … }` or `"UnionDistinct": { … }` subtree SHALL appear under the new `"Union"` key with byte-identical inner contents (modulo the same renames applied recursively to nested SelectQuery objects), and `"UnionMode"` SHALL render as `"ALL"` or `"DISTINCT"` at the SelectQuery node that owns the populated subtree. +- The EXCEPT fixture's `"Except": { … }` subtree SHALL appear at the same key with the same inner contents, and `"ExceptMode": ""` SHALL render at every populated EXCEPT node. + +The format and beautify goldens for the four set-op fixtures SHALL remain byte-identical. + +#### Scenario: Non-set-op JSON golden experiences a structured rename + addition +- **WHEN** `TestParser_ParseStatements/select_expr.sql` (any small SELECT golden without UNION/EXCEPT/INTERSECT) is run against the post-change parser and the golden is regenerated +- **THEN** at each SelectQuery rendering the diff against the pre-change golden consists of exactly two removed lines (`"UnionAll": null,` and `"UnionDistinct": null,`) and exactly five added lines (`"Union": null,`, `"UnionMode": "",`, `"ExceptMode": "",`, `"Intersect": null,`, `"IntersectMode": ""`) with no positional movement of other fields + +#### Scenario: UNION JSON golden migrates the populated subtree +- **WHEN** `TestParser_ParseStatements/select_with_union_distinct.sql` is run against the post-change parser and the golden is regenerated +- **THEN** the previously-populated `"UnionDistinct": { … }` subtree appears under `"Union"` AND `"UnionMode": "DISTINCT"` appears at the outer SelectQuery AND no other AST field has shifted + +#### Scenario: EXCEPT JSON golden gains the mode discriminator +- **WHEN** `TestParser_ParseStatements/select_with_multi_except.sql` is run against the post-change parser and the golden is regenerated +- **THEN** the `"Except": { … }` subtree stays at the same field AND `"ExceptMode": ""` appears at every populated EXCEPT node AND no other AST field has shifted (besides the per-SelectQuery rename + additions also applied here) + +#### Scenario: Pre-existing set-op format goldens unchanged +- **WHEN** `TestParser_Format` and `TestParser_FormatBeautify` are run after this change against `select_with_union_distinct.sql`, `select_with_multi_union.sql`, `select_with_multi_union_distinct.sql`, and `select_with_multi_except.sql` +- **THEN** all eight golden files (four format, four beautify) match byte-for-byte without `-update` + +### Requirement: Existing parser, lexer, and unrelated golden behaviour SHALL be preserved + +This change SHALL NOT alter the lexer (beyond the `KeywordIntersect` addition), SHALL NOT introduce or rename any visitor method, SHALL NOT modify `parseSelectStmt` or any helper involved in parsing optional clauses (`tryParseSettingsClause`, etc.), SHALL NOT add `omitempty` or `-` JSON tags to any existing or new field, and SHALL NOT cause any golden-file fixture whose AST does not contain a `SelectQuery` to drift. + +#### Scenario: Non-SelectQuery JSON goldens unchanged +- **WHEN** the full golden suite is run after this change +- **THEN** every JSON golden whose rendered AST does not contain a `SelectQuery` (e.g. DDL-only fixtures, ALTER fixtures) matches byte-for-byte without `-update` + +#### Scenario: TestParser_InvalidSyntax unchanged +- **WHEN** `TestParser_InvalidSyntax` is run after this change +- **THEN** the test passes with the same set of error inputs that pass today (note: the error *message* for `SELECT 1 UNION `-style inputs may change from "expected ALL or DISTINCT" to "expected SELECT, WITH or (", but `require.Error` is the only assertion, so this remains a passing test) + +#### Scenario: No existing fixture uses INTERSECT as an identifier +- **WHEN** the keyword `KeywordIntersect = "INTERSECT"` is added to `parser/keyword.go` +- **THEN** no pre-existing fixture under `parser/testdata/` parses differently because `INTERSECT` is now reserved (verified by grep before implementation; if a future fixture needs to use it as an identifier, the fix is backtick-quoting) + +### Requirement: Mixed-operator precedence is explicitly NOT a guarantee of this change + +This change SHALL NOT modify the right-recursive parsing model that `parseSelectQuery` inherits from today's implementation. Mixed-operator chains such as `a INTERSECT b UNION c` or `a UNION ALL b EXCEPT c` MAY parse with associativity that does not match ClickHouse's documented precedence rules (INTERSECT binds tighter than UNION/EXCEPT; UNION/EXCEPT are left-to-right at equal precedence). Fixing this is anticipated as a separate future change. + +#### Scenario: Same-operator chains are unambiguous +- **WHEN** `SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3`, `SELECT 1 INTERSECT ALL SELECT 2 INTERSECT DISTINCT SELECT 3`, or any same-operator multi-leg chain is parsed +- **THEN** each level of the chain has the same operator pointer populated with the correct per-level mode, and round-trip through the formatter yields the same operator-and-modifier sequence + +#### Scenario: Mixed-operator chains are NOT asserted to match ClickHouse precedence +- **WHEN** `SELECT 1 INTERSECT SELECT 2 UNION ALL SELECT 3` is parsed +- **THEN** `ParseStmts` returns no error (the SQL is syntactically accepted) but this change does NOT assert that the resulting AST corresponds to ClickHouse's `(SELECT 1 INTERSECT SELECT 2) UNION ALL SELECT 3` semantics; the right-recursive parse produces `SELECT 1 INTERSECT (SELECT 2 UNION ALL SELECT 3)`, and consumers MUST NOT rely on cross-operator precedence being correct until a follow-up change addresses it diff --git a/parser/ast.go b/parser/ast.go index 06bb3b8b..77f5571d 100644 --- a/parser/ast.go +++ b/parser/ast.go @@ -8,6 +8,30 @@ const ( OrderDirectionDesc OrderDirection = "DESC" ) +type UnionMode string + +const ( + UnionModeNone UnionMode = "" + UnionModeAll UnionMode = "ALL" + UnionModeDistinct UnionMode = "DISTINCT" +) + +type ExceptMode string + +const ( + ExceptModeNone ExceptMode = "" + ExceptModeAll ExceptMode = "ALL" + ExceptModeDistinct ExceptMode = "DISTINCT" +) + +type IntersectMode string + +const ( + IntersectModeNone IntersectMode = "" + IntersectModeAll IntersectMode = "ALL" + IntersectModeDistinct IntersectMode = "DISTINCT" +) + type Expr interface { Pos() Pos End() Pos @@ -5112,9 +5136,12 @@ type SelectQuery struct { Limit *LimitClause Settings *SettingsClause Format *FormatClause - UnionAll *SelectQuery - UnionDistinct *SelectQuery + Union *SelectQuery + UnionMode UnionMode Except *SelectQuery + ExceptMode ExceptMode + Intersect *SelectQuery + IntersectMode IntersectMode } func (s *SelectQuery) Pos() Pos { @@ -5200,18 +5227,18 @@ func (s *SelectQuery) Accept(visitor ASTVisitor) error { return err } } - if s.UnionAll != nil { - if err := s.UnionAll.Accept(visitor); err != nil { + if s.Union != nil { + if err := s.Union.Accept(visitor); err != nil { return err } } - if s.UnionDistinct != nil { - if err := s.UnionDistinct.Accept(visitor); err != nil { + if s.Except != nil { + if err := s.Except.Accept(visitor); err != nil { return err } } - if s.Except != nil { - if err := s.Except.Accept(visitor); err != nil { + if s.Intersect != nil { + if err := s.Intersect.Accept(visitor); err != nil { return err } } diff --git a/parser/format.go b/parser/format.go index e3fd4a41..896b9041 100644 --- a/parser/format.go +++ b/parser/format.go @@ -2339,21 +2339,42 @@ func (s *SelectQuery) FormatSQL(formatter *Formatter) { formatter.Break() formatter.WriteExpr(s.Format) } - if s.UnionAll != nil { + if s.Union != nil { formatter.Break() - formatter.WriteString("UNION ALL") - formatter.Break() - formatter.WriteExpr(s.UnionAll) - } else if s.UnionDistinct != nil { - formatter.Break() - formatter.WriteString("UNION DISTINCT") + switch s.UnionMode { //nolint:exhaustive + case UnionModeAll: + formatter.WriteString("UNION ALL") + case UnionModeDistinct: + formatter.WriteString("UNION DISTINCT") + default: + formatter.WriteString("UNION") + } formatter.Break() - formatter.WriteExpr(s.UnionDistinct) + formatter.WriteExpr(s.Union) } else if s.Except != nil { formatter.Break() - formatter.WriteString("EXCEPT") + switch s.ExceptMode { //nolint:exhaustive + case ExceptModeAll: + formatter.WriteString("EXCEPT ALL") + case ExceptModeDistinct: + formatter.WriteString("EXCEPT DISTINCT") + default: + formatter.WriteString("EXCEPT") + } formatter.Break() formatter.WriteExpr(s.Except) + } else if s.Intersect != nil { + formatter.Break() + switch s.IntersectMode { //nolint:exhaustive + case IntersectModeAll: + formatter.WriteString("INTERSECT ALL") + case IntersectModeDistinct: + formatter.WriteString("INTERSECT DISTINCT") + default: + formatter.WriteString("INTERSECT") + } + formatter.Break() + formatter.WriteExpr(s.Intersect) } } diff --git a/parser/keyword.go b/parser/keyword.go index be1c4f10..25cef034 100644 --- a/parser/keyword.go +++ b/parser/keyword.go @@ -113,6 +113,7 @@ const ( KeywordInsert = "INSERT" KeywordInterval = "INTERVAL" KeywordInterpolate = "INTERPOLATE" + KeywordIntersect = "INTERSECT" KeywordInto = "INTO" KeywordIp = "IP" KeywordIs = "IS" @@ -372,6 +373,7 @@ var keywords = NewSet( KeywordInsert, KeywordInterval, KeywordInterpolate, + KeywordIntersect, KeywordInto, KeywordIp, KeywordIs, diff --git a/parser/parser_column.go b/parser/parser_column.go index feb1ee15..020a1b16 100644 --- a/parser/parser_column.go +++ b/parser/parser_column.go @@ -419,6 +419,8 @@ func (p *Parser) isSelectItemTerminatorKeyword() bool { return true case p.matchKeyword(KeywordExcept): return true + case p.matchKeyword(KeywordIntersect): + return true default: return false } @@ -848,7 +850,7 @@ func (p *Parser) parseSelectItem() (*SelectItem, error) { modifiers := make([]*FunctionExpr, 0) for { - if p.matchKeyword(KeywordExcept) { + if p.matchKeyword(KeywordExcept) && (p.peekTokenKind(TokenKindLParen) || p.peekTokenKind(TokenKindIdent)) { modifier, err := p.parseExceptExpr(p.Pos()) if err != nil { return nil, err diff --git a/parser/parser_query.go b/parser/parser_query.go index 682bfe2b..63dad82c 100644 --- a/parser/parser_query.go +++ b/parser/parser_query.go @@ -974,6 +974,17 @@ func (p *Parser) parseSubQuery(_ Pos) (*SubQuery, error) { }, nil } +func (p *Parser) consumeOptionalSetOpModifier() string { + switch { + case p.tryConsumeKeywords(KeywordAll): + return "ALL" + case p.tryConsumeKeywords(KeywordDistinct): + return "DISTINCT" + default: + return "" + } +} + func (p *Parser) parseSelectQuery(_ Pos) (*SelectQuery, error) { if !p.matchKeyword(KeywordSelect) && !p.matchKeyword(KeywordWith) && !p.matchTokenKind(TokenKindLParen) { return nil, fmt.Errorf("expected SELECT, WITH or (, got %s", p.lastTokenKind()) @@ -986,28 +997,29 @@ func (p *Parser) parseSelectQuery(_ Pos) (*SelectQuery, error) { } switch { case p.tryConsumeKeywords(KeywordUnion): - switch { - case p.tryConsumeKeywords(KeywordAll): - unionAllExpr, err := p.parseSelectQuery(p.Pos()) - if err != nil { - return nil, err - } - selectStmt.UnionAll = unionAllExpr - case p.tryConsumeKeywords(KeywordDistinct): - unionDistinctExpr, err := p.parseSelectQuery(p.Pos()) - if err != nil { - return nil, err - } - selectStmt.UnionDistinct = unionDistinctExpr - default: - return nil, fmt.Errorf("expected ALL or DISTINCT, got %s", p.lastTokenKind()) + mode := UnionMode(p.consumeOptionalSetOpModifier()) + next, err := p.parseSelectQuery(p.Pos()) + if err != nil { + return nil, err } + selectStmt.Union = next + selectStmt.UnionMode = mode case p.tryConsumeKeywords(KeywordExcept): - exceptExpr, err := p.parseSelectQuery(p.Pos()) + mode := ExceptMode(p.consumeOptionalSetOpModifier()) + next, err := p.parseSelectQuery(p.Pos()) + if err != nil { + return nil, err + } + selectStmt.Except = next + selectStmt.ExceptMode = mode + case p.tryConsumeKeywords(KeywordIntersect): + mode := IntersectMode(p.consumeOptionalSetOpModifier()) + next, err := p.parseSelectQuery(p.Pos()) if err != nil { return nil, err } - selectStmt.Except = exceptExpr + selectStmt.Intersect = next + selectStmt.IntersectMode = mode } if hasParen { if err := p.expectTokenKind(TokenKindRParen); err != nil { diff --git a/parser/parser_test.go b/parser/parser_test.go index 4e6e28bf..97dc1b94 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -308,6 +308,29 @@ func TestParser_With_VariableInSettings(t *testing.T) { } } +// Covers the full 3x3 matrix of set operators {UNION, EXCEPT, INTERSECT} times +// {bare, ALL, DISTINCT}, plus SETTINGS clauses combined with set ops. +func TestParser_With_SetOperators(t *testing.T) { + validSQLs := []string{ + "SELECT 1 UNION SELECT 2", + "SELECT 1 UNION ALL SELECT 2", + "SELECT 1 UNION DISTINCT SELECT 2", + "SELECT 1 EXCEPT SELECT 2", + "SELECT 1 EXCEPT ALL SELECT 2", + "SELECT 1 EXCEPT DISTINCT SELECT 2", + "SELECT 1 INTERSECT SELECT 2", + "SELECT 1 INTERSECT ALL SELECT 2", + "SELECT 1 INTERSECT DISTINCT SELECT 2", + "SELECT 1 SETTINGS max_threads=1 UNION SELECT 2 SETTINGS max_threads=2", + "SELECT 1 INTERSECT ALL SELECT 2 SETTINGS max_threads=2", + } + for _, sql := range validSQLs { + parser := NewParser(sql) + _, err := parser.ParseStmts() + require.NoError(t, err, "Failed to parse: %s", sql) + } +} + // 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 0ec9d658..a84b0613 100644 --- a/parser/testdata/basic/output/quantile_functions.sql.golden.json +++ b/parser/testdata/basic/output/quantile_functions.sql.golden.json @@ -119,8 +119,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file diff --git a/parser/testdata/ddl/output/bug_001.sql.golden.json b/parser/testdata/ddl/output/bug_001.sql.golden.json index 029d265f..d1616a6a 100644 --- a/parser/testdata/ddl/output/bug_001.sql.golden.json +++ b/parser/testdata/ddl/output/bug_001.sql.golden.json @@ -568,9 +568,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "Populate": false, 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 6d8f8437..fc0326c4 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 @@ -130,9 +130,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } } 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 7de4a72c..2ac5ebae 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 @@ -535,9 +535,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "Populate": false, 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 fa71349b..3be7be9a 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 @@ -242,9 +242,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "Populate": false, 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 ec66da9f..4f7bf4cf 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 @@ -353,9 +353,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "Populate": false, 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 8564946b..e078bd13 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 @@ -499,9 +499,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "AliasPos": 444, @@ -549,9 +552,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "Populate": true, 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 c228bf3f..e42132c1 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 @@ -142,9 +142,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "Populate": false, 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 f9c877ed..2974d0e1 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 @@ -208,9 +208,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "Populate": false, 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 95a7a7b5..e4e3e4f6 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 @@ -606,9 +606,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "Populate": false, 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 6a0b151a..b84b0978 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 @@ -153,9 +153,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "Populate": false, @@ -284,9 +287,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "Populate": false, 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 598cccef..68c172b1 100644 --- a/parser/testdata/ddl/output/create_or_replace.sql.golden.json +++ b/parser/testdata/ddl/output/create_or_replace.sql.golden.json @@ -475,9 +475,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } }, 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 c20613bf..73e5b458 100644 --- a/parser/testdata/ddl/output/create_view_basic.sql.golden.json +++ b/parser/testdata/ddl/output/create_view_basic.sql.golden.json @@ -149,9 +149,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } } 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 0dbba59a..cbfd7974 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 @@ -100,9 +100,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } } 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 09e05785..3c81af7e 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 @@ -163,9 +163,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } } diff --git a/parser/testdata/ddl/output/describe_subquery.sql.golden.json b/parser/testdata/ddl/output/describe_subquery.sql.golden.json index e1a1fffd..92dd7ff5 100644 --- a/parser/testdata/ddl/output/describe_subquery.sql.golden.json +++ b/parser/testdata/ddl/output/describe_subquery.sql.golden.json @@ -60,9 +60,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "HasFinal": false 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 436393e2..52649a33 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 @@ -116,9 +116,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } ] 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 8a7ddfad..4961e1d3 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 @@ -78,9 +78,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } ] \ No newline at end of file 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 7571df50..1e3e7f6a 100644 --- a/parser/testdata/dml/output/insert_with_select.sql.golden.json +++ b/parser/testdata/dml/output/insert_with_select.sql.golden.json @@ -107,9 +107,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } ] \ No newline at end of file diff --git a/parser/testdata/query/format/beautify/select_with_bare_union.sql b/parser/testdata/query/format/beautify/select_with_bare_union.sql new file mode 100644 index 00000000..e9c5ad87 --- /dev/null +++ b/parser/testdata/query/format/beautify/select_with_bare_union.sql @@ -0,0 +1,10 @@ +-- Origin SQL: +SELECT 1 AS v UNION SELECT 2 AS v + + +-- Beautify SQL: +SELECT + 1 AS v +UNION +SELECT + 2 AS v; diff --git a/parser/testdata/query/format/beautify/select_with_except_all.sql b/parser/testdata/query/format/beautify/select_with_except_all.sql new file mode 100644 index 00000000..b195d1f6 --- /dev/null +++ b/parser/testdata/query/format/beautify/select_with_except_all.sql @@ -0,0 +1,10 @@ +-- Origin SQL: +SELECT 1 AS v EXCEPT ALL SELECT 2 AS v + + +-- Beautify SQL: +SELECT + 1 AS v +EXCEPT ALL +SELECT + 2 AS v; diff --git a/parser/testdata/query/format/beautify/select_with_except_distinct.sql b/parser/testdata/query/format/beautify/select_with_except_distinct.sql new file mode 100644 index 00000000..426a479c --- /dev/null +++ b/parser/testdata/query/format/beautify/select_with_except_distinct.sql @@ -0,0 +1,10 @@ +-- Origin SQL: +SELECT 1 AS v EXCEPT DISTINCT SELECT 2 AS v + + +-- Beautify SQL: +SELECT + 1 AS v +EXCEPT DISTINCT +SELECT + 2 AS v; diff --git a/parser/testdata/query/format/beautify/select_with_intersect.sql b/parser/testdata/query/format/beautify/select_with_intersect.sql new file mode 100644 index 00000000..3500420c --- /dev/null +++ b/parser/testdata/query/format/beautify/select_with_intersect.sql @@ -0,0 +1,10 @@ +-- Origin SQL: +SELECT 1 AS v INTERSECT SELECT 2 AS v + + +-- Beautify SQL: +SELECT + 1 AS v +INTERSECT +SELECT + 2 AS v; diff --git a/parser/testdata/query/format/beautify/select_with_intersect_modifiers.sql b/parser/testdata/query/format/beautify/select_with_intersect_modifiers.sql new file mode 100644 index 00000000..4a32a9c0 --- /dev/null +++ b/parser/testdata/query/format/beautify/select_with_intersect_modifiers.sql @@ -0,0 +1,13 @@ +-- Origin SQL: +SELECT 1 AS v INTERSECT ALL SELECT 2 AS v INTERSECT DISTINCT SELECT 3 AS v + + +-- Beautify SQL: +SELECT + 1 AS v +INTERSECT ALL +SELECT + 2 AS v +INTERSECT DISTINCT +SELECT + 3 AS v; diff --git a/parser/testdata/query/format/beautify/select_with_union_settings.sql b/parser/testdata/query/format/beautify/select_with_union_settings.sql new file mode 100644 index 00000000..b8635565 --- /dev/null +++ b/parser/testdata/query/format/beautify/select_with_union_settings.sql @@ -0,0 +1,14 @@ +-- Origin SQL: +SELECT 1 AS v SETTINGS max_threads = 1 UNION SELECT 2 AS v SETTINGS max_threads = 2 + + +-- Beautify SQL: +SELECT + 1 AS v +SETTINGS + max_threads=1 +UNION +SELECT + 2 AS v +SETTINGS + max_threads=2; diff --git a/parser/testdata/query/format/select_with_bare_union.sql b/parser/testdata/query/format/select_with_bare_union.sql new file mode 100644 index 00000000..9c5a6636 --- /dev/null +++ b/parser/testdata/query/format/select_with_bare_union.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +SELECT 1 AS v UNION SELECT 2 AS v + + +-- Format SQL: +SELECT 1 AS v UNION SELECT 2 AS v; diff --git a/parser/testdata/query/format/select_with_except_all.sql b/parser/testdata/query/format/select_with_except_all.sql new file mode 100644 index 00000000..869b408a --- /dev/null +++ b/parser/testdata/query/format/select_with_except_all.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +SELECT 1 AS v EXCEPT ALL SELECT 2 AS v + + +-- Format SQL: +SELECT 1 AS v EXCEPT ALL SELECT 2 AS v; diff --git a/parser/testdata/query/format/select_with_except_distinct.sql b/parser/testdata/query/format/select_with_except_distinct.sql new file mode 100644 index 00000000..5b3142c1 --- /dev/null +++ b/parser/testdata/query/format/select_with_except_distinct.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +SELECT 1 AS v EXCEPT DISTINCT SELECT 2 AS v + + +-- Format SQL: +SELECT 1 AS v EXCEPT DISTINCT SELECT 2 AS v; diff --git a/parser/testdata/query/format/select_with_intersect.sql b/parser/testdata/query/format/select_with_intersect.sql new file mode 100644 index 00000000..0aa11571 --- /dev/null +++ b/parser/testdata/query/format/select_with_intersect.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +SELECT 1 AS v INTERSECT SELECT 2 AS v + + +-- Format SQL: +SELECT 1 AS v INTERSECT SELECT 2 AS v; diff --git a/parser/testdata/query/format/select_with_intersect_modifiers.sql b/parser/testdata/query/format/select_with_intersect_modifiers.sql new file mode 100644 index 00000000..ef171ff9 --- /dev/null +++ b/parser/testdata/query/format/select_with_intersect_modifiers.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +SELECT 1 AS v INTERSECT ALL SELECT 2 AS v INTERSECT DISTINCT SELECT 3 AS v + + +-- Format SQL: +SELECT 1 AS v INTERSECT ALL SELECT 2 AS v INTERSECT DISTINCT SELECT 3 AS v; diff --git a/parser/testdata/query/format/select_with_union_settings.sql b/parser/testdata/query/format/select_with_union_settings.sql new file mode 100644 index 00000000..04982cca --- /dev/null +++ b/parser/testdata/query/format/select_with_union_settings.sql @@ -0,0 +1,6 @@ +-- Origin SQL: +SELECT 1 AS v SETTINGS max_threads = 1 UNION SELECT 2 AS v SETTINGS max_threads = 2 + + +-- Format SQL: +SELECT 1 AS v SETTINGS max_threads=1 UNION SELECT 2 AS v SETTINGS max_threads=2; 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 7389e52a..9014b8e8 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 @@ -87,9 +87,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 37, @@ -636,8 +639,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 cd58cb0e..824fb167 100644 --- a/parser/testdata/query/output/create_window_view.sql.golden.json +++ b/parser/testdata/query/output/create_window_view.sql.golden.json @@ -212,9 +212,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } } 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 4101c122..a23277ef 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 @@ -154,9 +154,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "HasFinal": false @@ -295,8 +298,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 54fff726..eb7a50f7 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 @@ -146,8 +146,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 47e901ed..117add32 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 @@ -113,9 +113,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, "Alias": null } @@ -201,8 +204,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 0e089112..60411b23 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 @@ -116,8 +116,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file diff --git a/parser/testdata/query/output/select_cast.sql.golden.json b/parser/testdata/query/output/select_cast.sql.golden.json index fe1d9e54..066b9b7d 100644 --- a/parser/testdata/query/output/select_cast.sql.golden.json +++ b/parser/testdata/query/output/select_cast.sql.golden.json @@ -48,9 +48,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 36, @@ -98,9 +101,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 72, @@ -158,9 +164,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 104, @@ -209,8 +218,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 2e0cc7a7..8b42988c 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 @@ -34,9 +34,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 27, @@ -89,8 +92,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 7792625b..01e9ef0c 100644 --- a/parser/testdata/query/output/select_concat_expr.sql.golden.json +++ b/parser/testdata/query/output/select_concat_expr.sql.golden.json @@ -39,9 +39,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 19, @@ -93,9 +96,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 45, @@ -158,8 +164,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 c84d2eed..f35bb3bf 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 @@ -79,8 +79,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 c6bda7bf..bf642d00 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 @@ -153,8 +153,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file diff --git a/parser/testdata/query/output/select_expr.sql.golden.json b/parser/testdata/query/output/select_expr.sql.golden.json index 8146309a..f1e40332 100644 --- a/parser/testdata/query/output/select_expr.sql.golden.json +++ b/parser/testdata/query/output/select_expr.sql.golden.json @@ -41,8 +41,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 018dab2b..7bad66f4 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 @@ -446,8 +446,11 @@ }, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 dca2c2c8..7a0824de 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 @@ -87,9 +87,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 37, @@ -190,9 +193,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 75, @@ -351,8 +357,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 8422e5f6..09eeda48 100644 --- a/parser/testdata/query/output/select_json_type.sql.golden.json +++ b/parser/testdata/query/output/select_json_type.sql.golden.json @@ -88,9 +88,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 26, @@ -153,9 +156,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 78, @@ -203,9 +209,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 116, @@ -263,9 +272,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 159, @@ -329,9 +341,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 207, @@ -401,8 +416,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 3610a210..257cb20b 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 @@ -56,8 +56,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 fc4cb5d2..d7896de8 100644 --- a/parser/testdata/query/output/select_not_regexp.sql.golden.json +++ b/parser/testdata/query/output/select_not_regexp.sql.golden.json @@ -80,8 +80,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 d6768644..61ddef41 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 @@ -70,8 +70,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 973099df..d37a63a1 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 @@ -184,9 +184,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "HasFinal": false @@ -231,8 +234,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 17948c34..9f6a9d23 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 @@ -184,9 +184,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "HasFinal": false @@ -246,8 +249,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 c3ef367a..3f9f3a26 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 @@ -209,9 +209,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "HasFinal": false @@ -301,8 +304,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 9f416462..cd2fe9a3 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 @@ -185,9 +185,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "HasFinal": false @@ -251,8 +254,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 72799ba0..c2d57101 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 @@ -182,8 +182,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 87e7127f..aa1f454b 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 @@ -163,9 +163,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "HasFinal": false @@ -224,8 +227,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file diff --git a/parser/testdata/query/output/select_regexp.sql.golden.json b/parser/testdata/query/output/select_regexp.sql.golden.json index feae7037..8faf6693 100644 --- a/parser/testdata/query/output/select_regexp.sql.golden.json +++ b/parser/testdata/query/output/select_regexp.sql.golden.json @@ -80,8 +80,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file diff --git a/parser/testdata/query/output/select_simple.sql.golden.json b/parser/testdata/query/output/select_simple.sql.golden.json index 514213db..91a60ce2 100644 --- a/parser/testdata/query/output/select_simple.sql.golden.json +++ b/parser/testdata/query/output/select_simple.sql.golden.json @@ -431,8 +431,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 778940b2..5ee6bebd 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 @@ -82,8 +82,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 994ef312..234c86d5 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 @@ -182,8 +182,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 263c9fc6..9a358ce9 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 @@ -128,9 +128,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } ] @@ -219,8 +222,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 aaad6b83..44239cf6 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 @@ -131,8 +131,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 5adf2063..2f6d1ad5 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 @@ -220,8 +220,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 48028203..bb0dff9f 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 @@ -206,8 +206,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 b3c6fe37..416b1293 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 @@ -39,9 +39,12 @@ }, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 18, @@ -88,9 +91,12 @@ }, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 45, @@ -132,8 +138,11 @@ }, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 619fbf9b..6216ac6f 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 @@ -62,8 +62,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 6f4a9048..846944ca 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 @@ -67,9 +67,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, { @@ -133,9 +136,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } ] @@ -289,8 +295,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 34b5ee2b..0c10d9ad 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 @@ -163,8 +163,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 3be1c853..e47ad3c3 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 @@ -51,9 +51,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "Modifiers": [], @@ -163,9 +166,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, { @@ -201,8 +207,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 1d2611e6..29c8a8d1 100644 --- a/parser/testdata/query/output/select_when_condition.sql.golden.json +++ b/parser/testdata/query/output/select_when_condition.sql.golden.json @@ -54,8 +54,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 d455d5c5..1e0f2733 100644 --- a/parser/testdata/query/output/select_window_comprehensive.sql.golden.json +++ b/parser/testdata/query/output/select_window_comprehensive.sql.golden.json @@ -2254,8 +2254,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 a0b579d9..c8b60c2b 100644 --- a/parser/testdata/query/output/select_window_cte.sql.golden.json +++ b/parser/testdata/query/output/select_window_cte.sql.golden.json @@ -194,9 +194,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, { @@ -355,9 +358,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } ] @@ -604,8 +610,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 a76f64da..9db23557 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 @@ -154,8 +154,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 a01de528..5e933b4f 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 @@ -154,8 +154,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 351f896f..9fe9b057 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 @@ -285,8 +285,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 bf16b7a4..e77c94e1 100644 --- a/parser/testdata/query/output/select_window_params.sql.golden.json +++ b/parser/testdata/query/output/select_window_params.sql.golden.json @@ -499,8 +499,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 new file mode 100644 index 00000000..0f0b79e2 --- /dev/null +++ b/parser/testdata/query/output/select_with_bare_union.sql.golden.json @@ -0,0 +1,87 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 13, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 7, + "NumEnd": 8, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 12, + "NameEnd": 13 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "Union": { + "SelectPos": 20, + "StatementEnd": 33, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 27, + "NumEnd": 28, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 32, + "NameEnd": 33 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + }, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + } +] \ No newline at end of file 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 595c0120..1b41a642 100644 --- a/parser/testdata/query/output/select_with_distinct.sql.golden.json +++ b/parser/testdata/query/output/select_with_distinct.sql.golden.json @@ -90,8 +90,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 fb80f3ef..fea865f4 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 @@ -52,8 +52,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 3aa8d553..babf4e94 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 @@ -144,8 +144,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 014a1880..f3ea3145 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 @@ -75,8 +75,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 new file mode 100644 index 00000000..1bc6f79d --- /dev/null +++ b/parser/testdata/query/output/select_with_except_all.sql.golden.json @@ -0,0 +1,87 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 13, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 7, + "NumEnd": 8, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 12, + "NameEnd": 13 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": { + "SelectPos": 25, + "StatementEnd": 38, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 32, + "NumEnd": 33, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 37, + "NameEnd": 38 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + }, + "ExceptMode": "ALL", + "Intersect": null, + "IntersectMode": "" + } +] \ No newline at end of file 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 new file mode 100644 index 00000000..c952c765 --- /dev/null +++ b/parser/testdata/query/output/select_with_except_distinct.sql.golden.json @@ -0,0 +1,87 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 13, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 7, + "NumEnd": 8, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 12, + "NameEnd": 13 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": { + "SelectPos": 30, + "StatementEnd": 43, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 37, + "NumEnd": 38, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 42, + "NameEnd": 43 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + }, + "ExceptMode": "DISTINCT", + "Intersect": null, + "IntersectMode": "" + } +] \ No newline at end of file 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 65f86ba4..b25f1565 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 @@ -214,9 +214,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 174, @@ -327,8 +330,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file diff --git a/parser/testdata/query/output/select_with_intersect.sql.golden.json b/parser/testdata/query/output/select_with_intersect.sql.golden.json new file mode 100644 index 00000000..63d04044 --- /dev/null +++ b/parser/testdata/query/output/select_with_intersect.sql.golden.json @@ -0,0 +1,87 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 13, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 7, + "NumEnd": 8, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 12, + "NameEnd": 13 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": { + "SelectPos": 24, + "StatementEnd": 37, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 31, + "NumEnd": 32, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 36, + "NameEnd": 37 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + }, + "IntersectMode": "" + } +] \ No newline at end of file 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 new file mode 100644 index 00000000..7ee05644 --- /dev/null +++ b/parser/testdata/query/output/select_with_intersect_modifiers.sql.golden.json @@ -0,0 +1,129 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 13, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 7, + "NumEnd": 8, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 12, + "NameEnd": 13 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": { + "SelectPos": 28, + "StatementEnd": 41, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 35, + "NumEnd": 36, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 40, + "NameEnd": 41 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": { + "SelectPos": 61, + "StatementEnd": 74, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 68, + "NumEnd": 69, + "Literal": "3", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 73, + "NameEnd": 74 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": null, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + }, + "IntersectMode": "DISTINCT" + }, + "IntersectMode": "ALL" + } +] \ No newline at end of file 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 2cc85c6f..a437143b 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 @@ -103,8 +103,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 d63d1fa9..85de40ab 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 @@ -224,8 +224,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 e7fe2041..3858050e 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 @@ -42,9 +42,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 23, @@ -112,8 +115,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 754af02d..0ef9e9d6 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 @@ -50,9 +50,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, { @@ -99,9 +102,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } ] @@ -207,8 +213,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 c2b051cd..6227f472 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 @@ -66,8 +66,11 @@ }, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 31f65739..b14e567f 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 @@ -458,8 +458,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 9ca1e71a..0828b427 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 @@ -243,8 +243,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 1a1fde0b..9da11f74 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 @@ -69,8 +69,8 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, + "Union": null, + "UnionMode": "", "Except": { "SelectPos": 41, "StatementEnd": 72, @@ -141,8 +141,8 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, + "Union": null, + "UnionMode": "", "Except": { "SelectPos": 81, "StatementEnd": 112, @@ -213,10 +213,19 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null - } - } + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + }, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + }, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 28760eba..a5ac9249 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 @@ -49,9 +49,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, { @@ -97,9 +100,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, { @@ -145,9 +151,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } ] @@ -416,8 +425,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 19ab954c..49dfde4b 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 @@ -52,8 +52,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 4a0d39cd..07eaf33f 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 @@ -35,7 +35,7 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": { + "Union": { "SelectPos": 25, "StatementEnd": 39, "With": null, @@ -71,7 +71,7 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": { + "Union": { "SelectPos": 50, "StatementEnd": 64, "With": null, @@ -107,14 +107,23 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, - "UnionDistinct": null, - "Except": null + "UnionMode": "ALL", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, - "UnionDistinct": null, - "Except": null + "UnionMode": "ALL", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 8aeaa6c0..fd3cdfc6 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 @@ -35,8 +35,7 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": { + "Union": { "SelectPos": 30, "StatementEnd": 44, "With": null, @@ -72,8 +71,7 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": { + "Union": { "SelectPos": 60, "StatementEnd": 74, "With": null, @@ -109,12 +107,23 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, - "Except": null + "UnionMode": "DISTINCT", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, - "Except": null + "UnionMode": "DISTINCT", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 558fb217..cded196e 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 @@ -125,8 +125,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "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 77cd5a90..154ba845 100644 --- a/parser/testdata/query/output/select_with_placeholder.sql.golden.json +++ b/parser/testdata/query/output/select_with_placeholder.sql.golden.json @@ -70,8 +70,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 fc7c74eb..74d87fd9 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 @@ -306,9 +306,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 222, @@ -394,8 +397,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 a88ae4e8..e7cfc697 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 @@ -84,9 +84,12 @@ ] }, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 90, @@ -173,9 +176,12 @@ ] }, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 186, @@ -262,9 +268,12 @@ ] }, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 282, @@ -359,9 +368,12 @@ "NameEnd": 415 } }, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 418, @@ -481,9 +493,12 @@ }, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "AliasPos": 487, @@ -573,9 +588,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } }, "AliasPos": 530, @@ -708,8 +726,11 @@ ] }, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 66f3150e..2f06c0bb 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 @@ -52,8 +52,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 8c87f878..043c7588 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 @@ -50,9 +50,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } ] @@ -106,8 +109,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 7b87cc4f..971eca04 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 @@ -57,8 +57,7 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": { + "Union": { "SelectPos": 59, "StatementEnd": 121, "With": null, @@ -124,10 +123,17 @@ "NameEnd": 121 } }, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, - "Except": null + "UnionMode": "DISTINCT", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 new file mode 100644 index 00000000..5fcdef22 --- /dev/null +++ b/parser/testdata/query/output/select_with_union_settings.sql.golden.json @@ -0,0 +1,127 @@ +[ + { + "SelectPos": 0, + "StatementEnd": 38, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 7, + "NumEnd": 8, + "Literal": "1", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 12, + "NameEnd": 13 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": { + "SettingsPos": 14, + "ListEnd": 38, + "Items": [ + { + "SettingsPos": 23, + "Name": { + "Name": "max_threads", + "QuoteType": 1, + "NamePos": 23, + "NameEnd": 34 + }, + "Expr": { + "NumPos": 37, + "NumEnd": 38, + "Literal": "1", + "Base": 10 + } + } + ] + }, + "Format": null, + "Union": { + "SelectPos": 45, + "StatementEnd": 83, + "With": null, + "Top": null, + "HasDistinct": false, + "DistinctOn": null, + "SelectItems": [ + { + "Expr": { + "NumPos": 52, + "NumEnd": 53, + "Literal": "2", + "Base": 10 + }, + "Modifiers": [], + "Alias": { + "Name": "v", + "QuoteType": 1, + "NamePos": 57, + "NameEnd": 58 + } + } + ], + "From": null, + "Window": null, + "Prewhere": null, + "Where": null, + "GroupBy": null, + "WithTotal": false, + "Having": null, + "OrderBy": null, + "LimitBy": null, + "Limit": null, + "Settings": { + "SettingsPos": 59, + "ListEnd": 83, + "Items": [ + { + "SettingsPos": 68, + "Name": { + "Name": "max_threads", + "QuoteType": 1, + "NamePos": 68, + "NameEnd": 79 + }, + "Expr": { + "NumPos": 82, + "NumEnd": 83, + "Literal": "2", + "Base": 10 + } + } + ] + }, + "Format": null, + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + }, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" + } +] \ No newline at end of file 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 c6d61d46..23c6a268 100644 --- a/parser/testdata/query/output/select_with_variable.sql.golden.json +++ b/parser/testdata/query/output/select_with_variable.sql.golden.json @@ -50,9 +50,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } } ] @@ -106,8 +109,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 f9f31c0a..195fc43f 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 @@ -381,8 +381,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file 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 3e14b13b..45bdcbed 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 @@ -49,9 +49,12 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" }, { "SelectPos": 22, @@ -127,8 +130,11 @@ "Limit": null, "Settings": null, "Format": null, - "UnionAll": null, - "UnionDistinct": null, - "Except": null + "Union": null, + "UnionMode": "", + "Except": null, + "ExceptMode": "", + "Intersect": null, + "IntersectMode": "" } ] \ No newline at end of file diff --git a/parser/testdata/query/select_with_bare_union.sql b/parser/testdata/query/select_with_bare_union.sql new file mode 100644 index 00000000..873d63d4 --- /dev/null +++ b/parser/testdata/query/select_with_bare_union.sql @@ -0,0 +1 @@ +SELECT 1 AS v UNION SELECT 2 AS v diff --git a/parser/testdata/query/select_with_except_all.sql b/parser/testdata/query/select_with_except_all.sql new file mode 100644 index 00000000..cf20b7cc --- /dev/null +++ b/parser/testdata/query/select_with_except_all.sql @@ -0,0 +1 @@ +SELECT 1 AS v EXCEPT ALL SELECT 2 AS v diff --git a/parser/testdata/query/select_with_except_distinct.sql b/parser/testdata/query/select_with_except_distinct.sql new file mode 100644 index 00000000..f863780a --- /dev/null +++ b/parser/testdata/query/select_with_except_distinct.sql @@ -0,0 +1 @@ +SELECT 1 AS v EXCEPT DISTINCT SELECT 2 AS v diff --git a/parser/testdata/query/select_with_intersect.sql b/parser/testdata/query/select_with_intersect.sql new file mode 100644 index 00000000..a899b4f6 --- /dev/null +++ b/parser/testdata/query/select_with_intersect.sql @@ -0,0 +1 @@ +SELECT 1 AS v INTERSECT SELECT 2 AS v diff --git a/parser/testdata/query/select_with_intersect_modifiers.sql b/parser/testdata/query/select_with_intersect_modifiers.sql new file mode 100644 index 00000000..b5198c26 --- /dev/null +++ b/parser/testdata/query/select_with_intersect_modifiers.sql @@ -0,0 +1 @@ +SELECT 1 AS v INTERSECT ALL SELECT 2 AS v INTERSECT DISTINCT SELECT 3 AS v diff --git a/parser/testdata/query/select_with_union_settings.sql b/parser/testdata/query/select_with_union_settings.sql new file mode 100644 index 00000000..15e04b1d --- /dev/null +++ b/parser/testdata/query/select_with_union_settings.sql @@ -0,0 +1 @@ +SELECT 1 AS v SETTINGS max_threads = 1 UNION SELECT 2 AS v SETTINGS max_threads = 2 diff --git a/parser/walk.go b/parser/walk.go index bfc58c65..af3bc4a5 100644 --- a/parser/walk.go +++ b/parser/walk.go @@ -63,10 +63,13 @@ func Walk(node Expr, fn WalkFunc) bool { if !Walk(n.Settings, fn) { return false } - if !Walk(n.UnionAll, fn) { + if !Walk(n.Union, fn) { return false } - if !Walk(n.UnionDistinct, fn) { + if !Walk(n.Except, fn) { + return false + } + if !Walk(n.Intersect, fn) { return false } if !Walk(n.Format, fn) {