Skip to content

fix(plan): preserve ENUM and SET types in views - #26604

Open
ck89119 wants to merge 23 commits into
matrixorigin:mainfrom
ck89119:issue-26226-main
Open

fix(plan): preserve ENUM and SET types in views#26604
ck89119 wants to merge 23 commits into
matrixorigin:mainfrom
ck89119:issue-26226-main

Conversation

@ck89119

@ck89119 ck89119 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What type of PR is this?

  • API-change
  • BUG
  • Improvement
  • Documentation
  • Feature
  • Test and CI
  • Code Refactoring

Which issue(s) this PR fixes:

issue #26226

What this PR does / why we need it:

Preserve the original ENUM and SET type metadata when a view directly projects those columns.

At view creation, persisted catalog columns retain their complete special-type definitions. When the saved view SQL is rebound, the planner restores the raw ENUM/SET index expression at the view output boundary. Outer queries therefore keep string display results while ORDER BY uses definition order and CTAS recreates ENUM/SET columns.

The shared source-expression validation also replaces the previous CTAS type assertion with a safe fallback for non-transparent or malformed expressions.

Tests cover persisted view metadata, full view rebind, ENUM ordering keys, CTAS from a view, ENUM, SET, explicit view column names, ordinary VARCHAR controls, and malformed expression fallback.

@ck89119
ck89119 marked this pull request as ready for review August 3, 2026 06:06
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@XuPeng-SH XuPeng-SH left a comment

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.

Deep-reviewed exact head bb144fc0c1c23c4b7439fc6f2a6359ee649b07c9. The earlier DISTINCT value-semantics blocker is fixed, but one systemic correctness issue remains.

P1: persisted ENUM/SET metadata is recovered from only one physical plan shape

At pkg/sql/plan/build_ddl.go:170-184, genViewTableDef derives the catalog type by applying mysqlSpecialTypeSourceType only to the final step's ProjectList. The helper at pkg/sql/plan/mysql_special_types.go:258-283 recognizes only the exact display-wrapper-over-ColRef shape. A transparent planner boundary turns that final expression into a VARCHAR ColRef, so the same source column silently gets a different persisted view type.

Minimal counterexample:

CREATE VIEW v_direct AS
SELECT priority, flags FROM nation;

CREATE VIEW v_order AS
SELECT priority, flags FROM nation ORDER BY priority, flags;

Using the same ENUM/SET source definitions, I built both statements at this head and inspected CreateView.TableDef.Cols:

view MySQL 8.4.10 catalog this head
direct projection ENUM / SET ENUM / SET
same projection + ORDER BY ENUM / SET VARCHAR / VARCHAR

The same loss occurs for transparent GROUP BY, DISTINCT, derived-table, and CTE shapes. UNION ALL is the nearest useful negative control: MySQL exposes VARCHAR there, so unconditionally propagating the type through every boundary would also be wrong.

This violates the view-boundary invariant of the PR: persist the special type iff the visible output remains a transparent projection of the same ENUM/SET definition under MySQL semantics; semantic string expressions and set-operation outputs must clear it.

Please carry explicit, narrow source-type provenance through the planner boundaries that preserve it and consume that provenance when generating the view schema, rather than rediscovering it from only the final expression shape. Please also add public/catalog assertions (for example SHOW COLUMNS or information_schema.columns) for direct, ORDER BY, GROUP BY, DISTINCT, and derived-table projections, with UNION/mixed expressions as negative controls. The current DISTINCT regression checks visible row count, but not its persisted column metadata.

Validation performed:

  • exact-head planner probe across direct/GROUP BY/DISTINCT/ORDER BY/derived/CTE/UNION shapes;
  • MySQL 8.4.10 behavior used as the oracle for the same DDL;
  • all seven PR-focused pkg/sql/plan tests pass locally;
  • git diff --check passes.

@XuPeng-SH XuPeng-SH left a comment

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.

Deep-reviewed exact head 037e84666da1cd35f2e5ab7ad603acd7aa97c952. The previous catalog-shape blocker is only partially fixed; one end-to-end P1 remains.

P1: transparent derived/CTE view provenance is not restored on rebind, so CTAS still loses SET/ENUM

The new provenance path makes the persisted view column special, but appendViewMySQLSpecialTypeBoundary at pkg/sql/plan/query_builder.go:8903 and :8927 still recognizes only an exact display-wrapper expression. A transparent derived table or CTE ends in a VARCHAR ColRef with mysqlSpecialColumnTypes provenance, so view rebind does not restore the raw ENUM/SET type at the completed view boundary.

