Skip to content

Commit 036b796

Browse files
committed
sql/types: reintroduce downgradeType for NAME compatibility in mixed clusters
This patch restores the `downgradeType` hook to maintain serialization compatibility for the NAME type in mixed-version clusters (v25.3 and 25.4). In previous releases, a NAME type could be misinterpreted by older nodes. Specifically, a v25.3 node deserializing a NAME encoded by a v25.4 node would mistakenly treat it as text (`oid.T_text`). This was due to the old `upgradeType` logic overwriting the OID when no `VisibleType` was set, causing `T_name` to degrade to `T_text`. PR #148694 addressed this issue for v25.4 by changing `upgradeType` (to avoid clobbering a provided OID) and removing the `downgradeType` function. However, the follow-up fix for mixed-version serialization (PR #152633) relied on the `InternalType.VisibleType` field to tag string types for older nodes. Because the NAME type doesn’t use a `VisibleType` (it’s represented as a STRING family with a special OID), that change didn’t cover NAME. As a result, NAME remained broken in a 25.3/25.4 mixed cluster. This commit explicitly handles NAME during serialization. When sending a NAME type to the wire, we downgrade it to the legacy encoding: we set its type family to `Family=name` (legacy enum value 11) so that v25.3 nodes recognize it as a NAME. Additionally, if the locale is an empty string, we set it to `nil` to ensure older versions don’t misclassify the NAME as a collated string. Together, these steps preserve the correct type interpretation on older nodes. Once all nodes are on v25.4, this compatibility shim can be removed. At that point, the improved upgrade logic in v25.4 (which no longer overrides a valid OID) means we won’t need to downgrade types for backward compatibility. We may also perform a one time descriptor migration to update any NAME descriptors to the modern format after the cluster upgrade. In short, `downgradeType` for NAME is a temporary measure to bridge the v25.3 gap. Fixes #153322 Release note: None
1 parent 892744f commit 036b796

File tree

5 files changed

+137
-7
lines changed

5 files changed

+137
-7
lines changed

