diff --git a/src/main/kotlin/kestrel/engine/SelectRunner.kt b/src/main/kotlin/kestrel/engine/SelectRunner.kt index a80f9b9..a592c98 100644 --- a/src/main/kotlin/kestrel/engine/SelectRunner.kt +++ b/src/main/kotlin/kestrel/engine/SelectRunner.kt @@ -1630,11 +1630,37 @@ class SelectRunner(private val catalog: Catalog) { is ParameterExpression -> "\$${expression.index}" } + private fun describeFunctionArguments(expression: FunctionExpression): String { + if (expression.star) return "*" + val distinctPrefix = if (expression.distinct) "DISTINCT " else "" + val value = expression.argument?.let { describeExpression(it) } ?: "" + val separator = expression.separator ?: return "$distinctPrefix$value" + val orderBy = if (expression.orderBy.isNotEmpty()) { + " ORDER BY " + expression.orderBy.joinToString(", ") { key -> describeExpression(key.expression) + orderKeySuffix(key) } + } else "" + return "$distinctPrefix$value, ${describeExpression(separator)}$orderBy" + } + + private fun orderKeySuffix(key: OrderKey): String { + val direction = if (key.descending) " DESC" else "" + val nulls = when (key.nullsFirst) { + true -> " NULLS FIRST" + false -> " NULLS LAST" + null -> "" + } + return direction + nulls + } + + private fun describeFilterClause(expression: FunctionExpression): String = + expression.filter?.let { " FILTER (WHERE ${describeExpression(it)})" } ?: "" + private fun literalText(value: Value): String = when (value) { is Value.IntegerValue -> value.value.toString() 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" } @@ -1646,5 +1672,12 @@ class SelectRunner(private val catalog: Catalog) { val offset: Int ) - private class HashKeys(val buildExpression: Expression, val probeExpression: Expression) + private class HashKeys(val buildExpression: Expression, val probeExpression: Expression, val collation: Collation) + + private companion object { + const val MAX_RECURSION_ITERATIONS = 10_000 + + /** Kinds whose result actually depends on which rows fall inside the frame. */ + val FRAME_CAPABLE_KINDS = setOf(WindowFunctionKind.Aggregate, WindowFunctionKind.FirstValue, WindowFunctionKind.LastValue) + } } diff --git a/src/main/kotlin/kestrel/engine/StatementResult.kt b/src/main/kotlin/kestrel/engine/StatementResult.kt index ede1cd9..af09e8c 100644 --- a/src/main/kotlin/kestrel/engine/StatementResult.kt +++ b/src/main/kotlin/kestrel/engine/StatementResult.kt @@ -6,20 +6,46 @@ sealed interface StatementResult data class SelectResult(val columns: List, val rows: List>) : StatementResult -data class InsertResult(val count: Int) : StatementResult +data class InsertResult(val count: Int, val returning: SelectResult? = null) : StatementResult -data class UpdateResult(val count: Int) : StatementResult +data class UpdateResult(val count: Int, val returning: SelectResult? = null) : StatementResult -data class DeleteResult(val count: Int) : StatementResult +data class DeleteResult(val count: Int, val returning: SelectResult? = null) : StatementResult data class CreateTableResult(val tableName: String) : StatementResult data class DropTableResult(val tableName: String) : StatementResult +/** [tableNames] is every table actually truncated - the statement's own list plus, under CASCADE, every transitive FOREIGN KEY referencer added to it. */ +data class TruncateTableResult(val tableNames: List) : StatementResult + +data class MergeResult( + val insertedCount: Int, + val updatedCount: Int, + val deletedCount: Int, + val returning: SelectResult? = null +) : StatementResult + +data class AlterTableResult(val tableName: String) : StatementResult + data class CreateIndexResult(val indexName: String) : StatementResult data class DropIndexResult(val indexName: String) : StatementResult +data class CreateViewResult(val viewName: String) : StatementResult + +data class DropViewResult(val viewName: String) : StatementResult + +data class CreateTriggerResult(val triggerName: String) : StatementResult + +data class DropTriggerResult(val triggerName: String) : StatementResult + +data class CreateSequenceResult(val sequenceName: String) : StatementResult + +data class DropSequenceResult(val sequenceName: String) : StatementResult + +data class AlterSequenceResult(val sequenceName: String) : StatementResult + data class ExplainResult(val plan: List) : StatementResult data object BeginResult : StatementResult @@ -27,3 +53,14 @@ data object BeginResult : StatementResult data object CommitResult : StatementResult data object RollbackResult : StatementResult + +data class SavepointResult(val name: String) : StatementResult + +data class ReleaseSavepointResult(val name: String) : StatementResult + +data class RollbackToSavepointResult(val name: String) : StatementResult + +data class PrepareResult(val name: String) : StatementResult + +/** [name] is `null` exactly when [all] is `true` (`DEALLOCATE ALL`), mirroring [kestrel.sql.ast.DeallocateStatement]. */ +data class DeallocateResult(val name: String?, val all: Boolean) : StatementResult diff --git a/src/main/kotlin/kestrel/engine/TriggerRowContext.kt b/src/main/kotlin/kestrel/engine/TriggerRowContext.kt new file mode 100644 index 0000000..20282e1 --- /dev/null +++ b/src/main/kotlin/kestrel/engine/TriggerRowContext.kt @@ -0,0 +1,39 @@ +package kestrel.engine + +import kestrel.KestrelException +import kestrel.catalog.TableSchema +import kestrel.sql.ast.ColumnExpression +import kestrel.storage.Collation +import kestrel.storage.Row +import kestrel.storage.Value + +/** + * Binds `NEW`/`OLD` for the one row a firing trigger is currently processing, against [schema] - + * always the trigger's OWN table (the one named in its `ON `), never whatever table a + * nested body statement happens to target. [newRow] is null for a DELETE trigger, [oldRow] is null + * for an INSERT trigger; both are non-null only inside an UPDATE trigger. + */ +class TriggerRowContext(private val schema: TableSchema, private val newRow: Row?, private val oldRow: Row?) { + fun matches(expression: ColumnExpression): Boolean { + val qualifier = expression.qualifier ?: return false + return (qualifier.equals("new", ignoreCase = true) && newRow != null) || + (qualifier.equals("old", ignoreCase = true) && oldRow != null) + } + + fun resolve(expression: ColumnExpression): Value { + val row = if (expression.qualifier!!.equals("new", ignoreCase = true)) newRow!! else oldRow!! + val index = schema.columnIndex(expression.name) + ?: throw KestrelException("no such column: ${expression.name}") + return row.values[index] + } + + /** Column-existence check with no row required, for validating a reference before it is ever fired. */ + fun checkColumnExists(expression: ColumnExpression) { + if (schema.columnIndex(expression.name) == null) { + throw KestrelException("no such column: ${expression.name}") + } + } + + /** `NEW.col`/`OLD.col` reads the same table's schema either way, so its declared COLLATE never depends on which side [expression] names. */ + fun declaredCollation(expression: ColumnExpression): Collation? = schema.column(expression.name)?.collation +}