feat(persistence): extend SqlWriter with expressionText and triggerText

This commit is contained in:
dualfroz committed 2026-09-02 18:15:58 +02:00
1 parent 16da420d79
commit 6ff6e4e91e
1 file changed
+195 -5
@@ -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<Statement>
): 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) {