Skip to content

Commit 4dbcbcf

Browse files
refactor(tables): mint column ids via generateId (uuid), drop collision-check plumbing
1 parent 07597cc commit 4dbcbcf

5 files changed

Lines changed: 35 additions & 85 deletions

File tree

apps/sim/app/api/table/[tableId]/import/route.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,10 @@ describe('POST /api/table/[tableId]/import', () => {
397397
expect.objectContaining({ name: 'email', type: 'string' }),
398398
])
399399
// Existing columns have no id (legacy) → keyed by name; the new `email`
400-
// column was assigned id `col_short-id` (mocked generateShortId).
400+
// column was assigned id `col_deadbeefcafef00d` (mocked generateId).
401401
expect(appendRows()).toEqual([
402-
{ name: 'Alice', age: 30, 'col_short-id': 'a@x.io' },
403-
{ name: 'Bob', age: 40, 'col_short-id': 'b@x.io' },
402+
{ name: 'Alice', age: 30, col_deadbeefcafef00d: 'a@x.io' },
403+
{ name: 'Bob', age: 40, col_deadbeefcafef00d: 'b@x.io' },
404404
])
405405
})
406406

apps/sim/app/api/table/[tableId]/import/route.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
type CsvHeaderMapping,
2323
CsvImportValidationError,
2424
coerceRowsForTable,
25-
collectColumnIds,
2625
createCsvParser,
2726
dispatchAfterBatchInsert,
2827
generateColumnId,
@@ -193,7 +192,6 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
193192
}
194193

195194
const usedNames = new Set(table.schema.columns.map((c) => c.name.toLowerCase()))
196-
const takenIds = new Set(collectColumnIds(table.schema))
197195
const updatedMapping: CsvHeaderMapping = { ...effectiveMapping }
198196
const newColumns: TableSchema['columns'] = []
199197

