(
table: TableDefinition,
additions: { id?: string; name: string; type: string; required?: boolean; unique?: boolean }[],
rows: RowData[],
ctx: { workspaceId: string; userId?: string; requestId: string }
)
| 144 | * resolves (post-commit), mirroring the other batch-insert sites. |
| 145 | */ |
| 146 | export async function importAppendRows( |
| 147 | table: TableDefinition, |
| 148 | additions: { id?: string; name: string; type: string; required?: boolean; unique?: boolean }[], |
| 149 | rows: RowData[], |
| 150 | ctx: { workspaceId: string; userId?: string; requestId: string } |
| 151 | ): Promise<{ inserted: TableRow[]; table: TableDefinition }> { |
| 152 | // Gate capacity before opening the tx — the lookup is a separate pool read. |
| 153 | const rowLimit = await assertRowCapacity({ |
| 154 | workspaceId: ctx.workspaceId, |
| 155 | currentRowCount: table.rowCount, |
| 156 | addedRows: rows.length, |
| 157 | }) |
| 158 | const result = await db.transaction(async (trx) => { |
| 159 | let working = table |
| 160 | if (additions.length > 0) { |
| 161 | // Take the row-order lock before creating columns so this path uses the |
| 162 | // same rows_pos → user_table_definitions order as plain inserts. Creating |
| 163 | // columns first would lock the definition row before rows_pos, inverting |
| 164 | // the order and deadlocking concurrent inserts on this table. The lock is |
| 165 | // re-entrant, so the per-batch acquire below is a no-op. |
| 166 | await acquireRowOrderLock(trx, table.id) |
| 167 | working = await addTableColumnsWithTx(trx, table, additions, ctx.requestId) |
| 168 | } |
| 169 | const inserted: TableRow[] = [] |
| 170 | for (let i = 0; i < rows.length; i += CSV_MAX_BATCH_SIZE) { |
| 171 | const batch = rows.slice(i, i + CSV_MAX_BATCH_SIZE) |
| 172 | const batchInserted = await batchInsertRowsWithTx( |
| 173 | trx, |
| 174 | { tableId: working.id, rows: batch, workspaceId: ctx.workspaceId, userId: ctx.userId }, |
| 175 | working, |
| 176 | generateId().slice(0, 8) |
| 177 | ) |
| 178 | inserted.push(...batchInserted) |
| 179 | } |
| 180 | return { inserted, table: working } |
| 181 | }) |
| 182 | notifyTableRowUsage({ |
| 183 | workspaceId: ctx.workspaceId, |
| 184 | currentRowCount: table.rowCount, |
| 185 | addedRows: result.inserted.length, |
| 186 | limit: rowLimit, |
| 187 | }) |
| 188 | return result |
| 189 | } |
| 190 | |
| 191 | /** |
| 192 | * Owns the replace-import transaction: optionally creates the new columns, then |
no test coverage detected