pkg/sql/logictest/testdata/logic_test/crdb_internal_catalog

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,4 @@ SELECT id, strip_volatile(descriptor) FROM crdb_internal.kv_catalog_descriptor W
149149
111 {"table": {"checks": [{"columnIds": [1], "constraintId": 2, "expr": "k > 0:::INT8", "name": "ck"}], "columns": [{"id": 1, "name": "k", "type": {"family": "IntFamily", "oid": 20, "width": 64}}, {"id": 2, "name": "v", "nullable": true, "type": {"family": "StringFamily", "oid": 25}}], "dependedOnBy": [{"columnIds": [1, 2], "id": 112}], "formatVersion": 3, "id": 111, "name": "kv", "nextColumnId": 3, "nextConstraintId": 3, "nextIndexId": 2, "nextMutationId": 1, "parentId": 106, "primaryIndex": {"constraintId": 1, "encodingType": 1, "foreignKey": {}, "geoConfig": {}, "id": 1, "interleave": {}, "keyColumnDirections": ["ASC"], "keyColumnIds": [1], "keyColumnNames": ["k"], "name": "kv_pkey", "partitioning": {}, "sharded": {}, "storeColumnIds": [2], "storeColumnNames": ["v"], "unique": true, "vecConfig": {}, "version": 4}, "privileges": {"ownerProto": "root", "users": [{"privileges": "2", "userProto": "admin", "withGrantOption": "2"}, {"privileges": "2", "userProto": "root", "withGrantOption": "2"}], "version": 3}, "replacementOf": {"time": {}}, "schemaLocked": true, "unexposedParentSchemaId": 107, "version": "7"}}
150150
112 {"table": {"columns": [{"id": 1, "name": "k", "nullable": true, "type": {"family": "IntFamily", "oid": 20, "width": 64}}, {"id": 2, "name": "v", "nullable": true, "type": {"family": "StringFamily", "oid": 25}}, {"defaultExpr": "unique_rowid()", "hidden": true, "id": 3, "name": "rowid", "type": {"family": "IntFamily", "oid": 20, "width": 64}}], "dependsOn": [111], "formatVersion": 3, "id": 112, "indexes": [{"createdExplicitly": true, "foreignKey": {}, "geoConfig": {}, "id": 2, "interleave": {}, "keyColumnDirections": ["ASC"], "keyColumnIds": [2], "keyColumnNames": ["v"], "keySuffixColumnIds": [3], "name": "idx", "partitioning": {}, "sharded": {}, "vecConfig": {}, "version": 4}], "isMaterializedView": true, "name": "mv", "nextColumnId": 4, "nextConstraintId": 2, "nextIndexId": 4, "nextMutationId": 1, "parentId": 106, "primaryIndex": {"constraintId": 1, "encodingType": 1, "foreignKey": {}, "geoConfig": {}, "id": 1, "interleave": {}, "keyColumnDirections": ["ASC"], "keyColumnIds": [3], "keyColumnNames": ["rowid"], "name": "mv_pkey", "partitioning": {}, "sharded": {}, "storeColumnIds": [1, 2], "storeColumnNames": ["k", "v"], "unique": true, "vecConfig": {}, "version": 4}, "privileges": {"ownerProto": "root", "users": [{"privileges": "2", "userProto": "admin", "withGrantOption": "2"}, {"privileges": "2", "userProto": "root", "withGrantOption": "2"}], "version": 3}, "replacementOf": {"time": {}}, "unexposedParentSchemaId": 107, "version": "8", "viewQuery": "SELECT k, v FROM db.public.kv"}}
151151
113 {"function": {"functionBody": "SELECT json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(json_remove_path(d, ARRAY['table':::STRING, 'families':::STRING]:::STRING[]), ARRAY['table':::STRING, 'nextFamilyId':::STRING]:::STRING[]), ARRAY['table':::STRING, 'indexes':::STRING, '0':::STRING, 'createdAtNanos':::STRING]:::STRING[]), ARRAY['table':::STRING, 'indexes':::STRING, '1':::STRING, 'createdAtNanos':::STRING]:::STRING[]), ARRAY['table':::STRING, 'indexes':::STRING, '2':::STRING, 'createdAtNanos':::STRING]:::STRING[]), ARRAY['table':::STRING, 'primaryIndex':::STRING, 'createdAtNanos':::STRING]:::STRING[]), ARRAY['table':::STRING, 'createAsOfTime':::STRING]:::STRING[]), ARRAY['table':::STRING, 'modificationTime':::STRING]:::STRING[]), ARRAY['function':::STRING, 'modificationTime':::STRING]:::STRING[]), ARRAY['type':::STRING, 'modificationTime':::STRING]:::STRING[]), ARRAY['schema':::STRING, 'modificationTime':::STRING]:::STRING[]), ARRAY['database':::STRING, 'modificationTime':::STRING]:::STRING[]);", "id": 113, "lang": "SQL", "name": "strip_volatile", "nullInputBehavior": "CALLED_ON_NULL_INPUT", "params": [{"class": "IN", "name": "d", "type": {"family": "JsonFamily", "oid": 3802}}], "parentId": 104, "parentSchemaId": 105, "privileges": {"ownerProto": "root", "users": [{"privileges": "2", "userProto": "admin", "withGrantOption": "2"}, {"privileges": "1048576", "userProto": "public"}, {"privileges": "2", "userProto": "root", "withGrantOption": "2"}], "version": 3}, "returnType": {"type": {"family": "JsonFamily", "oid": 3802}}, "version": "1", "volatility": "STABLE"}}
152-
4294966962 {"table": {"columns": [{"id": 1, "name": "f_table_catalog", "nullable": true, "type": {"family": "StringFamily", "oid": 19}}, {"id": 2, "name": "f_table_schema", "nullable": true, "type": {"family": "StringFamily", "oid": 19}}, {"id": 3, "name": "f_table_name", "nullable": true, "type": {"family": "StringFamily", "oid": 19}}, {"id": 4, "name": "f_geography_column", "nullable": true, "type": {"family": "StringFamily", "oid": 19}}, {"id": 5, "name": "coord_dimension", "nullable": true, "type": {"family": "IntFamily", "oid": 20, "width": 64}}, {"id": 6, "name": "srid", "nullable": true, "type": {"family": "IntFamily", "oid": 20, "width": 64}}, {"id": 7, "name": "type", "nullable": true, "type": {"family": "StringFamily", "oid": 25}}], "formatVersion": 3, "id": 4294966962, "name": "geography_columns", "nextColumnId": 8, "nextConstraintId": 2, "nextIndexId": 2, "nextMutationId": 1, "primaryIndex": {"constraintId": 1, "foreignKey": {}, "geoConfig": {}, "id": 1, "interleave": {}, "partitioning": {}, "sharded": {}, "vecConfig": {}}, "privileges": {"ownerProto": "node", "users": [{"privileges": "32", "userProto": "public"}], "version": 3}, "replacementOf": {"time": {}}, "unexposedParentSchemaId": 4294966963, "version": "1"}}
152+
4294966962 {"table": {"columns": [{"id": 1, "name": "f_table_catalog", "nullable": true, "type": {"family": 11, "oid": 19}}, {"id": 2, "name": "f_table_schema", "nullable": true, "type": {"family": 11, "oid": 19}}, {"id": 3, "name": "f_table_name", "nullable": true, "type": {"family": 11, "oid": 19}}, {"id": 4, "name": "f_geography_column", "nullable": true, "type": {"family": 11, "oid": 19}}, {"id": 5, "name": "coord_dimension", "nullable": true, "type": {"family": "IntFamily", "oid": 20, "width": 64}}, {"id": 6, "name": "srid", "nullable": true, "type": {"family": "IntFamily", "oid": 20, "width": 64}}, {"id": 7, "name": "type", "nullable": true, "type": {"family": "StringFamily", "oid": 25}}], "formatVersion": 3, "id": 4294966962, "name": "geography_columns", "nextColumnId": 8, "nextConstraintId": 2, "nextIndexId": 2, "nextMutationId": 1, "primaryIndex": {"constraintId": 1, "foreignKey": {}, "geoConfig": {}, "id": 1, "interleave": {}, "partitioning": {}, "sharded": {}, "vecConfig": {}}, "privileges": {"ownerProto": "node", "users": [{"privileges": "32", "userProto": "public"}], "version": 3}, "replacementOf": {"time": {}}, "unexposedParentSchemaId": 4294966963, "version": "1"}}

