Skip to content

Commit 3044802

Browse files
committed
Address PR review feedback (#7119)
- preserve explicit nulls from source-owned conversion normalization - normalize hooked values before select migration - cover null and select conversion rewrites
1 parent aeb5efe commit 3044802

4 files changed

Lines changed: 59 additions & 5 deletions

File tree

apps/sim/lib/table/column-types/extension-points.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ describe('column type extension points', () => {
6969
).toBe('stored-value')
7070
})
7171

72+
it('preserves an intentional null from source normalization', () => {
73+
Object.assign(definition, {
74+
valueForConversion: () => null,
75+
})
76+
77+
expect(
78+
valueForTypeConversion(
79+
'stored-value',
80+
{ name: 'source', type: 'string' },
81+
{ name: 'target', type: 'number' }
82+
)
83+
).toBeNull()
84+
})
85+
7286
it('lets a type own CSV import coercion', () => {
7387
Object.assign(definition, {
7488
coerceImport: (value: unknown) => `imported:${String(value)}`,

apps/sim/lib/table/column-types/registry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ export function valueForTypeConversion(
9696
source: ColumnDefinition,
9797
target: ColumnDefinition
9898
): JsonValue {
99-
return columnTypeOf(source).valueForConversion?.(value, target) ?? value
99+
const normalized = columnTypeOf(source).valueForConversion?.(value, target)
100+
return normalized === undefined ? value : normalized
100101
}
101102

102103
/** This type's own metadata errors; types carrying no metadata report none. */

apps/sim/lib/table/columns/retype-cell.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,25 @@
22
* @vitest-environment node
33
*/
44

5-
import { describe, expect, it } from 'vitest'
5+
import { afterEach, describe, expect, it } from 'vitest'
6+
import { COLUMN_TYPE_REGISTRY } from '@/lib/table/column-types'
67
import { retypeCellRewrite } from '@/lib/table/columns/service'
78
import type { ColumnDefinition } from '@/lib/table/types'
89

910
const column = (over: Partial<ColumnDefinition>): ColumnDefinition =>
1011
({ name: 'col', type: 'string', ...over }) as ColumnDefinition
1112

13+
const sourceDefinition = COLUMN_TYPE_REGISTRY.string
14+
const originalValueForConversion = sourceDefinition.valueForConversion
15+
16+
afterEach(() => {
17+
if (originalValueForConversion === undefined) {
18+
Reflect.deleteProperty(sourceDefinition, 'valueForConversion')
19+
return
20+
}
21+
Object.assign(sourceDefinition, { valueForConversion: originalValueForConversion })
22+
})
23+
1224
describe('retypeCellRewrite', () => {
1325
it('preserves an empty string the target type can hold', () => {
1426
// `''` is a real stored value: `coerceRowValues` keeps it for `string`, and
@@ -32,6 +44,29 @@ describe('retypeCellRewrite', () => {
3244
expect(retypeCellRewrite('true', column({ type: 'boolean' }))).toEqual({ value: true })
3345
})
3446

47+
it('writes back null produced by source normalization', () => {
48+
Object.assign(sourceDefinition, { valueForConversion: () => null })
49+
50+
expect(
51+
retypeCellRewrite('stored-value', column({ type: 'number' }), column({ type: 'string' }))
52+
).toEqual({ value: null })
53+
})
54+
55+
it('coerces source-normalized values into select storage', () => {
56+
Object.assign(sourceDefinition, { valueForConversion: () => 'Choice' })
57+
58+
expect(
59+
retypeCellRewrite(
60+
'stored-value',
61+
column({
62+
type: 'select',
63+
options: [{ id: 'opt_choice', name: 'Choice' }],
64+
}),
65+
column({ type: 'string' })
66+
)
67+
).toEqual({ value: 'opt_choice' })
68+
})
69+
3570
it('skips a cell whose stored value already matches the coercion', () => {
3671
expect(retypeCellRewrite('kept', column({ type: 'json' }))).toBeNull()
3772
expect(retypeCellRewrite(3, column({ type: 'json' }))).toBeNull()

apps/sim/lib/table/columns/service.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,8 @@ export function retypeCellRewrite(
777777
? valueForTypeConversion(value as JsonValue, source, target)
778778
: (value as JsonValue)
779779

780+
if (effective === null) return { value: null }
781+
780782
if (!isValueCompatibleWithColumn(effective, target)) {
781783
// Incompatible non-blanks never reach here: the compatibility scan already
782784
// refused the whole conversion for them.
@@ -919,6 +921,7 @@ export async function updateColumnType(
919921
const isSelectType = data.newType === 'select'
920922
const targetOptions = data.options ?? column.options ?? []
921923
const targetMultiple = data.multiple ?? column.multiple
924+
const sourceNormalizesConversion = columnTypeOf(column).valueForConversion !== undefined
922925
// Leaving `select` behind: stored cells hold option ids, which mean nothing
923926
// once the column is text/number/etc. Check compatibility against the option
924927
// NAME — that's what the cell will actually become (migrated below).
@@ -1035,9 +1038,7 @@ export async function updateColumnType(
10351038
resolved: new Map<string, JsonValue>(),
10361039
}
10371040
await migrationFrom(column.type)?.(migrationContext)
1038-
if (isSelectType) {
1039-
await migrationTo(data.newType)?.(migrationContext)
1040-
} else {
1041+
if (!isSelectType || sourceNormalizesConversion) {
10411042
let rewriteAfterId: string | undefined
10421043
while (true) {
10431044
const rows = await readColumnRetypePage(
@@ -1065,6 +1066,9 @@ export async function updateColumnType(
10651066
if (rows.length < retypeScanBatchSize) break
10661067
}
10671068
}
1069+
if (isSelectType) {
1070+
await migrationTo(data.newType)?.(migrationContext)
1071+
}
10681072

10691073
// A `unique` arriving with this retype is validated HERE, against the values
10701074
// the conversion just wrote — not by the separate constraint write that

0 commit comments

Comments
 (0)