diff --git a/src/main/kotlin/kestrel/cli/Repl.kt b/src/main/kotlin/kestrel/cli/Repl.kt index 66e5754..2368f4d 100644 --- a/src/main/kotlin/kestrel/cli/Repl.kt +++ b/src/main/kotlin/kestrel/cli/Repl.kt @@ -319,12 +319,34 @@ class Repl( } is CreateTableResult -> output.appendLine("Table ${result.tableName} created") is DropTableResult -> output.appendLine("Table ${result.tableName} dropped") + is TruncateTableResult -> output.appendLine("Table(s) ${result.tableNames.joinToString(", ")} truncated") + is MergeResult -> { + output.appendLine( + "${rowsWord(result.insertedCount)} inserted, ${rowsWord(result.updatedCount)} updated, " + + "${rowsWord(result.deletedCount)} deleted" + ) + result.returning?.let { output.appendLine(TableRenderer.render(it)) } + } + is AlterTableResult -> output.appendLine("Table ${result.tableName} altered") is CreateIndexResult -> output.appendLine("Index ${result.indexName} created") is DropIndexResult -> output.appendLine("Index ${result.indexName} dropped") + is CreateViewResult -> output.appendLine("View ${result.viewName} created") + is DropViewResult -> output.appendLine("View ${result.viewName} dropped") + is CreateTriggerResult -> output.appendLine("Trigger ${result.triggerName} created") + is DropTriggerResult -> output.appendLine("Trigger ${result.triggerName} dropped") + is CreateSequenceResult -> output.appendLine("Sequence ${result.sequenceName} created") + is DropSequenceResult -> output.appendLine("Sequence ${result.sequenceName} dropped") + is AlterSequenceResult -> output.appendLine("Sequence ${result.sequenceName} altered") is ExplainResult -> result.plan.forEach { output.appendLine(it) } BeginResult -> output.appendLine("Transaction started") CommitResult -> output.appendLine("Transaction committed") RollbackResult -> output.appendLine("Transaction rolled back") + is SavepointResult -> output.appendLine("Savepoint ${result.name} created") + is ReleaseSavepointResult -> output.appendLine("Savepoint ${result.name} released") + is RollbackToSavepointResult -> output.appendLine("Rolled back to savepoint ${result.name}") + is PrepareResult -> output.appendLine("Prepared statement ${result.name} created") + is DeallocateResult -> + output.appendLine(if (result.all) "All prepared statements deallocated" else "Prepared statement ${result.name} deallocated") } } @@ -332,14 +354,15 @@ class Repl( val parts = command.split(Regex("\\s+"), limit = 2) when (parts[0]) { ".exit", ".quit" -> return true - ".tables" -> { - val names = catalog.tableNames - if (names.isEmpty()) output.appendLine("(no tables)") else names.forEach { output.appendLine(it) } - } - ".indexes" -> { - val names = catalog.indexNames - if (names.isEmpty()) output.appendLine("(no indexes)") else names.forEach { output.appendLine(it) } - } + ".tables" -> printNames(tableNames(), "(no tables)") + ".indexes" -> printNames(indexNames(), "(no indexes)") + ".views" -> printNames(viewNames(), "(no views)") + ".triggers" -> printNames(triggerNames(), "(no triggers)") + ".sequences" -> printNames(sequenceNames(), "(no sequences)") + ".prepared" -> printNames( + diskExecutor?.preparedStatementNames ?: executor.preparedStatementNames, + "(no prepared statements)" + ) ".schema" -> { if (parts.size < 2 || parts[1].isBlank()) { printError("usage: .schema ") @@ -352,25 +375,87 @@ class Repl( return false } + private fun printNames(names: List, emptyMessage: String) { + if (names.isEmpty()) output.appendLine(emptyMessage) else names.forEach { output.appendLine(it) } + } + + private fun tableNames(): List = diskCatalog?.tableNames ?: catalog.tableNames + private fun viewNames(): List = diskCatalog?.viewNames ?: catalog.viewNames + private fun triggerNames(): List = diskCatalog?.triggerNames ?: catalog.triggerNames + private fun sequenceNames(): List = diskCatalog?.sequenceNames ?: catalog.sequenceNames + + private fun indexNames(): List { + val disk = diskCatalog ?: return catalog.indexNames + return disk.tableNames.flatMap { disk.table(it).secondaryIndexes.map { index -> index.name } } + } + + /** Every `CREATE [UNIQUE] INDEX ...;` line for [tableName], one engine or the other - never both. */ + private fun indexLines(tableName: String): List { + val disk = diskCatalog + if (disk != null) { + val table = disk.tableOrNull(tableName) ?: return emptyList() + return table.secondaryIndexes.map { index -> + val uniqueKeyword = if (index.unique) "UNIQUE " else "" + "CREATE ${uniqueKeyword}INDEX ${index.name} ON $tableName (${index.columnNames.joinToString(", ")});" + } + } + return catalog.indexDefinitions() + .filter { it.tableName.equals(tableName, ignoreCase = true) } + .map { index -> + val uniqueKeyword = if (index.unique) "UNIQUE " else "" + "CREATE ${uniqueKeyword}INDEX ${index.name} ON $tableName (${index.columnDisplayName});" + } + } + + /** Every `CREATE TRIGGER ...` block for [tableName], one engine or the other - never both. */ + private fun triggerLines(tableName: String): List { + val definitions = diskCatalog?.triggerDefinitions() ?: catalog.triggerDefinitions() + return definitions + .filter { it.tableName.equals(tableName, ignoreCase = true) } + .map { SqlWriter.triggerText(it) } + } + private fun printSchema(tableName: String) { - val table = catalog.tableOrNull(tableName) - if (table == null) { + val schema = diskCatalog?.tableOrNull(tableName)?.schema + ?: diskCatalog?.temporaryTableOrNull(tableName)?.schema + ?: catalog.tableOrNull(tableName)?.schema + if (schema == null) { printError("no such table: $tableName") return } - val schema = table.schema val builder = StringBuilder() builder.appendLine("CREATE TABLE ${schema.name} (") - schema.columns.forEachIndexed { index, column -> + val lines = mutableListOf() + schema.columns.forEach { column -> val constraints = buildString { if (column.primaryKey) append(" PRIMARY KEY") if (column.notNull && !column.primaryKey) append(" NOT NULL") + column.default?.let { append(" DEFAULT ${SqlWriter.expressionText(it)}") } } - val comma = if (index < schema.columns.size - 1) "," else "" - builder.appendLine(" ${column.name} ${column.type.displayName}$constraints$comma") + lines.add(" ${column.name} ${column.type.displayWith(column.precision, column.scale)}$constraints") } - builder.append(");") + if (schema.compositePrimaryKey.isNotEmpty()) { + lines.add(" PRIMARY KEY (${schema.compositePrimaryKey.joinToString(", ")})") + } + schema.foreignKeys.forEach { fk -> + val onDelete = if (fk.onDelete != ForeignKeyAction.NoAction) " ON DELETE ${fk.onDelete.sqlKeyword}" else "" + val onUpdate = if (fk.onUpdate != ForeignKeyAction.NoAction) " ON UPDATE ${fk.onUpdate.sqlKeyword}" else "" + lines.add( + " FOREIGN KEY (${fk.columns.joinToString(", ")}) REFERENCES ${fk.referencedTable} " + + "(${fk.referencedColumns.joinToString(", ")})$onDelete$onUpdate" + ) + } + schema.checks.forEach { check -> + lines.add(" CHECK (${SqlWriter.expressionText(check.expression)})") + } + schema.uniqueConstraints.forEach { unique -> + lines.add(" UNIQUE (${unique.columns.joinToString(", ")})") + } + builder.append(lines.joinToString(",\n")) + builder.append("\n);") output.appendLine(builder.toString()) + indexLines(schema.name).forEach { output.appendLine(it) } + triggerLines(schema.name).forEach { output.appendLine(it) } } private fun printError(message: String?) { diff --git a/src/main/kotlin/kestrel/cli/TableRenderer.kt b/src/main/kotlin/kestrel/cli/TableRenderer.kt index 1209ce9..3265494 100644 --- a/src/main/kotlin/kestrel/cli/TableRenderer.kt +++ b/src/main/kotlin/kestrel/cli/TableRenderer.kt @@ -31,6 +31,8 @@ object TableRenderer { is Value.RealValue -> value.value.toString() is Value.TextValue -> value.value is Value.BooleanValue -> value.value.toString() + is Value.DecimalValue -> value.value.toPlainString() + is Value.DateValue -> value.value.toString() is Value.NullValue -> "NULL" }