Minimal public-path counterexample:

CREATE VIEW v_derived AS
SELECT flags FROM (SELECT id, flags FROM t) d WHERE id = 2;

CREATE TABLE copied_derived AS SELECT flags FROM v_derived;

At this head:

  • information_schema.columns reports v_derived.flags as SET;
  • copied_derived.flags is VARCHAR, not SET;
  • the same failure occurs through a CTE view.

MySQL 8.4.10 produces SET for both the view and the CTAS table. This is also the downstream contract of issue #26226, not just a catalog-display difference.

Please consume the same narrow provenance when restoring the completed view boundary (or otherwise carry it into CTAS), while continuing to clear it for UNION and semantic string expressions. Add an end-to-end CTAS assertion for derived and CTE views, with UNION as the negative control; the current tests assert only the view catalog type for these shapes.

Validation performed:

  • all seven PR-focused pkg/sql/plan tests pass;
  • exact-head embedded SQL regression fails with expected SET, actual VARCHAR for copied_derived;
  • MySQL 8.4.10 used as the oracle for the same DDL;
  • git diff --check passes.

@XuPeng-SH XuPeng-SH left a comment

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.

Deep-reviewed exact head 630e72a31e0988b5bbce89319721df9fa56d3b49. The previous derived/CTE catalog and CTAS blockers are fixed, but one end-to-end P1 remains.

P1: an ORDER BY view corrupts a non-injective SET value at the new type-restoration boundary

bindView now calls appendMySQLSpecialTypeBoundary after the complete view plan (pkg/sql/plan/query_builder.go:8889). When the root is a SORT, canExposeRaw is false (:8915-8916), so the fallback takes the SQL-visible VARCHAR and calls funcCastForSetType to recreate a SET value (:8942-8964). That bitmap -> display string -> bitmap round trip is not reversible for legal SET definitions.

Public-path reproducer:

CREATE TABLE t (id INT PRIMARY KEY, flags SET('', 'a'));
INSERT INTO t VALUES (1, ''), (2, 1);
CREATE VIEW v_order AS SELECT id, flags FROM t ORDER BY flags;

SELECT CAST(flags AS UNSIGNED) FROM t       WHERE id = 2; -- 1
SELECT CAST(flags AS UNSIGNED) FROM v_order WHERE id = 2; -- 0 on this head

I reproduced the expected 0x1, actual 0x0 result in the PR embedded-cluster regression by adding only the v_order value assertion. The existing test already creates the same non-injective SET and an ORDER BY view, but checks only its catalog type, so this value corruption is currently missed. The unchanged PR test passes.

This is the same boundary invariant from the earlier DISTINCT fix viewed from the other side: semantic operators must consume the visible value, but a row-preserving operator must not force reconstruction of the stored ENUM/SET value from that visible value afterward. Please carry the original raw value through row-preserving boundaries (including the same derived/CTE shapes) instead of reverse-casting the display string, and add bitmap assertions for ORDER BY plus its transparent nested variants while retaining the DISTINCT visible-value regression.

Validation performed on the exact head:

  • full ./pkg/sql/plan tests pass;
  • the unchanged TestIssue26226ViewDistinctUsesVisibleSetValue passes;
  • go list, go build, and go vet for the affected packages pass;
  • git diff --check passes.

@ck89119

ck89119 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Deep-reviewed exact head 88258ec61a65b7ff4330fd9d0f5bd084bc019be1. The new raw sidecar fixes the reported ORDER BY value corruption: I verified direct ORDER BY, nested derived/CTE ORDER BY, LIMIT, view-of-view, and a multiply referenced ordered CTE. Two independent set-boundary P1s remain.

P1: identical UNION DISTINCT arms bypass the set-operation type boundary

getUnionSelects drops the identical right arm at pkg/sql/plan/utils.go:1015-1019; buildUnionWithResultLen then rewrites the remaining arm to an ordinary SELECT DISTINCT at pkg/sql/plan/query_builder.go:3345-3350. The relational simplification is valid for rows, but it also skips setOperationOutputType, so the new catalog-provenance path mistakes the output for a direct ENUM/SET projection.

Public reproducer:

CREATE TABLE t (
  priority ENUM('low','medium','high'),
  flags SET('','a')
);

