feat(sql): add createTemporaryTable and temporaryTableOrNull
This commit is contained in:
1 parent
249b3e03fd
commit
eb27ffd7dc
1 file changed
+162
@@ -329,3 +329,165 @@ class DiskCatalog private constructor(
|
||||
return table
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a brand new `CREATE TEMP TABLE`/`CREATE TEMPORARY TABLE` - see [temporaryTables]'
|
||||
* own KDoc for why this never touches [engine]/[directory] at all, unlike [createTable]. Name
|
||||
* collisions are checked against every namespace a permanent table already checks against
|
||||
* ([rowsByName]/[viewsByName]) plus every other temp table, deliberately NOT allowing a temp
|
||||
* table to shadow a same-named permanent table/view the way SQLite does - see [DiskExecutor]'s
|
||||
* own resolution order for the reasoning (a single, simpler, unambiguous namespace is a
|
||||
* deliberate, narrower scope-cut for this executor).
|
||||
*/
|
||||
fun createTemporaryTable(schema: TableSchema): Table {
|
||||
val key = schema.name.lowercase()
|
||||
if (rowsByName.containsKey(key)) throw DiskCatalogException("table '${schema.name}' already exists")
|
||||
if (viewsByName.containsKey(key)) throw DiskCatalogException("a view named '${schema.name}' already exists")
|
||||
if (temporaryTables.containsKey(key)) throw DiskCatalogException("table '${schema.name}' already exists")
|
||||
val table = Table(schema)
|
||||
temporaryTables[key] = table
|
||||
return table
|
||||
}
|
||||
|
||||
fun temporaryTableOrNull(name: String): Table? = temporaryTables[name.lowercase()]
|
||||
|
||||
fun dropTemporaryTable(name: String) {
|
||||
temporaryTables.remove(name.lowercase()) ?: throw DiskCatalogException("no such temporary table: $name")
|
||||
}
|
||||
|
||||
/**
|
||||
* Every currently-open temp table, oldest first - used by [DiskExecutor]'s BEGIN/SAVEPOINT/
|
||||
* ROLLBACK handling to snapshot/restore live [Table.rows] lists around a disk transaction. Since
|
||||
* `CREATE`/`DROP TEMPORARY TABLE` are DDL and already rejected inside an explicit transaction
|
||||
* (see [DiskExecutor.isDdl]), the *set* of temp tables never changes mid-transaction - only their
|
||||
* row contents can - so returning the live objects (not a name list) is enough for that caller to
|
||||
* snapshot/restore without a second lookup.
|
||||
*/
|
||||
val temporaryTableValues: List<Table> get() = temporaryTables.values.toList()
|
||||
|
||||
/**
|
||||
* Drops a table's directory entry so it is no longer visible, and frees every page its heap
|
||||
* and indexes (primary and secondary) own back to the shared [kestrel.store.page.DiskManager]
|
||||
* free list, via [DiskTable.destroyStorage] - so a later `CREATE TABLE`/`CREATE INDEX` reuses
|
||||
* this space instead of only ever growing the data file. RESTRICT (default) is blocked by any view depending on it, directly or
|
||||
* through another view, or by any other table's FOREIGN KEY referencing it (a self-referencing
|
||||
* FK does not count - it disappears together with the table itself). [cascade] drops every
|
||||
* dependent view (transitively, diamond-safe like [dropView]) and strips - never drops - every
|
||||
* other table's FOREIGN KEY referencing this one, mirroring [kestrel.catalog.Catalog.dropTable]'s
|
||||
* `dropDependentViews`/`dropDependentForeignKeys`: CASCADE never implies dropping a table that
|
||||
* merely references the one being dropped, only the constraint pointing at it. Everything - the
|
||||
* view deletions, the table's own row, and every stripped referencing schema - commits in one
|
||||
* transaction so a mid-way failure leaves nothing half-dropped. A trigger defined on [name] is
|
||||
* always dropped alongside it, CASCADE or not - mirrors [kestrel.catalog.Catalog.dropTable]: a
|
||||
* trigger without its table makes no sense to keep around, unlike a FOREIGN KEY reference, so
|
||||
* there is nothing to RESTRICT against.
|
||||
*/
|
||||
fun dropTable(name: String, cascade: Boolean = false) {
|
||||
val key = name.lowercase()
|
||||
val row = rowsByName[key] ?: run {
|
||||
if (viewsByName.containsKey(key)) throw DiskCatalogException("'$name' is a view, use DROP VIEW")
|
||||
throw DiskCatalogException("no such table: $name")
|
||||
}
|
||||
val viewClosure = dependentViewsOrThrow(name, cascade)
|
||||
val referencingTables = dependentForeignKeysOrThrow(name, cascade)
|
||||
val droppedTriggerKeys = triggersByName.filterValues { it.definition.tableName.equals(name, ignoreCase = true) }.keys.toList()
|
||||
|
||||
val txn = engine.beginTransaction()
|
||||
try {
|
||||
for (viewKey in viewClosure) directory.delete(txn, viewsByName.getValue(viewKey).rid)
|
||||
for (triggerKey in droppedTriggerKeys) directory.delete(txn, triggersByName.getValue(triggerKey).rid)
|
||||
directory.delete(txn, row.rid)
|
||||
row.table.destroyStorage(txn)
|
||||
for ((otherKey, other) in referencingTables) {
|
||||
val stripped = other.table.schema.foreignKeys.filterNot { it.referencedTable.equals(name, ignoreCase = true) }
|
||||
other.table.replaceSchemaMetadata(withForeignKeys(other.table.schema, stripped))
|
||||
persistEntry(otherKey, txn)
|
||||
}
|
||||
engine.commit(txn)
|
||||
} catch (failure: Exception) {
|
||||
engine.abort(txn)
|
||||
throw failure
|
||||
}
|
||||
for (viewKey in viewClosure) viewsByName.remove(viewKey)
|
||||
for (triggerKey in droppedTriggerKeys) triggersByName.remove(triggerKey)
|
||||
rowsByName.remove(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* RESTRICT check against every view depending on [name] - returns the transitive closure to
|
||||
* delete instead when [cascade] is set. See [dropTable]'s KDoc.
|
||||
*/
|
||||
private fun dependentViewsOrThrow(name: String, cascade: Boolean): Set<String> {
|
||||
val closure = transitiveViewDependents(name.lowercase())
|
||||
if (closure.isEmpty() || cascade) return closure
|
||||
val names = closure.mapNotNull { viewsByName[it]?.entry?.viewName }
|
||||
throw DiskCatalogException("cannot drop '$name' because view(s) ${names.joinToString(", ")} depend on it")
|
||||
}
|
||||
|
||||
/**
|
||||
* RESTRICT check against every other table's FOREIGN KEY referencing [name] - returns those
|
||||
* rows instead when [cascade] is set, so the caller can strip the constraint from each. See
|
||||
* [dropTable]'s KDoc.
|
||||
*/
|
||||
private fun dependentForeignKeysOrThrow(name: String, cascade: Boolean): List<Map.Entry<String, DirectoryRow>> {
|
||||
val key = name.lowercase()
|
||||
val referencing = rowsByName.entries.filter { (otherKey, other) ->
|
||||
otherKey != key && other.table.schema.foreignKeys.any { fk -> fk.referencedTable.equals(name, ignoreCase = true) }
|
||||
}
|
||||
if (referencing.isEmpty() || cascade) return referencing
|
||||
val names = referencing.map { it.value.table.schema.name }
|
||||
throw DiskCatalogException(
|
||||
"cannot drop '$name' because table(s) ${names.joinToString(", ")} have a FOREIGN KEY referencing it"
|
||||
)
|
||||
}
|
||||
|
||||
private fun rowOrThrow(tableName: String): DirectoryRow {
|
||||
val key = tableName.lowercase()
|
||||
rowsByName[key]?.let { return it }
|
||||
if (viewsByName.containsKey(key)) throw DiskCatalogException("cannot modify view: $tableName")
|
||||
throw DiskCatalogException("no such table: $tableName")
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors [kestrel.catalog.Catalog.truncateTables]: wiping every row of [tableNames] makes every
|
||||
* row of any OTHER table with a FOREIGN KEY into one of them dangling at once, so RESTRICT
|
||||
* (default) blocks on that regardless of the FK's own `onDelete` action - there is no per-row
|
||||
* delete for it to act on - and [cascade] means "also truncate the whole referencing table", a
|
||||
* transitive closure over FOREIGN KEY edges computed exactly like [dependentForeignKeysOrThrow]'s
|
||||
* single-hop version, never a per-row [DiskForeignKeyEnforcer.cascadeDelete]-style plan. Every
|
||||
* table in the closure is emptied against the SAME [txn] the caller already opened (see
|
||||
* [DiskExecutor.executeTruncate]), so it shares that statement's commit/rollback exactly like a
|
||||
* WHERE-less DELETE does - unlike RESTART IDENTITY's sequence reset, which
|
||||
* [DiskExecutor.executeTruncate] deliberately runs outside any transaction, mirroring [nextValue]'s
|
||||
* own already-non-transactional sequence position.
|
||||
*/
|
||||
fun truncateTables(txn: Long, tableNames: List<String>, cascade: Boolean): List<String> {
|
||||
tableNames.forEach { rowOrThrow(it) }
|
||||
val closure = truncateClosure(tableNames.map { it.lowercase() }.toCollection(linkedSetOf()), cascade, tableNames)
|
||||
for (key in closure) {
|
||||
val table = rowsByName.getValue(key).table
|
||||
for ((rid, _) in table.scan(txn)) table.delete(txn, rid)
|
||||
}
|
||||
return closure.map { rowsByName.getValue(it).entry.tableName }
|
||||
}
|
||||
|
||||
private fun truncateClosure(found: LinkedHashSet<String>, cascade: Boolean, requestedNames: List<String>): LinkedHashSet<String> {
|
||||
var frontier: Set<String> = found
|
||||
while (true) {
|
||||
val referencing = rowsByName.values.filter { row ->
|
||||
row.entry.tableName.lowercase() !in found &&
|
||||
row.table.schema.foreignKeys.any { fk -> fk.referencedTable.lowercase() in frontier }
|
||||
}
|
||||
if (referencing.isEmpty()) return found
|
||||
if (!cascade) {
|
||||
val names = referencing.map { it.entry.tableName }
|
||||
throw DiskCatalogException(
|
||||
"cannot truncate ${requestedNames.joinToString(", ")} because table(s) " +
|
||||
"${names.joinToString(", ")} have a FOREIGN KEY referencing them; use CASCADE"
|
||||
)
|
||||
}
|
||||
val next = referencing.map { it.entry.tableName.lowercase() }.toSet()
|
||||
found.addAll(next)
|
||||
frontier = next
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user