pkg/sql/logictest/testdata/logic_test/mixed_version_char

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# LogicTest: cockroach-go-testserver-25.2
22

3+
subtest regression_152629
4+
35
# This test verifies that the "char" type is handled correctly in a mixed
46
# version cluster. The reason it's being tested is because during 25.4
57
# development we began removing the legacy code for serializing types in the
@@ -25,3 +27,48 @@ user root nodeidx=2
2527

2628
statement ok
2729
insert into t values (ARRAY['abc'::STRING, ''::STRING], 'cat'::STRING);
30+
31+
subtest end
32+
33+
subtest regression_153322
34+
35+
# This is a repro scenario for 153322. It involves the NAME data type, which is
36+
# a string type column type.
37+
38+
# 1 is already upgraded but adding a redundant one here incase this subtest is
39+
# run in isolation.
40+
upgrade 1
41+
42+
user root nodeidx=1
43+
44+
# Create the function using the NAME data type in the upgraded version.
45+
statement ok
46+
CREATE FUNCTION test_name_func(p NAME) RETURNS TEXT LANGUAGE SQL AS 'SELECT p::TEXT'
47+
48+
query T
49+
SELECT test_name_func('test_value')
50+
----
51+
test_value
52+
53+
user root nodeidx=2
54+
55+
# Attempt to access the function on the old non-upgraded versions. These had
56+
# previously failed to resolve the function.
57+
58+
query T
59+
SELECT test_name_func('test_value')
60+
----
61+
test_value
62+
63+
statement ok
64+
ALTER FUNCTION test_name_func(NAME) RENAME TO test_name_func_renamed
65+
66+
query T
67+
SELECT test_name_func_renamed('renamed_test')
68+
----
69+
renamed_test
70+
71+
statement ok
72+
DROP FUNCTION test_name_func_renamed(NAME);
73+
74+
subtest end

