feat(persistence): extend SqlWriter with functionArgumentsText
This commit is contained in:
1 parent
349b06d470
commit
260d649276
1 file changed
+181
@@ -485,11 +485,190 @@ object SqlWriter {
|
|||||||
is ParameterExpression -> "\$${expression.index}"
|
is ParameterExpression -> "\$${expression.index}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** STRING_AGG's `separator`/`orderBy` and DISTINCT are the only [FunctionExpression] fields beyond `argument`/`star` - every other aggregate falls through the `?: ""` exactly as before. */
|
||||||
|
private fun functionArgumentsText(function: FunctionExpression): String {
|
||||||
|
if (function.star) return "*"
|
||||||
|
val distinctPrefix = if (function.distinct) "DISTINCT " else ""
|
||||||
|
val value = function.argument?.let { expression(it) } ?: ""
|
||||||
|
val separator = function.separator ?: return "$distinctPrefix$value"
|
||||||
|
val orderBy = if (function.orderBy.isNotEmpty()) {
|
||||||
|
" ORDER BY " + function.orderBy.joinToString(", ") { key -> expression(key.expression) + orderKeySuffix(key) }
|
||||||
|
} else ""
|
||||||
|
return "$distinctPrefix$value, ${expression(separator)}$orderBy"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `DESC`/`NULLS FIRST`/`NULLS LAST` are omitted when unspecified, so a plain ascending key with the engine's own default null ordering dumps exactly as written, matching every other optional-clause writer in this file. */
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Without this, a view with `COUNT(x) FILTER (WHERE ...)` would dump as plain `COUNT(x)` and silently drop the predicate on reopen. */
|
||||||
|
private fun filterClauseText(function: FunctionExpression): String =
|
||||||
|
function.filter?.let { " FILTER (WHERE ${expression(it)})" } ?: ""
|
||||||
|
|
||||||
|
private fun windowArgumentsText(window: WindowExpression): String {
|
||||||
|
if (window.star) return "*"
|
||||||
|
return listOfNotNull(window.argument, window.offset, window.defaultValue).joinToString(", ") { expression(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun overClauseText(window: WindowExpression): String {
|
||||||
|
val parts = mutableListOf<String>()
|
||||||
|
if (window.partitionBy.isNotEmpty()) {
|
||||||
|
parts.add("PARTITION BY " + window.partitionBy.joinToString(", ") { expression(it) })
|
||||||
|
}
|
||||||
|
if (window.orderBy.isNotEmpty()) {
|
||||||
|
parts.add(
|
||||||
|
"ORDER BY " + window.orderBy.joinToString(", ") { key -> expression(key.expression) + orderKeySuffix(key) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
window.frame?.let { frame ->
|
||||||
|
val unit = when (frame.unit) {
|
||||||
|
FrameUnit.Rows -> "ROWS"
|
||||||
|
FrameUnit.Range -> "RANGE"
|
||||||
|
FrameUnit.Groups -> "GROUPS"
|
||||||
|
}
|
||||||
|
val exclude = when (frame.exclude) {
|
||||||
|
FrameExclude.NoOthers -> ""
|
||||||
|
FrameExclude.CurrentRow -> " EXCLUDE CURRENT ROW"
|
||||||
|
FrameExclude.Group -> " EXCLUDE GROUP"
|
||||||
|
FrameExclude.Ties -> " EXCLUDE TIES"
|
||||||
|
}
|
||||||
|
parts.add("$unit BETWEEN ${frameBoundText(frame.start)} AND ${frameBoundText(frame.end)}$exclude")
|
||||||
|
}
|
||||||
|
return parts.joinToString(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun frameBoundText(bound: FrameBound): String = when (bound.type) {
|
||||||
|
FrameBoundType.UnboundedPreceding -> "UNBOUNDED PRECEDING"
|
||||||
|
FrameBoundType.UnboundedFollowing -> "UNBOUNDED FOLLOWING"
|
||||||
|
FrameBoundType.CurrentRow -> "CURRENT ROW"
|
||||||
|
FrameBoundType.Preceding -> "${expression(bound.offset!!)} PRECEDING"
|
||||||
|
FrameBoundType.Following -> "${expression(bound.offset!!)} FOLLOWING"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryText(query: QueryStatement): String = when (query) {
|
||||||
|
is SelectStatement -> selectText(query)
|
||||||
|
is SetOperationStatement -> setOperationText(query)
|
||||||
|
is WithQuery -> withQueryText(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOperationText(statement: SetOperationStatement): String = buildString {
|
||||||
|
append(queryText(statement.left))
|
||||||
|
append(" ")
|
||||||
|
append(setOperatorKeyword(statement.operator))
|
||||||
|
append(" ")
|
||||||
|
append(queryText(statement.right))
|
||||||
|
statement.orderBy?.let { append(orderByText(it)) }
|
||||||
|
statement.limit?.let { append(" LIMIT $it") }
|
||||||
|
statement.offset?.let { append(" OFFSET $it") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setOperatorKeyword(operator: SetOperator): String = when (operator) {
|
||||||
|
SetOperator.Union -> "UNION"
|
||||||
|
SetOperator.UnionAll -> "UNION ALL"
|
||||||
|
SetOperator.Intersect -> "INTERSECT"
|
||||||
|
SetOperator.IntersectAll -> "INTERSECT ALL"
|
||||||
|
SetOperator.Except -> "EXCEPT"
|
||||||
|
SetOperator.ExceptAll -> "EXCEPT ALL"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun withQueryText(query: WithQuery): String = buildString {
|
||||||
|
append("WITH ")
|
||||||
|
if (query.recursive) append("RECURSIVE ")
|
||||||
|
append(query.ctes.joinToString(", ") { cte ->
|
||||||
|
val columns = cte.columns?.let { "(${it.joinToString(", ")})" } ?: ""
|
||||||
|
"${cte.name}$columns AS (${queryText(cte.query)})"
|
||||||
|
})
|
||||||
|
append(" ")
|
||||||
|
append(queryText(query.body))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectText(select: SelectStatement): String = buildString {
|
||||||
|
append("SELECT ")
|
||||||
|
if (select.distinct) {
|
||||||
|
append("DISTINCT ")
|
||||||
|
if (select.distinctOn.isNotEmpty()) {
|
||||||
|
append("ON (")
|
||||||
|
append(select.distinctOn.joinToString(", ") { expression(it) })
|
||||||
|
append(") ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append(projectionText(select.projection))
|
||||||
|
append(" FROM ")
|
||||||
|
append(fromItemText(select.from))
|
||||||
|
for (join in select.joins) {
|
||||||
|
append(
|
||||||
|
when (join.type) {
|
||||||
|
JoinType.Left -> " LEFT JOIN "
|
||||||
|
JoinType.Right -> " RIGHT JOIN "
|
||||||
|
JoinType.Full -> " FULL JOIN "
|
||||||
|
JoinType.Inner -> " INNER JOIN "
|
||||||
|
JoinType.Cross -> " CROSS JOIN "
|
||||||
|
}
|
||||||
|
)
|
||||||
|
append(fromItemText(join.from))
|
||||||
|
join.on?.let { append(" ON "); append(expression(it)) }
|
||||||
|
}
|
||||||
|
select.where?.let { append(" WHERE "); append(expression(it)) }
|
||||||
|
if (select.groupingSpec != null || select.groupBy.isNotEmpty()) {
|
||||||
|
append(" GROUP BY ")
|
||||||
|
append(groupByText(select.groupingSpec, select.groupBy))
|
||||||
|
}
|
||||||
|
select.having?.let { append(" HAVING "); append(expression(it)) }
|
||||||
|
select.orderBy?.let { append(orderByText(it)) }
|
||||||
|
select.limit?.let { append(" LIMIT $it") }
|
||||||
|
select.offset?.let { append(" OFFSET $it") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun groupByText(spec: GroupingSpec?, groupBy: List<Expression>): String = when (spec) {
|
||||||
|
null -> groupBy.joinToString(", ") { expression(it) }
|
||||||
|
is RollupSpec -> "ROLLUP (" + spec.expressions.joinToString(", ") { expression(it) } + ")"
|
||||||
|
is CubeSpec -> "CUBE (" + spec.expressions.joinToString(", ") { expression(it) } + ")"
|
||||||
|
is GroupingSetsSpec -> "GROUPING SETS (" + spec.sets.joinToString(", ") { groupingSetElementText(it) } + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun groupingSetElementText(set: List<Expression>): String =
|
||||||
|
if (set.isEmpty()) "()" else "(" + set.joinToString(", ") { expression(it) } + ")"
|
||||||
|
|
||||||
|
private fun orderByText(orderBy: OrderBy): String =
|
||||||
|
" ORDER BY " + orderBy.keys.joinToString(", ") { key -> expression(key.expression) + orderKeySuffix(key) }
|
||||||
|
|
||||||
|
private fun fromItemText(item: FromItem): String = when (item) {
|
||||||
|
is TableSource -> item.name + (item.alias?.let { " AS $it" } ?: "")
|
||||||
|
is SubquerySource -> (if (item.lateral) "LATERAL " else "") + "(${queryText(item.select)}) AS ${item.alias}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun projectionText(projection: Projection): String = when (projection) {
|
||||||
|
is StarProjection -> "*"
|
||||||
|
is ExpressionProjection -> projection.items.joinToString(", ") { item ->
|
||||||
|
val base = expression(item.expression)
|
||||||
|
item.alias?.let { "$base AS $it" } ?: base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DECIMAL/DATE values render as `CAST('...' AS type)` (not a bare quoted string)
|
||||||
|
* because column coercion is strict everywhere else in this engine (no implicit
|
||||||
|
* TEXT-to-DECIMAL/DATE on INSERT/UPDATE) - the CAST is what actually converts the
|
||||||
|
* text back on replay. The re-cast uses the value's own scale (already the column's
|
||||||
|
* declared scale, since it can only have gotten here via coercion or CAST) with a
|
||||||
|
* generous precision, so it reproduces the exact BigDecimal rather than rounding
|
||||||
|
* through a Double the way a bare numeric literal would.
|
||||||
|
*/
|
||||||
private fun literal(value: Value): String = when (value) {
|
private fun literal(value: Value): String = when (value) {
|
||||||
is Value.IntegerValue -> value.value.toString()
|
is Value.IntegerValue -> value.value.toString()
|
||||||
is Value.RealValue -> realText(value.value)
|
is Value.RealValue -> realText(value.value)
|
||||||
is Value.TextValue -> "'" + value.value.replace("'", "''") + "'"
|
is Value.TextValue -> "'" + value.value.replace("'", "''") + "'"
|
||||||
is Value.BooleanValue -> value.value.toString()
|
is Value.BooleanValue -> value.value.toString()
|
||||||
|
is Value.DecimalValue ->
|
||||||
|
"CAST('${value.value.toPlainString()}' AS DECIMAL($LITERAL_DECIMAL_PRECISION, ${value.value.scale()}))"
|
||||||
|
is Value.DateValue -> "CAST('${value.value}' AS DATE)"
|
||||||
is Value.NullValue -> "NULL"
|
is Value.NullValue -> "NULL"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,4 +676,6 @@ object SqlWriter {
|
|||||||
val plain = value.toString()
|
val plain = value.toString()
|
||||||
return if (plain.contains('E') || plain.contains('e')) BigDecimal(value).toPlainString() else plain
|
return if (plain.contains('E') || plain.contains('e')) BigDecimal(value).toPlainString() else plain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val LITERAL_DECIMAL_PRECISION = 38
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user