feat(sql): add renameColumn and renameTable to DiskCatalog
This commit is contained in:
1 parent
3972e07d37
commit
dafa4f8940
1 file changed
+167
@@ -652,3 +652,170 @@ class DiskCatalog private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Always fully propagates rather than being rejected the way [dropColumn] can be - mirrors
|
||||
* [kestrel.catalog.Catalog.renameColumn]: this table's own PRIMARY KEY/FOREIGN KEY/UNIQUE/CHECK
|
||||
* definitions, any other table's FOREIGN KEY referencing this column, and any secondary index
|
||||
* defined on it (see [DiskTable.renameColumnAndIndexes]) all get the new name.
|
||||
*/
|
||||
fun renameColumn(tableName: String, oldName: String, newName: String) {
|
||||
val key = tableName.lowercase()
|
||||
val row = rowOrThrow(tableName)
|
||||
val schema = row.table.schema
|
||||
if (schema.columnIndex(oldName) == null) {
|
||||
throw DiskCatalogException("no such column: $oldName in table $tableName")
|
||||
}
|
||||
if (!oldName.equals(newName, ignoreCase = true) && schema.columnIndex(newName) != null) {
|
||||
throw DiskCatalogException("column $newName already exists in table $tableName")
|
||||
}
|
||||
|
||||
val newColumns = schema.columns.map { column ->
|
||||
val renamed = if (column.name.equals(oldName, ignoreCase = true)) column.copy(name = newName) else column
|
||||
// A GENERATED column can only ever reference this table's own columns, so renaming one
|
||||
// of them must rewrite that reference too - mirrors kestrel.catalog.Catalog.renameColumn.
|
||||
renamed.generated?.let { renamed.copy(generated = CheckExpressionColumns.rename(it, oldName, newName)) } ?: renamed
|
||||
}
|
||||
val newForeignKeys = schema.foreignKeys.map { fk ->
|
||||
val selfReference = fk.referencedTable.equals(tableName, ignoreCase = true)
|
||||
fk.copy(
|
||||
columns = renameInList(fk.columns, oldName, newName),
|
||||
referencedColumns = if (selfReference) renameInList(fk.referencedColumns, oldName, newName) else fk.referencedColumns
|
||||
)
|
||||
}
|
||||
val newUniqueConstraints = schema.uniqueConstraints.map { it.copy(columns = renameInList(it.columns, oldName, newName)) }
|
||||
val newChecks = schema.checks.map { CheckConstraint(it.name, CheckExpressionColumns.rename(it.expression, oldName, newName)) }
|
||||
val newCompositePrimaryKey = renameInList(schema.compositePrimaryKey, oldName, newName)
|
||||
val newSchema = TableSchema(schema.name, newColumns, newForeignKeys, newChecks, newUniqueConstraints, newCompositePrimaryKey)
|
||||
|
||||
val affectedOthers = rowsByName.entries.filter { (otherKey, other) ->
|
||||
otherKey != key && other.table.schema.foreignKeys.any { fk ->
|
||||
fk.referencedTable.equals(tableName, ignoreCase = true) && fk.referencedColumns.any { it.equals(oldName, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
|
||||
val txn = engine.beginTransaction()
|
||||
try {
|
||||
row.table.renameColumnAndIndexes(newSchema, oldName, newName)
|
||||
persistEntry(key, txn)
|
||||
for ((otherKey, other) in affectedOthers) {
|
||||
val updatedForeignKeys = other.table.schema.foreignKeys.map { fk ->
|
||||
if (fk.referencedTable.equals(tableName, ignoreCase = true)) {
|
||||
fk.copy(referencedColumns = renameInList(fk.referencedColumns, oldName, newName))
|
||||
} else {
|
||||
fk
|
||||
}
|
||||
}
|
||||
other.table.replaceSchemaMetadata(withForeignKeys(other.table.schema, updatedForeignKeys))
|
||||
persistEntry(otherKey, txn)
|
||||
}
|
||||
engine.commit(txn)
|
||||
} catch (failure: Exception) {
|
||||
engine.abort(txn)
|
||||
throw failure
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a table and propagates the identifier everywhere it is used by name - mirrors
|
||||
* [kestrel.catalog.Catalog.renameTable]: this table's own FOREIGN KEY(s) that self-reference it,
|
||||
* any other table's FOREIGN KEY pointing at it, every view whose defining query mentions it
|
||||
* anywhere (see [RelationRename]), and every trigger defined ON it (just the [TriggerDefinition.
|
||||
* tableName] field, re-rendered and re-persisted - trigger body statements are NOT rewritten,
|
||||
* same documented scope-cut as [kestrel.catalog.Catalog.renameTable]). Unlike the in-memory
|
||||
* engine there is no separate global index map to fix up - every secondary index lives inside its
|
||||
* own [DiskTable], keyed by column name, which a table rename never touches.
|
||||
*/
|
||||
fun renameTable(oldName: String, newName: String) {
|
||||
val oldKey = oldName.lowercase()
|
||||
val row = rowOrThrow(oldName)
|
||||
val newKey = newName.lowercase()
|
||||
if (!oldName.equals(newName, ignoreCase = true)) {
|
||||
if (rowsByName.containsKey(newKey)) throw DiskCatalogException("table '$newName' already exists")
|
||||
if (viewsByName.containsKey(newKey)) throw DiskCatalogException("a view named '$newName' already exists")
|
||||
}
|
||||
|
||||
val schema = row.table.schema
|
||||
val selfReferencing = schema.foreignKeys.any { it.referencedTable.equals(oldName, ignoreCase = true) }
|
||||
val ownNewForeignKeys = if (selfReferencing) {
|
||||
schema.foreignKeys.map { fk -> if (fk.referencedTable.equals(oldName, ignoreCase = true)) fk.copy(referencedTable = newName) else fk }
|
||||
} else {
|
||||
schema.foreignKeys
|
||||
}
|
||||
val newSchema = TableSchema(newName, schema.columns, ownNewForeignKeys, schema.checks, schema.uniqueConstraints, schema.compositePrimaryKey)
|
||||
|
||||
val affectedOthers = rowsByName.entries.filter { (otherKey, other) ->
|
||||
otherKey != oldKey && other.table.schema.foreignKeys.any { it.referencedTable.equals(oldName, ignoreCase = true) }
|
||||
}
|
||||
val affectedViews = viewsByName.entries.filter { (_, view) -> DependencyAnalysis.referencesRelation(view.query, oldName) }
|
||||
val affectedTriggers = triggersByName.entries.filter { (_, trigger) -> trigger.definition.tableName.equals(oldName, ignoreCase = true) }
|
||||
|
||||
val txn = engine.beginTransaction()
|
||||
try {
|
||||
row.table.replaceSchemaMetadata(newSchema)
|
||||
if (oldKey != newKey) {
|
||||
rowsByName.remove(oldKey)
|
||||
row.keyBox.value = newKey
|
||||
rowsByName[newKey] = row
|
||||
}
|
||||
persistEntry(newKey, txn)
|
||||
|
||||
for ((otherKey, other) in affectedOthers) {
|
||||
val updatedForeignKeys = other.table.schema.foreignKeys.map { fk ->
|
||||
if (fk.referencedTable.equals(oldName, ignoreCase = true)) fk.copy(referencedTable = newName) else fk
|
||||
}
|
||||
other.table.replaceSchemaMetadata(withForeignKeys(other.table.schema, updatedForeignKeys))
|
||||
persistEntry(otherKey, txn)
|
||||
}
|
||||
|
||||
for ((viewKey, viewRow) in affectedViews) {
|
||||
val renamedQuery = RelationRename.rename(viewRow.query, oldName, newName)
|
||||
val newEntry = ViewDirectoryEntry(
|
||||
viewRow.entry.viewName, SqlWriter.write(CreateViewStatement(viewRow.entry.viewName, renamedQuery, orReplace = false))
|
||||
)
|
||||
val rid = directory.update(txn, viewRow.rid, taggedView(newEntry))
|
||||
viewsByName[viewKey] = ViewRow(rid, newEntry, renamedQuery)
|
||||
}
|
||||
|
||||
for ((triggerKey, triggerRow) in affectedTriggers) {
|
||||
val renamedDefinition = triggerRow.definition.copy(tableName = newName)
|
||||
val newEntry = TriggerDirectoryEntry(triggerRow.entry.triggerName, SqlWriter.triggerText(renamedDefinition))
|
||||
val rid = directory.update(txn, triggerRow.rid, taggedTrigger(newEntry))
|
||||
triggersByName[triggerKey] = TriggerRow(rid, newEntry, renamedDefinition)
|
||||
}
|
||||
|
||||
engine.commit(txn)
|
||||
} catch (failure: Exception) {
|
||||
engine.abort(txn)
|
||||
throw failure
|
||||
}
|
||||
}
|
||||
|
||||
private fun withForeignKeys(schema: TableSchema, foreignKeys: List<ForeignKeyConstraint>): TableSchema = TableSchema(
|
||||
schema.name, schema.columns, foreignKeys, schema.checks, schema.uniqueConstraints, schema.compositePrimaryKey
|
||||
)
|
||||
|
||||
/**
|
||||
* `ALTER TABLE t ADD CONSTRAINT name FOREIGN KEY (...) REFERENCES ...` - mirrors
|
||||
* [kestrel.catalog.Catalog.addForeignKey]: [kestrel.store.sql.DiskExecutor.executeAddConstraint]
|
||||
* has already validated every existing row against [foreignKey] before calling this, so installing
|
||||
* it is pure schema metadata - [DiskTable.replaceSchemaMetadata] plus a re-persisted directory
|
||||
* entry, no row rewrite (row shape is unchanged) and no index to build.
|
||||
*/
|
||||
fun addForeignKey(tableName: String, foreignKey: ForeignKeyConstraint) {
|
||||
val key = tableName.lowercase()
|
||||
val row = rowOrThrow(tableName)
|
||||
val schema = row.table.schema
|
||||
val newSchema = TableSchema(
|
||||
schema.name, schema.columns, schema.foreignKeys + foreignKey, schema.checks, schema.uniqueConstraints, schema.compositePrimaryKey
|
||||
)
|
||||
val txn = engine.beginTransaction()
|
||||
try {
|
||||
row.table.replaceSchemaMetadata(newSchema)
|
||||
persistEntry(key, txn)
|
||||
engine.commit(txn)
|
||||
} catch (failure: Exception) {
|
||||
engine.abort(txn)
|
||||
throw failure
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user