CREATE VIEW v_union AS
SELECT priority, flags FROM t
UNION
SELECT priority, flags FROM t;

SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'v_union'
ORDER BY ordinal_position;

At this head the result is ENUM / SET; MySQL 8.4.8 returns VARCHAR / VARCHAR. CREATE TABLE copied AS SELECT priority, flags FROM v_union likewise creates ENUM/SET here but VARCHAR/VARCHAR in MySQL. The existing negative test covers only UNION ALL, which takes the normal two-arm path and therefore misses this optimization.

Please preserve the set-operation output type contract even when identical UNION arms are simplified, and add public catalog plus CTAS assertions for both UNION and UNION ALL.

P1: recursive CTE output leaks catalog provenance from its anchor

The recursive path separately reconciles mysqlSpecialOrderTypes across the anchor and recursive members at pkg/sql/plan/query_builder.go:4162-4280, but there is no corresponding catalog-provenance suppression. The final addBinding(initLastNodeID, ...) at :4323 therefore lets addBinding rediscover mysqlSpecialColumnTypes from the anchor at :9696-9700, even though a recursive CTE is a set-operation boundary.

Public reproducer:

CREATE VIEW v_recursive AS
WITH RECURSIVE r(priority, flags, n) AS (
  SELECT priority, flags, 1 FROM t
  UNION ALL
  SELECT priority, flags, n + 1 FROM r WHERE n < 2
)
SELECT priority, flags FROM r;

At this head both v_recursive and CTAS from it persist ENUM / SET. MySQL 8.4.8 persists VARCHAR / VARCHAR for both. This reproduces even when every recursive member uses the same source definition, so catalog provenance must be cleared at the recursive CTE boundary; ordering provenance can remain a separate compatibility decision.

Please add an end-to-end recursive-view catalog/CTAS negative control alongside the non-recursive CTE positive case.

Validation performed:

  • exact-head planner matrix across direct, ORDER BY, GROUP BY, DISTINCT, derived, non-recursive CTE, UNION/UNION ALL, INTERSECT, MINUS, ROLLUP, GROUPING SETS, and recursive CTE shapes;
  • exact-head embedded SQL checks of information_schema.columns and CTAS for both failures;
  • MySQL 8.4.8 catalog/CTAS oracle for the same SQL;
  • exact-head ORDER BY value checks across direct/nested/LIMIT/view-of-view/multi-reference CTE paths.

@ck89119

ck89119 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Deep-reviewed exact head 6b1f393215cddc87ed217b178ef64d8229bfc637. The two previously reported set-boundary blockers are fixed: identical UNION DISTINCT and recursive CTE outputs now persist VARCHAR in both views and CTAS. One end-to-end P1 remains.

P1: GROUP BY / DISTINCT views persist ENUM/SET metadata but expose VARCHAR execution values

genViewTableDef now uses output provenance to persist the source ENUM/SET type (pkg/sql/plan/build_ddl.go:191-195). However, when such a view is rebound, appendMySQLSpecialTypeBoundary leaves the SQL-visible VARCHAR unchanged if there is no raw sidecar or exact display wrapper (pkg/sql/plan/query_builder.go:8992-8995). That is necessary for row-preserving boundaries such as ORDER BY, but it is not the MySQL result contract after GROUP BY or DISTINCT has already consumed the visible value: the boundary must expose the canonical ENUM ordinal / SET bitmap corresponding to that result.

Public reproducer:

CREATE TABLE t (
  priority ENUM('low','medium','high'),
  flags SET('', 'a', 'b')
);
INSERT INTO t VALUES
  ('low', ''),
  ('medium', 1),
  ('high', 'a');

CREATE VIEW v_group AS
  SELECT priority, flags FROM t GROUP BY priority, flags;
CREATE VIEW v_distinct AS
  SELECT DISTINCT priority, flags FROM t;

SELECT CAST(priority AS UNSIGNED), CAST(flags AS UNSIGNED) FROM v_group;
SELECT CAST(priority AS UNSIGNED), CAST(flags AS UNSIGNED) FROM v_distinct;

At this head, both queries fail with:

invalid argument cast to uint64, bad value low

Both view catalogs nevertheless report ENUM / SET. MySQL 8.4.8 reports the same ENUM / SET catalog types and returns (1,0), (2,0), (3,2) from both queries. In particular, the legal non-injective SET bitmaps 0 and 1 both display as ''; after the semantic operator, MySQL exposes the canonical bitmap 0 for that visible result.

