From 6ff6e4e91e7875832ceb67483770993ee2143286 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 18:15:58 +0200 Subject: [PATCH] feat(persistence): extend SqlWriter with expressionText and triggerText --- .../kotlin/kestrel/persistence/SqlWriter.kt | 200 +++++++++++++++++- 1 file changed, 195 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/kestrel/persistence/SqlWriter.kt b/src/main/kotlin/kestrel/persistence/SqlWriter.kt index c0ef7d7..1d11e98 100644 --- a/src/main/kotlin/kestrel/persistence/SqlWriter.kt +++ b/src/main/kotlin/kestrel/persistence/SqlWriter.kt @@ -2,46 +2,236 @@ package kestrel.persistence import java.math.BigDecimal import kestrel.catalog.Catalog +import kestrel.catalog.CheckConstraint import kestrel.catalog.Column +import kestrel.catalog.ForeignKeyAction +import kestrel.catalog.ForeignKeyConstraint +import kestrel.catalog.Sequence +import kestrel.catalog.SequenceDefinition +import kestrel.catalog.UniqueConstraint +import kestrel.catalog.TriggerDefinition +import kestrel.catalog.sqlKeyword +import kestrel.sql.ast.AddColumn +import kestrel.sql.ast.AddConstraint +import kestrel.sql.ast.AddedCheck +import kestrel.sql.ast.AddedConstraint +import kestrel.sql.ast.AddedForeignKey +import kestrel.sql.ast.AddedUnique +import kestrel.sql.ast.AlterTableStatement import kestrel.sql.ast.Assignment +import kestrel.sql.ast.BetweenExpression import kestrel.sql.ast.BinaryExpression +import kestrel.sql.ast.CallExpression +import kestrel.sql.ast.CaseExpression +import kestrel.sql.ast.CastExpression +import kestrel.sql.ast.ChangeColumnType +import kestrel.sql.ast.CollateExpression import kestrel.sql.ast.ColumnExpression +import kestrel.sql.ast.ConflictDoNothing +import kestrel.sql.ast.ConflictDoUpdate +import kestrel.sql.ast.AlterSequenceStatement import kestrel.sql.ast.CreateIndexStatement +import kestrel.sql.ast.CreateSequenceStatement import kestrel.sql.ast.CreateTableStatement +import kestrel.sql.ast.CreateTriggerStatement +import kestrel.sql.ast.CreateViewStatement +import kestrel.sql.ast.CubeSpec +import kestrel.sql.ast.CurrentValueExpression import kestrel.sql.ast.DeleteStatement +import kestrel.sql.ast.DropColumn +import kestrel.sql.ast.DropConstraint import kestrel.sql.ast.DropIndexStatement +import kestrel.sql.ast.DropMode +import kestrel.sql.ast.DropSequenceStatement import kestrel.sql.ast.DropTableStatement +import kestrel.sql.ast.DropTriggerStatement +import kestrel.sql.ast.DropViewStatement +import kestrel.sql.ast.ExistsExpression import kestrel.sql.ast.Expression +import kestrel.sql.ast.ExpressionProjection +import kestrel.sql.ast.FrameBound +import kestrel.sql.ast.FrameBoundType +import kestrel.sql.ast.FrameExclude +import kestrel.sql.ast.FrameUnit +import kestrel.sql.ast.FromItem import kestrel.sql.ast.FunctionExpression +import kestrel.sql.ast.GroupingSetsSpec +import kestrel.sql.ast.GroupingSpec +import kestrel.sql.ast.InExpression +import kestrel.sql.ast.InSubqueryExpression import kestrel.sql.ast.InsertStatement +import kestrel.sql.ast.IsNullExpression +import kestrel.sql.ast.JoinType +import kestrel.sql.ast.LikeExpression import kestrel.sql.ast.LiteralExpression +import kestrel.sql.ast.MergeActionExpression +import kestrel.sql.ast.ParameterExpression +import kestrel.sql.ast.MergeMatchedDelete +import kestrel.sql.ast.MergeMatchedUpdate +import kestrel.sql.ast.MergeNotMatchedInsert +import kestrel.sql.ast.MergeStatement +import kestrel.sql.ast.MergeWhenClause +import kestrel.sql.ast.NextValueExpression +import kestrel.sql.ast.OnConflictClause +import kestrel.sql.ast.OrderBy +import kestrel.sql.ast.OrderKey +import kestrel.sql.ast.Projection +import kestrel.sql.ast.QueryStatement +import kestrel.sql.ast.RenameColumn +import kestrel.sql.ast.RenameTable +import kestrel.sql.ast.RollupSpec +import kestrel.sql.ast.ScalarSubqueryExpression +import kestrel.sql.ast.SelectStatement +import kestrel.sql.ast.SetOperationStatement +import kestrel.sql.ast.SetOperator +import kestrel.sql.ast.StarProjection import kestrel.sql.ast.Statement +import kestrel.sql.ast.SubquerySource +import kestrel.sql.ast.TableSource +import kestrel.sql.ast.TriggerEvent +import kestrel.sql.ast.TriggerTiming +import kestrel.sql.ast.TruncateTableStatement import kestrel.sql.ast.UnaryExpression import kestrel.sql.ast.UpdateStatement +import kestrel.sql.ast.WindowExpression +import kestrel.sql.ast.WithQuery +import kestrel.storage.Collation import kestrel.storage.Value +import kestrel.storage.displayWith object SqlWriter { + fun expressionText(expression: Expression): String = expression(expression) + fun canPersist(statement: Statement): Boolean = statement is CreateTableStatement || statement is DropTableStatement || + statement is TruncateTableStatement || + statement is AlterTableStatement || statement is CreateIndexStatement || statement is DropIndexStatement || + statement is CreateViewStatement || + statement is DropViewStatement || + statement is CreateTriggerStatement || + statement is DropTriggerStatement || + statement is CreateSequenceStatement || + statement is DropSequenceStatement || + statement is AlterSequenceStatement || statement is InsertStatement || statement is UpdateStatement || - statement is DeleteStatement + statement is DeleteStatement || + statement is MergeStatement fun write(statement: Statement): String = when (statement) { - is CreateTableStatement -> "CREATE TABLE ${statement.tableName} (" + - statement.columns.joinToString(", ") { columnDefinition(it) } + ");" - is DropTableStatement -> "DROP TABLE ${statement.tableName};" + is CreateTableStatement -> "CREATE ${if (statement.temporary) "TEMPORARY " else ""}TABLE ${statement.tableName} (" + + createTableBodyText( + statement.columns, + statement.foreignKeys, + statement.checks, + statement.uniqueConstraints, + statement.compositePrimaryKey + ) + ");" + is DropTableStatement -> "DROP TABLE ${statement.tableName}${dropModeText(statement.mode)};" + is TruncateTableStatement -> writeTruncate(statement) + is AlterTableStatement -> alterTableText(statement) is CreateIndexStatement -> - "CREATE INDEX ${statement.indexName} ON ${statement.tableName} (${statement.columnName});" + "CREATE ${if (statement.unique) "UNIQUE " else ""}INDEX ${statement.indexName} ON " + + "${statement.tableName} (${statement.columnNames.joinToString(", ")});" is DropIndexStatement -> "DROP INDEX ${statement.indexName};" + is CreateViewStatement -> + "CREATE ${if (statement.orReplace) "OR REPLACE " else ""}VIEW ${statement.viewName} AS " + + "${queryText(statement.query)};" + is DropViewStatement -> "DROP VIEW ${statement.viewName}${dropModeText(statement.mode)};" + is CreateTriggerStatement -> triggerText( + statement.triggerName, statement.timing, statement.event, statement.tableName, statement.whenClause, statement.body + ) + is DropTriggerStatement -> "DROP TRIGGER ${statement.triggerName};" + is CreateSequenceStatement -> createSequenceText(statement) + is DropSequenceStatement -> "DROP SEQUENCE ${statement.sequenceName};" + is AlterSequenceStatement -> alterSequenceText(statement) is InsertStatement -> writeInsert(statement) is UpdateStatement -> writeUpdate(statement) is DeleteStatement -> writeDelete(statement) + is MergeStatement -> writeMerge(statement) else -> throw IllegalArgumentException("statement is not persistable") } + fun triggerText(definition: TriggerDefinition): String = + triggerText(definition.name, definition.timing, definition.event, definition.tableName, definition.whenClause, definition.body) + + private fun triggerText( + name: String, + timing: TriggerTiming, + event: TriggerEvent, + tableName: String, + whenClause: Expression?, + body: List + ): String = buildString { + append("CREATE TRIGGER $name ") + append(if (timing == TriggerTiming.Before) "BEFORE" else "AFTER") + append(" ") + append( + when (event) { + TriggerEvent.Insert -> "INSERT" + TriggerEvent.Update -> "UPDATE" + TriggerEvent.Delete -> "DELETE" + } + ) + append(" ON $tableName FOR EACH ROW") + whenClause?.let { append(" WHEN ${expression(it)}") } + append(" BEGIN ") + for (statement in body) { + append(write(statement)) + append(" ") + } + append("END;") + } + + private fun createSequenceText(statement: CreateSequenceStatement): String = buildString { + append("CREATE SEQUENCE ${statement.sequenceName}") + statement.incrementBy?.let { append(" INCREMENT BY $it") } + statement.minValue?.let { append(" MINVALUE $it") } + statement.maxValue?.let { append(" MAXVALUE $it") } + statement.startValue?.let { append(" START WITH $it") } + if (statement.cycle) append(" CYCLE") + append(";") + } + + private fun alterSequenceText(statement: AlterSequenceStatement): String = buildString { + append("ALTER SEQUENCE ${statement.sequenceName}") + statement.incrementBy?.let { append(" INCREMENT BY $it") } + when { + statement.clearMinValue -> append(" NO MINVALUE") + statement.minValue != null -> append(" MINVALUE ${statement.minValue}") + } + when { + statement.clearMaxValue -> append(" NO MAXVALUE") + statement.maxValue != null -> append(" MAXVALUE ${statement.maxValue}") + } + if (statement.restart) { + append(" RESTART") + statement.restartWith?.let { append(" WITH $it") } + } + when (statement.cycle) { + true -> append(" CYCLE") + false -> append(" NO CYCLE") + null -> Unit + } + append(";") + } + + /** + * Bakes the sequence's CURRENT position into `START WITH`, rather than dumping + * [SequenceDefinition.startValue] as written at CREATE time, so a reload resumes NEXTVAL + * exactly where this session left off instead of reissuing already-handed-out values - see + * [Sequence]'s own doc on why the counter (unlike the CREATE/DROP/ALTER SEQUENCE structure + * every other part of [dump] is free to just replay literally) needs this special treatment. + */ + private fun sequenceDumpText(sequence: Sequence): String { + val definition = sequence.definition + val cycle = if (definition.cycle) " CYCLE" else "" + return "CREATE SEQUENCE ${definition.name} INCREMENT BY ${definition.incrementBy} " + + "MINVALUE ${definition.minValue} MAXVALUE ${definition.maxValue} START WITH ${sequence.peekNext()}$cycle;" + } + fun dump(catalog: Catalog): String { val builder = StringBuilder() for (tableName in catalog.tableNames) {