Skip to content

Fix view returning wrong results after DETACH/ATTACH with UNION and INTERSECT#100390

Merged
alexey-milovidov merged 21 commits intomasterfrom
fix-view-union-intersect-precedence
Apr 25, 2026
Merged

Fix view returning wrong results after DETACH/ATTACH with UNION and INTERSECT#100390
alexey-milovidov merged 21 commits intomasterfrom
fix-view-union-intersect-precedence

Conversation

@alexey-milovidov
Copy link
Copy Markdown
Member

When a view definition contains mixed UNION and INTERSECT operators (e.g. SELECT 1 UNION DISTINCT SELECT 2 INTERSECT SELECT 3), the INTERSECT precedence was lost after DETACH/ATTACH or server restart. This happened because NormalizeSelectWithUnionQueryVisitor was called without first running SelectIntersectExceptQueryVisitor to resolve INTERSECT/EXCEPT precedence. The normalizer doesn't understand INTERSECT/EXCEPT modes and would incorrectly drop SELECT branches connected by these operators.

The fix adds SelectIntersectExceptQueryVisitor before every call to NormalizeSelectWithUnionQueryVisitor in all affected code paths: StorageView, DatabaseOrdinary, DatabaseReplicated, DatabaseBackup, InterpreterSystemQuery, and UserDefinedSQLFunctionFactory.

Fixes #99257

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Fix views with mixed UNION and INTERSECT/EXCEPT operators returning wrong results after DETACH/ATTACH or server restart.

Documentation entry for user-facing changes

  • Documentation is written (mandatory for new features)

…NTERSECT

When a view definition contains mixed `UNION` and `INTERSECT` operators,
the `INTERSECT` precedence was lost after `DETACH`/`ATTACH`. This happened
because `NormalizeSelectWithUnionQueryVisitor` was called without first
running `SelectIntersectExceptQueryVisitor` to resolve `INTERSECT`/`EXCEPT`
precedence. The normalizer doesn't understand `INTERSECT`/`EXCEPT` modes
and would incorrectly drop SELECT branches connected by these operators.

The fix adds `SelectIntersectExceptQueryVisitor` before every call to
`NormalizeSelectWithUnionQueryVisitor` in all affected code paths:
`StorageView`, `DatabaseOrdinary`, `DatabaseReplicated`, `DatabaseBackup`,
`InterpreterSystemQuery`, and `UserDefinedSQLFunctionFactory`.

Fixes #99257

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@clickhouse-gh
Copy link
Copy Markdown
Contributor

clickhouse-gh Bot commented Mar 22, 2026

Workflow [PR], commit [5e5e362]

Summary:


AI Review

Summary

This PR fixes incorrect view semantics after DETACH/ATTACH (and related metadata restore paths) for mixed UNION with INTERSECT/EXCEPT by running SelectIntersectExceptQueryVisitor before NormalizeSelectWithUnionQueryVisitor where metadata is reparsed. The change is consistent across the affected attach/restore/function-normalization paths and is covered by a focused stateless regression test, so the patch looks correct.

ClickHouse Rules
Item Status Notes
Deletion logging
Serialization versioning
Core-area scrutiny
No test removal
Experimental gate
No magic constants
Backward compatibility
SettingsChangesHistory.cpp
PR metadata quality
Safe rollout
Compilation time
No large/binary files
Final Verdict

