Skip to content

Commit

Permalink
Merge pull request #1059 from slick/tmp/sql-indent
Browse files Browse the repository at this point in the history
SQL Indentation by @imcharsi
  • Loading branch information
szeiger committed Jan 30, 2015
2 parents 42dbccf + 86974bd commit 2d71cd1
Show file tree
Hide file tree
Showing 16 changed files with 190 additions and 149 deletions.
1 change: 1 addition & 0 deletions common-test-resources/application.conf
@@ -1,4 +1,5 @@
slick {
ansiDump = true
unicodeDump = true
sqlIndent = true
}
Expand Up @@ -25,26 +25,60 @@ object MacroSupportInterpolationImpl {
val sb = new StringBuilder
val len = str.length
var pos = 0
def flushSB: Unit = if(!sb.isEmpty) {
exprs += append(Literal(Constant(sb.toString)))
sb.clear()
}
while(pos < len) {
val c = str.charAt(pos)
if(c == '\\') {
pos += 1
if(pos < len) {
val c2 = str.charAt(pos)
if(c2 == '(' || c2 == ')') {
if(!sb.isEmpty) {
exprs += append(Literal(Constant(sb.toString)))
sb.clear()
str.charAt(pos) match {
case '\\' =>
pos += 1
if(pos < len) {
str.charAt(pos) match {
case c2 @ ('(' | ')') => // optional parentheses
flushSB
exprs += If(
Select(skipParens, newTermName(NameTransformer.encode("unary_!"))),
append(Literal(Constant(c2))),
ctx.universe.EmptyTree
)
case '{' => // optional open parentheses with indent
flushSB
exprs += If(
Select(skipParens, newTermName(NameTransformer.encode("unary_!"))),
Block(List(
append(Literal(Constant('('))),
Select(sqlBuilder, newTermName("newLineIndent"))
), Literal(Constant(()))),
ctx.universe.EmptyTree
)
case '}' => // optional close parentheses with dedent
flushSB
exprs += If(
Select(skipParens, newTermName(NameTransformer.encode("unary_!"))),
Block(List(
Select(sqlBuilder, newTermName("newLineDedent")),
append(Literal(Constant(')')))
), Literal(Constant(()))),
ctx.universe.EmptyTree
)
case '[' => // open parenthesis with indent
sb append '('
flushSB
exprs += Select(sqlBuilder, newTermName("newLineIndent"))
case ']' => // close parenthesis with dedent
flushSB
exprs += Select(sqlBuilder, newTermName("newLineDedent"))
sb append ')'
case 'n' =>
flushSB
exprs += Select(sqlBuilder, newTermName("newLineOrSpace"))
case c2 =>
ctx.abort(ctx.enclosingPosition, "Invalid escaped character '"+c2+"' in literal \""+str+"\"")
}
exprs += If(
Select(skipParens, newTermName(NameTransformer.encode("unary_!"))),
append(Literal(Constant(c2))),
ctx.universe.EmptyTree
)
} else sb append c
//else ctx.abort(ctx.enclosingPosition, "Invalid escaped character '"+c2+"' in literal \""+str+"\"")
}
} else sb append c
}
case c => sb append c
}
pos += 1
}
if(!sb.isEmpty)
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/reference.conf
Expand Up @@ -8,4 +8,7 @@ slick {

# Dump individual Select and Ref nodes instead of a single Path
dumpPaths = false

# Use multi-line, indented formatting for SQL statements
sqlIndent = false
}
6 changes: 3 additions & 3 deletions src/main/scala/scala/slick/driver/H2Driver.scala
Expand Up @@ -76,9 +76,9 @@ trait H2Driver extends JdbcDriver { driver =>
}

override protected def buildFetchOffsetClause(fetch: Option[Node], offset: Option[Node]) = (fetch, offset) match {
case (Some(t), Some(d)) => b" limit $t offset $d"
case (Some(t), None ) => b" limit $t"
case (None, Some(d) ) => b" limit -1 offset $d"
case (Some(t), Some(d)) => b"\nlimit $t offset $d"
case (Some(t), None ) => b"\nlimit $t"
case (None, Some(d) ) => b"\nlimit -1 offset $d"
case _ =>
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/scala/slick/driver/HsqldbDriver.scala
Expand Up @@ -83,9 +83,9 @@ trait HsqldbDriver extends JdbcDriver { driver =>
}

override protected def buildFetchOffsetClause(fetch: Option[Node], offset: Option[Node]) = (fetch, offset) match {
case (Some(t), Some(d)) => b" limit $t offset $d"
case (Some(t), None ) => b" limit $t"
case (None, Some(d) ) => b" offset $d"
case (Some(t), Some(d)) => b"\nlimit $t offset $d"
case (Some(t), None ) => b"\nlimit $t"
case (None, Some(d) ) => b"\noffset $d"
case _ =>
}
}
Expand Down
Expand Up @@ -145,7 +145,7 @@ trait JdbcStatementBuilderComponent { driver: JdbcDriver =>
scanJoins(c.from)
buildSelectClause(c)
buildFromClause(c.from)
if(limit0) b" where 1=0"
if(limit0) b"\nwhere 1=0"
else buildWhereClause(c.where)
buildGroupByClause(c.groupBy)
buildOrderByClause(c.orderBy)
Expand Down Expand Up @@ -182,46 +182,46 @@ trait JdbcStatementBuilderComponent { driver: JdbcDriver =>
}

protected def buildFromClause(from: Seq[(Symbol, Node)]) = building(FromPart) {
if(from.isEmpty) scalarFrom.foreach { s => b" from $s" }
if(from.isEmpty) scalarFrom.foreach { s => b"\nfrom $s" }
else {
b" from "
b"\nfrom "
b.sep(from, ", ") { case (sym, n) => buildFrom(n, Some(sym)) }
}
}

protected def buildWhereClause(where: Seq[Node]) = building(WherePart) {
if(!where.isEmpty) {
b" where "
b"\nwhere "
expr(where.reduceLeft((a, b) => Library.And.typed[Boolean](a, b)), true)
}
}

protected def buildGroupByClause(groupBy: Option[Node]) = building(OtherPart) {
groupBy.foreach { e => b" group by !$e" }
groupBy.foreach { e => b"\ngroup by !$e" }
}

protected def buildOrderByClause(order: Seq[(Node, Ordering)]) = building(OtherPart) {
if(!order.isEmpty) {
b" order by "
b"\norder by "
b.sep(order, ", "){ case (n, o) => buildOrdering(n, o) }
}
}

protected def buildFetchOffsetClause(fetch: Option[Node], offset: Option[Node]) = building(OtherPart) {
(fetch, offset) match {
/* SQL:2008 syntax */
case (Some(t), Some(d)) => b" offset $d row fetch next $t row only"
case (Some(t), None) => b" fetch next $t row only"
case (None, Some(d)) => b" offset $d row"
case (Some(t), Some(d)) => b"\noffset $d row fetch next $t row only"
case (Some(t), None) => b"\nfetch next $t row only"
case (None, Some(d)) => b"\noffset $d row"
case _ =>
}
}

protected def buildSelectPart(n: Node): Unit = n match {
case c: Comprehension =>
b"("
b"\["
buildComprehension(c)
b")"
b"\]"
case n =>
expr(n, true)
}
Expand All @@ -234,24 +234,24 @@ trait JdbcStatementBuilderComponent { driver: JdbcDriver =>
addAlias
case j @ Join(leftGen, rightGen, left, right, jt, on) =>
buildFrom(left, Some(leftGen))
b" ${jt.sqlName} join "
b"\n${jt.sqlName} join "
buildFrom(right, Some(rightGen))
on match {
case LiteralNode(true) =>
if(!supportsEmptyJoinConditions) b" on 1=1"
case _ => b" on !$on"
if(!supportsEmptyJoinConditions) b"\non 1=1"
case _ => b"\non !$on"
}
case Union(left, right, all, _, _) =>
b"\("
b"\{"
buildFrom(left, None, true)
if(all) b" union all " else b" union "
if(all) b"\nunion all " else b"\nunion "
buildFrom(right, None, true)
b"\)"
b"\}"
addAlias
case n =>
b"\("
b"\{"
buildComprehension(toComprehension(n, true))
b"\)"
b"\}"
addAlias
}
}
Expand Down Expand Up @@ -287,11 +287,11 @@ trait JdbcStatementBuilderComponent { driver: JdbcDriver =>
expr(IfThenElse(Vector(ch, LiteralNode(1), LiteralNode(0))), skipParens)
case RewriteBooleans.ToRealBoolean(ch) =>
expr(Library.==.typed[Boolean](ch, LiteralNode(true)), skipParens)
case Library.Exists(c: Comprehension) if(!supportsTuples) =>
case Library.Exists(c: Comprehension) =>
/* If tuples are not supported, selecting multiple individial columns
* in exists(select ...) is probably not supported, either, so we rewrite
* such sub-queries to "select *". */
b"exists(!${c.copy(select = None)})"
b"exists\[!${(if(supportsTuples) c else c.copy(select = None)): Node}\]"
case Library.Concat(l, r) if concatOperator.isDefined =>
b"\($l${concatOperator.get}$r\)"
case Library.User() if !capabilities.contains(RelationalProfile.capabilities.functionUser) =>
Expand Down Expand Up @@ -369,9 +369,9 @@ trait JdbcStatementBuilderComponent { driver: JdbcDriver =>
b += symbolName(struct) += '.' += symbolName(field)
case OptionApply(ch) => expr(ch, skipParens)
case n => // try to build a sub-query
b"\("
b"\{"
buildComprehension(toComprehension(n))
b"\)"
b"\}"
}

protected def buildOrdering(n: Node, o: Ordering) {
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/scala/slick/driver/MySQLDriver.scala
Expand Up @@ -131,9 +131,9 @@ trait MySQLDriver extends JdbcDriver { driver =>
}

override protected def buildFetchOffsetClause(fetch: Option[Node], offset: Option[Node]) = (fetch, offset) match {
case (Some(t), Some(d)) => b" limit $d,$t"
case (Some(t), None ) => b" limit $t"
case (None, Some(d)) => b" limit $d,18446744073709551615"
case (Some(t), Some(d)) => b"\nlimit $d,$t"
case (Some(t), None ) => b"\nlimit $t"
case (None, Some(d)) => b"\nlimit $d,18446744073709551615"
case _ =>
}

Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/scala/slick/driver/PostgresDriver.scala
Expand Up @@ -111,9 +111,9 @@ trait PostgresDriver extends JdbcDriver { driver =>
override protected val supportsEmptyJoinConditions = false

override protected def buildFetchOffsetClause(fetch: Option[Node], offset: Option[Node]) = (fetch, offset) match {
case (Some(t), Some(d)) => b" limit $t offset $d"
case (Some(t), None ) => b" limit $t"
case (None, Some(d)) => b" offset $d"
case (Some(t), Some(d)) => b"\nlimit $t offset $d"
case (Some(t), None ) => b"\nlimit $t"
case (None, Some(d)) => b"\noffset $d"
case _ =>
}

Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/scala/slick/driver/SQLiteDriver.scala
Expand Up @@ -145,9 +145,9 @@ trait SQLiteDriver extends JdbcDriver { driver =>
}

override protected def buildFetchOffsetClause(fetch: Option[Node], offset: Option[Node]) = (fetch, offset) match {
case (Some(t), Some(d)) => b" limit $d,$t"
case (Some(t), None ) => b" limit $t"
case (None, Some(d)) => b" limit $d,-1"
case (Some(t), Some(d)) => b"\nlimit $d,$t"
case (Some(t), None ) => b"\nlimit $t"
case (None, Some(d)) => b"\nlimit $d,-1"
case _ =>
}

Expand Down
25 changes: 16 additions & 9 deletions src/main/scala/scala/slick/jdbc/JdbcBackend.scala
Expand Up @@ -12,7 +12,7 @@ import javax.naming.InitialContext
import scala.slick.action._
import scala.slick.backend.{DatabasePublisher, DatabaseComponent, RelationalBackend}
import scala.slick.SlickException
import scala.slick.util.{SlickLogger, AsyncExecutor}
import scala.slick.util.{LogUtil, GlobalConfig, SlickLogger, AsyncExecutor}
import scala.slick.util.ConfigExtensionMethods._

import org.slf4j.LoggerFactory
Expand All @@ -21,9 +21,6 @@ import com.typesafe.config.{ConfigFactory, Config}
/** A JDBC-based database back-end which can be used for <em>Plain SQL</em> queries
* and with all [[scala.slick.driver.JdbcProfile]]-based drivers. */
trait JdbcBackend extends RelationalBackend {
protected[jdbc] lazy val statementLogger = new SlickLogger(LoggerFactory.getLogger(classOf[JdbcBackend].getName+".statement"))
protected[jdbc] lazy val benchmarkLogger = new SlickLogger(LoggerFactory.getLogger(classOf[JdbcBackend].getName+".benchmark"))

type This = JdbcBackend
type Database = DatabaseDef
type Session = SessionDef
Expand Down Expand Up @@ -241,7 +238,7 @@ trait JdbcBackend extends RelationalBackend {
defaultType: ResultSetType = ResultSetType.ForwardOnly,
defaultConcurrency: ResultSetConcurrency = ResultSetConcurrency.ReadOnly,
defaultHoldability: ResultSetHoldability = ResultSetHoldability.Default): PreparedStatement = {
statementLogger.debug("Preparing statement: "+sql)
JdbcBackend.logStatement("Preparing statement", sql)
loggingPreparedStatement(decorateStatement(resultSetHoldability.withDefault(defaultHoldability) match {
case ResultSetHoldability.Default =>
val rsType = resultSetType.withDefault(defaultType).intValue
Expand All @@ -258,12 +255,14 @@ trait JdbcBackend extends RelationalBackend {
}

final def prepareInsertStatement(sql: String, columnNames: Array[String] = new Array[String](0)): PreparedStatement = {
statementLogger.debug("Preparing insert statement: "+sql+", returning: "+columnNames.mkString(","))
if(JdbcBackend.statementLogger.isDebugEnabled)
JdbcBackend.logStatement("Preparing insert statement (returning: "+columnNames.mkString(",")+")", sql)
loggingPreparedStatement(decorateStatement(conn.prepareStatement(sql, columnNames)))
}

final def prepareInsertStatement(sql: String, columnIndexes: Array[Int]): PreparedStatement = {
statementLogger.debug("Preparing insert statement: "+sql+", returning indexes: "+columnIndexes.mkString(","))
if(JdbcBackend.statementLogger.isDebugEnabled)
JdbcBackend.logStatement("Preparing insert statement (returning indexes: "+columnIndexes.mkString(",")+")", sql)
loggingPreparedStatement(decorateStatement(conn.prepareStatement(sql, columnIndexes)))
}

Expand Down Expand Up @@ -359,10 +358,10 @@ trait JdbcBackend extends RelationalBackend {
}

protected def loggingStatement(st: Statement): Statement =
if(statementLogger.isDebugEnabled || benchmarkLogger.isDebugEnabled) new LoggingStatement(st) else st
if(JdbcBackend.statementLogger.isDebugEnabled || JdbcBackend.benchmarkLogger.isDebugEnabled) new LoggingStatement(st) else st

protected def loggingPreparedStatement(st: PreparedStatement): PreparedStatement =
if(statementLogger.isDebugEnabled || benchmarkLogger.isDebugEnabled) new LoggingPreparedStatement(st) else st
if(JdbcBackend.statementLogger.isDebugEnabled || JdbcBackend.benchmarkLogger.isDebugEnabled) new LoggingPreparedStatement(st) else st

/** Start a `transactionally` block */
private[slick] def startInTransaction: Unit
Expand Down Expand Up @@ -477,4 +476,12 @@ object JdbcBackend extends JdbcBackend {
case class StatementParameters(rsType: ResultSetType, rsConcurrency: ResultSetConcurrency,
rsHoldability: ResultSetHoldability, statementInit: Statement => Unit)
val defaultStatementParameters = StatementParameters(ResultSetType.Auto, ResultSetConcurrency.Auto, ResultSetHoldability.Auto, null)

protected[jdbc] lazy val statementLogger = new SlickLogger(LoggerFactory.getLogger(classOf[JdbcBackend].getName+".statement"))
protected[jdbc] lazy val benchmarkLogger = new SlickLogger(LoggerFactory.getLogger(classOf[JdbcBackend].getName+".benchmark"))

protected[jdbc] def logStatement(msg: String, stmt: String) = if(statementLogger.isDebugEnabled) {
val s = if(GlobalConfig.sqlIndent) msg + ":\n" + LogUtil.multilineBorder(stmt) else msg + ": " + stmt
statementLogger.debug(s)
}
}
3 changes: 3 additions & 0 deletions src/main/scala/scala/slick/util/GlobalConfig.scala
Expand Up @@ -20,6 +20,9 @@ object GlobalConfig {
/** Use Unixode box characters in table dumps */
val unicodeDump = config.getBoolean("slick.unicodeDump")

/** Use multi-line, indented formatting for SQL statements */
val sqlIndent = config.getBoolean("slick.sqlIndent")

/** Get a `Config` object for a Slick driver */
def driverConfig(name: String): Config = {
val path = "slick.driver." + name
Expand Down
14 changes: 14 additions & 0 deletions src/main/scala/scala/slick/util/LogUtil.scala
@@ -0,0 +1,14 @@
package scala.slick.util

/** Utilities for logging and creating tree & table dumps. */
private[slick] object LogUtil {
val (cNormal, cGreen, cYellow, cBlue, cCyan) =
if(GlobalConfig.ansiDump) ("\u001B[0m", "\u001B[32m", "\u001B[33m", "\u001B[34m", "\u001B[36m")
else ("", "", "", "", "")

private[this] val multi = if(GlobalConfig.unicodeDump) "\u2507 " else " "
private[this] val multilineBorderPrefix = cYellow + multi + cNormal

def multilineBorder(s: String): String =
multilineBorderPrefix + s.replace("\n", "\n" + multilineBorderPrefix)
}
8 changes: 8 additions & 0 deletions src/main/scala/scala/slick/util/MacroSupport.scala
Expand Up @@ -16,8 +16,16 @@ class MacroSupportInterpolation(context: StringContext) {
* <ul>
* <li>'(': if(!skipParens) sqlBuilder += '('</li>
* <li>')': if(!skipParens) sqlBuilder += ')'</li>
* <li>'[': Add a '(' plus newline and increase indentation</li>
* <li>'[': Decrease indentation and add a newline plus ')'</li>
* <li>'{': Like '[' but only if(!skipParens)</li>
* <li>'}': Like ']' but only if(!skipParens)</li>
* <li>'n': Add a newline plus indentation</li>
* </ul>
*
* If 'slick.sqlIndent' is not set in application.conf, no newlines are inserted and indentation
* is replaced by a single space character.
*
* Variables inside the string can be prefixed by another symbol before the
* standard '$' escape character to modify their meaning:
*
Expand Down

0 comments on commit 2d71cd1

Please sign in to comment.