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