@@ -209,8 +207,7 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
209207
const inferredType = inferColumnType(rows.map((r) => r[header]))
210208
// Pre-assign the id so the prospective schema (used to coerce rows) and
211209
// the persisted column (created in importAppendRows) share the same key.
212-
const id = generateColumnId(takenIds)
213-
takenIds.add(id)
210+
const id = generateColumnId()
214211
additions.push({ id, name: columnName, type: inferredType })
215212
newColumns.push({
216213
id,

apps/sim/lib/table/__tests__/column-keys.test.ts

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,17 @@
33
*/
44
import { describe, expect, it, vi } from 'vitest'
55

6-
const { mockGenerateShortId } = vi.hoisted(() => ({
7-
mockGenerateShortId: vi.fn(),
6+
const { mockGenerateId } = vi.hoisted(() => ({
7+
mockGenerateId: vi.fn(),
88
}))
99

1010
vi.mock('@sim/utils/id', () => ({
11-
generateShortId: mockGenerateShortId,
11+
generateId: mockGenerateId,
1212
}))
1313

1414
import {
1515
buildIdByName,
1616
buildNameById,
17-
collectColumnIds,
1817
filterNamesToIds,
1918
generateColumnId,
2019
getColumnId,
@@ -36,22 +35,15 @@ describe('getColumnId', () => {
3635
})
3736

3837
describe('generateColumnId', () => {
39-
it('mints a col_-prefixed id not already taken', () => {
40-
let n = 0
41-
mockGenerateShortId.mockImplementation(() => `gen${n++}`)
42-
expect(generateColumnId([])).toBe('col_gen0')
38+
it('mints a col_-prefixed id with the uuid dashes stripped', () => {
39+
mockGenerateId.mockReturnValue('11111111-2222-4333-8444-555566667777')
40+
expect(generateColumnId()).toBe('col_11111111222243338444555566667777')
4341
})
4442

45-
it('skips collisions against existing ids', () => {
46-
let n = 0
47-
mockGenerateShortId.mockImplementation(() => ['dup', 'dup', 'fresh'][n++] ?? 'x')
48-
expect(generateColumnId(['col_dup'])).toBe('col_fresh')
49-
})
50-
51-
it('falls back to a counter suffix when the RNG is fully deterministic', () => {
52-
mockGenerateShortId.mockReturnValue('fixed')
53-
// 'col_fixed' is taken and the RNG never changes → counter fallback.
54-
expect(generateColumnId(['col_fixed'])).toBe('col_fixed_1')
43+
it('produces an id that satisfies NAME_PATTERN (valid JSONB key / filter field)', () => {
44+
mockGenerateId.mockReturnValue('0a1b2c3d-4e5f-4607-8809-0a1b2c3d4e5f')
45+
// Must start with a letter/underscore and contain only [a-z0-9_].
46+
expect(generateColumnId()).toMatch(/^[a-z_][a-z0-9_]*$/i)
5547
})
5648
})
5749

@@ -69,9 +61,6 @@ describe('name ↔ id maps', () => {
6961
it('buildNameById maps storage id → display name', () => {
7062
expect(Object.fromEntries(buildNameById(schema))).toEqual({ col_1: 'email', age: 'age' })
7163
})
72-
it('collectColumnIds resolves every column', () => {
73-
expect(collectColumnIds(schema)).toEqual(['col_1', 'age'])
74-
})
7564
})
7665

7766
describe('row data translation', () => {
@@ -124,7 +113,7 @@ describe('filter / sort translation', () => {
124113

125114
describe('withGeneratedColumnIds', () => {
126115
it('stamps ids on id-less columns and remaps group refs name → id', () => {
127-
mockGenerateShortId.mockReturnValueOnce('a').mockReturnValueOnce('b')
116+
mockGenerateId.mockReturnValueOnce('a').mockReturnValueOnce('b')
128117
const schema: TableSchema = {
129118
columns: [
130119
{ name: 'email', type: 'string', workflowGroupId: 'g1' },

apps/sim/lib/table/column-keys.ts

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,9 @@
88
* map builders here.
99
*/
1010

11-
import { generateShortId } from '@sim/utils/id'
11+
import { generateId } from '@sim/utils/id'
1212
import type { ColumnDefinition, Filter, RowData, Sort, TableSchema, WorkflowGroup } from './types'
1313

14-
/**
15-
* Alphanumeric alphabet (no `-`) so a generated `col_…` id satisfies
16-
* `NAME_PATTERN` — filter/sort field validation runs on the id, and the id is
17-
* embedded as a JSONB key, so it must contain only safe characters.
18-
*/
19-
const COLUMN_ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
20-
2114
/**
2215
* Resolves a column's stable storage key. Falls back to `name` for legacy
2316
* columns that predate the id backfill — those rows were written keyed by name,
@@ -28,32 +21,16 @@ export function getColumnId(col: Pick<ColumnDefinition, 'id' | 'name'>): string
2821
}
2922

3023
/**
31-
* Mints a fresh, collision-free column id. Generated ids are opaque (`col_…`)
32-
* and deliberately distinct from display names so renames never disturb them.
33-
* Checked against `existingIds` (which includes legacy name-as-id values) so a
34-
* generated id can never alias an existing column.
24+
* Mints a fresh column id. Generated ids are opaque (`col_<uuid>`) and
25+
* deliberately distinct from display names so renames never disturb them. The
26+
* `col_` prefix is required: the id is validated against `NAME_PATTERN` (it's a
27+
* JSONB key and a filter/sort field) which must start with a letter/underscore,
28+
* and a bare UUID can start with a digit. Dashes are stripped for the same
29+
* reason. A v4 UUID's 122 random bits make a collision within a table's columns
30+
* effectively impossible, so no uniqueness check is needed.
3531
*/
36-
export function generateColumnId(existingIds: Iterable<string>): string {
37-
const taken = new Set(existingIds)
38-
const mint = () => `col_${generateShortId(16, COLUMN_ID_ALPHABET)}`
39-
for (let i = 0; i < 100; i++) {
40-
const id = mint()
41-
if (!taken.has(id)) return id
42-
}
43-
// Deterministic-RNG fallback (e.g. a fixed-value `generateShortId` mock in
44-
// tests): append a counter so a batch of column adds can't collide/loop.
45-
let n = 1
46-
let id = `${mint()}_${n}`
47-
while (taken.has(id)) {
48-
n++
49-
id = `${mint()}_${n}`
50-
}
51-
return id
52-
}
53-
54-
/** All current column ids in a schema (resolved via `getColumnId`). */
55-
export function collectColumnIds(schema: TableSchema): string[] {
56-
return schema.columns.map(getColumnId)
32+
export function generateColumnId(): string {
33+
return `col_${generateId().replace(/-/g, '')}`
5734
}
5835

5936
/**
@@ -75,15 +52,13 @@ export function columnMatchesRef(col: ColumnDefinition, ref: string): boolean {
7552
* already carry an id.
7653
*/
7754
export function withGeneratedColumnIds(schema: TableSchema): TableSchema {
78-
const taken = new Set(schema.columns.map(getColumnId))
7955
const idByName = new Map<string, string>()
8056
const columns = schema.columns.map((col) => {
8157
if (col.id) {
8258
idByName.set(col.name, col.id)
8359
return col
8460
}
85-
const id = generateColumnId(taken)
86-
taken.add(id)
61+
const id = generateColumnId()
8762
idByName.set(col.name, id)
8863
return { ...col, id }
8964
})

apps/sim/lib/table/service.ts

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import { generateRestoreName } from '@/lib/core/utils/restore-name'
3838
import type { DbOrTx } from '@/lib/db/types'
3939
import { materializeExecutionData } from '@/lib/logs/execution/trace-store'
4040
import {
41-
collectColumnIds,
4241
columnMatchesRef,
4342
generateColumnId,
4443
getColumnId,
@@ -585,7 +584,7 @@ export async function addTableColumn(
585584
const newColumn: TableSchema['columns'][number] = {
586585
// Honor a caller-provided id (undo of a delete reuses the original id);
587586
// otherwise mint a fresh one.
588-
id: column.id ?? generateColumnId(collectColumnIds(schema)),
587+
id: column.id ?? generateColumnId(),
589588
name: column.name,
590589
type: column.type as TableSchema['columns'][number]['type'],
591590
required: column.required ?? false,
@@ -662,7 +661,6 @@ export async function addTableColumnsWithTx(
662661
if (columns.length === 0) return table
663662

664663
const usedNames = new Set(table.schema.columns.map((c) => c.name.toLowerCase()))
665-
const takenIds = new Set(collectColumnIds(table.schema))
666664
const additions: TableSchema['columns'] = []
667665

668666
for (const column of columns) {
@@ -688,8 +686,7 @@ export async function addTableColumnsWithTx(
688686
usedNames.add(lower)
689687
// Honor a caller-assigned id (the CSV append path pre-assigns so coercion
690688
// and persistence agree); otherwise mint one.
691-
const id = column.id ?? generateColumnId(takenIds)
692-
takenIds.add(id)
689+
const id = column.id ?? generateColumnId()
693690
additions.push({
694691
id,
695692
name: column.name,
@@ -3932,13 +3929,9 @@ export async function addWorkflowGroup(
39323929
// Assign stable ids to the new output columns, then rewrite the group's
39333930
// column refs from name → id so outputs/deps/inputMappings key on ids —
39343931
// matching the row-data storage key and surviving future renames.
3935-
const takenIds = new Set(collectColumnIds(schema))
3936-
const outputColumns = data.outputColumns.map((col) => {
3937-
if (col.id) return col
3938-
const id = generateColumnId(takenIds)
3939-
takenIds.add(id)
3940-
return { ...col, id }
3941-
})
3932+
const outputColumns = data.outputColumns.map((col) =>
3933+
col.id ? col : { ...col, id: generateColumnId() }
3934+
)
39423935
const updatedColumns = [...schema.columns, ...outputColumns]
39433936
const idByName = new Map(updatedColumns.map((c) => [c.name, getColumnId(c)]))
39443937
const group = remapGroupColumnRefs(data.group, idByName)
@@ -4083,13 +4076,9 @@ export async function updateWorkflowGroup(
40834076
// row-data storage key). New output columns get ids first; then output
40844077
// `columnName`, deps, input mappings, and mapping-update targets are
40854078
// remapped name → id. Callers that already pass ids are unaffected.
4086-
const takenIds = new Set(collectColumnIds(schema))
4087-
const newColDefs = (data.newOutputColumns ?? []).map((col) => {
4088-
if (col.id) return col
4089-
const id = generateColumnId(takenIds)
4090-
takenIds.add(id)
4091-
return { ...col, id }
4092-
})
4079+
const newColDefs = (data.newOutputColumns ?? []).map((col) =>
4080+
col.id ? col : { ...col, id: generateColumnId() }
4081+
)
40934082
const idByName = new Map(
40944083
[...schema.columns, ...newColDefs].map((c) => [c.name, getColumnId(c)])
40954084
)
@@ -4474,7 +4463,7 @@ export async function addWorkflowGroupOutput(
44744463
}
44754464

44764465
const newColDef: ColumnDefinition = {
4477-
id: generateColumnId(collectColumnIds(schema)),
4466+
id: generateColumnId(),
44784467
name: columnName,
44794468
type: newColumnType,
44804469
required: false,

0 commit comments

Comments
 (0)