From 482aee1815f475c66632cfcac061759c736224ec Mon Sep 17 00:00:00 2001 From: Samik R Date: Sun, 15 Sep 2024 23:10:20 +0530 Subject: [PATCH 1/2] Initial implementation --- scalasql/core/src/DbApi.scala | 24 ++++++++++++++++++++--- scalasql/core/src/Queryable.scala | 10 ++++++++++ scalasql/query/src/GetGeneratedKeys.scala | 2 ++ scalasql/query/src/Query.scala | 19 ++++++++++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/scalasql/core/src/DbApi.scala b/scalasql/core/src/DbApi.scala index be9aa10..7e9582c 100644 --- a/scalasql/core/src/DbApi.scala +++ b/scalasql/core/src/DbApi.scala @@ -214,12 +214,30 @@ object DbApi { s"Single row query must return 1 result, not ${results.size}" ) results.head.asInstanceOf[R] - } else { + } else res.toVector.asInstanceOf[R] - } } } - + + def runOption[Q, R](query: Q, fetchSize: Int = -1, queryTimeoutSeconds: Int = -1)( + implicit qr: Queryable[Q, Option[R]], + fileName: sourcecode.FileName, + lineNum: sourcecode.Line + ): Option[R] = { + assert(qr.isSingleRowOption(query)) + val res = stream(query, fetchSize, queryTimeoutSeconds)( + qr.asInstanceOf[Queryable[Q, Seq[?]]], + fileName, + lineNum + ) + val results = res.take(2).toVector + if (results.size == 0) None + else if (results.size == 1) Some(results.head.asInstanceOf[R]) + else { + throw new AssertionError(s"Single row query must return 1 result, not ${results.size}") + } + } + def stream[Q, R](query: Q, fetchSize: Int = -1, queryTimeoutSeconds: Int = -1)( implicit qr: Queryable[Q, Seq[R]], fileName: sourcecode.FileName, diff --git a/scalasql/core/src/Queryable.scala b/scalasql/core/src/Queryable.scala index d3d1880..44cf920 100644 --- a/scalasql/core/src/Queryable.scala +++ b/scalasql/core/src/Queryable.scala @@ -48,6 +48,15 @@ trait Queryable[-Q, R] { */ def isSingleRow(q: Q): Boolean + /** + * Whether this query expects a single row to be returned, if so we can assert on + * the number of rows and raise an error if 2+ rows are present. Returns + * None if 0 rows are present. + * @param q + * @return + */ + def isSingleRowOption(q: Q): Boolean + /** * Converts the given queryable value into a [[SqlStr]], that can then be executed * by the underlying SQL JDBC interface @@ -91,6 +100,7 @@ object Queryable { def isGetGeneratedKeys(q: Q): Option[Queryable.Row[?, ?]] = None def isExecuteUpdate(q: Q): Boolean = false def isSingleRow(q: Q): Boolean = true + def isSingleRowOption(q: Q): Boolean = true def walkLabels(): Seq[List[String]] def walkLabels(q: Q): Seq[List[String]] = walkLabels() diff --git a/scalasql/query/src/GetGeneratedKeys.scala b/scalasql/query/src/GetGeneratedKeys.scala index 9c7e3da..9ccb0a8 100644 --- a/scalasql/query/src/GetGeneratedKeys.scala +++ b/scalasql/query/src/GetGeneratedKeys.scala @@ -9,6 +9,7 @@ import scalasql.core.{Context, Expr, Queryable, SqlStr, WithSqlExpr} */ trait GetGeneratedKeys[Q, R] extends Query[Seq[R]] { def single: Query.Single[R] = new Query.Single(this) + def singleOption: Query.SingleOption[R] = new Query.SingleOption(this) } object GetGeneratedKeys { @@ -24,6 +25,7 @@ object GetGeneratedKeys { protected def queryWalkLabels(): Seq[List[String]] = Nil protected def queryWalkExprs(): Seq[Expr[?]] = Nil protected override def queryIsSingleRow = false + protected override def queryIsSingleRowOption = false protected override def queryIsExecuteUpdate = true override private[scalasql] def renderSql(ctx: Context): SqlStr = Renderable.renderSql(base)(ctx) diff --git a/scalasql/query/src/Query.scala b/scalasql/query/src/Query.scala index a48f99d..a4889ab 100644 --- a/scalasql/query/src/Query.scala +++ b/scalasql/query/src/Query.scala @@ -11,6 +11,7 @@ trait Query[R] extends Renderable { protected def queryWalkLabels(): Seq[List[String]] protected def queryWalkExprs(): Seq[Expr[?]] protected def queryIsSingleRow: Boolean + protected def queryIsSingleRowOption: Boolean protected def queryGetGeneratedKeys: Option[Queryable.Row[?, ?]] = None protected def queryIsExecuteUpdate: Boolean = false @@ -26,6 +27,7 @@ object Query { protected def queryWalkLabels(): Seq[List[String]] = Nil protected def queryWalkExprs(): Seq[Expr[?]] = Nil protected override def queryIsSingleRow = true + protected override def queryIsSingleRowOption = true protected override def queryIsExecuteUpdate = true } @@ -38,6 +40,7 @@ object Query { protected def queryWalkLabels() = query.queryWalkLabels() protected def queryWalkExprs() = query.queryWalkExprs() protected override def queryIsSingleRow = query.queryIsSingleRow + protected override def queryIsSingleRowOption = query.queryIsSingleRowOption protected override def queryIsExecuteUpdate = query.queryIsExecuteUpdate } @@ -49,6 +52,7 @@ object Query { protected def queryWalkLabels() = qr.walkLabels(expr) protected def queryWalkExprs() = qr.walkExprs(expr) protected override def queryIsSingleRow = qr.isSingleRow(expr) + protected override def queryIsSingleRowOption = qr.isSingleRowOption(expr) protected override def queryIsExecuteUpdate = qr.isExecuteUpdate(expr) } @@ -57,6 +61,7 @@ object Query { def walkLabels[R](q: Query[R]) = q.queryWalkLabels() def walkSqlExprs[R](q: Query[R]) = q.queryWalkExprs() def isSingleRow[R](q: Query[R]) = q.queryIsSingleRow + def isSingleRowOption[R](q: Query[R]) = q.queryIsSingleRowOption def construct[R](q: Query[R], args: Queryable.ResultSetIterator) = q.queryConstruct(args) /** @@ -70,6 +75,7 @@ object Query { override def walkLabels(q: Q) = q.queryWalkLabels() override def walkExprs(q: Q) = q.queryWalkExprs() override def isSingleRow(q: Q) = q.queryIsSingleRow + override def isSingleRowOption(q: Q) = q.queryIsSingleRowOption def renderSql(q: Q, ctx: Context): SqlStr = q.renderSql(ctx) @@ -86,4 +92,17 @@ object Query { protected override def queryConstruct(args: Queryable.ResultSetIterator): R = query.queryConstruct(args).asInstanceOf[R] } + + /** + * A [[Query]] that wraps another [[Query]] but sets [[queryIsSingleRow]] to `true` + */ + class SingleOption[R](protected val query: Query[Seq[R]]) extends Query.DelegateQuery[R] + { + protected override def queryIsSingleRowOption: Boolean = true + + private[scalasql] def renderSql(ctx: Context): SqlStr = Renderable.renderSql(query)(ctx) + + protected override def queryConstruct(args: Queryable.ResultSetIterator): R = + query.queryConstruct(args).asInstanceOf[R] + } } From 16e5f000ca484655322d098d0080f45586d0ca4f Mon Sep 17 00:00:00 2001 From: Samik R Date: Sun, 15 Sep 2024 23:14:27 +0530 Subject: [PATCH 2/2] formatting revert --- scalasql/core/src/DbApi.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scalasql/core/src/DbApi.scala b/scalasql/core/src/DbApi.scala index 7e9582c..3e2b3a0 100644 --- a/scalasql/core/src/DbApi.scala +++ b/scalasql/core/src/DbApi.scala @@ -214,8 +214,9 @@ object DbApi { s"Single row query must return 1 result, not ${results.size}" ) results.head.asInstanceOf[R] - } else + } else { res.toVector.asInstanceOf[R] + } } }