Please add boundary-sensitive post-operation re-encoding for GROUP BY/DISTINCT outputs while retaining the raw-sidecar path for row-preserving ORDER BY/derived/CTE shapes. The public regression should assert numeric ENUM/SET semantics (not only catalog type and DISTINCT row count) for both GROUP BY and DISTINCT.

Validation on this exact head:

  • full catalog-provenance matrix: direct, alias, filter, ORDER BY/LIMIT, GROUP BY, DISTINCT, derived, CTE/multi-reference CTE, join, window, all set operations, recursive CTE, ROLLUP, GROUPING SETS, and expressions;
  • full view-to-CTAS matrix, including identical UNION DISTINCT and recursive CTE negative controls;
  • public embedded SQL value checks for row-preserving direct/ORDER BY/derived/CTE/view-of-view paths;
  • independent failing embedded SQL checks for both GROUP BY and DISTINCT numeric semantics;
  • MySQL 8.4.8 catalog and value oracle for the same cases;
  • all PR-focused tests and full ./pkg/sql/plan pass unchanged.

@ck89119

ck89119 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Deep-reviewed exact head 5e242cc776a314f7d5ec539df0340e64efa8a43c. The prior SET-side GROUP BY / DISTINCT canonicalization blocker is fixed: independent SET-only numeric checks now pass for both operations, including the non-injective SET('', 'a') case. The broader valid matrix also passes across direct/alias/filter/order/limit, derived/CTE/multi-reference CTE, joins/windows, GROUP/DISTINCT, set operations, recursive CTE, ROLLUP/grouping sets, nested views, CTAS, and INSERT.

One deterministic P1 remains in the newly added public regression itself.

P1: TestIssue26226ViewDistinctUsesVisibleSetValue is guaranteed red before it can validate the fix

There are two independent blockers:

  1. At pkg/tests/issues/issue_26226_test.go:75, the test creates:

    create view v_semantic_group_order as
    select priority, flags from semantic_t
    group by priority, flags order by flags

    This fails immediately with:

    not supported: definition-order sorting of projected ENUM/SET values with non-unique display labels or ambiguous SET display values
    

    execSQLRequire therefore stops at line 78 and none of the value assertions run. I reproduced this locally on the exact head, and the current UT Coverage on Ubuntu/x86 job fails at the same statement in this test. The same-block DISTINCT ... ORDER BY flags form is rejected for the same reason. Both limitations also reproduce on clean base 6091b418fd529badca4e1855843bd38804de396f, so this is not evidence that the new canonicalization regressed; it is an unsupported baseline behavior newly made mandatory by the test.

  2. If the statement above is removed, the loop at line 160 still starts with CAST(priority AS UNSIGNED). That ENUM cast fails with invalid argument cast to uint64, bad value low even against the direct base table on the clean base, so the combined ENUM/SET assertion would fail before isolating the SET fix. In contrast, independent CAST(flags AS UNSIGNED) assertions for both GROUP BY and DISTINCT pass on this head, and ENUM definition-order assertions plus catalog/CTAS checks pass as well.

MySQL 8.4.8 accepts the same GROUP/DISTINCT ordering forms and returns the expected ENUM ordinals and canonical SET bitmaps, so either behavior could be brought into scope. Otherwise, please keep the regression aligned with the supported surface: remove the unsupported same-block ordering statement, assert SET numeric semantics independently, and use ENUM definition order plus catalog/CTAS as the ENUM oracle. Then run this exact public test itself; planner-only tests cannot catch these execution-time failures.

Validation on this head:

  • TestIssue26226ViewDistinctUsesVisibleSetValue: fails at issue_26226_test.go:78 with the error above;
  • independent complete embedded SQL matrix with valid oracles: passes;
  • all PR-focused planner tests: pass;
  • full ./pkg/sql/plan: pass;
  • CGo-aware go build and go vet for the affected packages: pass;
  • git diff --check: pass.

@ck89119

ck89119 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Deep-reviewed exact head 8e90d5de734016d1d97349fafe3243e4c1a8c43b. The previous public-regression blocker is fixed: TestIssue26226ViewDistinctUsesVisibleSetValue now passes, and the complete ENUM/SET catalog/value matrix passes across row-preserving, semantic, nested, set-operation, recursive, CTAS, and INSERT boundaries. One independent P1 remains.

