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
|
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