You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(tables): validate a retype's unique against the values it writes
Validated the last partial-update seam with a focused investigation rather
than assuming. The answer was split.
`required` is already safe: `updateColumnType` runs the same `countEmptyCells`
against the constraint the request is about to set, which is why that check
exists.
`unique` was not, and the reachable case commits the unrecoverable half. A
text column holding "5" and "5.0", PATCHed with {type: number, unique: true}:
the conversion succeeds and coerces both to 5, then the separate constraint
write finds duplicates and 400s — with the column already numeric and "5.0"
irreversibly rewritten. A pre-scan of the raw text finds nothing; the
conversion is what manufactures the duplicate. The retype now carries `unique`
and checks it after the write-back, against the values it just wrote.
Constraint changes on a workflow-output column were the same shape — rejected
by the constraint write, after a type change had committed. Now rejected in the
route's pre-flight block, before any write.
The duplicate scan is extracted and shared between both paths for the same
reason `countEmptyCells` is: two copies of one rule is the drift that produced
the original required-check bug.
Deliberately NOT merging `updateColumnType` and `updateColumnConstraints`. They
assert different lock levels (destructive vs schema-only) and only the retype
needs the full row scan, so merging would either force a constraints-only
toggle to materialize every row or reintroduce the branching it was meant to
remove. With both reachable failures pre-validated, what remains at the seam is
concurrent races no in-process check can close.
error: `Cannot change constraints on workflow-output column "${currentColumn.name}". Constraints aren't applicable to columns whose values come from workflow execution.`,
error: `Cannot change constraints on workflow-output column "${currentColumn.name}". Constraints aren't applicable to columns whose values come from workflow execution.`,
Copy file name to clipboardExpand all lines: apps/sim/lib/table/columns/service.ts
+36-5Lines changed: 36 additions & 5 deletions
Original file line number
Diff line number
Diff line change
@@ -485,6 +485,26 @@ export async function deleteColumns(
485
485
returndef
486
486
}
487
487
488
+
/**
489
+
* Whether any two rows share a stored value in this column.
490
+
*
491
+
* Shared by the constraint write and the retype's pre-validation so the two
492
+
* cannot drift — the same reason {@link countEmptyCells} is shared. A retype
493
+
* that sets `unique` in the same request has to run this against the values the
494
+
* conversion is ABOUT to write, not the ones on disk: coercing `"5"` and `"5.0"`
495
+
* to a number manufactures a duplicate that no pre-scan of the raw text sees.
496
+
*/
497
+
asyncfunctionhasDuplicateValues(
498
+
trx: DbTransaction,
499
+
tableId: string,
500
+
columnKey: string
501
+
): Promise<boolean>{
502
+
constduplicates=(awaittrx.execute(
503
+
sql`SELECT ${userTableRows.data}->>${columnKey}::text AS val, count(*) AS cnt FROM ${userTableRows} WHERE table_id = ${tableId} AND ${userTableRows.data} ? ${columnKey} AND ${userTableRows.data}->>${columnKey}::text IS NOT NULL GROUP BY val HAVING count(*) > 1 LIMIT 1`
504
+
))as{val: string;cnt: number}[]
505
+
returnduplicates.length>0
506
+
}
507
+
488
508
/**
489
509
* Validates a pending rename against the schema it will land in, and returns
490
510
* the renamed column.
@@ -762,6 +782,21 @@ export async function updateColumnType(
`Cannot change column "${column.name}" to type "${data.newType}" and set it as unique: the converted values contain duplicates.`
796
+
)
797
+
}
798
+
}
799
+
765
800
awaittrx
766
801
.update(userTableDefinitions)
767
802
.set({schema: updatedSchema,updatedAt: now})
@@ -829,11 +864,7 @@ export async function updateColumnConstraints(
829
864
}
830
865
831
866
if(data.unique===true&&!column.unique){
832
-
constduplicates=(awaittrx.execute(
833
-
sql`SELECT ${userTableRows.data}->>${columnKey}::text AS val, count(*) AS cnt FROM ${userTableRows} WHERE table_id = ${data.tableId} AND ${userTableRows.data} ? ${columnKey} AND ${userTableRows.data}->>${columnKey}::text IS NOT NULL GROUP BY val HAVING count(*) > 1 LIMIT 1`
0 commit comments