( data: UpdateColumnConstraintsData, requestId: string )
| 547 | * @throws Error if table not found, column not found, or existing data violates the constraint |
| 548 | */ |
| 549 | export async function updateColumnConstraints( |
| 550 | data: UpdateColumnConstraintsData, |
| 551 | requestId: string |
| 552 | ): Promise<TableDefinition> { |
| 553 | return withLockedTable(data.tableId, async (table, trx) => { |
| 554 | // Scale both statement and idle timeouts to row count: the required/unique |
| 555 | // validation runs between separate queries inside this transaction, leaving |
| 556 | // it briefly idle. Match `updateColumnType` so the default 5s |
| 557 | // `idle_in_transaction_session_timeout` can't abort a valid change on a |
| 558 | // large table. |
| 559 | const timeoutMs = scaledStatementTimeoutMs(table.rowCount ?? 0, { |
| 560 | baseMs: 60_000, |
| 561 | perRowMs: 2, |
| 562 | }) |
| 563 | await setTableTxTimeouts(trx, { statementMs: timeoutMs, idleMs: timeoutMs }) |
| 564 | |
| 565 | const schema = table.schema |
| 566 | const columnIndex = schema.columns.findIndex((c) => columnMatchesRef(c, data.columnName)) |
| 567 | if (columnIndex === -1) { |
| 568 | throw new Error(`Column "${data.columnName}" not found`) |
| 569 | } |
| 570 | |
| 571 | const column = schema.columns[columnIndex] |
| 572 | const columnKey = getColumnId(column) |
| 573 | if (column.workflowGroupId) { |
| 574 | throw new Error( |
| 575 | `Cannot change constraints on workflow-output column "${column.name}". Constraints aren't applicable to columns whose values come from workflow execution.` |
| 576 | ) |
| 577 | } |
| 578 | if (data.required === true && !column.required) { |
| 579 | const [result] = await trx |
| 580 | .select({ count: count() }) |
| 581 | .from(userTableRows) |
| 582 | .where( |
| 583 | and( |
| 584 | eq(userTableRows.tableId, data.tableId), |
| 585 | sql`(NOT (${userTableRows.data} ? ${columnKey}) OR ${userTableRows.data}->>${columnKey}::text IS NULL)` |
| 586 | ) |
| 587 | ) |
| 588 | |
| 589 | if (result.count > 0) { |
| 590 | throw new Error( |
| 591 | `Cannot set column "${column.name}" as required: ${result.count} row(s) have null or missing values` |
| 592 | ) |
| 593 | } |
| 594 | } |
| 595 | |
| 596 | if (data.unique === true && !column.unique) { |
| 597 | const duplicates = (await trx.execute( |
| 598 | 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` |
| 599 | )) as { val: string; cnt: number }[] |
| 600 | |
| 601 | if (duplicates.length > 0) { |
| 602 | throw new Error(`Cannot set column "${column.name}" as unique: duplicate values exist`) |
| 603 | } |
| 604 | } |
| 605 | |
| 606 | const updatedColumns = schema.columns.map((c, i) => |
no test coverage detected