Skip to content

Commit ec6808d

Browse files
refactor(ts): construct Column/Field as discriminated unions (#1468)
Split from #1462: build Column/Field as immutable discriminated-union literals (adds ColumnWithoutName) instead of `{} as Column` plus mutation.
1 parent 34a3509 commit ec6808d

6 files changed

Lines changed: 192 additions & 128 deletions

File tree

ts/src/decoding/decodingTestUtils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { DictionaryType } from "../metadata/tile/dictionaryType";
55
import { LengthType } from "../metadata/tile/lengthType";
66
import { OffsetType } from "../metadata/tile/offsetType";
77
import IntWrapper from "./intWrapper";
8-
import { type Column, type Field, ComplexType, ScalarType } from "../metadata/tileset/tilesetMetadata";
8+
import { type Column, type Field, ColumnScope, ComplexType, ScalarType } from "../metadata/tileset/tilesetMetadata";
99
import { encodeBooleanRle, encodeStrings, createStringLengths } from "../encoding/encodingUtils";
1010
import { encodeVarintInt32Value, encodeVarintInt32 } from "../encoding/integerEncodingUtils";
1111
import type { RleEncodedStreamMetadata, StreamMetadata } from "../metadata/tile/streamMetadataDecoder";
@@ -74,6 +74,7 @@ export function createColumnMetadataForStruct(
7474
return {
7575
name: columnName,
7676
nullable: false,
77+
columnScope: ColumnScope.FEATURE,
7778
complexType: {
7879
physicalType: ComplexType.STRUCT,
7980
children,

ts/src/metadata/tileset/embeddedTilesetMetadataDecoder.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,19 @@ describe("embeddedTilesetMetadataDecoder", () => {
228228
}).toThrow("Missing layer name");
229229
});
230230

231+
it("should throw for an unsupported column type code", () => {
232+
const buffer = concatenateBuffers(
233+
encodeFieldName("layer"),
234+
encodeTypeCode(4096),
235+
encodeChildCount(1),
236+
encodeTypeCode(5), // 5 is not a valid column type code
237+
);
238+
239+
expect(() => {
240+
decodeEmbeddedTileSetMetadata(buffer, new IntWrapper(0));
241+
}).toThrow("Unsupported column type code 5");
242+
});
243+
231244
it("should decode logical ID metadata with implicit id column name", () => {
232245
const typeCode = 3;
233246
const buffer = concatenateBuffers(

ts/src/metadata/tileset/embeddedTilesetMetadataDecoder.ts

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,11 @@ function decodeString(src: Uint8Array, offset: IntWrapper): string {
2929
* Used when decoding Field metadata which has the same format as Column.
3030
*/
3131
function columnToField(column: Column): Field {
32-
return {
33-
name: column.name,
34-
nullable: column.nullable,
35-
scalarField: column.scalarType,
36-
complexField: column.complexType,
37-
type: column.type === "scalarType" ? "scalarField" : "complexField",
38-
};
32+
const name = column.name;
33+
const nullable = column.nullable;
34+
return column.type === "scalarType"
35+
? { type: "scalarField", scalarField: column.scalarType, name, nullable }
36+
: { type: "complexField", complexField: column.complexType, name, nullable };
3937
}
4038

4139
/**
@@ -44,21 +42,20 @@ function columnToField(column: Column): Field {
4442
export function decodeField(src: Uint8Array, offset: IntWrapper): Field {
4543
const typeCode = decodeVarintInt32(src, offset, 1)[0] >>> 0;
4644

47-
if (typeCode < 10 || typeCode > 30) {
45+
const base = typeCode >= 10 ? decodeColumnType(typeCode) : null;
46+
if (!base) {
4847
throw new Error(`Unsupported field type code ${typeCode}. Supported: ${SUPPORTED_FIELD_TYPES}`);
4948
}
5049

51-
const column = decodeColumnType(typeCode);
50+
// Field type codes (10-30) always carry an explicit name.
51+
const column: Column = { ...base, name: decodeString(src, offset) };
5252

53-
if (columnTypeHasName(typeCode)) {
54-
column.name = decodeString(src, offset);
55-
}
56-
57-
if (columnTypeHasChildren(typeCode)) {
53+
if (column.type === "complexType" && columnTypeHasChildren(typeCode)) {
54+
const complexCol = column.complexType;
5855
const childCount = decodeVarintInt32(src, offset, 1)[0] >>> 0;
59-
column.complexType.children = new Array(childCount);
56+
complexCol.children = new Array(childCount);
6057
for (let i = 0; i < childCount; i++) {
61-
column.complexType.children[i] = decodeField(src, offset);
58+
complexCol.children[i] = decodeField(src, offset);
6259
}
6360
}
6461

@@ -70,24 +67,26 @@ export function decodeField(src: Uint8Array, offset: IntWrapper): Field {
7067
*/
7168
function decodeColumn(src: Uint8Array, offset: IntWrapper): Column {
7269
const typeCode = decodeVarintInt32(src, offset, 1)[0] >>> 0;
73-
const column = decodeColumnType(typeCode);
70+
const base = decodeColumnType(typeCode);
7471

75-
if (!column) {
72+
if (!base) {
7673
throw new Error(`Unsupported column type code ${typeCode}. Supported: ${SUPPORTED_COLUMN_TYPES}`);
7774
}
7875

76+
let name: string;
7977
if (columnTypeHasName(typeCode)) {
80-
column.name = decodeString(src, offset);
78+
name = decodeString(src, offset);
79+
} else if (typeCode <= 3) {
80+
name = "id";
81+
} else if (typeCode === 4) {
82+
name = "geometry";
8183
} else {
82-
// ID and GEOMETRY columns have implicit names
83-
if (typeCode >= 0 && typeCode <= 3) {
84-
column.name = "id";
85-
} else if (typeCode === 4) {
86-
column.name = "geometry";
87-
}
84+
throw new Error(`Unsupported column type code ${typeCode}. Supported: ${SUPPORTED_COLUMN_TYPES}`);
8885
}
8986

90-
if (columnTypeHasChildren(typeCode)) {
87+
const column: Column = { ...base, name };
88+
89+
if (column.type === "complexType" && columnTypeHasChildren(typeCode)) {
9190
// Only STRUCT (typeCode 30) has children
9291
const childCount = decodeVarintInt32(src, offset, 1)[0] >>> 0;
9392
const complexCol = column.complexType;

ts/src/metadata/tileset/tilesetMetadata.ts

Lines changed: 46 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -31,63 +31,66 @@ export const LogicalComplexType = {
3131
RANGE_MAP: 1,
3232
} as const;
3333

34-
export interface TileSetMetadata {
35-
version?: number | null;
34+
export type TileSetMetadata = {
35+
version?: number;
3636
featureTables: FeatureTableSchema[];
37-
name?: string | null;
38-
description?: string | null;
39-
attribution?: string | null;
40-
minZoom?: number | null;
41-
maxZoom?: number | null;
37+
name?: string;
38+
description?: string;
39+
attribution?: string;
40+
minZoom?: number;
41+
maxZoom?: number;
4242
bounds: number[];
4343
center: number[];
44-
}
44+
};
4545

46-
export interface FeatureTableSchema {
47-
name?: string | null;
46+
export type FeatureTableSchema = {
47+
name: string;
4848
columns: Column[];
49-
}
49+
};
5050

51-
export interface Column {
52-
name?: string | null;
53-
nullable?: boolean | null;
54-
columnScope?: number | null;
55-
scalarType?: ScalarColumn | null;
56-
complexType?: ComplexColumn | null;
57-
type?: "scalarType" | "complexType";
58-
}
51+
export type Column = {
52+
name: string;
53+
nullable: boolean;
54+
columnScope: number;
55+
} & (
56+
| { type: "scalarType"; scalarType: ScalarColumn; complexType?: undefined }
57+
| { type: "complexType"; complexType: ComplexColumn; scalarType?: undefined }
58+
);
5959

60-
export interface ScalarColumn {
61-
longID?: boolean | null;
62-
physicalType?: number | null;
63-
logicalType?: number | null;
60+
/** `Omit` that distributes over the union members of {@link Column}, preserving the `type` discriminant. */
61+
export type ColumnWithoutName = Column extends infer C ? (C extends Column ? Omit<C, "name"> : never) : never;
62+
63+
export type ScalarColumn = {
64+
longID: boolean;
65+
physicalType?: number;
66+
logicalType?: number;
6467
type?: "physicalType" | "logicalType";
65-
}
68+
};
6669

67-
export interface ComplexColumn {
68-
physicalType?: number | null;
69-
logicalType?: number | null;
70+
export type ComplexColumn = {
71+
physicalType?: number;
72+
logicalType?: number;
7073
children: Field[];
7174
type?: "physicalType" | "logicalType";
72-
}
75+
};
7376

74-
export interface Field {
75-
name?: string | null;
76-
nullable?: boolean | null;
77-
scalarField?: ScalarField | null;
78-
complexField?: ComplexField | null;
79-
type?: "scalarField" | "complexField";
80-
}
77+
export type Field = {
78+
name?: string;
79+
nullable?: boolean;
80+
} & (
81+
| { type: "scalarField"; scalarField: ScalarField; complexField?: undefined }
82+
| { type: "complexField"; complexField: ComplexField; scalarField?: undefined }
83+
);
8184

82-
export interface ScalarField {
83-
physicalType?: number | null;
84-
logicalType?: number | null;
85+
export type ScalarField = {
86+
physicalType?: number;
87+
logicalType?: number;
8588
type?: "physicalType" | "logicalType";
86-
}
89+
};
8790

88-
export interface ComplexField {
89-
physicalType?: number | null;
90-
logicalType?: number | null;
91+
export type ComplexField = {
92+
physicalType?: number;
93+
logicalType?: number;
9194
children: Field[];
9295
type?: "physicalType" | "logicalType";
93-
}
96+
};

ts/src/metadata/tileset/typeMap.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,55 @@ describe("typeMap helpers", () => {
2727
}
2828
});
2929

30+
it("should decode the GEOMETRY type code as a non-nullable complex column", () => {
31+
expect(decodeColumnType(4)).toMatchObject({
32+
nullable: false,
33+
type: "complexType",
34+
complexType: { type: "physicalType", physicalType: ComplexType.GEOMETRY },
35+
});
36+
});
37+
38+
it("should decode the STRUCT type code as a non-nullable complex column", () => {
39+
expect(decodeColumnType(30)).toMatchObject({
40+
nullable: false,
41+
type: "complexType",
42+
complexType: { type: "physicalType", physicalType: ComplexType.STRUCT },
43+
});
44+
});
45+
46+
it("should decode scalar type codes with the nullable flag in the low bit", () => {
47+
const cases: Array<{ even: number; physicalType: number }> = [
48+
{ even: 10, physicalType: ScalarType.BOOLEAN },
49+
{ even: 12, physicalType: ScalarType.INT_8 },
50+
{ even: 14, physicalType: ScalarType.UINT_8 },
51+
{ even: 16, physicalType: ScalarType.INT_32 },
52+
{ even: 18, physicalType: ScalarType.UINT_32 },
53+
{ even: 20, physicalType: ScalarType.INT_64 },
54+
{ even: 22, physicalType: ScalarType.UINT_64 },
55+
{ even: 24, physicalType: ScalarType.FLOAT },
56+
{ even: 26, physicalType: ScalarType.DOUBLE },
57+
{ even: 28, physicalType: ScalarType.STRING },
58+
];
59+
60+
for (const { even, physicalType } of cases) {
61+
expect(decodeColumnType(even)).toMatchObject({
62+
nullable: false,
63+
type: "scalarType",
64+
scalarType: { type: "physicalType", physicalType },
65+
});
66+
expect(decodeColumnType(even + 1)).toMatchObject({
67+
nullable: true,
68+
type: "scalarType",
69+
scalarType: { type: "physicalType", physicalType },
70+
});
71+
}
72+
});
73+
74+
it("should return null for unsupported type codes", () => {
75+
expect(decodeColumnType(5)).toBeNull();
76+
expect(decodeColumnType(99)).toBeNull();
77+
});
78+
3079
it("should return false for physical scalar columns even when column is named id", () => {
3180
const physicalIdNamedColumn = {
3281
name: "id",

0 commit comments

Comments
 (0)