Skip to content

Commit

Permalink
Adds configurable AST -> SQL printer (#1183)
Browse files Browse the repository at this point in the history
  • Loading branch information
RCHowell authored Sep 28, 2023
1 parent 4e2a184 commit 6a18bb7
Show file tree
Hide file tree
Showing 9 changed files with 2,738 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Adds overridden `toString()` method for Sprout-generated code.
- Adds CURRENT_DATE session variable to PartiQL.g4 and PartiQLParser
- Adds configurable AST to SQL pretty printer. Usage in Java `AstKt.sql(ast)` or in Kotlin `ast.sql()`.
### Changed
Expand Down
13 changes: 13 additions & 0 deletions partiql-ast/src/main/kotlin/org/partiql/ast/Ast.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package org.partiql.ast

import org.partiql.ast.builder.AstFactoryImpl
import org.partiql.ast.sql.SqlBlock
import org.partiql.ast.sql.SqlDialect
import org.partiql.ast.sql.SqlLayout
import org.partiql.ast.sql.sql

/**
* Singleton instance of the default factory; also accessible via `AstFactory.DEFAULT`.
Expand All @@ -13,3 +17,12 @@ object Ast : AstBaseFactory()
public abstract class AstBaseFactory : AstFactoryImpl() {
// internal default overrides here
}

/**
* Pretty-print this [AstNode] as SQL text with the given [SqlLayout]
*/
@JvmOverloads
public fun AstNode.sql(
layout: SqlLayout = SqlLayout.DEFAULT,
dialect: SqlDialect = SqlDialect.PARTIQL,
): String = accept(dialect, SqlBlock.Nil).sql(layout)
39 changes: 39 additions & 0 deletions partiql-ast/src/main/kotlin/org/partiql/ast/sql/Sql.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.partiql.ast.sql

// a <> b <-> a concat b

internal infix fun SqlBlock.concat(rhs: SqlBlock): SqlBlock = link(this, rhs)

internal infix fun SqlBlock.concat(text: String): SqlBlock = link(this, text(text))

internal infix operator fun SqlBlock.plus(rhs: SqlBlock): SqlBlock = link(this, rhs)

internal infix operator fun SqlBlock.plus(text: String): SqlBlock = link(this, text(text))

// Shorthand

internal val NIL = SqlBlock.Nil

internal val NL = SqlBlock.NL

internal fun text(text: String) = SqlBlock.Text(text)

internal fun link(lhs: SqlBlock, rhs: SqlBlock) = SqlBlock.Link(lhs, rhs)

internal fun nest(block: () -> SqlBlock) = SqlBlock.Nest(block())

internal fun list(start: String?, end: String?, delimiter: String? = ",", items: () -> List<SqlBlock>): SqlBlock {
var h: SqlBlock = NIL
h = if (start != null) h + start else h
h += nest {
val kids = items()
var list: SqlBlock = NIL
kids.foldIndexed(list) { i, a, item ->
list += item
list = if (delimiter != null && (i + 1) < kids.size) a + delimiter else a
list
}
}
h = if (end != null) h + end else h
return h
}
89 changes: 89 additions & 0 deletions partiql-ast/src/main/kotlin/org/partiql/ast/sql/SqlBlock.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package org.partiql.ast.sql

/**
* Write this [SqlBlock] tree as SQL text with the given [SqlLayout].
*
* @param layout SQL formatting ruleset
* @return SQL text
*/
public fun SqlBlock.sql(layout: SqlLayout = SqlLayout.DEFAULT): String = layout.format(this)

/**
* Representation of some textual corpus; akin to Wadler's "A prettier printer" Document type.
*/
sealed interface SqlBlock {

public override fun toString(): String

public fun <R, C> accept(visitor: BlockVisitor<R, C>, ctx: C): R

public object Nil : SqlBlock {

override fun toString() = ""

override fun <R, C> accept(visitor: BlockVisitor<R, C>, ctx: C): R = visitor.visitNil(this, ctx)
}

public object NL : SqlBlock {

override fun toString() = "\n"

override fun <R, C> accept(visitor: BlockVisitor<R, C>, ctx: C): R = visitor.visitNewline(this, ctx)
}

public class Text(val text: String) : SqlBlock {

override fun toString() = text

override fun <R, C> accept(visitor: BlockVisitor<R, C>, ctx: C): R = visitor.visitText(this, ctx)
}

public class Nest(val child: SqlBlock) : SqlBlock {

override fun toString() = child.toString()

override fun <R, C> accept(visitor: BlockVisitor<R, C>, ctx: C): R = visitor.visitNest(this, ctx)
}

// Use link block rather than linked-list block.next as it makes pre-order traversal trivial
public class Link(val lhs: SqlBlock, val rhs: SqlBlock) : SqlBlock {

override fun toString() = lhs.toString() + rhs.toString()

override fun <R, C> accept(visitor: BlockVisitor<R, C>, ctx: C): R = visitor.visitLink(this, ctx)
}
}

public interface BlockVisitor<R, C> {

public fun visit(block: SqlBlock, ctx: C): R

public fun visitNil(block: SqlBlock.Nil, ctx: C): R

public fun visitNewline(block: SqlBlock.NL, ctx: C): R

public fun visitText(block: SqlBlock.Text, ctx: C): R

public fun visitNest(block: SqlBlock.Nest, ctx: C): R

public fun visitLink(block: SqlBlock.Link, ctx: C): R
}

public abstract class BlockBaseVisitor<R, C> : BlockVisitor<R, C> {

public abstract fun defaultReturn(block: SqlBlock, ctx: C): R

public open fun defaultVisit(block: SqlBlock, ctx: C) = defaultReturn(block, ctx)

public override fun visit(block: SqlBlock, ctx: C): R = block.accept(this, ctx)

public override fun visitNil(block: SqlBlock.Nil, ctx: C): R = defaultVisit(block, ctx)

public override fun visitNewline(block: SqlBlock.NL, ctx: C): R = defaultVisit(block, ctx)

public override fun visitText(block: SqlBlock.Text, ctx: C): R = defaultVisit(block, ctx)

public override fun visitNest(block: SqlBlock.Nest, ctx: C): R = defaultVisit(block, ctx)

public override fun visitLink(block: SqlBlock.Link, ctx: C): R = defaultVisit(block, ctx)
}
Loading

0 comments on commit 6a18bb7

Please sign in to comment.