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) {