
org.tresql.Query.scala Maven / Gradle / Ivy
The newest version!
package org.tresql
import java.sql.{CallableStatement, PreparedStatement, ResultSet, SQLException}
import CoreTypes.RowConverter
import org.tresql.ast.Exp
import org.tresql.metadata.TypeMapper
import scala.annotation.tailrec
trait Query extends QueryBuilder with TypedQuery {
def apply(expr: String, params: Any*)(implicit resources: Resources): DynamicResult =
exec(expr, normalizePars(params: _*), resources).asInstanceOf[DynamicResult]
def compiledResult[T <: RowLike](expr: String, params: Any*)(
implicit resources: Resources): Result[T] =
exec(expr, normalizePars(params: _*), resources).asInstanceOf[Result[T]]
private[tresql] def converters: Map[List[Int], RowConverter[_ <: RowLike]] = null
private def exec(
expr: String,
params: Map[String, Any],
resources: Resources
): Result[_ <: RowLike] = {
val builtExpr = build(expr, params, false)(resources)
if (builtExpr == null) SingleValueResult(null) else {
builtExpr() match {
case r: Result[_] => r
case x => SingleValueResult(x)
}
}
}
def build(
expr: String,
params: Map[String, Any] = null,
reusableExpr: Boolean = true
)(implicit resources: Resources): Expr = {
require(resources != null, "Resources cannot be null.")
resources.log(expr, Nil, LogTopic.tresql)
val pars =
if (resources.params.isEmpty) params
else if (params != null) resources.params ++ params else resources.params
newInstance(new Env(pars, resources, reusableExpr), 0, 0).buildExpr(expr)
}
def buildFromAst(
expr: Exp,
params: Map[String, Any] = null,
reusableExpr: Boolean = true
)(implicit resources: Resources): Expr = {
require(resources != null, "Resources cannot be null.")
val pars =
if (resources.params.isEmpty) params
else if (params != null) resources.params ++ params else resources.params
newInstance(new Env(pars, resources, reusableExpr), 0, 0).buildExpr(expr)
}
/** QueryBuilder methods **/
private[tresql] def newInstance(
e: Env,
idx: Int,
chIdx: Int
) = {
if (converters != null) e.rowConverters = converters
val qpos = chIdx :: queryPos
new Query {
override def env = e
override private[tresql] def queryPos = qpos
override private[tresql] var bindIdx = idx
}
}
private[tresql] def normalizePars(pars: Any*): Map[String, Any] = pars match {
case Seq(m: Map[String @unchecked, Any @unchecked]) => m
case l => l.zipWithIndex.map { case (v, k) => (k + 1).toString -> v }.toMap
}
private[tresql] def sel(sql: String, cols: QueryBuilder#ColsExpr): Result[_ <: RowLike] = try {
val (rs, columns, visibleColCount) = sel_result(sql, cols)
val result = env.rowConverter(queryPos).map { conv =>
new CompiledSelectResult(rs, columns, env, sql,registeredBindVariables,
env.maxResultSize, visibleColCount, conv)
}.getOrElse {
new DynamicSelectResult(rs, columns, env, sql, registeredBindVariables, env.maxResultSize, visibleColCount)
}
env.result = result
result
} catch {
case ex: SQLException =>
throw new TresqlException(sql, bindVarsValues(registeredBindVariables), ex)
}
private[this] def sel_result(sql: String, cols: QueryBuilder#ColsExpr):
(ResultSet, Vector[Column], Int) = { //jdbc result, columns, visible column count
val st = statement(sql, env)
var i = 0
val rs = st.executeQuery
val md = rs.getMetaData
var visibleColCount = -1
def jdbcRcols = (1 to md.getColumnCount).foldLeft(List[Column]()) {
(l, j) => i += 1; Column(i, md.getColumnLabel(j), null) :: l
} reverse
def rcol(c: QueryBuilder#ColExpr) = if (c.separateQuery) Column(-1, c.name, c.col) else {
i += 1; Column(i, c.name, null)
}
def rcols = if (cols.hasHidden) {
val res = cols.cols.zipWithIndex.foldLeft((List[Column](), Map[Expr, Int](), 0)) {
(r, c) => (rcol(c._1) :: r._1,
if (c._1.hidden) r._2 + (c._1.col -> c._2) else r._2,
if (!c._1.hidden) r._3 + 1 else r._3)
}
env.updateExprs(res._2)
(res._1.reverse, res._3)
} else (cols.cols map rcol, -1)
val columns =
if (cols.hasAll) Vector(cols.cols.flatMap { c =>
if (c.col.isInstanceOf[QueryBuilder#AllExpr]) jdbcRcols else List(rcol(c))
}: _*)
else if (cols.hasIdentAll) Vector(jdbcRcols ++ (cols.cols.filter(_.separateQuery) map rcol) :_*)
else rcols match {
case (c, -1) => Vector(c: _*)
case (c, s) =>
visibleColCount = s
Vector(c: _*)
}
(rs, columns, visibleColCount)
}
private[tresql] def update(sql: String) = try {
val st = statement(sql, env)
try {
st.executeUpdate
} finally if (!env.reusableExpr) {
st.close
env.statement = null
}
} catch {
case ex: SQLException =>
throw new TresqlException(sql, bindVarsValues(registeredBindVariables), ex)
}
private[tresql] def call(sql: String): Result[RowLike] = try {
val st = statement(sql, env, true).asInstanceOf[CallableStatement]
var result: Result[RowLike] = null
var outs: List[Any] = null
try {
if (st.execute) {
val rs = st.getResultSet
val md = rs.getMetaData
val res = env.rowConverter(queryPos).map { conv =>
new CompiledSelectResult(
rs,
Vector(1 to md.getColumnCount map { i => Column(i, md.getColumnLabel(i), null) }: _*),
env,
sql,
registeredBindVariables,
env.maxResultSize,
-1,
conv)
}.getOrElse {
new DynamicSelectResult(
rs,
Vector(1 to md.getColumnCount map { i => Column(i, md.getColumnLabel(i), null) }: _*),
env,
sql,
registeredBindVariables,
env.maxResultSize
)
}
env.result = res
result = res
}
outs = registeredBindVariables map (_()) collect { case x: OutPar =>
val p = x.asInstanceOf[OutPar]
p.value = p.value match {
case null => st.getObject(p.idx)
case i: Int =>
val x = st.getInt(p.idx); if (st.wasNull) null else x
case l: Long =>
val x = st.getLong(p.idx); if (st.wasNull) null else x
case d: Double =>
val x = st.getDouble(p.idx); if (st.wasNull) null else x
case f: Float =>
val x = st.getFloat(p.idx); if (st.wasNull) null else x
// Allow the user to specify how they want the Date handled based on the input type
case t: java.sql.Timestamp => st.getTimestamp(p.idx)
case d: java.sql.Date => st.getDate(p.idx)
case t: java.sql.Time => st.getTime(p.idx)
/* java.util.Date has to go last, since the java.sql date/time classes subclass it. By default we
* assume a Timestamp value */
case d: java.util.Date => st.getTimestamp(p.idx)
case b: Boolean => st.getBoolean(p.idx)
case s: String => st.getString(p.idx)
case bn: java.math.BigDecimal => st.getBigDecimal(p.idx)
case bd: BigDecimal => val x = st.getBigDecimal(p.idx); if (st.wasNull) null else BigDecimal(x)
}
p.value
}
} finally if (result == null && !env.reusableExpr) {
st.close
env.statement = null
}
if (outs.isEmpty) result
else env.rowConverter(queryPos).map { conv =>
new CompiledArrayResult(if (result== null) outs else result :: outs, conv)
}.getOrElse(new DynamicArrayResult(if (result== null) outs else result :: outs))
} catch {
case ex: SQLException =>
throw new TresqlException(sql, bindVarsValues(registeredBindVariables), ex)
}
private def statement(sql: String, env: Env, call: Boolean = false) = {
log(sql, registeredBindVariables)
val conn = env.conn
if (conn == null) throw new NullPointerException(
"""Connection not found in environment.""")
val st = if (env.reusableExpr)
if (env.statement == null) {
val s = if (call) conn.prepareCall(sql) else {
conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)
}
env.statement = s
s
} else env.statement
else if (call) conn.prepareCall(sql)
else conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)
if (env.queryTimeout > 0) st.setQueryTimeout(env.queryTimeout)
if (env.fetchSize > 0) st.setFetchSize(env.fetchSize)
val bindValues = registeredBindVariables.map {
case v: QueryBuilder#VarExpr => (v.allowArrBind, v())
case e => (false, e())
}
bindVars(st, bindValues)
st
}
private def bindVars(st: PreparedStatement, bindVariables: List[(Boolean, Any)]) = {
var idx = 1
def bindVar(allowArrBind: Boolean, p: Any): Unit = {
try p match {
case null => st.setNull(idx, java.sql.Types.NULL)
case i: Int => st.setInt(idx, i)
case l: Long => st.setLong(idx, l)
case d: Double => st.setDouble(idx, d)
case f: Float => st.setFloat(idx, f)
case i: java.lang.Integer => st.setInt(idx, i)
case l: java.lang.Long => st.setLong(idx, l)
case d: java.lang.Double => st.setDouble(idx, d)
case f: java.lang.Float => st.setFloat(idx, f)
// Allow the user to specify how they want the Date handled based on the input type
case t: java.sql.Timestamp => st.setTimestamp(idx, t)
case d: java.sql.Date => st.setDate(idx, d)
case t: java.sql.Time => st.setTime(idx, t)
/* java.util.Date has to go last, since the java.sql date/time classes subclass it. By default we
* assume a java.sql.Date value */
case d: java.util.Date => st.setTimestamp(idx, new java.sql.Timestamp(d.getTime))
case c: java.util.Calendar => st.setTimestamp(idx, new java.sql.Timestamp(c.getTime.getTime))
case d: java.time.LocalDate => st.setDate(idx, java.sql.Date.valueOf(d))
case t: java.time.LocalDateTime => st.setTimestamp(idx, java.sql.Timestamp.valueOf(t))
case t: java.time.LocalTime => st.setTime(idx, java.sql.Time.valueOf(t))
case b: Boolean => st.setBoolean(idx, b)
case b: java.lang.Boolean => st.setBoolean(idx, b)
case s: String => st.setString(idx, s)
case bn: java.math.BigDecimal => st.setBigDecimal(idx, bn)
case bd: BigDecimal => st.setBigDecimal(idx, bd.bigDecimal)
case bi: java.math.BigInteger => st.setBigDecimal(idx, new java.math.BigDecimal(bi))
case bi: BigInt => st.setBigDecimal(idx, new java.math.BigDecimal(bi.bigInteger))
case in: java.io.InputStream => st.setBinaryStream(idx, in)
case bl: java.sql.Blob => st.setBlob(idx, bl)
case rd: java.io.Reader => st.setCharacterStream(idx, rd)
case cl: java.sql.Clob => st.setClob(idx, cl)
case ab: Array[Byte] => st.setBytes(idx, ab)
case ar: java.sql.Array => st.setArray(idx, ar)
//array binding
case a: Array[_] =>
if (allowArrBind) bindColl(a.toSeq)
else bindArr(st, a, idx) // unable to construct sql array since type is unknown
case o: Option[_] =>
bindVar(allowArrBind, o.orNull)
idx -= 1
case i: scala.collection.Iterable[_] =>
if (allowArrBind) bindColl(i)
else st.setString(idx, toJsonString(i)) // convert complex object to json string for not to fail
case p@InOutPar(v) =>
bindVar(allowArrBind, v)
idx -= 1
registerOutPar(st.asInstanceOf[CallableStatement], p, idx)
//OutPar must be matched bellow InOutPar since it is superclass of InOutPar
case p@OutPar(_) => registerOutPar(st.asInstanceOf[CallableStatement], p, idx)
//unknown object
case obj => st.setObject(idx, obj)
} catch {
case e:Exception => throw new RuntimeException("Failed to bind variable at index " +
(idx - 1) + ". Value: " + (String.valueOf(p) match {
case x if x.length > 100 => x.substring(0, 100) + "..."
case x => x
}) + " of class " + (if (p == null) "null" else p.getClass),e)
}
idx += 1
}
def bindColl(i: Iterable[_]) = {
i.foreach(bindVar(true, _))
idx -= 1
}
def bindArr(st: java.sql.PreparedStatement, value: Array[_], idx: Int) = {
def normalizeArr(arr: Array[_]) = {
def n[T](a: Array[T], m: T => Object): Array[Object] = a map { case null => null case x => m(x) }
arr match {
case a: Array[BigDecimal] => n[BigDecimal](a, _.bigDecimal)
case a: Array[BigInt] => n[BigInt](a, _.bigInteger)
case a: Array[java.util.Date] => n[java.util.Date](a, d => new java.sql.Timestamp(d.getTime))
case a: Array[java.util.Calendar] => n[java.util.Calendar](a, c => new java.sql.Timestamp(c.getTime.getTime))
case a: Array[java.time.LocalDate] => n[java.time.LocalDate](a, java.sql.Date.valueOf)
case a: Array[java.time.LocalDateTime] => n[java.time.LocalDateTime](a, java.sql.Timestamp.valueOf)
case a: Array[java.time.LocalTime] => n[java.time.LocalTime](a, java.sql.Time.valueOf)
case a => a.asInstanceOf[Array[Object]]
}
}
val conn = st.getConnection
val vendor = env.dialect(SQLVendorExpr())
val arrayScalaType = Manifest.classType(value.getClass.getComponentType).toString
val arraySqlType = env.metadata
.to_sql_type(vendor, env.metadata.from_jdbc_type(TypeMapper.scalaToJdbc(arrayScalaType)))
val bv = conn.createArrayOf(arraySqlType, normalizeArr(value))
st.setArray(idx, bv)
}
bindVariables.foreach { case (arrb, v) => bindVar(arrb, v) }
}
private def registerOutPar(st: CallableStatement, par: OutPar, idx: Int) = {
import java.sql.Types._
par.idx = idx
par.value match {
case null => st.registerOutParameter(idx, NULL)
case i: Int => st.registerOutParameter(idx, INTEGER)
case l: Long => st.registerOutParameter(idx, BIGINT)
case d: Double => st.registerOutParameter(idx, DECIMAL)
case f: Float => st.registerOutParameter(idx, DECIMAL)
case i: java.lang.Integer => st.registerOutParameter(idx, INTEGER)
case l: java.lang.Long => st.registerOutParameter(idx, BIGINT)
case d: java.lang.Double => st.registerOutParameter(idx, DECIMAL)
case f: java.lang.Float => st.registerOutParameter(idx, DECIMAL)
// Allow the user to specify how they want the Date handled based on the input type
case t: java.sql.Timestamp => st.registerOutParameter(idx, TIMESTAMP)
case d: java.sql.Date => st.registerOutParameter(idx, DATE)
case t: java.sql.Time => st.registerOutParameter(idx, TIME)
/* java.util.Date has to go last, since the java.sql date/time classes subclass it. By default we
* assume a Timestamp value */
case d: java.util.Date => st.registerOutParameter(idx, TIMESTAMP)
case b: Boolean => st.registerOutParameter(idx, BOOLEAN)
case b: java.lang.Boolean => st.registerOutParameter(idx, BOOLEAN)
case s: String => st.registerOutParameter(idx, VARCHAR)
case bn: java.math.BigDecimal => st.registerOutParameter(idx, DECIMAL, bn.scale)
case bd: BigDecimal => st.registerOutParameter(idx, DECIMAL, bd.scale)
//unknown object
case obj => st.registerOutParameter(idx, OTHER)
}
}
private def bindVarsValues(bindVars: List[Expr]) = {
// scala 3
//val lf = Option(env.bindVarLogFilter).map(_ orElse { case (_, v) => v }).getOrElse(_._2)
val lf: ((String, Any)) => Any = Option(env.bindVarLogFilter)
.map(_ orElse { case (_, v) => v }: PartialFunction[(String, Any), Any]).getOrElse(_._2)
bindVars.flatMap {
case v: VarExpr => List(v.fullName -> lf((v.fullName, v())))
case r: ResExpr => List(r.name -> r())
case id: IdExpr => List(s"#${id.seqName}" -> id.peek)
case ir: IdRefExpr => List(s":#${ir.seqName}" -> ir.peek)
case irid: ORT#IdRefIdExpr => List(s":#${irid.idRefSeq}#${irid.idSeq}" -> irid())
case _ => Nil
}
}
private def log(sql: String, bindVars: List[Expr]) = {
env.log(sql, Nil, LogTopic.sql)
env.log(sql, bindVarsValues(bindVars), LogTopic.sql_with_params)
env.log(
bindVarsValues(bindVars)
.map { case (n, v) => s"$n -> ${String.valueOf(v)}"}
.mkString("[", ", ", "]"),
Nil, LogTopic.params
)
}
// TODO make this method accessible to resources for unknown type binding customization
private def toJsonString(value: Any) = {
def dumpJson(value: Any, sb: StringBuilder): Unit = value match {
case m: Map[String @unchecked, _] =>
sb.append('{')
printSeq(m, sb.append(',')) { m =>
printString(m._1, sb)
sb.append(':')
dumpJson(m._2, sb)
}
sb.append('}')
case seq: Seq[_] =>
sb.append('[')
printSeq(seq, sb.append(','))(dumpJson(_, sb))
sb.append(']')
case s: String => printString(s, sb)
case n: Number => sb.append(n)
case b: Boolean => sb.append(b)
case x => printString(String.valueOf(x), sb)
}
// copied from spray.json.JsonPrinter to avoid dependency
def printString(s: String, sb: StringBuilder): Unit = {
@tailrec def firstToBeEncoded(ix: Int = 0): Int =
if (ix == s.length) -1 else if (requiresEncoding(s.charAt(ix))) ix else firstToBeEncoded(ix + 1)
sb.append('"')
firstToBeEncoded() match {
case -1 => sb.append(s)
case first =>
sb.append(s, 0, first)
@tailrec def append(ix: Int): Unit =
if (ix < s.length) {
s.charAt(ix) match {
case c if !requiresEncoding(c) => sb.append(c)
case '"' => sb.append("\\\"")
case '\\' => sb.append("\\\\")
case '\b' => sb.append("\\b")
case '\f' => sb.append("\\f")
case '\n' => sb.append("\\n")
case '\r' => sb.append("\\r")
case '\t' => sb.append("\\t")
case x if x <= 0xF => sb.append("\\u000").append(Integer.toHexString(x))
case x if x <= 0xFF => sb.append("\\u00").append(Integer.toHexString(x))
case x if x <= 0xFFF => sb.append("\\u0").append(Integer.toHexString(x))
case x => sb.append("\\u").append(Integer.toHexString(x))
}
append(ix + 1)
}
append(first)
}
sb.append('"')
}
def printSeq[A](iterable: Iterable[A], printSeparator: => Unit)(f: A => Unit): Unit = {
var first = true
iterable.foreach { a =>
if (first) first = false else printSeparator
f(a)
}
}
def requiresEncoding(c: Char): Boolean =
c match {
case '"' => true
case '\\' => true
case c => c < 0x20
}
val sb = new StringBuilder
dumpJson(value, sb)
sb.toString
}
}
/** Out parameter box for callable statement */
class OutPar(var value: Any) {
private[tresql] var idx = 0
def this() = this(null)
override def toString = "OutPar(" + value + ")"
}
object OutPar {
def apply() = new OutPar()
def apply(value: Any) = new OutPar(value)
def unapply(par: OutPar): Option[Any] = Some(par.value)
}
/** In out parameter box for callable statement */
class InOutPar(v: Any) extends OutPar(v) {
override def toString = "InOutPar(" + value + ")"
}
object InOutPar {
def apply(value: Any) = new InOutPar(value)
def unapply(par: InOutPar): Option[Any] = Some(par.value)
}
object Query extends Query
© 2015 - 2025 Weber Informatics LLC | Privacy Policy