From 25deb2df380d05ebef5c5bb929b6062ac88181fc Mon Sep 17 00:00:00 2001 From: Sakib Hadziavdic Date: Mon, 8 Jan 2024 22:23:47 +0100 Subject: [PATCH] Interpolate 'inline val' as a literal string in query --- squery/src/ba/sake/squery/query.scala | 7 +-- squery/src/ba/sake/squery/sql.scala | 47 +++++++++++++++---- .../ba/sake/squery/write/SqlArgument.scala | 14 ------ .../src/ba/sake/squery/PostgresSuite.scala | 1 - .../test/src/ba/sake/squery/SquerySuite.scala | 42 +++++++++++++---- 5 files changed, 73 insertions(+), 38 deletions(-) delete mode 100644 squery/src/ba/sake/squery/write/SqlArgument.scala diff --git a/squery/src/ba/sake/squery/query.scala b/squery/src/ba/sake/squery/query.scala index acac94c..ac0d4d7 100644 --- a/squery/src/ba/sake/squery/query.scala +++ b/squery/src/ba/sake/squery/query.scala @@ -2,20 +2,17 @@ package ba.sake.squery import java.{sql => jsql} import scala.util.Using - import net.sf.jsqlparser.parser.CCJSqlParserUtil import net.sf.jsqlparser.statement.select.Select import net.sf.jsqlparser.statement.update.Update import net.sf.jsqlparser.statement.delete.Delete import net.sf.jsqlparser.JSQLParserException - import com.typesafe.scalalogging.Logger - -import ba.sake.squery.write.SqlArgument +import ba.sake.squery.DynamicArg case class Query( private[squery] val sqlString: String, - private[squery] val arguments: Seq[SqlArgument[?]] + private[squery] val arguments: Seq[DynamicArg[?]] ) { private val logger = Logger(getClass.getName) diff --git a/squery/src/ba/sake/squery/sql.scala b/squery/src/ba/sake/squery/sql.scala index ca605a1..f806351 100644 --- a/squery/src/ba/sake/squery/sql.scala +++ b/squery/src/ba/sake/squery/sql.scala @@ -1,27 +1,58 @@ package ba.sake.squery -import ba.sake.squery.write.SqlArgument +import scala.compiletime.* +import scala.compiletime.ops.any.* import scala.collection.mutable.ListBuffer +import ba.sake.squery.write.* +import ba.sake.squery.Query -// arg can be a simple value -// or another query -type SqlInterpolatorArg = SqlArgument[?] | Query +// TODO make derived SqlArgument[CaseClass] +// and insert (?, ?) automatically ?? +// or autogenerate it with scalafix, easier + +case class LiteralString(value: String) + +case class DynamicArg[T](value: T)(using val sqlWrite: SqlWrite[T]) + +type LiteralOrDynamicString = LiteralString | DynamicArg[String] + +// strings are treated specially: +// - literal-type strings are just passed through +// - dynamic strings are interpolated with ?, of course +given string2LiteralString[T <: Singleton & String]: Conversion[T, LiteralOrDynamicString] with + transparent inline def apply(value: T): LiteralOrDynamicString = + inline constValue[IsConst[T]] match + case true => LiteralString(value) + case false => DynamicArg(value)(using SqlWrite[String]) + +given sqlWrite2DynamicArg[T: SqlWrite]: Conversion[T, DynamicArg[T]] with + def apply(value: T): DynamicArg[T] = + DynamicArg(value) + +/* +arg can be: +- a literal string https://scala-slick.org/doc/3.2.0/sql.html#splicing-literal-values +- a simple value +- or another query + */ +type SqlInterpolatorArgOrQuery = LiteralString | DynamicArg[?] | Query /** Implementation of `sql""` interpolator. For a query sql"SELECT .. WHERE $a > 5 AND b = 'abc' ", there have to be * `SqlWrite` typeclass instances for types of $a and $b. */ extension (sc: StringContext) { - // TODO implement as a macro, so we get a statically known string literal... !?? ZOMG :OO - def sql(args: SqlInterpolatorArg*): Query = + inline def sql(args: SqlInterpolatorArgOrQuery*): Query = val stringPartsIter = sc.parts.iterator val argsIter = args.iterator var sb = StringBuilder(stringPartsIter.next()) - val allArgs = ListBuffer.empty[SqlArgument[?]] + val allArgs = ListBuffer.empty[DynamicArg[?]] while stringPartsIter.hasNext do { argsIter.next() match - case sqlArg: SqlArgument[?] => + case literalString: LiteralString => + sb.append(literalString.value) + case sqlArg: DynamicArg[?] => sb.append("?") allArgs += sqlArg case nestedQuery: Query => diff --git a/squery/src/ba/sake/squery/write/SqlArgument.scala b/squery/src/ba/sake/squery/write/SqlArgument.scala deleted file mode 100644 index 6b9a32c..0000000 --- a/squery/src/ba/sake/squery/write/SqlArgument.scala +++ /dev/null @@ -1,14 +0,0 @@ -package ba.sake.squery.write - -import ba.sake.squery.Query - -case class SqlArgument[T]( - value: T -)(using val sqlWrite: SqlWrite[T]) - -object SqlArgument { - - given writeable2sqlArgument[T: SqlWrite]: Conversion[T, SqlArgument[T]] with { - def apply(value: T): SqlArgument[T] = new SqlArgument(value) - } -} diff --git a/squery/test/src/ba/sake/squery/PostgresSuite.scala b/squery/test/src/ba/sake/squery/PostgresSuite.scala index 2a68462..62bc646 100644 --- a/squery/test/src/ba/sake/squery/PostgresSuite.scala +++ b/squery/test/src/ba/sake/squery/PostgresSuite.scala @@ -5,7 +5,6 @@ import java.time.Instant import java.time.temporal.ChronoUnit import scala.collection.decorators._ import org.testcontainers.containers.PostgreSQLContainer -import ba.sake.squery.write.SqlArgument class PostgresSuite extends munit.FunSuite { diff --git a/squery/test/src/ba/sake/squery/SquerySuite.scala b/squery/test/src/ba/sake/squery/SquerySuite.scala index 4a1c6c3..e7c1e72 100644 --- a/squery/test/src/ba/sake/squery/SquerySuite.scala +++ b/squery/test/src/ba/sake/squery/SquerySuite.scala @@ -4,27 +4,34 @@ import java.util.UUID import java.time.Instant import java.time.temporal.ChronoUnit import org.testcontainers.containers.PostgreSQLContainer -import ba.sake.squery.write.SqlArgument +import ba.sake.squery.DynamicArg class SquerySuite extends munit.FunSuite { - test("Query concat") { + test("Interpolate literal/constant in query") { + inline val columns = "id, name" + val q = sql"""SELECT ${columns} FROM customers""" - val p1 = "a_customer" - val q1 = sql"""SELECT id FROM customers WHERE name = $p1""" + assertEquals( + q.sqlString, + """SELECT id, name FROM customers""" + ) + assertEquals(q.arguments, Seq()) + } + test("Interpolate value in query") { + val p1 = "a_customer" val p2 = "a_customer2" - val q2 = sql"""OR name = ${p2}""" + val q = sql"""SELECT id FROM customers WHERE name IN ($p1, $p2)""" - val q = q1 ++ q2 assertEquals( q.sqlString, - """SELECT id FROM customers WHERE name = ? OR name = ?""" + """SELECT id FROM customers WHERE name IN (?, ?)""" ) - assertEquals(q.arguments, Seq(p1, p2).map(SqlArgument(_))) + assertEquals(q.arguments, Seq(p1, p2).map(DynamicArg.apply)) } - test("Query in query") { + test("Interpolate query in query") { val likeArg = "%Bob%" val queryWhere = sql"WHERE name ILIKE ${likeArg}" @@ -35,7 +42,22 @@ class SquerySuite extends munit.FunSuite { q.sqlString, """SELECT id FROM customers WHERE name ILIKE ? LIMIT ?""" ) - assertEquals(q.arguments, Seq(SqlArgument(likeArg), SqlArgument(limitArg))) + assertEquals(q.arguments, Seq(DynamicArg(likeArg), DynamicArg(limitArg))) + } + + test("Query concat ++") { + val p1 = "a_customer" + val q1 = sql"""SELECT id FROM customers WHERE name = $p1""" + + val p2 = "a_customer2" + val q2 = sql"""OR name = ${p2}""" + + val q = q1 ++ q2 + assertEquals( + q.sqlString, + """SELECT id FROM customers WHERE name = ? OR name = ?""" + ) + assertEquals(q.arguments, Seq(p1, p2).map(DynamicArg.apply)) } test("DbAction") {