feat(sql): add addUniqueConstraint and addCheckConstraint to DiskCatalog
This commit is contained in:
1 parent
dafa4f8940
commit
d6d8dadb3d
1 file changed
+165
@@ -819,3 +819,168 @@ class DiskCatalog private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** [addForeignKey]'s counterpart for `ADD CONSTRAINT name UNIQUE (...)` - see its own doc for the validate-before-install contract. */
|
||||||
|
fun addUniqueConstraint(tableName: String, unique: UniqueConstraint) {
|
||||||
|
val key = tableName.lowercase()
|
||||||
|
val row = rowOrThrow(tableName)
|
||||||
|
val schema = row.table.schema
|
||||||
|
val newSchema = TableSchema(
|
||||||
|
schema.name, schema.columns, schema.foreignKeys, schema.checks, schema.uniqueConstraints + unique, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** [addForeignKey]'s counterpart for `ADD CONSTRAINT name CHECK (...)` - see its own doc for the validate-before-install contract. */
|
||||||
|
fun addCheckConstraint(tableName: String, check: CheckConstraint) {
|
||||||
|
val key = tableName.lowercase()
|
||||||
|
val row = rowOrThrow(tableName)
|
||||||
|
val schema = row.table.schema
|
||||||
|
val newSchema = TableSchema(
|
||||||
|
schema.name, schema.columns, schema.foreignKeys, schema.checks + check, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `ALTER TABLE t DROP CONSTRAINT name [RESTRICT | CASCADE]` - mirrors
|
||||||
|
* [kestrel.catalog.Catalog.dropConstraint]: finds a named CHECK, UNIQUE, or FOREIGN KEY
|
||||||
|
* constraint on [tableName] (case-insensitive; a PRIMARY KEY has no name to match against) and
|
||||||
|
* removes it. Only a UNIQUE constraint has cross-table fallout - see [dropUniqueConstraint].
|
||||||
|
*/
|
||||||
|
fun dropConstraint(tableName: String, name: String, cascade: Boolean) {
|
||||||
|
val key = tableName.lowercase()
|
||||||
|
val row = rowOrThrow(tableName)
|
||||||
|
val schema = row.table.schema
|
||||||
|
val foreignKey = schema.foreignKeys.find { it.name?.equals(name, ignoreCase = true) == true }
|
||||||
|
val check = schema.checks.find { it.name?.equals(name, ignoreCase = true) == true }
|
||||||
|
val unique = schema.uniqueConstraints.find { it.name?.equals(name, ignoreCase = true) == true }
|
||||||
|
when {
|
||||||
|
foreignKey != null -> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check != null -> {
|
||||||
|
val newSchema = TableSchema(
|
||||||
|
schema.name, schema.columns, schema.foreignKeys, schema.checks - check,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unique != null -> dropUniqueConstraint(tableName, unique, cascade)
|
||||||
|
else -> throw DiskCatalogException("no such constraint: $name in table $tableName")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors [kestrel.catalog.Catalog.dropUniqueConstraint]: a FOREIGN KEY's `referencedColumns`
|
||||||
|
* must exactly match one of the parent's enforced-unique key groups to have been accepted in the
|
||||||
|
* first place, so dropping a UNIQUE constraint can silently invalidate any FOREIGN KEY (on this
|
||||||
|
* table itself, via a self-reference, or on any other table) that leans on exactly this column
|
||||||
|
* set for its uniqueness guarantee, UNLESS another remaining group still covers it. RESTRICT
|
||||||
|
* (default) blocks the drop when a real dependent exists; CASCADE strips that FOREIGN KEY right
|
||||||
|
* along with it, mirroring [dropTable]'s own FOREIGN KEY handling.
|
||||||
|
*/
|
||||||
|
private fun dropUniqueConstraint(tableName: String, unique: UniqueConstraint, cascade: Boolean) {
|
||||||
|
val key = tableName.lowercase()
|
||||||
|
val schema = rowOrThrow(tableName).table.schema
|
||||||
|
val remainingUniques = schema.uniqueConstraints - unique
|
||||||
|
val targetSet = unique.columns.map { it.lowercase() }.toSet()
|
||||||
|
val stillEnforced = TableSchema(
|
||||||
|
schema.name, schema.columns, schema.foreignKeys, schema.checks, remainingUniques, schema.compositePrimaryKey
|
||||||
|
).enforcedUniqueKeyGroups().any { group -> group.map { it.lowercase() }.toSet() == targetSet }
|
||||||
|
|
||||||
|
fun dependsOnTarget(fk: ForeignKeyConstraint) =
|
||||||
|
fk.referencedTable.equals(tableName, ignoreCase = true) && fk.referencedColumns.map { it.lowercase() }.toSet() == targetSet
|
||||||
|
|
||||||
|
val dependents = if (stillEnforced) emptyList() else rowsByName.values.filter { other -> other.table.schema.foreignKeys.any(::dependsOnTarget) }
|
||||||
|
if (!cascade && dependents.isNotEmpty()) {
|
||||||
|
val names = dependents.map { it.table.schema.name }.distinct()
|
||||||
|
throw DiskCatalogException(
|
||||||
|
"cannot drop constraint ${unique.name} because table(s) ${names.joinToString(", ")} have a " +
|
||||||
|
"FOREIGN KEY referencing it; use CASCADE"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val txn = engine.beginTransaction()
|
||||||
|
try {
|
||||||
|
for (other in dependents) {
|
||||||
|
val otherKey = other.table.schema.name.lowercase()
|
||||||
|
val strippedForeignKeys = other.table.schema.foreignKeys.filterNot(::dependsOnTarget)
|
||||||
|
other.table.replaceSchemaMetadata(withForeignKeys(other.table.schema, strippedForeignKeys))
|
||||||
|
persistEntry(otherKey, txn)
|
||||||
|
}
|
||||||
|
// Re-fetched: a self-referencing FOREIGN KEY on tableName itself may have just been
|
||||||
|
// rewritten above (dependents is not restricted to OTHER tables).
|
||||||
|
val current = rowOrThrow(tableName)
|
||||||
|
val newSchema = TableSchema(
|
||||||
|
current.table.schema.name, current.table.schema.columns, current.table.schema.foreignKeys,
|
||||||
|
current.table.schema.checks, current.table.schema.uniqueConstraints - unique, current.table.schema.compositePrimaryKey
|
||||||
|
)
|
||||||
|
current.table.replaceSchemaMetadata(newSchema)
|
||||||
|
persistEntry(key, txn)
|
||||||
|
engine.commit(txn)
|
||||||
|
} catch (failure: Exception) {
|
||||||
|
engine.abort(txn)
|
||||||
|
throw failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renameInList(names: List<String>, oldName: String, newName: String): List<String> =
|
||||||
|
names.map { if (it.equals(oldName, ignoreCase = true)) newName else it }
|
||||||
|
|
||||||
|
/** Convenience for a single-column secondary index; delegates to the general, composite-capable overload below. */
|
||||||
|
fun createIndex(tableName: String, indexName: String, columnName: String, unique: Boolean = false): SecondaryIndexHandle =
|
||||||
|
createIndex(tableName, indexName, listOf(columnName), unique)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a secondary index over [columnNames] (more than one entry for a composite,
|
||||||
|
* multi-column index) - see [SecondaryIndexHandle]/[DiskTable] for how a composite key is
|
||||||
|
* encoded and how uniqueness is enforced.
|
||||||
|
*/
|
||||||
|
fun createIndex(tableName: String, indexName: String, columnNames: List<String>, unique: Boolean = false): SecondaryIndexHandle {
|
||||||
|
require(columnNames.isNotEmpty()) { "index must cover at least one column" }
|
||||||
|
val key = tableName.lowercase()
|
||||||
|
val row = rowsByName[key] ?: throw DiskCatalogException("no such table: $tableName")
|
||||||
|
if (row.table.secondaryIndexes.any { it.name.equals(indexName, ignoreCase = true) }) {
|
||||||
|
throw DiskCatalogException("index '$indexName' already exists")
|
||||||
|
}
|
||||||
|
val columnIndices = columnNames.map { columnName ->
|
||||||
|
row.table.schema.columnIndex(columnName)
|
||||||
|
?: throw DiskCatalogException("no such column: $columnName in table $tableName")
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user