Skip to content

Commit 04569de

Browse files
committed
fix(tables): read the remaining row cells by own key in bulk validation, replace dedupe, and the upsert probe
1 parent 093602b commit 04569de

4 files changed

Lines changed: 84 additions & 11 deletions

File tree

‎apps/sim/lib/table/bulk-update-concurrency.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ vi.mock('@/lib/table/sql', () => ({
3535

3636
vi.mock('@/lib/table/trigger', () => tableTriggerMock)
3737

38-
vi.mock('@/lib/table/validation', () => ({
38+
vi.mock('@/lib/table/validation', async (importOriginal) => ({
39+
cellOf: (await importOriginal<typeof import('@/lib/table/validation')>()).cellOf,
3940
validateRowSize: hoisted.validateRowSize,
4041
coerceRowToSchema: hoisted.coerceRowToSchema,
4142
coerceRowValues: vi.fn(),

‎apps/sim/lib/table/rows/row-writes.integration.ts‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,74 @@ describe('table row writes against real PostgreSQL', () => {
11261126
expect(result.affectedCount).toBe(2)
11271127
})
11281128

1129+
it('does not count a required legacy column named after a prototype key as supplied', async () => {
1130+
const table = await createTable([
1131+
{ name: 'constructor', type: 'string', required: true },
1132+
{ id: 'note', name: 'note', type: 'string' },
1133+
])
1134+
await seedRows(table.id, [
1135+
{ id: `${table.id}-a`, data: { constructor: 'a', note: 'n' }, orderKey: 'a0' },
1136+
])
1137+
1138+
const result = await updateRowsByFilter(
1139+
table,
1140+
{
1141+
filter: { note: 'n' },
1142+
data: { note: 'patched' },
1143+
limit: 10,
1144+
secretProvenance: undefined,
1145+
capabilityGovernedUserId: null,
1146+
},
1147+
'prototype-key'
1148+
)
1149+
1150+
expect(result.affectedCount).toBe(1)
1151+
})
1152+
1153+
it('replaces rows that leave a unique legacy column named after a prototype key empty', async () => {
1154+
const table = await createTable([
1155+
{ name: 'constructor', type: 'string', unique: true },
1156+
{ id: 'note', name: 'note', type: 'string' },
1157+
])
1158+
1159+
await replaceTableRows(
1160+
{
1161+
tableId: table.id,
1162+
workspaceId,
1163+
rows: [{ note: 'a' }, { note: 'b' }],
1164+
secretProvenance: undefined,
1165+
},
1166+
table,
1167+
'prototype-key'
1168+
)
1169+
1170+
const [{ count }] = await control<{ count: number }[]>`SELECT count(*)::int AS count
1171+
FROM user_table_rows WHERE table_id = ${table.id}`
1172+
expect(count).toBe(2)
1173+
})
1174+
1175+
it('refuses an upsert missing its conflict target named after a prototype key', async () => {
1176+
const table = await createTable([
1177+
{ name: 'constructor', type: 'string', unique: true },
1178+
{ id: 'note', name: 'note', type: 'string' },
1179+
])
1180+
1181+
await expect(
1182+
upsertRow(
1183+
{
1184+
tableId: table.id,
1185+
workspaceId,
1186+
data: { note: 'a' },
1187+
conflictTarget: 'constructor',
1188+
secretProvenance: undefined,
1189+
capabilityGovernedUserId: null,
1190+
},
1191+
table,
1192+
'prototype-key'
1193+
)
1194+
).rejects.toThrow(/requires a value for the conflict target/)
1195+
})
1196+
11291197
it('refuses a bulk update writing one value to rows of a column made unique since its snapshot', async () => {
11301198
const table = await seededTable()
11311199
await updateColumnConstraints(

‎apps/sim/lib/table/rows/service.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ import type {
110110
UpsertRowData,
111111
} from '@/lib/table/types'
112112
import {
113+
cellOf,
113114
checkBatchUniqueConstraintsDb,
114115
checkUniqueConstraintsDb,
115116
coerceRowToSchema,
@@ -611,7 +612,7 @@ export async function replaceTableRowsWithTx(
611612
// Coerced rows are keyed by column id, not display name — reading
612613
// `row[col.name]` silently misses renamed columns and lets dupes through.
613614
const colId = getColumnId(col)
614-
const value = row[colId]
615+
const value = cellOf(row, colId)
615616
if (value === null || value === undefined) continue
616617
const normalized = uniqueValueKey(value, col)
617618
const map = seen.get(colId)!
@@ -756,7 +757,7 @@ function resolveUpsertTarget(
756757
* matches a stored `123`).
757758
*/
758759
function upsertConflictProbe(target: ColumnDefinition, row: RowData): SQL {
759-
const targetValue = row[getColumnId(target)]
760+
const targetValue = cellOf(row, getColumnId(target))
760761
if (targetValue === undefined || targetValue === null) {
761762
// Surface the display name, not the internal id — v1 callers pass a name.
762763
throw new OrchestrationError(
@@ -2130,7 +2131,7 @@ function validateBulkUpdatePatch(
21302131
policy: UncoercibleValuePolicy | undefined
21312132
): void {
21322133
const suppliedColumns = table.schema.columns.filter((column) => {
2133-
const value = patch[getColumnId(column)]
2134+
const value = cellOf(patch, getColumnId(column))
21342135
return value !== null && value !== undefined
21352136
})
21362137
if (suppliedColumns.length === 0) return

‎apps/sim/lib/table/update-runner.test.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@ vi.mock('@/lib/table/rows/ordering', () => ({
3131
}))
3232
vi.mock('@/lib/table/events', () => tableEventsMock)
3333
vi.mock('@/lib/table/sql', () => ({ buildFilterClause: mockBuildFilterClause }))
34-
vi.mock('@/lib/table/validation', async (importOriginal) => ({
35-
uniqueColumnsInPatch: (await importOriginal<typeof import('@/lib/table/validation')>())
36-
.uniqueColumnsInPatch,
37-
validateRowSize: mockValidateRowSize,
38-
coerceRowToSchema: mockCoerceRowToSchema,
39-
coerceRowValues: mockCoerceRowValues,
40-
}))
34+
vi.mock('@/lib/table/validation', async (importOriginal) => {
35+
const actual = await importOriginal<typeof import('@/lib/table/validation')>()
36+
return {
37+
cellOf: actual.cellOf,
38+
uniqueColumnsInPatch: actual.uniqueColumnsInPatch,
39+
validateRowSize: mockValidateRowSize,
40+
coerceRowToSchema: mockCoerceRowToSchema,
41+
coerceRowValues: mockCoerceRowValues,
42+
}
43+
})
4144
vi.mock('@/lib/table/constants', () => ({
4245
...tableConstantsMock,
4346
TABLE_LIMITS: { ...tableConstantsMock.TABLE_LIMITS, DELETE_PAGE_SIZE: 2, UPDATE_BATCH_SIZE: 100 },

0 commit comments

Comments
 (0)