Status: ✅ Approve

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label Mar 22, 2026
Comment thread src/Storages/StorageView.cpp Outdated
/// This is needed when the AST is freshly parsed from stored metadata
/// (e.g. during ATTACH) and has not been through executeQuery's visitors.
{
SelectIntersectExceptQueryVisitor::Data data{SetOperationMode::DISTINCT, SetOperationMode::DISTINCT};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hardcodes INTERSECT/EXCEPT defaults to DISTINCT, which can change semantics for views created or attached when intersect_default_mode / except_default_mode are ALL.

The other touched code paths read these defaults from settings; this one should do the same (or preserve already-resolved operators only) to avoid behavior drift after DETACH/ATTACH.

Comment thread tests/queries/0_stateless/04054_view_union_intersect_precedence.sql
alexey-milovidov and others added 2 commits March 28, 2026 18:48
…ctor, extend tests

The `StorageView` constructor was calling `SelectIntersectExceptQueryVisitor` on
the inner query AST, but this is incorrect for code paths where the AST has
already been normalized (e.g., `view()` table function). The visitor expects
`modes.size() + 1 == selects.size()`, which does not hold after
`NormalizeSelectWithUnionQueryVisitor` has flattened nested unions. This caused a
`LOGICAL_ERROR` exception (and server abort with `abort_on_logical_error` enabled)
when running queries like `SELECT ... FROM view(SELECT 1 UNION ALL (SELECT 2 UNION ALL SELECT 3))`.

The visitor call in `StorageView` is unnecessary because:
- For `CREATE VIEW` via `executeQuery`: the AST is already processed by
  `SelectIntersectExceptQueryVisitor` in the `executeQuery` pipeline.
- For `ATTACH` from stored metadata: the Database code paths
  (`DatabaseOrdinary`, `DatabaseReplicated`, `DatabaseBackup`) already apply
  the visitor with proper settings before constructing `StorageView`.

Also address review feedback:
- Remove hardcoded `SetOperationMode::DISTINCT` that ignored
  `intersect_default_mode` / `except_default_mode` settings.
- Add test cases for `EXCEPT` operator.
- Add test cases with `intersect_default_mode = 'ALL'` and
  `except_default_mode = 'ALL'` to verify settings-dependent behavior
  is preserved after `DETACH`/`ATTACH`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SET intersect_default_mode = 'ALL';
SET except_default_mode = 'ALL';

CREATE VIEW v2 AS (SELECT 1 c0, 1 c1 UNION DISTINCT SELECT 2, 2 INTERSECT SELECT 3, 3);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These ALL-mode checks are not actually sensitive to ALL vs DISTINCT semantics: both INTERSECT and EXCEPT inputs here contain no duplicates, so SET intersect_default_mode = 'ALL' / SET except_default_mode = 'ALL' produces the same result as DISTINCT.

As written, this can still pass even if mode preservation regresses during DETACH/ATTACH.

Please add cases with duplicates so result multiplicity differs between ALL and DISTINCT, for example:

  • INTERSECT: SELECT number % 2 FROM numbers(6) INTERSECT SELECT number % 2 FROM numbers(4)
  • EXCEPT: SELECT number % 2 FROM numbers(6) EXCEPT SELECT number % 2 FROM numbers(3)

and verify before/after DETACH/ATTACH under ALL defaults.

alexey-milovidov and others added 8 commits March 29, 2026 00:56
…ory, improve tests

The previous commit removed `SelectIntersectExceptQueryVisitor` from the
`StorageView` constructor, but this broke the `ATTACH TABLE` path because
the Database code paths that apply the visitor only run during server startup
(`loadTablesMetadata`), not during explicit `ATTACH TABLE` which goes through
`InterpreterCreateQuery` → `StorageFactory`.

Move the visitor call to `registerStorageView` (the factory function for View
storage) where we have access to context settings. This:
- Fixes the `ATTACH TABLE` path: the freshly-parsed AST gets INTERSECT/EXCEPT
  precedence resolved before `NormalizeSelectWithUnionQueryVisitor` runs.
- Is a safe no-op for `CREATE VIEW` via `executeQuery`: the AST has already been
  processed and contains no INTERSECT/EXCEPT modes in `list_of_modes`.
- Does not affect the `view()` table function: it creates `StorageView` directly,
  bypassing the factory.
- Uses `intersect_default_mode` / `except_default_mode` settings from context
  instead of hardcoding `SetOperationMode::DISTINCT`.

Also improve ALL-mode tests (v2, v3) to use data with duplicates so that
`INTERSECT ALL` / `EXCEPT ALL` actually produce different results from their
`DISTINCT` counterparts, making the tests sensitive to the setting being
correctly propagated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@alexey-milovidov
Copy link
Copy Markdown
Member Author

The Stress test (arm_msan) failure is fixed by #101239, which should be merged first. After it is merged, please update the branch to include the fix.

@alexey-milovidov
Copy link
Copy Markdown
Member Author

The test 02859_replicated_db_name_zookeeper is fixed in #101952

@alexey-milovidov
Copy link
Copy Markdown
Member Author

The Can't adjust last granule error in CI is a known issue. The fix is in #101641

@clickhouse-gh
Copy link
Copy Markdown
Contributor

clickhouse-gh Bot commented Apr 24, 2026

LLVM Coverage Report

Metric Baseline Current Δ
Lines 83.80% 83.80% +0.00%
Functions 91.10% 91.10% +0.00%
Branches 76.20% 76.30% +0.10%

Changed lines: 80.43% (37/46) · Uncovered code

Full report · Diff report

Copy link
Copy Markdown
Member Author

@alexey-milovidov alexey-milovidov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks alright.

@alexey-milovidov alexey-milovidov added this pull request to the merge queue Apr 25, 2026
@alexey-milovidov alexey-milovidov self-assigned this Apr 25, 2026
Merged via the queue into master with commit be7d7a1 Apr 25, 2026
165 checks passed
@alexey-milovidov alexey-milovidov deleted the fix-view-union-intersect-precedence branch April 25, 2026 15:33
@robot-ch-test-poll3 robot-ch-test-poll3 added the pr-synced-to-cloud The PR is synced to the cloud repo label Apr 25, 2026
@clickgapai
Copy link
Copy Markdown
Contributor

Hi — this PR may need backporting to 26.3 (LTS), 26.2, 26.1, 25.8 (LTS), but no backport label was found.

Affected code: DatabaseOrdinary::loadTablesMetadata, DatabaseReplicated::recoverLostReplica, StorageView constructor, DatabaseBackup::loadTablesMetadata, InterpreterSystemQuery::restoreDatabaseFromKeeperPath, normalizeCreateFunctionQuery — core code present in all supported branches.

Why: This bug causes silent wrong results for views with INTERSECT/EXCEPT after DETACH/ATTACH or server restart. Both NormalizeSelectWithUnionQueryVisitor in DatabaseOrdinary and SelectIntersectExceptQueryVisitor predate all supported branches (both introduced ~2021). The affected code paths exist in all supported versions.

If this should be backported, consider adding pr-must-backport or a version-specific label (e.g. v26.3-must-backport). Ignore this if backporting is not applicable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-bugfix Pull request with bugfix, not backported by default pr-synced-to-cloud The PR is synced to the cloud repo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

View bad result after re-attaching

3 participants