feat(catalog): extend Catalog with renameInList and replaceTable
This commit is contained in:
1 parent
39d9b61277
commit
db58d7127a
1 file changed
+192
-8
@@ -577,23 +577,135 @@ class Catalog {
|
|||||||
|
|
||||||
val dependents = if (stillEnforced) emptyList() else tables.values.filter { other -> other.schema.foreignKeys.any(::dependsOnTarget) }
|
val dependents = if (stillEnforced) emptyList() else tables.values.filter { other -> other.schema.foreignKeys.any(::dependsOnTarget) }
|
||||||
if (!cascade && dependents.isNotEmpty()) {
|
if (!cascade && dependents.isNotEmpty()) {
|
||||||
|
val names = dependents.map { it.schema.name }.distinct()
|
||||||
|
throw KestrelException(
|
||||||
|
"cannot drop constraint ${unique.name} because table(s) ${names.joinToString(", ")} have a " +
|
||||||
|
"FOREIGN KEY referencing it; use CASCADE"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (other in dependents) {
|
||||||
|
val key = other.schema.name.lowercase()
|
||||||
|
val newSchema = TableSchema(
|
||||||
|
other.schema.name, other.schema.columns, other.schema.foreignKeys.filterNot(::dependsOnTarget),
|
||||||
|
other.schema.checks, other.schema.uniqueConstraints, other.schema.compositePrimaryKey, other.schema.temporary
|
||||||
|
)
|
||||||
|
val replacement = Table(newSchema)
|
||||||
|
replacement.rows.addAll(other.rows)
|
||||||
|
tables[key] = replacement
|
||||||
|
}
|
||||||
|
// Re-fetched: a self-referencing FOREIGN KEY on tableName itself may have just been rewritten
|
||||||
|
// above (dependents is not restricted to OTHER tables - a self-reference is a real dependent
|
||||||
|
// here, unlike dropDependentForeignKeys' table-drop case where it disappears for free).
|
||||||
|
val current = table(tableName)
|
||||||
|
val newSchema = TableSchema(
|
||||||
|
current.schema.name, current.schema.columns, current.schema.foreignKeys, current.schema.checks,
|
||||||
|
current.schema.uniqueConstraints - unique, current.schema.compositePrimaryKey, current.schema.temporary
|
||||||
|
)
|
||||||
|
replaceTable(tableName, newSchema, current.rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renameInList(names: List<String>, oldName: String, newName: String): List<String> =
|
||||||
|
names.map { if (it.equals(oldName, ignoreCase = true)) newName else it }
|
||||||
|
|
||||||
|
private fun replaceTable(tableName: String, newSchema: TableSchema, rows: List<Row>) {
|
||||||
|
val key = tableName.lowercase()
|
||||||
|
val replacement = Table(newSchema)
|
||||||
|
replacement.rows.addAll(rows)
|
||||||
|
tables[key] = replacement
|
||||||
|
}
|
||||||
|
|
||||||
|
fun table(tableName: String): Table {
|
||||||
|
val key = tableName.lowercase()
|
||||||
|
tables[key]?.let { return it }
|
||||||
|
if (views.containsKey(key)) {
|
||||||
|
throw KestrelException("cannot modify view: $tableName")
|
||||||
|
}
|
||||||
|
throw KestrelException("no such table: $tableName")
|
||||||
|
}
|
||||||
|
|
||||||
fun tableOrNull(tableName: String): Table? = tables[tableName.lowercase()]
|
fun tableOrNull(tableName: String): Table? = tables[tableName.lowercase()]
|
||||||
|
|
||||||
val tableNames: List<String> get() = tables.values.map { it.schema.name }
|
val tableNames: List<String> get() = tables.values.map { it.schema.name }
|
||||||
|
|
||||||
|
fun createView(name: String, query: QueryStatement, orReplace: Boolean) {
|
||||||
|
val key = name.lowercase()
|
||||||
|
if (tables.containsKey(key)) {
|
||||||
|
throw KestrelException("a table named '$name' already exists")
|
||||||
|
}
|
||||||
|
if (!orReplace && views.containsKey(key)) {
|
||||||
|
throw KestrelException("view '$name' already exists")
|
||||||
|
}
|
||||||
|
views[key] = ViewDefinition(name, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dropView(name: String, cascade: Boolean = false) {
|
||||||
|
val key = name.lowercase()
|
||||||
|
if (!views.containsKey(key)) {
|
||||||
|
if (tables.containsKey(key)) {
|
||||||
|
throw KestrelException("'$name' is a table, use DROP TABLE")
|
||||||
|
}
|
||||||
|
throw KestrelException("no such view: $name")
|
||||||
|
}
|
||||||
|
dropDependentViews(name, cascade)
|
||||||
|
views.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the full transitive closure of views depending on [name] (directly or
|
||||||
|
* through other views) before touching anything, so a "diamond" - two different
|
||||||
|
* dependents that both end up pulling in the same further-removed view - can't
|
||||||
|
* make a later removal fail with "no such view" because an earlier one already
|
||||||
|
* deleted it.
|
||||||
|
*/
|
||||||
|
private fun dropDependentViews(name: String, cascade: Boolean) {
|
||||||
|
val closure = transitiveViewDependents(name.lowercase())
|
||||||
|
if (closure.isEmpty()) return
|
||||||
|
if (!cascade) {
|
||||||
|
val names = closure.mapNotNull { views[it]?.name }
|
||||||
|
throw KestrelException(
|
||||||
|
"cannot drop '$name' because view(s) ${names.joinToString(", ")} depend on it; use CASCADE"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (key in closure) views.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun transitiveViewDependents(rootKey: String): Set<String> {
|
||||||
|
val found = linkedSetOf<String>()
|
||||||
|
var frontier = setOf(rootKey)
|
||||||
|
while (true) {
|
||||||
|
val next = views.values
|
||||||
|
.filter { it.name.lowercase() != rootKey && it.name.lowercase() !in found }
|
||||||
|
.filter { view -> frontier.any { DependencyAnalysis.referencesRelation(view.query, it) } }
|
||||||
|
.map { it.name.lowercase() }
|
||||||
|
.toSet()
|
||||||
|
if (next.isEmpty()) return found
|
||||||
|
found.addAll(next)
|
||||||
|
frontier = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun viewOrNull(name: String): ViewDefinition? = views[name.lowercase()]
|
||||||
|
|
||||||
|
val viewNames: List<String> get() = views.values.map { it.name }
|
||||||
|
|
||||||
fun createIndex(definition: IndexDefinition) {
|
fun createIndex(definition: IndexDefinition) {
|
||||||
val key = definition.name.lowercase()
|
val key = definition.name.lowercase()
|
||||||
if (indexes.containsKey(key)) {
|
if (indexes.containsKey(key)) {
|
||||||
throw KestrelException("index '${definition.name}' already exists")
|
throw KestrelException("index '${definition.name}' already exists")
|
||||||
}
|
}
|
||||||
|
require(definition.columnNames.isNotEmpty()) { "index must cover at least one column" }
|
||||||
val table = table(definition.tableName)
|
val table = table(definition.tableName)
|
||||||
if (table.schema.columnIndex(definition.columnName) == null) {
|
for (columnName in definition.columnNames) {
|
||||||
throw KestrelException("no such column: ${definition.columnName} in table ${definition.tableName}")
|
if (table.schema.columnIndex(columnName) == null) {
|
||||||
|
throw KestrelException("no such column: $columnName in table ${definition.tableName}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val index = SortedIndex(definition)
|
// Populated before registering in [indexes] so that a UNIQUE index created over a table
|
||||||
indexes[key] = index
|
// with pre-existing duplicate values fails cleanly, without leaving a half-built index
|
||||||
|
// behind for later statements to trip over.
|
||||||
|
val index = SortedIndex(definition, collationsFor(table.schema, definition.columnNames))
|
||||||
reindexOne(table, index)
|
reindexOne(table, index)
|
||||||
|
indexes[key] = index
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dropIndex(indexName: String) {
|
fun dropIndex(indexName: String) {
|
||||||
@@ -602,9 +714,9 @@ class Catalog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun indexOn(tableName: String, columnName: String): SortedIndex? = indexes.values.firstOrNull {
|
/** Every index defined on [tableName], single-column or composite, in creation order. */
|
||||||
it.definition.tableName.equals(tableName, ignoreCase = true) &&
|
fun indexesOn(tableName: String): List<SortedIndex> = indexes.values.filter {
|
||||||
it.definition.columnName.equals(columnName, ignoreCase = true)
|
it.definition.tableName.equals(tableName, ignoreCase = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
val indexNames: List<String> get() = indexes.values.map { it.definition.name }
|
val indexNames: List<String> get() = indexes.values.map { it.definition.name }
|
||||||
@@ -618,9 +730,81 @@ class Catalog {
|
|||||||
.forEach { reindexOne(table, it) }
|
.forEach { reindexOne(table, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires in [name]'s creation order for a given table/timing/event, matching the order
|
||||||
|
* multiple triggers on the same event fire in Postgres/SQLite.
|
||||||
|
*/
|
||||||
|
fun createTrigger(definition: TriggerDefinition) {
|
||||||
|
val key = definition.name.lowercase()
|
||||||
|
if (triggers.containsKey(key)) {
|
||||||
|
throw KestrelException("trigger '${definition.name}' already exists")
|
||||||
|
}
|
||||||
|
table(definition.tableName)
|
||||||
|
triggers[key] = definition
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dropTrigger(name: String) {
|
||||||
|
if (triggers.remove(name.lowercase()) == null) {
|
||||||
|
throw KestrelException("no such trigger: $name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun triggersOn(tableName: String, timing: TriggerTiming, event: TriggerEvent): List<TriggerDefinition> =
|
||||||
|
triggers.values.filter {
|
||||||
|
it.tableName.equals(tableName, ignoreCase = true) && it.timing == timing && it.event == event
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasTriggers(tableName: String): Boolean = triggers.values.any { it.tableName.equals(tableName, ignoreCase = true) }
|
||||||
|
|
||||||
|
val triggerNames: List<String> get() = triggers.values.map { it.name }
|
||||||
|
|
||||||
|
fun triggerDefinitions(): List<TriggerDefinition> = triggers.values.toList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `name` must not already be a sequence (redundant CREATE); unlike tables/views, sequence
|
||||||
|
* names live in their own namespace here (Postgres puts them in the same relation namespace as
|
||||||
|
* tables, but this engine has no need for that overlap check to matter for anything it
|
||||||
|
* implements).
|
||||||
|
*/
|
||||||
|
fun createSequence(definition: SequenceDefinition) {
|
||||||
|
val key = definition.name.lowercase()
|
||||||
|
if (sequences.containsKey(key)) {
|
||||||
|
throw KestrelException("sequence '${definition.name}' already exists")
|
||||||
|
}
|
||||||
|
sequences[key] = Sequence(definition)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dropSequence(name: String) {
|
||||||
|
if (sequences.remove(name.lowercase()) == null) {
|
||||||
|
throw KestrelException("no such sequence: $name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always installs a brand new [Sequence] instance (via [Sequence.altered]) rather than mutating
|
||||||
|
* the existing one in place - see [Sequence]'s own doc for why that is what makes ALTER SEQUENCE
|
||||||
|
* (unlike NEXTVAL) transactional.
|
||||||
|
*/
|
||||||
|
fun alterSequence(name: String, newDefinition: SequenceDefinition, restartAt: Long?) {
|
||||||
|
val key = name.lowercase()
|
||||||
|
val existing = sequences[key] ?: throw KestrelException("no such sequence: $name")
|
||||||
|
sequences[key] = existing.altered(newDefinition, restartAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sequence(name: String): Sequence = sequenceOrNull(name) ?: throw KestrelException("no such sequence: $name")
|
||||||
|
|
||||||
|
fun sequenceOrNull(name: String): Sequence? = sequences[name.lowercase()]
|
||||||
|
|
||||||
|
val sequenceNames: List<String> get() = sequences.values.map { it.definition.name }
|
||||||
|
|
||||||
|
fun sequenceDefinitions(): List<SequenceDefinition> = sequences.values.map { it.definition }
|
||||||
|
|
||||||
fun snapshot(): CatalogSnapshot = CatalogSnapshot(
|
fun snapshot(): CatalogSnapshot = CatalogSnapshot(
|
||||||
tables = tables.mapValues { (_, table) -> table.schema to table.rows.toList() },
|
tables = tables.mapValues { (_, table) -> table.schema to table.rows.toList() },
|
||||||
indexDefinitions = indexes.values.map { it.definition }
|
indexDefinitions = indexes.values.map { it.definition },
|
||||||
|
views = views.toMap(),
|
||||||
|
triggers = triggers.toMap(),
|
||||||
|
sequences = sequences.toMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
fun restore(snapshot: CatalogSnapshot) {
|
fun restore(snapshot: CatalogSnapshot) {
|
||||||
|
|||||||
Reference in new issue
Block a user