()
| 136 | * map onto the existing schema, optionally auto-creating `createColumns` first. |
| 137 | */ |
| 138 | const resolveSetup = async () => { |
| 139 | const headers = Object.keys(sample[0]) |
| 140 | |
| 141 | if (mode === 'create') { |
| 142 | const inferred = inferSchemaFromCsv(headers, sample) |
| 143 | // Stamp ids so the imported table is id-native (rows coerce + persist by |
| 144 | // the same ids). |
| 145 | schema = withGeneratedColumnIds({ columns: inferred.columns.map(normalizeColumn) }) |
| 146 | headerToColumn = inferred.headerToColumn |
| 147 | await setTableSchemaForImport(tableId, schema) |
| 148 | return |
| 149 | } |
| 150 | |
| 151 | // append / replace into an existing table. |
| 152 | let targetSchema = table.schema |
| 153 | let effectiveMapping: CsvHeaderMapping = |
| 154 | payload.mapping ?? buildAutoMapping(headers, table.schema) |
| 155 | |
| 156 | if (payload.createColumns && payload.createColumns.length > 0) { |
| 157 | const unknown = payload.createColumns.filter((h) => !headers.includes(h)) |
| 158 | if (unknown.length > 0) { |
| 159 | throw new Error(`Columns to create are not in the CSV: ${unknown.join(', ')}`) |
| 160 | } |
| 161 | const usedNames = new Set(table.schema.columns.map((c) => c.name.toLowerCase())) |
| 162 | const additions: { name: string; type: string }[] = [] |
| 163 | const updatedMapping: CsvHeaderMapping = { ...effectiveMapping } |
| 164 | for (const header of payload.createColumns) { |
| 165 | const base = sanitizeName(header) |
| 166 | let columnName = base |
| 167 | let suffix = 2 |
| 168 | while (usedNames.has(columnName.toLowerCase())) { |
| 169 | columnName = `${base}_${suffix}` |
| 170 | suffix++ |
| 171 | } |
| 172 | usedNames.add(columnName.toLowerCase()) |
| 173 | additions.push({ name: columnName, type: inferColumnType(sample.map((r) => r[header])) }) |
| 174 | updatedMapping[header] = columnName |
| 175 | } |
| 176 | const updated = await addImportColumns(table, additions, requestId) |
| 177 | targetSchema = updated.schema |
| 178 | effectiveMapping = updatedMapping |
| 179 | } |
| 180 | |
| 181 | const validation = validateMapping({ |
| 182 | csvHeaders: headers, |
| 183 | mapping: effectiveMapping, |
| 184 | tableSchema: targetSchema, |
| 185 | }) |
| 186 | schema = targetSchema |
| 187 | headerToColumn = validation.effectiveMap |
| 188 | |
| 189 | // Replace deletes existing rows only after schema/mapping validation passes, so an |
| 190 | // invalid or empty file fails the import with the old rows still intact (a mid-stream |
| 191 | // insert failure after this point leaves a partial replace — replace is destructive). |
| 192 | if (mode === 'replace') await deleteAllTableRows(tableId) |
| 193 | } |
| 194 | |
| 195 | const flush = async (rows: Record<string, unknown>[]) => { |
no test coverage detected