fix: scan Enum8/Enum16 columns into integer destinations - #1921
fix: scan Enum8/Enum16 columns into integer destinations#1921polyglotAI-bot wants to merge 2 commits into
Conversation
Enum8/Enum16 ScanRow only accepted *string/**string destinations, so scanning an enum column into an integer (its underlying ordinal) returned a ColumnConverterError, even though the write side already accepts int8/int16/int values (PR #802). Add integer destination cases that return the numeric ordinal: Enum8 -> *int8/*int16/*int32/*int64/*int (+ **... variants) Enum16 -> *int16/*int32/*int64/*int (+ **... variants) int8 is intentionally not offered for Enum16 because Enum16 ordinals can exceed the int8 range and would silently truncate; that destination remains an explicit ColumnConverterError. Nullable(Enum) inherits the fix via Nullable.ScanRow delegation. Fixes: #1918
There was a problem hiding this comment.
Pull request overview
Fixes enum read/write symmetry in the native driver by allowing Enum8/Enum16 columns to be scanned into signed integer destinations (returning the underlying ordinal), matching existing insert-side support and addressing #1918.
Changes:
- Extend
Enum8.ScanRowto support*int8/*int16/*int32/*int64/*int(and**variants). - Extend
Enum16.ScanRowto support*int16/*int32/*int64/*int(and**variants), intentionally excludingint8to avoid truncation. - Add an integration regression test covering integer scans, negative ordinals, pointer-to-pointer destinations, and nullable enums.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
lib/column/enum8.go |
Adds integer destination handling in ScanRow for Enum8 ordinals. |
lib/column/enum16.go |
Adds integer destination handling in ScanRow for Enum16 ordinals (excluding int8). |
tests/issues/1918_test.go |
Adds regression coverage for scanning enums into signed integer destinations (incl. nullable + ** pointers). |
Comments suppressed due to low confidence (1)
tests/issues/1918_test.go:135
- The nullable-table subtest has the same idempotency issue as the outer table: CREATE can fail if
test_1918_nullablealready exists, and cleanup is only registered after a successful CREATE. Dropping first and usingt.Cleanupprevents flakiness on reruns.
require.NoError(t, conn.Exec(ctx, `
CREATE TABLE test_1918_nullable (
n8 Nullable(Enum8 ('a' = -5, 'c' = 42))
, n16 Nullable(Enum16('z' = 1000))
) Engine MergeTree() ORDER BY tuple()
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
🤖 Claude reviewAdds integer scan destinations to Key concerns:
Surface coverage: the fix lands on the native Blind spots: no live ClickHouse here, so these come from tracing the scan path rather than running Verdict: General findings
Inline comments are attached to the relevant lines. This summary updates in place on re-review. |
…hint Enum16->int8
- Nullable.ScanRow: add **int to the NULL clear-list. The PR added *int/**int
as Enum scan destinations, but Nullable clears a NULL destination via an
explicit **T list that omitted **int, so a NULL scanned into a reused *int
left a stale non-nil pointer. All other widths added (**int8/16/32/64) were
already in the list.
- Enum16.ScanRow: reject *int8/**int8 explicitly with an actionable Hint
("Enum16 values may exceed the int8 range; use *int16 or wider"), matching the
generated numeric columns' convention.
- tests/issues/1918_test.go: DROP TABLE IF EXISTS before CREATE + register
cleanup up-front (idempotent reruns); seed pointer destinations non-nil before
the NULL scan so a missing nil-out is detectable, and add a *int NULL case
(regression pin for the Nullable fix); assert the specific Enum16->int8
rejection message.
Description
Fixes #1918.
Enum8.ScanRow/Enum16.ScanRowonly accepted*string/**stringdestinations (plus
sql.Scanner). Any integer destination fell through to aColumnConverterError, so reading an enum column into its underlying numericordinal was impossible — even though the write side has accepted integer
values since #802 ("Support inserting Enums as int8/int16/int values"). This
restores read/write symmetry.
proto.Enum8isint8-based andproto.Enum16isint16-based, so the rawvalue returned by
col.col.Row(row)is the ordinal. The fix adds integerdestination cases that return it.
Changes
lib/column/enum8.go—ScanRownow handles*int8,*int16,*int32,*int64,*int(and the**pointer-to-pointer variants). All are losslesswidenings of the
int8base.lib/column/enum16.go—ScanRownow handles*int16,*int32,*int64,*int(and**variants).int8is intentionally not offered forEnum16: anEnum16ordinal can exceed theint8range (e.g.1000) andwould silently truncate, so an
int8destination forEnum16stays anexplicit
ColumnConverterErrorrather than corrupting data (consistent withthe "hard to use incorrectly" design principle). Only signed integer types are
added, matching the signed enum ordinals and the write side.
Nullable(Enum)inherits the fix automatically —Nullable.ScanRowdelegates element scanning to the base
Enumcolumn'sScanRow.Test
tests/issues/1918_test.go(native round-trip against a real server):Enum8scanned into every signed width (int8/int16/int32/int64/int).Enum16scanned intoint16/int32/int64/int, using ordinal1000(outsideint8range) to prove the wider destinations are not truncated.-5,-300) preserve their sign.**int8etc.).stringdestinations still work unchanged, andEnum16 -> int8remains an error (no silent truncation).Nullable(Enum)into integer destinations (value scanned;NULLleaves apointer destination nil).
Each integer subtest fails on
mainwithclickhouse [ScanRow]: converting Enum8 to *int8 is unsupportedand passeswith this change; the surrounding existing enum unit and integration tests
continue to pass unchanged.
Known limitation / follow-up (out of scope here)
Array(Enum)→[]intandMap(String, Enum)→map[string]intare notaddressed by this PR: they use a different, reflection-based element-scan path
(
Array.scanSlice→setJSONFieldValue, andMap.ScanRow's whole-type match),not
Enum.ScanRow. Neither has ever worked, neither is a regression, and fixingthem touches shared code used by all container element types — so it belongs in a
separate change (and, for
Map, may be intentional). Kept this PR to one focusedfix.
Pre-PR validation gate
main, pass here)ScanRowtype switch)AGENTS.md(regression test intests/issues/, pointer receivers,t.Cleanupfor connections)