( data: DeleteColumnData, requestId: string )
| 287 | * @throws Error if table not found, column not found, or it's the last column |
| 288 | */ |
| 289 | export async function deleteColumn( |
| 290 | data: DeleteColumnData, |
| 291 | requestId: string |
| 292 | ): Promise<TableDefinition> { |
| 293 | const { def, stripKey } = await withLockedTable(data.tableId, async (table, trx) => { |
| 294 | const schema = table.schema |
| 295 | const columnIndex = schema.columns.findIndex((c) => columnMatchesRef(c, data.columnName)) |
| 296 | if (columnIndex === -1) { |
| 297 | throw new Error(`Column "${data.columnName}" not found`) |
| 298 | } |
| 299 | |
| 300 | if (schema.columns.length <= 1) { |
| 301 | throw new Error('Cannot delete the last column in a table') |
| 302 | } |
| 303 | |
| 304 | const targetColumn = schema.columns[columnIndex] |
| 305 | const actualName = targetColumn.name |
| 306 | const columnId = getColumnId(targetColumn) |
| 307 | const ownerGroupId = targetColumn.workflowGroupId |
| 308 | |
| 309 | // Drop this column's reference (by id) from every group's outputs and |
| 310 | // `columns` dependency. If the column is the last output of its parent |
| 311 | // group, the group itself is also removed (a group with zero outputs is |
| 312 | // invalid). |
| 313 | let groupRemovedId: string | null = null |
| 314 | const updatedGroups = (schema.workflowGroups ?? []) |
| 315 | .map((group) => { |
| 316 | let next = group |
| 317 | if (ownerGroupId && group.id === ownerGroupId) { |
| 318 | const remaining = group.outputs.filter((o) => o.columnName !== columnId) |
| 319 | if (remaining.length === 0) { |
| 320 | groupRemovedId = group.id |
| 321 | } |
| 322 | next = { ...next, outputs: remaining } |
| 323 | } |
| 324 | return stripGroupDeps(next, new Set([columnId])) |
| 325 | }) |
| 326 | .filter((g) => g.id !== groupRemovedId) |
| 327 | |
| 328 | const updatedSchema: TableSchema = { |
| 329 | ...schema, |
| 330 | columns: schema.columns.filter((_, i) => i !== columnIndex), |
| 331 | ...(updatedGroups.length > 0 ? { workflowGroups: updatedGroups } : {}), |
| 332 | } |
| 333 | const updatedMetadata = stripColumnIdsFromMetadata( |
| 334 | table.metadata as TableMetadata | null, |
| 335 | new Set([columnId]) |
| 336 | ) |
| 337 | assertValidSchema(updatedSchema, updatedMetadata?.columnOrder) |
| 338 | |
| 339 | const now = new Date() |
| 340 | |
| 341 | // Schema/metadata update commits now; the column's row-data storage is |
| 342 | // reclaimed in the background (fire-and-forget) — reads never surface the |
| 343 | // orphaned id since the column is already gone from the schema. |
| 344 | await trx |
| 345 | .update(userTableDefinitions) |
| 346 | .set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now }) |
no test coverage detected