( data: RenameColumnData, requestId: string )
| 153 | * @throws Error if table not found, column not found, or new name conflicts |
| 154 | */ |
| 155 | export async function renameColumn( |
| 156 | data: RenameColumnData, |
| 157 | requestId: string |
| 158 | ): Promise<TableDefinition> { |
| 159 | return withLockedTable(data.tableId, async (table, trx) => { |
| 160 | if (!NAME_PATTERN.test(data.newName)) { |
| 161 | throw new Error( |
| 162 | `Invalid column name "${data.newName}". Column names must start with a letter or underscore, followed by alphanumeric characters or underscores.` |
| 163 | ) |
| 164 | } |
| 165 | |
| 166 | if (data.newName.length > TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH) { |
| 167 | throw new Error( |
| 168 | `Column name exceeds maximum length (${TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH} characters)` |
| 169 | ) |
| 170 | } |
| 171 | |
| 172 | const schema = table.schema |
| 173 | const columnIndex = schema.columns.findIndex((c) => columnMatchesRef(c, data.oldName)) |
| 174 | if (columnIndex === -1) { |
| 175 | throw new Error(`Column "${data.oldName}" not found`) |
| 176 | } |
| 177 | |
| 178 | if ( |
| 179 | schema.columns.some( |
| 180 | (c, i) => i !== columnIndex && c.name.toLowerCase() === data.newName.toLowerCase() |
| 181 | ) |
| 182 | ) { |
| 183 | throw new Error(`Column "${data.newName}" already exists`) |
| 184 | } |
| 185 | |
| 186 | const targetColumn = schema.columns[columnIndex] |
| 187 | const actualOldName = targetColumn.name |
| 188 | |
| 189 | // Rename is metadata-only: stored rows, metadata, and workflow-group refs all |
| 190 | // key on the column's stable id, which a rename never changes — so this is a |
| 191 | // pure schema write, no per-row JSONB rewrite or group/metadata cascade. |
| 192 | // Stamp the current storage key as the id (for any not-yet-backfilled column) |
| 193 | // so existing rows stay reachable as the display name changes. |
| 194 | const columnId = targetColumn.id ?? actualOldName |
| 195 | const updatedColumns = schema.columns.map((c, i) => |
| 196 | i === columnIndex ? { ...c, id: columnId, name: data.newName } : c |
| 197 | ) |
| 198 | const updatedSchema: TableSchema = { ...schema, columns: updatedColumns } |
| 199 | assertValidSchema(updatedSchema, table.metadata?.columnOrder) |
| 200 | |
| 201 | const now = new Date() |
| 202 | await trx |
| 203 | .update(userTableDefinitions) |
| 204 | .set({ schema: updatedSchema, updatedAt: now }) |
| 205 | .where(eq(userTableDefinitions.id, data.tableId)) |
| 206 | |
| 207 | logger.info( |
| 208 | `[${requestId}] Renamed column "${actualOldName}" to "${data.newName}" in table ${data.tableId}` |
| 209 | ) |
| 210 | return { ...table, schema: updatedSchema, updatedAt: now } |
| 211 | }) |
| 212 | } |
no test coverage detected