P1: CTAS now leaks source defaults through derived/CTE boundaries and emits a replay-unsafe schema

genAsSelectCols now consumes OutputColumnProvenance for both special types and defaults at pkg/sql/plan/build_ddl.go:277-284. The same provenance deliberately carries DefaultOriginString through derived tables and non-recursive CTEs (pkg/sql/plan/build_ddl_test.go:735-738), and the CTAS unit test explicitly expects the derived-table default to survive at :777-779. MySQL does not preserve defaults across either boundary.

Public reproducer:

CREATE TABLE src (
  id INT PRIMARY KEY,
  e ENUM('low','medium','high') DEFAULT 'medium',
  s SET('', 'a', 'b') DEFAULT 'a',
  n INT DEFAULT 7
);
INSERT INTO src VALUES (1, 'low', '', 1);

CREATE TABLE c_derived AS
  SELECT e,s,n FROM (SELECT e,s,n FROM src) d;
CREATE TABLE c_cte AS
  WITH d AS (SELECT e,s,n FROM src) SELECT e,s,n FROM d;

At this head, information_schema.columns reports defaults 'medium', 'a', and 7 for both copied tables. SHOW CREATE TABLE likewise emits:

`e` enum('low','medium','high') DEFAULT 'medium',
`s` set('','a','b') DEFAULT 'a',
`n` int DEFAULT 7

The same public test passes on clean base 6091b418fd529badca4e1855843bd38804de396f, where all six derived/CTE defaults are NULL. MySQL 8.4.8 also returns NULL defaults for both shapes. Direct/ORDER BY/GROUP BY/DISTINCT controls preserve the defaults in MySQL, while UNION and derived/CTE clear them, so this is a boundary-specific contract rather than a blanket CTAS rule.

The head is internally inconsistent as well: despite the advertised defaults, INSERT INTO c_derived () VALUES () and the CTE equivalent still produce all-NULL rows because the CTAS Default.Expr is nil. Replaying the emitted SHOW CREATE TABLE can therefore turn non-executable catalog text into real defaults and change later omitted-column inserts.

Please separate special-type provenance from default inheritance, or clear only default provenance at derived/CTE boundaries while retaining the required ENUM/SET type metadata. The public regression should assert information_schema.columns.column_default and SHOW CREATE TABLE for direct, derived, CTE, and UNION controls, and the current derived-default unit expectation should be corrected.

Validation on this exact head:

  • PR public regression: pass;
  • full ./pkg/sql/plan: pass;
  • independent complete ENUM/SET catalog/value matrix: pass before the default-lineage assertions;
  • focused default-lineage public test: fails on the head and passes unchanged on clean base;
  • MySQL 8.4.8 schema and omitted-insert oracle: derived/CTE defaults are NULL;
  • CGo-aware build/vet and git diff --check: pass.

@XuPeng-SH XuPeng-SH left a comment

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.

Re-reviewed the latest head. The prior CTAS default leak is fixed at the right abstraction boundary: source provenance remains available for ENUM/SET type restoration, while default inheritance is explicitly disabled when crossing derived-table/CTE query boundaries. Direct projections still inherit defaults, and semantic/set boundaries remain isolated. I also validated direct ORDER BY/GROUP BY/DISTINCT, nested derived tables/CTEs, a multi-reference CTE, and the public issue regression; all passed.

@mergify

mergify Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • Entered queue2026-08-04 11:40 UTC · Rule: main · triggered by rule Automatic queue on approval for main
  • 🚫 Left the queue2026-08-04 12:02 UTC · at 80108dad6a5f51aad494b467599be4dec82e2c75

This pull request spent 21 minutes 32 seconds in the queue, with no time running CI.

Reason

Pull request #26604 has been dequeued

Queue conditions are not satisfied:

  • -conflict [📌 queue requirement]

Hint

You should look at the reason for the failure and decide if the pull request needs to be fixed or if you want to requeue it.
If you do update this pull request, it will automatically be requeued once the queue conditions match again.
If you think this was a flaky issue instead, you can requeue the pull request, without updating it, by posting a @mergifyio queue comment.

Tick the box to put this pull request back in the merge queue (same as @mergifyio queue).

  • Requeue this pull request

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

Labels

dequeued kind/bug Something isn't working size/XL Denotes a PR that changes [1000, 1999] lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants