( data: UpsertRowData, table: TableDefinition, requestId: string )
| 506 | * @throws Error if no unique columns, ambiguous conflict target, or capacity exceeded |
| 507 | */ |
| 508 | export async function upsertRow( |
| 509 | data: UpsertRowData, |
| 510 | table: TableDefinition, |
| 511 | requestId: string |
| 512 | ): Promise<UpsertResult> { |
| 513 | const schema = table.schema |
| 514 | const uniqueColumns = getUniqueColumns(schema) |
| 515 | |
| 516 | if (uniqueColumns.length === 0) { |
| 517 | throw new Error( |
| 518 | 'Upsert requires at least one unique column in the schema. Please add a unique constraint to a column or use insert instead.' |
| 519 | ) |
| 520 | } |
| 521 | |
| 522 | // Determine the single conflict target column, resolving to its stable |
| 523 | // storage id (the row-data key). `conflictTarget` may arrive as an id |
| 524 | // (first-party) or a name (legacy/internal) — match either. |
| 525 | let targetColumnKey: string |
| 526 | if (data.conflictTarget) { |
| 527 | const col = uniqueColumns.find( |
| 528 | (c) => getColumnId(c) === data.conflictTarget || c.name === data.conflictTarget |
| 529 | ) |
| 530 | if (!col) { |
| 531 | throw new Error( |
| 532 | `Column "${data.conflictTarget}" is not a unique column. Available unique columns: ${uniqueColumns.map((c) => c.name).join(', ')}` |
| 533 | ) |
| 534 | } |
| 535 | targetColumnKey = getColumnId(col) |
| 536 | } else if (uniqueColumns.length === 1) { |
| 537 | targetColumnKey = getColumnId(uniqueColumns[0]) |
| 538 | } else { |
| 539 | throw new Error( |
| 540 | `Table has multiple unique columns (${uniqueColumns.map((c) => c.name).join(', ')}). Specify a conflict column to indicate which one to match on.` |
| 541 | ) |
| 542 | } |
| 543 | |
| 544 | // Validate row data |
| 545 | const sizeValidation = validateRowSize(data.data) |
| 546 | if (!sizeValidation.valid) { |
| 547 | throw new Error(sizeValidation.errors.join(', ')) |
| 548 | } |
| 549 | |
| 550 | const schemaValidation = coerceRowToSchema(data.data, schema) |
| 551 | if (!schemaValidation.valid) { |
| 552 | throw new Error(`Schema validation failed: ${schemaValidation.errors.join(', ')}`) |
| 553 | } |
| 554 | |
| 555 | // Read the conflict-target value *after* coercion so `matchFilter` branches on |
| 556 | // the persisted type (e.g. a coerced `"123"` → `123` matches existing rows). |
| 557 | const targetValue = data.data[targetColumnKey] |
| 558 | if (targetValue === undefined || targetValue === null) { |
| 559 | // Surface the display name, not the internal id — v1 callers pass a name. |
| 560 | const targetColumnName = |
| 561 | uniqueColumns.find((c) => getColumnId(c) === targetColumnKey)?.name ?? targetColumnKey |
| 562 | throw new Error(`Upsert requires a value for the conflict target column "${targetColumnName}"`) |
| 563 | } |
| 564 | |
| 565 | // `data->` and `data->>` accept the JSON key as a parameterized text value; |
no test coverage detected