Skip to content

Commit 9ab2632

Browse files
committed
fix(tables): apply type+options atomically in updateColumnType, fix biome format in contract refine
1 parent 00181cd commit 9ab2632

5 files changed

Lines changed: 59 additions & 30 deletions

File tree

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,14 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Colu
119119

120120
if (updates.type) {
121121
updatedTable = await updateColumnType(
122-
{ tableId, columnName: updates.name ?? validated.columnName, newType: updates.type },
122+
{
123+
tableId,
124+
columnName: updates.name ?? validated.columnName,
125+
newType: updates.type,
126+
// Applied inside updateColumnType's transaction so a combined
127+
// type+options change is atomic.
128+
...(updates.options !== undefined ? { options: updates.options } : {}),
129+
},
123130
requestId
124131
)
125132
}
@@ -136,7 +143,7 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Colu
136143
)
137144
}
138145

139-
if (updates.options !== undefined) {
146+
if (updates.options !== undefined && !updates.type) {
140147
updatedTable = await updateColumnOptions(
141148
{ tableId, columnName: updates.name ?? validated.columnName, options: updates.options },
142149
requestId

apps/sim/app/api/v1/tables/[tableId]/columns/route.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,14 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Colu
141141

142142
if (updates.type) {
143143
updatedTable = await updateColumnType(
144-
{ tableId, columnName: updates.name ?? validated.columnName, newType: updates.type },
144+
{
145+
tableId,
146+
columnName: updates.name ?? validated.columnName,
147+
newType: updates.type,
148+
// Applied inside updateColumnType's transaction so a combined
149+
// type+options change is atomic.
150+
...(updates.options !== undefined ? { options: updates.options } : {}),
151+
},
145152
requestId
146153
)
147154
}
@@ -158,7 +165,7 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Colu
158165
)
159166
}
160167

161-
if (updates.options !== undefined) {
168+
if (updates.options !== undefined && !updates.type) {
162169
updatedTable = await updateColumnOptions(
163170
{ tableId, columnName: updates.name ?? validated.columnName, options: updates.options },
164171
requestId

apps/sim/lib/api/contracts/tables.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,7 @@ export const updateTableColumnBodySchema = z.object({
179179
(updates) =>
180180
updates.options === undefined || updates.type === undefined || updates.type === 'select',
181181
{
182-
message:
183-
'options can only be set when the column type is (or is being changed to) select',
182+
message: 'options can only be set when the column type is (or is being changed to) select',
184183
path: ['options'],
185184
}
186185
),

apps/sim/lib/table/service.ts

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4303,44 +4303,55 @@ export async function updateColumnType(
43034303
}
43044304

43054305
const column = schema.columns[columnIndex]
4306-
if (column.type === data.newType) {
4306+
if (column.type === data.newType && data.options === undefined) {
43074307
return table
43084308
}
4309+
4310+
if (data.options !== undefined) {
4311+
const optionsValidation = validateColumnOptions(data.options, column.name, data.newType)
4312+
if (!optionsValidation.valid) {
4313+
throw new Error(`Invalid column options: ${optionsValidation.errors.join('; ')}`)
4314+
}
4315+
}
4316+
43094317
const columnKey = getColumnId(column)
43104318

4311-
// Validate existing data is compatible with the new type
4312-
const rows = await trx
4313-
.select({ id: userTableRows.id, data: userTableRows.data })
4314-
.from(userTableRows)
4315-
.where(
4316-
and(
4317-
eq(userTableRows.tableId, data.tableId),
4318-
sql`${userTableRows.data} ? ${columnKey}`,
4319-
sql`${userTableRows.data}->>${columnKey}::text IS NOT NULL`
4319+
if (column.type !== data.newType) {
4320+
// Validate existing data is compatible with the new type
4321+
const rows = await trx
4322+
.select({ id: userTableRows.id, data: userTableRows.data })
4323+
.from(userTableRows)
4324+
.where(
4325+
and(
4326+
eq(userTableRows.tableId, data.tableId),
4327+
sql`${userTableRows.data} ? ${columnKey}`,
4328+
sql`${userTableRows.data}->>${columnKey}::text IS NOT NULL`
4329+
)
43204330
)
4321-
)
43224331

4323-
let incompatibleCount = 0
4324-
for (const row of rows) {
4325-
const rowData = row.data as RowData
4326-
const value = rowData[columnKey]
4327-
if (value === null || value === undefined) continue
4332+
let incompatibleCount = 0
4333+
for (const row of rows) {
4334+
const rowData = row.data as RowData
4335+
const value = rowData[columnKey]
4336+
if (value === null || value === undefined) continue
43284337

4329-
if (!isValueCompatibleWithType(value, data.newType)) {
4330-
incompatibleCount++
4338+
if (!isValueCompatibleWithType(value, data.newType)) {
4339+
incompatibleCount++
4340+
}
43314341
}
4332-
}
43334342

4334-
if (incompatibleCount > 0) {
4335-
throw new Error(
4336-
`Cannot change column "${column.name}" to type "${data.newType}": ${incompatibleCount} row(s) have incompatible values. Fix or remove the incompatible values first.`
4337-
)
4343+
if (incompatibleCount > 0) {
4344+
throw new Error(
4345+
`Cannot change column "${column.name}" to type "${data.newType}": ${incompatibleCount} row(s) have incompatible values. Fix or remove the incompatible values first.`
4346+
)
4347+
}
43384348
}
43394349

43404350
const updatedColumns = schema.columns.map((c, i) => {
43414351
if (i !== columnIndex) return c
43424352
const next = { ...c, type: data.newType }
4343-
return data.newType === 'select' ? next : omit(next, ['options'])
4353+
if (data.newType !== 'select') return omit(next, ['options'])
4354+
return data.options !== undefined ? { ...next, options: data.options } : next
43444355
})
43454356
const updatedSchema: TableSchema = { ...schema, columns: updatedColumns }
43464357
const now = new Date()

apps/sim/lib/table/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,11 @@ export interface UpdateColumnTypeData {
559559
tableId: string
560560
columnName: string
561561
newType: (typeof COLUMN_TYPES)[number]
562+
/**
563+
* Replacement option set applied atomically with the type change (same
564+
* locked transaction). Only valid when `newType` is `select`.
565+
*/
566+
options?: string[]
562567
}
563568

564569
export interface UpdateColumnOptionsData {

0 commit comments

Comments
 (0)