(ex: UniqueCheckExecutor)
| 583 | // LOCAL dies at its commit; it only penalizes plan shape, and the statements |
| 584 | // that follow in those transactions are tenant-scoped writes). |
| 585 | const checkColumns = async (ex: UniqueCheckExecutor) => { |
| 586 | for (const [columnId, { values, column }] of valuesByColumn) { |
| 587 | if (values.size === 0) continue |
| 588 | |
| 589 | if (!NAME_PATTERN.test(columnId)) { |
| 590 | throw new Error(`Invalid column id: ${columnId}`) |
| 591 | } |
| 592 | |
| 593 | const valueArray = Array.from(values) |
| 594 | const valueConditions = valueArray.map((normalizedValue) => { |
| 595 | // Check if the original values are strings (normalized values for strings are lowercase) |
| 596 | // We need to determine the type from the column definition or the first row that has this value |
| 597 | const isStringColumn = column.type === 'string' |
| 598 | |
| 599 | if (isStringColumn) { |
| 600 | return sql`lower(${userTableRows.data}->>${sql.raw(`'${columnId}'`)}) = ${normalizedValue}` |
| 601 | } |
| 602 | return sql`(${userTableRows.data}->${sql.raw(`'${columnId}'`)})::jsonb = ${normalizedValue}::jsonb` |
| 603 | }) |
| 604 | |
| 605 | const conflictingRows = await ex |
| 606 | .select({ |
| 607 | id: userTableRows.id, |
| 608 | data: userTableRows.data, |
| 609 | position: userTableRows.position, |
| 610 | }) |
| 611 | .from(userTableRows) |
| 612 | .where(and(eq(userTableRows.tableId, tableId), or(...valueConditions))) |
| 613 | .limit(valueArray.length) // We only need up to one conflict per value |
| 614 | |
| 615 | // Map conflicts back to batch rows |
| 616 | for (const conflict of conflictingRows) { |
| 617 | const conflictData = conflict.data as RowData |
| 618 | const conflictValue = conflictData[columnId] |
| 619 | const normalizedConflictValue = |
| 620 | typeof conflictValue === 'string' |
| 621 | ? conflictValue.toLowerCase() |
| 622 | : JSON.stringify(conflictValue) |
| 623 | |
| 624 | // Find which batch rows have this conflicting value |
| 625 | for (let i = 0; i < rows.length; i++) { |
| 626 | const rowValue = rows[i][columnId] |
| 627 | if (rowValue === null || rowValue === undefined) continue |
| 628 | |
| 629 | const normalizedRowValue = |
| 630 | typeof rowValue === 'string' ? rowValue.toLowerCase() : JSON.stringify(rowValue) |
| 631 | |
| 632 | if (normalizedRowValue === normalizedConflictValue) { |
| 633 | // Check if this row already has errors for this column |
| 634 | let rowError = rowErrors.find((e) => e.row === i) |
| 635 | if (!rowError) { |
| 636 | rowError = { row: i, errors: [] } |
| 637 | rowErrors.push(rowError) |
| 638 | } |
| 639 | |
| 640 | const errorMsg = `Column "${column.name}" must be unique. Value "${rowValue}" already exists in row ${conflict.position + 1}` |
| 641 | if (!rowError.errors.includes(errorMsg)) { |
| 642 | rowError.errors.push(errorMsg) |
no test coverage detected