pkg/sql/types/types.go

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2526,7 +2526,18 @@ func (t *T) IsPseudoType() bool {
25262526
//
25272527
// Marshal is part of the protoutil.Message interface.
25282528
func (t *T) Size() (n int) {
2529-
return t.InternalType.Size()
2529+
// non-NAME types don't need any serialization changes to be backwards compatible.
2530+
if t.InternalType.Oid != oid.T_name {
2531+
return t.InternalType.Size()
2532+
}
2533+
// Need to first downgrade the type before delegating to InternalType,
2534+
// because Marshal will downgrade.
2535+
temp := *t
2536+
err := temp.downgradeType()
2537+
if err != nil {
2538+
panic(errors.NewAssertionErrorWithWrappedErrf(err, "error during Size call"))
2539+
}
2540+
return temp.InternalType.Size()
25302541
}
25312542

25322543
// Identical is the internal implementation for T.Identical. See that comment
@@ -2832,12 +2843,50 @@ func (t *T) upgradeType() error {
28322843
return nil
28332844
}
28342845

2846+
// downgradeType transforms NAME types for backwards-compatibility with v25.3
2847+
// and earlier versions during mixed-version clusters. For non-NAME types, this
2848+
// is a no-op.
2849+
//
2850+
// NAME types are downgraded to use the legacy Family=name encoding so that v25.3
2851+
// nodes can correctly interpret them without losing NAME semantics.
2852+
//
2853+
// TODO(sql-foundations): Remove this function entirely once v25.4 is the minimum
2854+
// supported version and we no longer need to support mixed-version clusters with
2855+
// v25.3 nodes.
2856+
func (t *T) downgradeType() error {
2857+
// Set Family for NAME type for backwards-compatibility.
2858+
switch t.Family() {
2859+
case StringFamily, CollatedStringFamily:
2860+
t.InternalType.Family = name
2861+
// If locale is an empty string, we set it to nil to ensure older versions
2862+
// don’t misclassify the NAME as a collated string.
2863+
if t.InternalType.Locale != nil && len(*t.InternalType.Locale) == 0 {
2864+
t.InternalType.Locale = nil
2865+
}
2866+
}
2867+
2868+
return nil
2869+
}
2870+
28352871
// Marshal serializes a type into a byte representation using gogo protobuf
2836-
// serialization rules. It returns the resulting bytes as a slice.
2872+
// serialization rules. It returns the resulting bytes as a slice. The bytes
2873+
// are serialized in a format that is backwards-compatible with the previous
2874+
// version of CRDB so that clusters can run in mixed version mode during
2875+
// upgrade.
28372876
//
28382877
// bytes, err := protoutil.Marshal(&typ)
28392878
func (t *T) Marshal() (data []byte, err error) {
2840-
return protoutil.Marshal(&t.InternalType)
2879+
// non-NAME types don't need any serialization changes to be backwards compatible.
2880+
if t.InternalType.Oid != oid.T_name {
2881+
return protoutil.Marshal(&t.InternalType)
2882+
}
2883+
// First downgrade to a struct that will be serialized in a backwards-
2884+
// compatible bytes format.
2885+
temp := *t
2886+
if err := temp.downgradeType(); err != nil {
2887+
return nil, err
2888+
}
2889+
return protoutil.Marshal(&temp.InternalType)
28412890
}
28422891

28432892
// MarshalToSizedBuffer is like Mashal, except that it deserializes to
@@ -2846,7 +2895,15 @@ func (t *T) Marshal() (data []byte, err error) {
28462895
//
28472896
// Marshal is part of the protoutil.Message interface.
28482897
func (t *T) MarshalToSizedBuffer(data []byte) (int, error) {
2849-
return t.InternalType.MarshalToSizedBuffer(data)
2898+
// non-NAME types don't need any serialization changes to be backwards compatible.
2899+
if t.InternalType.Oid != oid.T_name {
2900+
return t.InternalType.MarshalToSizedBuffer(data)
2901+
}
2902+
temp := *t
2903+
if err := temp.downgradeType(); err != nil {
2904+
return 0, err
2905+
}
2906+
return temp.InternalType.MarshalToSizedBuffer(data)
28502907
}
28512908

28522909
// MarshalTo behaves like Marshal, except that it deserializes to an existing
@@ -2856,7 +2913,15 @@ func (t *T) MarshalToSizedBuffer(data []byte) (int, error) {
28562913
//
28572914
// Marshal is part of the protoutil.Message interface.
28582915
func (t *T) MarshalTo(data []byte) (int, error) {
2859-
return t.InternalType.MarshalTo(data)
2916+
// non-NAME types don't need any serialization changes to be backwards compatible.
2917+
if t.InternalType.Oid != oid.T_name {
2918+
return t.InternalType.MarshalTo(data)
2919+
}
2920+
temp := *t
2921+
if err := temp.downgradeType(); err != nil {
2922+
return 0, err
2923+
}
2924+
return temp.InternalType.MarshalTo(data)
28602925
}
28612926

28622927
// String returns the name of the type, similar to the Name method. However, it

pkg/sql/types/types_jsonpb.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,17 @@ import (
1919
// MarshalJSONPB marshals the T to json. This is necessary as otherwise
2020
// this field will be lost to the crdb_internal.pb_to_json and the likes.
2121
func (t *T) MarshalJSONPB(marshaler *jsonpb.Marshaler) ([]byte, error) {
22+
temp := *t
23+
// Only NAME types need changes to be backwards compatible.
24+
if temp.InternalType.Oid == oid.T_name {
25+
if err := temp.downgradeType(); err != nil {
26+
return nil, err
27+
}
28+
}
2229
// Map empty locale to nil so empty string does not appear in the JSON result.
2330
// TODO(rafi): When we upgrade to go1.24, we can modify the proto definition
2431
// of the locale field to use `[(gogoproto.jsontag) = ",omitzero"]` instead of
2532
// this workaround.
26-
temp := *t
2733
if temp.InternalType.Locale != nil && len(*temp.InternalType.Locale) == 0 {
2834
temp.InternalType.Locale = nil
2935
}

pkg/sql/types/types_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,7 @@ func TestUnmarshalCompat(t *testing.T) {
789789
{InternalType{Family: StringFamily, VisibleType: visibleVARCHAR, Width: 20}, MakeVarChar(20)},
790790
{InternalType{Family: StringFamily, VisibleType: visibleCHAR}, BPChar},
791791
{InternalType{Family: StringFamily, VisibleType: visibleQCHAR, Width: 1}, QChar},
792+
{InternalType{Family: name}, Name},
792793
}
793794

794795
for _, tc := range testCases {
@@ -981,6 +982,17 @@ func TestUpgradeType(t *testing.T) {
981982
Locale: &emptyLocale,
982983
}},
983984
},
985+
{
986+
desc: "legacy NAME support",
987+
input: &T{InternalType: InternalType{
988+
Family: name,
989+
}},
990+
expected: &T{InternalType: InternalType{
991+
Family: StringFamily,
992+
Oid: oid.T_name,
993+
Locale: &emptyLocale,
994+
}},
995+
},
984996
}
985997

986998
for _, tc := range testCases {

0 commit comments

Comments
 (0)