From ec4d13d3ebdefe86cbeb5c30c6e08651f53d92b3 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Thu, 5 Sep 2024 16:19:25 +0800 Subject: [PATCH 01/18] Add MSSQL deps --- build.sc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.sc b/build.sc index ce5623b..3601172 100644 --- a/build.sc +++ b/build.sc @@ -61,6 +61,8 @@ trait ScalaSql extends Common{ common => ivy"org.postgresql:postgresql:42.6.0", ivy"org.testcontainers:mysql:1.19.1", ivy"mysql:mysql-connector-java:8.0.33", + ivy"org.testcontainers:mssqlserver:1.19.1", + ivy"com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11", ivy"com.zaxxer:HikariCP:5.1.0" ) From 943d9257f3fb8b058edc613069c0593e8cf0e8a2 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Fri, 6 Sep 2024 12:21:26 +0800 Subject: [PATCH 02/18] Add MSSQL dialect --- scalasql/src/dialects/MsSqlDialect.scala | 71 ++++++++++++++++++++++++ scalasql/src/package.scala | 3 + 2 files changed, 74 insertions(+) create mode 100644 scalasql/src/dialects/MsSqlDialect.scala diff --git a/scalasql/src/dialects/MsSqlDialect.scala b/scalasql/src/dialects/MsSqlDialect.scala new file mode 100644 index 0000000..14569fc --- /dev/null +++ b/scalasql/src/dialects/MsSqlDialect.scala @@ -0,0 +1,71 @@ +package scalasql.dialects + +import scalasql.core.{Aggregatable, DbApi, DialectTypeMappers, Expr, TypeMapper} +import scalasql.operations +import scalasql.core.SqlStr.SqlStringSyntax +import scalasql.operations.{ConcatOps, MathOps, TrimOps} + +trait MsSqlDialect extends Dialect { + protected def dialectCastParams = false + + override implicit def IntType: TypeMapper[Int] = new MsSqlIntType + class MsSqlIntType extends IntType { override def castTypeString = "INT" } + + override implicit def StringType: TypeMapper[String] = new MsSqlStringType + class MsSqlStringType extends StringType { override def castTypeString = "VARCHAR" } + + override implicit def ExprStringOpsConv(v: Expr[String]): MsSqlDialect.ExprStringOps[String] = + new MsSqlDialect.ExprStringOps(v) + + override implicit def ExprBlobOpsConv( + v: Expr[geny.Bytes] + ): MsSqlDialect.ExprStringLikeOps[geny.Bytes] = + new MsSqlDialect.ExprStringLikeOps(v) + + implicit def ExprAggOpsConv[T](v: Aggregatable[Expr[T]]): operations.ExprAggOps[T] = + new MsSqlDialect.ExprAggOps(v) + + override implicit def DbApiOpsConv(db: => DbApi): MsSqlDialect.DbApiOps = + new MsSqlDialect.DbApiOps(this) +} + +object MsSqlDialect extends MsSqlDialect { + class DbApiOps(dialect: DialectTypeMappers) + extends scalasql.operations.DbApiOps(dialect) + with ConcatOps + with MathOps + + class ExprAggOps[T](v: Aggregatable[Expr[T]]) extends scalasql.operations.ExprAggOps[T](v) { + def mkString(sep: Expr[String] = null)(implicit tm: TypeMapper[T]): Expr[String] = { + val sepRender = Option(sep).getOrElse(sql"''") + v.aggregateExpr(expr => implicit ctx => sql"STRING_AGG($expr + '', $sepRender)") + } + } + + class ExprStringOps[T](v: Expr[T]) extends ExprStringLikeOps(v) with operations.ExprStringOps[T] + class ExprStringLikeOps[T](protected val v: Expr[T]) + extends operations.ExprStringLikeOps(v) + with TrimOps { + + override def +(x: Expr[T]): Expr[T] = Expr { implicit ctx => sql"($v + $x)" } + + override def startsWith(other: Expr[T]): Expr[Boolean] = Expr { implicit ctx => + sql"($v LIKE $other + '%')" + } + + override def endsWith(other: Expr[T]): Expr[Boolean] = Expr { implicit ctx => + sql"($v LIKE '%' + $other)" + } + + override def contains(other: Expr[T]): Expr[Boolean] = Expr { implicit ctx => + sql"($v LIKE '%' + $other + '%')" + } + + override def length: Expr[Int] = Expr { implicit ctx => sql"LEN($v)" } + + override def octetLength: Expr[Int] = Expr { implicit ctx => sql"DATALENGTH($v)" } + + def indexOf(x: Expr[T]): Expr[Int] = Expr { implicit ctx => sql"CHARINDEX($x, $v)" } + def reverse: Expr[T] = Expr { implicit ctx => sql"REVERSE($v)" } + } +} diff --git a/scalasql/src/package.scala b/scalasql/src/package.scala index 7a04ee2..c018dd2 100644 --- a/scalasql/src/package.scala +++ b/scalasql/src/package.scala @@ -55,4 +55,7 @@ package object scalasql { val SqliteDialect = dialects.SqliteDialect type SqliteDialect = dialects.SqliteDialect + + val MsSqlDialect = dialects.MsSqlDialect + type MsSqlDialect = dialects.MsSqlDialect } From f42dabba985b3c1ec99aa475e3177b438a3d2874 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Fri, 6 Sep 2024 12:21:43 +0800 Subject: [PATCH 03/18] Add MSSQL Example tests --- scalasql/test/src/ExampleTests.scala | 1 + scalasql/test/src/example/MsSqlExample.scala | 74 ++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 scalasql/test/src/example/MsSqlExample.scala diff --git a/scalasql/test/src/ExampleTests.scala b/scalasql/test/src/ExampleTests.scala index cecc902..bf703df 100644 --- a/scalasql/test/src/ExampleTests.scala +++ b/scalasql/test/src/ExampleTests.scala @@ -11,5 +11,6 @@ object ExampleTests extends TestSuite { test("h2") - example.H2Example.main(Array()) test("sqlite") - example.SqliteExample.main(Array()) test("hikari") - example.HikariCpExample.main(Array()) + test("mssql") - example.MsSqlExample.main(Array()) } } diff --git a/scalasql/test/src/example/MsSqlExample.scala b/scalasql/test/src/example/MsSqlExample.scala new file mode 100644 index 0000000..e8a3408 --- /dev/null +++ b/scalasql/test/src/example/MsSqlExample.scala @@ -0,0 +1,74 @@ +package scalasql.example + +import org.testcontainers.containers.MSSQLServerContainer +import scalasql.Table +import scalasql.MsSqlDialect._ + +object MsSqlExample { + case class ExampleProduct[T[_]]( + id: T[Int], + kebabCaseName: T[String], + name: T[String], + price: T[Double] + ) + + object ExampleProduct extends Table[ExampleProduct] + + lazy val mssql = { + println("Initializing MsSql") + val mssql = new MSSQLServerContainer("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04") + mssql.acceptLicense() + mssql.start() + mssql + } + + val dataSource = new com.microsoft.sqlserver.jdbc.SQLServerDataSource + dataSource.setURL(mssql.getJdbcUrl) + dataSource.setUser(mssql.getUsername) + dataSource.setPassword(mssql.getPassword) + + lazy val mssqlClient = new scalasql.DbClient.DataSource( + dataSource, + config = new scalasql.Config {} + ) + + def main(args: Array[String]): Unit = { + mssqlClient.transaction { db => + db.updateRaw(""" + CREATE TABLE example_product ( + id INT PRIMARY KEY IDENTITY(1, 1), + kebab_case_name VARCHAR(256), + name VARCHAR(256), + price DECIMAL(20, 2) + ); + """) + + val inserted = db.run( + ExampleProduct.insert.batched(_.kebabCaseName, _.name, _.price)( + ("face-mask", "Face Mask", 8.88), + ("guitar", "Guitar", 300), + ("socks", "Socks", 3.14), + ("skate-board", "Skate Board", 123.45), + ("camera", "Camera", 1000.00), + ("cookie", "Cookie", 0.10) + ) + ) + + assert(inserted == 6) + + val result = + db.run(ExampleProduct.select.filter(_.price > 10).sortBy(_.price).desc.map(_.name)) + + assert(result == Seq("Camera", "Guitar", "Skate Board")) + + db.run(ExampleProduct.update(_.name === "Cookie").set(_.price := 11.0)) + + db.run(ExampleProduct.delete(_.name === "Guitar")) + + val result2 = + db.run(ExampleProduct.select.filter(_.price > 10).sortBy(_.price).desc.map(_.name)) + + assert(result2 == Seq("Camera", "Skate Board", "Cookie")) + } + } +} From ab34ba24b074bdb46657828b349c4d28112ca70d Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Fri, 6 Sep 2024 16:29:02 +0800 Subject: [PATCH 04/18] Add MSSQL test suite --- scalasql/src/dialects/MsSqlDialect.scala | 21 ++++ .../test/resources/mssql-customer-schema.sql | 100 ++++++++++++++++++ scalasql/test/src/ConcreteTestSuites.scala | 48 ++++++++- .../test/src/dialects/MsSqlDialectTests.scala | 11 ++ scalasql/test/src/utils/ScalaSqlSuite.scala | 13 +++ 5 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 scalasql/test/resources/mssql-customer-schema.sql create mode 100644 scalasql/test/src/dialects/MsSqlDialectTests.scala diff --git a/scalasql/src/dialects/MsSqlDialect.scala b/scalasql/src/dialects/MsSqlDialect.scala index 14569fc..b840e4a 100644 --- a/scalasql/src/dialects/MsSqlDialect.scala +++ b/scalasql/src/dialects/MsSqlDialect.scala @@ -5,6 +5,8 @@ import scalasql.operations import scalasql.core.SqlStr.SqlStringSyntax import scalasql.operations.{ConcatOps, MathOps, TrimOps} +import java.time.{Instant, LocalDateTime, OffsetDateTime} + trait MsSqlDialect extends Dialect { protected def dialectCastParams = false @@ -14,6 +16,25 @@ trait MsSqlDialect extends Dialect { override implicit def StringType: TypeMapper[String] = new MsSqlStringType class MsSqlStringType extends StringType { override def castTypeString = "VARCHAR" } + override implicit def BooleanType: TypeMapper[Boolean] = new BooleanType + class MsSqlBooleanType extends BooleanType { override def castTypeString = "BIT" } + + override implicit def UtilDateType: TypeMapper[java.util.Date] = new MsSqlUtilDateType + class MsSqlUtilDateType extends UtilDateType { override def castTypeString = "DATETIME2" } + + override implicit def LocalDateTimeType: TypeMapper[LocalDateTime] = new MsSqlLocalDateTimeType + class MsSqlLocalDateTimeType extends LocalDateTimeType { + override def castTypeString = "DATETIME2" + } + + override implicit def InstantType: TypeMapper[Instant] = new MsSqlInstantType + class MsSqlInstantType extends InstantType { override def castTypeString = "DATETIME2" } + + override implicit def OffsetDateTimeType: TypeMapper[OffsetDateTime] = new MsSqlOffsetDateTimeType + class MsSqlOffsetDateTimeType extends OffsetDateTimeType { + override def castTypeString = "DATETIMEOFFSET" + } + override implicit def ExprStringOpsConv(v: Expr[String]): MsSqlDialect.ExprStringOps[String] = new MsSqlDialect.ExprStringOps(v) diff --git a/scalasql/test/resources/mssql-customer-schema.sql b/scalasql/test/resources/mssql-customer-schema.sql new file mode 100644 index 0000000..46ec2b0 --- /dev/null +++ b/scalasql/test/resources/mssql-customer-schema.sql @@ -0,0 +1,100 @@ +IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'shipping_info') + ALTER TABLE shipping_info DROP CONSTRAINT IF EXISTS fk_shipping_info_buyer; +IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'purchase') + ALTER TABLE purchase DROP CONSTRAINT IF EXISTS fk_purchase_shipping_info; +IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'purchase') + ALTER TABLE purchase DROP CONSTRAINT IF EXISTS fk_purchase_product; +DROP TABLE IF EXISTS buyer; +DROP TABLE IF EXISTS product; +DROP TABLE IF EXISTS shipping_info; +DROP TABLE IF EXISTS purchase; +DROP TABLE IF EXISTS data_types; +DROP TABLE IF EXISTS non_round_trip_types; +DROP TABLE IF EXISTS opt_cols; +DROP TABLE IF EXISTS nested; +DROP TABLE IF EXISTS enclosing; +DROP TABLE IF EXISTS invoice; +-- DROP SCHEMA IF EXISTS otherschema; + +CREATE TABLE buyer ( + id INT PRIMARY KEY IDENTITY(1, 1), + name VARCHAR(256), + date_of_birth DATE +); + +CREATE TABLE product ( + id INT PRIMARY KEY IDENTITY(1, 1), + kebab_case_name VARCHAR(256), + name VARCHAR(256), + price DECIMAL(20, 2) +); + +CREATE TABLE shipping_info ( + id INT PRIMARY KEY IDENTITY(1, 1), + buyer_id INT, + shipping_date DATE, + CONSTRAINT fk_shipping_info_buyer + FOREIGN KEY(buyer_id) REFERENCES buyer(id) +); + +CREATE TABLE purchase ( + id INT PRIMARY KEY IDENTITY(1, 1), + shipping_info_id INT, + product_id INT, + count INT, + total DECIMAL(20, 2), + CONSTRAINT fk_purchase_shipping_info + FOREIGN KEY(shipping_info_id) REFERENCES shipping_info(id), + CONSTRAINT fk_purchase_product + FOREIGN KEY(product_id) REFERENCES product(id) +); + +CREATE TABLE data_types ( + my_tiny_int TINYINT, + my_small_int SMALLINT, + my_int INT, + my_big_int BIGINT, + my_double FLOAT(53), + my_boolean BIT, + my_local_date DATE, + my_local_time TIME, + my_local_date_time DATETIME2, + my_util_date DATETIMEOFFSET, + my_instant DATETIMEOFFSET, + my_var_binary VARBINARY, + my_uuid UNIQUEIDENTIFIER, + my_enum VARCHAR(256) +-- my_offset_time TIME WITH TIME ZONE, + +); + +CREATE TABLE non_round_trip_types( + my_zoned_date_time DATETIMEOFFSET, + my_offset_date_time DATETIMEOFFSET +); + +CREATE TABLE opt_cols( + my_int INT, + my_int2 INT +); + +CREATE TABLE nested( + foo_id INT, + my_boolean BIT +); + +CREATE TABLE enclosing( + bar_id INT, + my_string VARCHAR(256), + foo_id INT, + my_boolean BIT +); + + +-- CREATE SCHEMA otherschema; + +-- CREATE TABLE otherschema.invoice( +-- id PRIMARY KEY IDENTITY(1, 1), +-- total DECIMAL(20, 2), +-- vendor_name VARCHAR(256) +-- ); diff --git a/scalasql/test/src/ConcreteTestSuites.scala b/scalasql/test/src/ConcreteTestSuites.scala index 8b906f3..50cac5f 100644 --- a/scalasql/test/src/ConcreteTestSuites.scala +++ b/scalasql/test/src/ConcreteTestSuites.scala @@ -35,7 +35,8 @@ import scalasql.dialects.{ MySqlDialectTests, PostgresDialectTests, SqliteDialectTests, - H2DialectTests + H2DialectTests, + MsSqlDialectTests } package postgres { @@ -267,3 +268,48 @@ package h2 { object H2DialectTests extends H2DialectTests } + +package mssql { + + import utils.MsSqlSuite + + object DbApiTests extends DbApiTests with MsSqlSuite + object TransactionTests extends TransactionTests with MsSqlSuite + + object SelectTests extends SelectTests with MsSqlSuite + object JoinTests extends JoinTests with MsSqlSuite + object FlatJoinTests extends FlatJoinTests with MsSqlSuite + object InsertTests extends InsertTests with MsSqlSuite + object UpdateTests extends UpdateTests with MsSqlSuite + object DeleteTests extends DeleteTests with MsSqlSuite + object CompoundSelectTests extends CompoundSelectTests with MsSqlSuite + object UpdateJoinTests extends UpdateJoinTests with MsSqlSuite + object UpdateSubQueryTests extends UpdateSubQueryTests with MsSqlSuite + // object ReturningTests extends ReturningTests with MsSqlSuite + // object OnConflictTests extends OnConflictTests with MsSqlSuite + object ValuesTests extends ValuesTests with MsSqlSuite + // object LateralJoinTests extends LateralJoinTests with MsSqlSuite + object WindowFunctionTests extends WindowFunctionTests with MsSqlSuite + object GetGeneratedKeysTests extends GetGeneratedKeysTests with MsSqlSuite + object SchemaTests extends SchemaTests with MsSqlSuite + + object SubQueryTests extends SubQueryTests with MsSqlSuite + object WithCteTests extends WithCteTests with MsSqlSuite + + object DbApiOpsTests extends DbApiOpsTests with MsSqlSuite + object ExprOpsTests extends ExprOpsTests with MsSqlSuite + object ExprBooleanOpsTests extends ExprBooleanOpsTests with MsSqlSuite + object ExprNumericOpsTests extends ExprNumericOpsTests with MsSqlSuite + object ExprSeqNumericOpsTests extends ExprAggNumericOpsTests with MsSqlSuite + object ExprSeqOpsTests extends ExprAggOpsTests with MsSqlSuite + object ExprStringOpsTests extends ExprStringOpsTests with MsSqlSuite + object ExprBlobOpsTests extends ExprBlobOpsTests with MsSqlSuite + object ExprMathOpsTests extends ExprMathOpsTests with MsSqlSuite + + object DataTypesTests extends datatypes.DataTypesTests with MsSqlSuite + + object OptionalTests extends datatypes.OptionalTests with MsSqlSuite + + object MsSqlDialectTests extends MsSqlDialectTests + +} diff --git a/scalasql/test/src/dialects/MsSqlDialectTests.scala b/scalasql/test/src/dialects/MsSqlDialectTests.scala new file mode 100644 index 0000000..39931da --- /dev/null +++ b/scalasql/test/src/dialects/MsSqlDialectTests.scala @@ -0,0 +1,11 @@ +package scalasql.dialects + +import scalasql._ +import utest._ +import utils.MsSqlSuite + +trait MsSqlDialectTests extends MsSqlSuite { + def description = "Operations specific to working with Microsoft SQL Databases" + + def tests = Tests {} +} diff --git a/scalasql/test/src/utils/ScalaSqlSuite.scala b/scalasql/test/src/utils/ScalaSqlSuite.scala index 7f84c2b..7721da4 100644 --- a/scalasql/test/src/utils/ScalaSqlSuite.scala +++ b/scalasql/test/src/utils/ScalaSqlSuite.scala @@ -83,3 +83,16 @@ trait MySqlSuite extends ScalaSqlSuite with MySqlDialect { checker.reset() } + +trait MsSqlSuite extends ScalaSqlSuite with MsSqlDialect { + val checker = new TestChecker( + scalasql.example.MsSqlExample.mssqlClient, + "mssql-customer-schema.sql", + "customer-data.sql", + getClass.getName, + suiteLine.value, + description + ) + + checker.reset() +} From 9ddd775f3aba8851baaa2556839e4628399b068d Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Fri, 6 Sep 2024 22:36:17 +0800 Subject: [PATCH 05/18] Set MSSQL_COLLATION for UTF-8 --- scalasql/test/src/example/MsSqlExample.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/scalasql/test/src/example/MsSqlExample.scala b/scalasql/test/src/example/MsSqlExample.scala index e8a3408..9879447 100644 --- a/scalasql/test/src/example/MsSqlExample.scala +++ b/scalasql/test/src/example/MsSqlExample.scala @@ -18,6 +18,7 @@ object MsSqlExample { println("Initializing MsSql") val mssql = new MSSQLServerContainer("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04") mssql.acceptLicense() + mssql.addEnv("MSSQL_COLLATION", "Latin1_General_100_CI_AS_SC_UTF8") mssql.start() mssql } From 88830b9916c0f4239026c20295668e5aead631ac Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Sat, 7 Sep 2024 02:10:10 +0800 Subject: [PATCH 06/18] Use + for MS SQL string concatenation in tests --- .../test/src/operations/DbAggOpsTests.scala | 6 ++++-- .../test/src/operations/DbBlobOpsTests.scala | 15 +++++++++++---- .../test/src/operations/DbStringOpsTests.scala | 15 +++++++++++---- scalasql/test/src/query/SelectTests.scala | 18 ++++++++++++++++++ scalasql/test/src/query/WithCteTests.scala | 10 ++++++++++ 5 files changed, 54 insertions(+), 10 deletions(-) diff --git a/scalasql/test/src/operations/DbAggOpsTests.scala b/scalasql/test/src/operations/DbAggOpsTests.scala index cd49e6b..781189d 100644 --- a/scalasql/test/src/operations/DbAggOpsTests.scala +++ b/scalasql/test/src/operations/DbAggOpsTests.scala @@ -100,7 +100,8 @@ trait ExprAggOpsTests extends ScalaSqlSuite { "SELECT STRING_AGG(buyer0.name || '', '') AS res FROM buyer buyer0", "SELECT GROUP_CONCAT(buyer0.name || '', '') AS res FROM buyer buyer0", "SELECT LISTAGG(buyer0.name || '', '') AS res FROM buyer buyer0", - "SELECT GROUP_CONCAT(CONCAT(buyer0.name, '') SEPARATOR '') AS res FROM buyer buyer0" + "SELECT GROUP_CONCAT(CONCAT(buyer0.name, '') SEPARATOR '') AS res FROM buyer buyer0", + "SELECT STRING_AGG(buyer0.name + '', '') AS res FROM buyer buyer0" ), value = "James Bond叉烧包Li Haoyi" ) @@ -112,7 +113,8 @@ trait ExprAggOpsTests extends ScalaSqlSuite { sqls = Seq( "SELECT STRING_AGG(buyer0.name || '', ?) AS res FROM buyer buyer0", "SELECT GROUP_CONCAT(buyer0.name || '', ?) AS res FROM buyer buyer0", - "SELECT GROUP_CONCAT(CONCAT(buyer0.name, '') SEPARATOR ?) AS res FROM buyer buyer0" + "SELECT GROUP_CONCAT(CONCAT(buyer0.name, '') SEPARATOR ?) AS res FROM buyer buyer0", + "SELECT STRING_AGG(buyer0.name + '', ?) AS res FROM buyer buyer0" ), value = "James Bond, 叉烧包, Li Haoyi" ) diff --git a/scalasql/test/src/operations/DbBlobOpsTests.scala b/scalasql/test/src/operations/DbBlobOpsTests.scala index 532a5c8..a1815f1 100644 --- a/scalasql/test/src/operations/DbBlobOpsTests.scala +++ b/scalasql/test/src/operations/DbBlobOpsTests.scala @@ -10,7 +10,11 @@ trait ExprBlobOpsTests extends ScalaSqlSuite { def tests = Tests { test("plus") - checker( query = Expr(Bytes("hello")) + Expr(Bytes("world")), - sqls = Seq("SELECT (? || ?) AS res", "SELECT CONCAT(?, ?) AS res"), + sqls = Seq( + "SELECT (? || ?) AS res", + "SELECT CONCAT(?, ?) AS res", + "SELECT (? + ?) AS res" + ), value = Bytes("helloworld") ) @@ -62,7 +66,8 @@ trait ExprBlobOpsTests extends ScalaSqlSuite { query = Expr(Bytes("Hello")).startsWith(Bytes("Hel")), sqls = Seq( "SELECT (? LIKE ? || '%') AS res", - "SELECT (? LIKE CONCAT(?, '%')) AS res" + "SELECT (? LIKE CONCAT(?, '%')) AS res", + "SELECT (? LIKE ? + '%') AS res" ), value = true ) @@ -71,7 +76,8 @@ trait ExprBlobOpsTests extends ScalaSqlSuite { query = Expr(Bytes("Hello")).endsWith(Bytes("llo")), sqls = Seq( "SELECT (? LIKE '%' || ?) AS res", - "SELECT (? LIKE CONCAT('%', ?)) AS res" + "SELECT (? LIKE CONCAT('%', ?)) AS res", + "SELECT (? LIKE '%' + ?) AS res" ), value = true ) @@ -80,7 +86,8 @@ trait ExprBlobOpsTests extends ScalaSqlSuite { query = Expr(Bytes("Hello")).contains(Bytes("ll")), sqls = Seq( "SELECT (? LIKE '%' || ? || '%') AS res", - "SELECT (? LIKE CONCAT('%', ?, '%')) AS res" + "SELECT (? LIKE CONCAT('%', ?, '%')) AS res", + "SELECT (? LIKE '%' + ? + '%') AS res" ), value = true ) diff --git a/scalasql/test/src/operations/DbStringOpsTests.scala b/scalasql/test/src/operations/DbStringOpsTests.scala index 436207a..0399de8 100644 --- a/scalasql/test/src/operations/DbStringOpsTests.scala +++ b/scalasql/test/src/operations/DbStringOpsTests.scala @@ -10,7 +10,11 @@ trait ExprStringOpsTests extends ScalaSqlSuite { def tests = Tests { test("plus") - checker( query = Expr("hello") + Expr("world"), - sqls = Seq("SELECT (? || ?) AS res", "SELECT CONCAT(?, ?) AS res"), + sqls = Seq( + "SELECT (? || ?) AS res", + "SELECT CONCAT(?, ?) AS res", + "SELECT (? + ?) AS res" + ), value = "helloworld" ) @@ -73,7 +77,8 @@ trait ExprStringOpsTests extends ScalaSqlSuite { query = Expr("Hello").startsWith("Hel"), sqls = Seq( "SELECT (? LIKE ? || '%') AS res", - "SELECT (? LIKE CONCAT(?, '%')) AS res" + "SELECT (? LIKE CONCAT(?, '%')) AS res", + "SELECT (? LIKE ? + '%') AS res" ), value = true ) @@ -82,7 +87,8 @@ trait ExprStringOpsTests extends ScalaSqlSuite { query = Expr("Hello").endsWith("llo"), sqls = Seq( "SELECT (? LIKE '%' || ?) AS res", - "SELECT (? LIKE CONCAT('%', ?)) AS res" + "SELECT (? LIKE CONCAT('%', ?)) AS res", + "SELECT (? LIKE '%' + ?) AS res" ), value = true ) @@ -91,7 +97,8 @@ trait ExprStringOpsTests extends ScalaSqlSuite { query = Expr("Hello").contains("ll"), sqls = Seq( "SELECT (? LIKE '%' || ? || '%') AS res", - "SELECT (? LIKE CONCAT('%', ?, '%')) AS res" + "SELECT (? LIKE CONCAT('%', ?, '%')) AS res", + "SELECT (? LIKE '%' + ? + '%') AS res" ), value = true ) diff --git a/scalasql/test/src/query/SelectTests.scala b/scalasql/test/src/query/SelectTests.scala index 9ead316..b7c2a52 100644 --- a/scalasql/test/src/query/SelectTests.scala +++ b/scalasql/test/src/query/SelectTests.scala @@ -551,6 +551,15 @@ trait SelectTests extends ScalaSqlSuite { WHEN (product0.price <= ?) THEN CONCAT(product0.name, ?) END AS res FROM product product0 + """, + """ + SELECT + CASE + WHEN (product0.price > ?) THEN (product0.name + ?) + WHEN (product0.price > ?) THEN (product0.name + ?) + WHEN (product0.price <= ?) THEN (product0.name + ?) + END AS res + FROM product product0 """ ), value = Seq( @@ -594,6 +603,15 @@ trait SelectTests extends ScalaSqlSuite { ELSE CONCAT(product0.name, ?) END AS res FROM product product0 + """, + """ + SELECT + CASE + WHEN (product0.price > ?) THEN (product0.name + ?) + WHEN (product0.price > ?) THEN (product0.name + ?) + ELSE (product0.name + ?) + END AS res + FROM product product0 """ ), value = Seq( diff --git a/scalasql/test/src/query/WithCteTests.scala b/scalasql/test/src/query/WithCteTests.scala index 93c837e..f4fb13b 100644 --- a/scalasql/test/src/query/WithCteTests.scala +++ b/scalasql/test/src/query/WithCteTests.scala @@ -28,6 +28,11 @@ trait WithCteTests extends ScalaSqlSuite { WITH cte0 (res) AS (SELECT buyer0.name AS res FROM buyer buyer0) SELECT CONCAT(cte0.res, ?) AS res FROM cte0 + """, + """ + WITH cte0 (res) AS (SELECT buyer0.name AS res FROM buyer buyer0) + SELECT (cte0.res + ?) AS res + FROM cte0 """ ), value = Seq("James Bond-suffix", "叉烧包-suffix", "Li Haoyi-suffix"), @@ -85,6 +90,11 @@ trait WithCteTests extends ScalaSqlSuite { WITH cte0 (name) AS (SELECT buyer0.name AS name FROM buyer buyer0) SELECT CONCAT(cte0.name, ?) AS res FROM cte0 + """, + """ + WITH cte0 (name) AS (SELECT buyer0.name AS name FROM buyer buyer0) + SELECT (cte0.name + ?) AS res + FROM cte0 """ ), value = Seq("James Bond-suffix", "叉烧包-suffix", "Li Haoyi-suffix"), From 13c8830c0daae226a5df9c6f5059b43c746a5bff Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Sat, 7 Sep 2024 02:12:59 +0800 Subject: [PATCH 07/18] Use datalength instead of octet_length for MS SQL in tests --- scalasql/test/src/operations/DbBlobOpsTests.scala | 2 +- scalasql/test/src/operations/DbStringOpsTests.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scalasql/test/src/operations/DbBlobOpsTests.scala b/scalasql/test/src/operations/DbBlobOpsTests.scala index a1815f1..838e4f6 100644 --- a/scalasql/test/src/operations/DbBlobOpsTests.scala +++ b/scalasql/test/src/operations/DbBlobOpsTests.scala @@ -32,7 +32,7 @@ trait ExprBlobOpsTests extends ScalaSqlSuite { test("octetLength") - checker( query = Expr(Bytes("叉烧包")).octetLength, - sql = "SELECT OCTET_LENGTH(?) AS res", + sqls = Seq("SELECT OCTET_LENGTH(?) AS res", "SELECT DATALENGTH(?) AS res"), value = 9, moreValues = Seq(6) // Not sure why HsqlExpr returns different value here ??? ) diff --git a/scalasql/test/src/operations/DbStringOpsTests.scala b/scalasql/test/src/operations/DbStringOpsTests.scala index 0399de8..a7db4a2 100644 --- a/scalasql/test/src/operations/DbStringOpsTests.scala +++ b/scalasql/test/src/operations/DbStringOpsTests.scala @@ -32,7 +32,7 @@ trait ExprStringOpsTests extends ScalaSqlSuite { test("octetLength") - checker( query = Expr("叉烧包").octetLength, - sql = "SELECT OCTET_LENGTH(?) AS res", + sqls = Seq("SELECT OCTET_LENGTH(?) AS res", "SELECT DATALENGTH(?) AS res"), value = 9, moreValues = Seq(6) // Not sure why HsqlExpr returns different value here ??? ) From e9ca43f2c2828d28e023175a3960909711e2c671 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Sat, 7 Sep 2024 13:04:28 +0800 Subject: [PATCH 08/18] Fix some MS SQL syntaxes in tests --- scalasql/test/src/operations/DbBlobOpsTests.scala | 8 ++++++-- scalasql/test/src/operations/DbStringOpsTests.scala | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/scalasql/test/src/operations/DbBlobOpsTests.scala b/scalasql/test/src/operations/DbBlobOpsTests.scala index 838e4f6..e23122c 100644 --- a/scalasql/test/src/operations/DbBlobOpsTests.scala +++ b/scalasql/test/src/operations/DbBlobOpsTests.scala @@ -26,7 +26,7 @@ trait ExprBlobOpsTests extends ScalaSqlSuite { test("length") - checker( query = Expr(Bytes("hello")).length, - sql = "SELECT LENGTH(?) AS res", + sqls = Seq("SELECT LENGTH(?) AS res", "SELECT LEN(?) AS res"), value = 5 ) @@ -39,7 +39,11 @@ trait ExprBlobOpsTests extends ScalaSqlSuite { test("position") - checker( query = Expr(Bytes("hello")).indexOf(Bytes("ll")), - sqls = Seq("SELECT POSITION(? IN ?) AS res", "SELECT INSTR(?, ?) AS res"), + sqls = Seq( + "SELECT POSITION(? IN ?) AS res", + "SELECT INSTR(?, ?) AS res", + "SELECT CHARINDEX(?, ?) AS res" + ), value = 3 ) // Not supported by postgres diff --git a/scalasql/test/src/operations/DbStringOpsTests.scala b/scalasql/test/src/operations/DbStringOpsTests.scala index a7db4a2..f5bda8a 100644 --- a/scalasql/test/src/operations/DbStringOpsTests.scala +++ b/scalasql/test/src/operations/DbStringOpsTests.scala @@ -26,7 +26,7 @@ trait ExprStringOpsTests extends ScalaSqlSuite { test("length") - checker( query = Expr("hello").length, - sql = "SELECT LENGTH(?) AS res", + sqls = Seq("SELECT LENGTH(?) AS res", "SELECT LEN(?) AS res"), value = 5 ) @@ -39,7 +39,11 @@ trait ExprStringOpsTests extends ScalaSqlSuite { test("position") - checker( query = Expr("hello").indexOf("ll"), - sqls = Seq("SELECT POSITION(? IN ?) AS res", "SELECT INSTR(?, ?) AS res"), + sqls = Seq( + "SELECT POSITION(? IN ?) AS res", + "SELECT INSTR(?, ?) AS res", + "SELECT CHARINDEX(?, ?) AS res" + ), value = 3 ) From b9e52bc88b4618a427d2474f6510d4f4e5110a5d Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Mon, 9 Sep 2024 01:02:05 +0800 Subject: [PATCH 09/18] Fix some Math functions for MS SQL --- scalasql/src/dialects/MsSqlDialect.scala | 8 +++++++- scalasql/test/src/operations/DbMathOpsTests.scala | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/scalasql/src/dialects/MsSqlDialect.scala b/scalasql/src/dialects/MsSqlDialect.scala index b840e4a..6898d7c 100644 --- a/scalasql/src/dialects/MsSqlDialect.scala +++ b/scalasql/src/dialects/MsSqlDialect.scala @@ -54,7 +54,13 @@ object MsSqlDialect extends MsSqlDialect { class DbApiOps(dialect: DialectTypeMappers) extends scalasql.operations.DbApiOps(dialect) with ConcatOps - with MathOps + with MathOps { + override def ln[T: Numeric](v: Expr[T]): Expr[Double] = Expr { implicit ctx => sql"LOG($v)" } + + override def atan2[T: Numeric](v: Expr[T], y: Expr[T]): Expr[Double] = Expr { implicit ctx => + sql"ATN2($v, $y)" + } + } class ExprAggOps[T](v: Aggregatable[Expr[T]]) extends scalasql.operations.ExprAggOps[T](v) { def mkString(sep: Expr[String] = null)(implicit tm: TypeMapper[T]): Expr[String] = { diff --git a/scalasql/test/src/operations/DbMathOpsTests.scala b/scalasql/test/src/operations/DbMathOpsTests.scala index 6212a87..1bd76ae 100644 --- a/scalasql/test/src/operations/DbMathOpsTests.scala +++ b/scalasql/test/src/operations/DbMathOpsTests.scala @@ -6,7 +6,7 @@ import utest._ trait ExprMathOpsTests extends ScalaSqlSuite { override implicit def DbApiOpsConv(db: => DbApi): DbApiOps & MathOps = ??? - def description = "Math operations; supported by H2/Postgres/MySql, not supported by Sqlite" + def description = "Math operations; supported by H2/Postgres/MySql/MsSql, not supported by Sqlite" def tests = Tests { test("power") - checker( @@ -23,7 +23,7 @@ trait ExprMathOpsTests extends ScalaSqlSuite { test("ln") - checker( query = db.ln(16.0), - sql = "SELECT LN(?) AS res" + sqls = Seq("SELECT LN(?) AS res", "SELECT LOG(?) AS res") ) test("log") - checker( @@ -73,7 +73,7 @@ trait ExprMathOpsTests extends ScalaSqlSuite { test("atan2") - checker( query = db.atan2(16.0, 23.0), - sql = "SELECT ATAN2(?, ?) AS res" + sqls = Seq("SELECT ATAN2(?, ?) AS res", "SELECT ATN2(?, ?) AS res") ) test("pi") - checker( From 98aa1b9d942daaee9f0716040e0976cd8281fde1 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Tue, 10 Sep 2024 11:10:40 +0800 Subject: [PATCH 10/18] Fix ORDER BY with NULL for MS SQL --- scalasql/src/dialects/MsSqlDialect.scala | 124 +++++++++++++++++- .../test/src/datatypes/OptionalTests.scala | 20 +++ 2 files changed, 141 insertions(+), 3 deletions(-) diff --git a/scalasql/src/dialects/MsSqlDialect.scala b/scalasql/src/dialects/MsSqlDialect.scala index 6898d7c..1c49cf4 100644 --- a/scalasql/src/dialects/MsSqlDialect.scala +++ b/scalasql/src/dialects/MsSqlDialect.scala @@ -1,8 +1,9 @@ package scalasql.dialects -import scalasql.core.{Aggregatable, DbApi, DialectTypeMappers, Expr, TypeMapper} -import scalasql.operations -import scalasql.core.SqlStr.SqlStringSyntax +import scalasql.query.{AscDesc, GroupBy, Join, Nulls, OrderBy, SubqueryRef, Table} +import scalasql.core.{Aggregatable, Context, DbApi, DialectTypeMappers, Expr, Queryable, TypeMapper, SqlStr} +import scalasql.{Sc, operations} +import scalasql.core.SqlStr.{Renderable, SqlStringSyntax} import scalasql.operations.{ConcatOps, MathOps, TrimOps} import java.time.{Instant, LocalDateTime, OffsetDateTime} @@ -43,6 +44,9 @@ trait MsSqlDialect extends Dialect { ): MsSqlDialect.ExprStringLikeOps[geny.Bytes] = new MsSqlDialect.ExprStringLikeOps(v) + override implicit def TableOpsConv[V[_[_]]](t: Table[V]): scalasql.dialects.TableOps[V] = + new MsSqlDialect.TableOps(t) + implicit def ExprAggOpsConv[T](v: Aggregatable[Expr[T]]): operations.ExprAggOps[T] = new MsSqlDialect.ExprAggOps(v) @@ -95,4 +99,118 @@ object MsSqlDialect extends MsSqlDialect { def indexOf(x: Expr[T]): Expr[Int] = Expr { implicit ctx => sql"CHARINDEX($x, $v)" } def reverse: Expr[T] = Expr { implicit ctx => sql"REVERSE($v)" } } + + class TableOps[V[_[_]]](t: Table[V]) extends scalasql.dialects.TableOps[V](t) { + + protected override def joinableToSelect: Select[V[Expr], V[Sc]] = { + val ref = Table.ref(t) + new SimpleSelect( + Table.metadata(t).vExpr(ref, dialectSelf).asInstanceOf[V[Expr]], + None, + false, + Seq(ref), + Nil, + Nil, + None + )( + t.containerQr + ) + } + } + + trait Select[Q, R] extends scalasql.query.Select[Q, R] { + override def newCompoundSelect[Q, R]( + lhs: scalasql.query.SimpleSelect[Q, R], + compoundOps: Seq[scalasql.query.CompoundSelect.Op[Q, R]], + orderBy: Seq[OrderBy], + limit: Option[Int], + offset: Option[Int] + )( + implicit qr: Queryable.Row[Q, R], + dialect: scalasql.core.DialectTypeMappers + ): scalasql.query.CompoundSelect[Q, R] = { + new CompoundSelect(lhs, compoundOps, orderBy, limit, offset) + } + + override def newSimpleSelect[Q, R]( + expr: Q, + exprPrefix: Option[Context => SqlStr], + preserveAll: Boolean, + from: Seq[Context.From], + joins: Seq[Join], + where: Seq[Expr[?]], + groupBy0: Option[GroupBy] + )( + implicit qr: Queryable.Row[Q, R], + dialect: scalasql.core.DialectTypeMappers + ): scalasql.query.SimpleSelect[Q, R] = { + new SimpleSelect(expr, exprPrefix, preserveAll, from, joins, where, groupBy0) + } + } + + class SimpleSelect[Q, R]( + expr: Q, + exprPrefix: Option[Context => SqlStr], + preserveAll: Boolean, + from: Seq[Context.From], + joins: Seq[Join], + where: Seq[Expr[?]], + groupBy0: Option[GroupBy] + )(implicit qr: Queryable.Row[Q, R]) + extends scalasql.query.SimpleSelect( + expr, + exprPrefix, + preserveAll, + from, + joins, + where, + groupBy0 + ) + with Select[Q, R] + + class CompoundSelect[Q, R]( + lhs: scalasql.query.SimpleSelect[Q, R], + compoundOps: Seq[scalasql.query.CompoundSelect.Op[Q, R]], + orderBy: Seq[OrderBy], + limit: Option[Int], + offset: Option[Int] + )(implicit qr: Queryable.Row[Q, R]) + extends scalasql.query.CompoundSelect(lhs, compoundOps, orderBy, limit, offset) + with Select[Q, R] { + protected override def selectRenderer(prevContext: Context): SubqueryRef.Wrapped.Renderer = + new CompoundSelectRenderer(this, prevContext) + } + + class CompoundSelectRenderer[Q, R]( + query: scalasql.query.CompoundSelect[Q, R], + prevContext: Context + ) extends scalasql.query.CompoundSelect.Renderer(query, prevContext) { + + override lazy val limitOpt = SqlStr + .flatten(CompoundSelectRendererForceLimit.limitToSqlStr(query.limit, query.offset)) + + override def orderToSqlStr(newCtx: Context) = { + SqlStr.optSeq(query.orderBy) { orderBys => + val orderStr = SqlStr.join( + orderBys.map { orderBy => + val exprStr = Renderable.renderSql(orderBy.expr)(newCtx) + + (orderBy.ascDesc, orderBy.nulls) match { + case (Some(AscDesc.Asc), None | Some(Nulls.First)) => sql"$exprStr ASC" + case (Some(AscDesc.Desc), Some(Nulls.First)) => + sql"IIF($exprStr IS NULL, 0, 1), $exprStr DESC" + case (Some(AscDesc.Asc), Some(Nulls.Last)) => sql"IIF($exprStr IS NULL, 1, 0), $exprStr ASC" + case (Some(AscDesc.Desc), None | Some(Nulls.Last)) => sql"$exprStr DESC" + case (None, None) => exprStr + case (None, Some(Nulls.First)) => sql"IIF($exprStr IS NULL, 0, 1), $exprStr" + case (None, Some(Nulls.Last)) => sql"IIF($exprStr IS NULL, 1, 0), $exprStr" + } + }, + SqlStr.commaSep + ) + + sql" ORDER BY $orderStr" + } + } + } } diff --git a/scalasql/test/src/datatypes/OptionalTests.scala b/scalasql/test/src/datatypes/OptionalTests.scala index 065506b..b98ce0f 100644 --- a/scalasql/test/src/datatypes/OptionalTests.scala +++ b/scalasql/test/src/datatypes/OptionalTests.scala @@ -399,6 +399,11 @@ trait OptionalTests extends ScalaSqlSuite { SELECT opt_cols0.my_int AS my_int, opt_cols0.my_int2 AS my_int2 FROM opt_cols opt_cols0 ORDER BY my_int IS NULL ASC, my_int + """, + """ + SELECT opt_cols0.my_int AS my_int, opt_cols0.my_int2 AS my_int2 + FROM opt_cols opt_cols0 + ORDER BY IIF(my_int IS NULL, 1, 0), my_int """ ), value = Seq( @@ -423,6 +428,11 @@ trait OptionalTests extends ScalaSqlSuite { SELECT opt_cols0.my_int AS my_int, opt_cols0.my_int2 AS my_int2 FROM opt_cols opt_cols0 ORDER BY my_int IS NULL DESC, my_int + """, + """ + SELECT opt_cols0.my_int AS my_int, opt_cols0.my_int2 AS my_int2 + FROM opt_cols opt_cols0 + ORDER BY IIF(my_int IS NULL, 0, 1), my_int """ ), value = Seq( @@ -444,6 +454,11 @@ trait OptionalTests extends ScalaSqlSuite { SELECT opt_cols0.my_int AS my_int, opt_cols0.my_int2 AS my_int2 FROM opt_cols opt_cols0 ORDER BY my_int IS NULL ASC, my_int ASC + """, + """ + SELECT opt_cols0.my_int AS my_int, opt_cols0.my_int2 AS my_int2 + FROM opt_cols opt_cols0 + ORDER BY IIF(my_int IS NULL, 1, 0), my_int ASC """ ), value = Seq( @@ -507,6 +522,11 @@ trait OptionalTests extends ScalaSqlSuite { SELECT opt_cols0.my_int AS my_int, opt_cols0.my_int2 AS my_int2 FROM opt_cols opt_cols0 ORDER BY my_int IS NULL DESC, my_int DESC + """, + """ + SELECT opt_cols0.my_int AS my_int, opt_cols0.my_int2 AS my_int2 + FROM opt_cols opt_cols0 + ORDER BY IIF(my_int IS NULL, 0, 1), my_int DESC """ ), value = Seq( From 3a6af7312e09f9e5e9db93c417c9ed76ccda0845 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Tue, 10 Sep 2024 13:35:17 +0800 Subject: [PATCH 11/18] Add more valid results for ORDER BY with NULL tests --- .../test/src/datatypes/OptionalTests.scala | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/scalasql/test/src/datatypes/OptionalTests.scala b/scalasql/test/src/datatypes/OptionalTests.scala index b98ce0f..7d72367 100644 --- a/scalasql/test/src/datatypes/OptionalTests.scala +++ b/scalasql/test/src/datatypes/OptionalTests.scala @@ -412,6 +412,14 @@ trait OptionalTests extends ScalaSqlSuite { OptCols[Sc](None, None), OptCols[Sc](None, Some(4)) ), + moreValues = Seq( + Seq( + OptCols[Sc](Some(1), Some(2)), + OptCols[Sc](Some(3), None), + OptCols[Sc](None, Some(4)), + OptCols[Sc](None, None) + ) + ), docs = """ `.nullsLast` and `.nullsFirst` translate to SQL `NULLS LAST` and `NULLS FIRST` clauses """ @@ -440,6 +448,14 @@ trait OptionalTests extends ScalaSqlSuite { OptCols[Sc](None, Some(4)), OptCols[Sc](Some(1), Some(2)), OptCols[Sc](Some(3), None) + ), + moreValues = Seq( + Seq( + OptCols[Sc](None, Some(4)), + OptCols[Sc](None, None), + OptCols[Sc](Some(1), Some(2)), + OptCols[Sc](Some(3), None) + ) ) ) test("ascNullsLast") - checker( @@ -466,6 +482,14 @@ trait OptionalTests extends ScalaSqlSuite { OptCols[Sc](Some(3), None), OptCols[Sc](None, None), OptCols[Sc](None, Some(4)) + ), + moreValues = Seq( + Seq( + OptCols[Sc](Some(1), Some(2)), + OptCols[Sc](Some(3), None), + OptCols[Sc](None, Some(4)), + OptCols[Sc](None, None) + ) ) ) test("ascNullsFirst") - checker( @@ -487,6 +511,14 @@ trait OptionalTests extends ScalaSqlSuite { OptCols[Sc](None, Some(4)), OptCols[Sc](Some(1), Some(2)), OptCols[Sc](Some(3), None) + ), + moreValues = Seq( + Seq( + OptCols[Sc](None, None), + OptCols[Sc](None, Some(4)), + OptCols[Sc](Some(1), Some(2)), + OptCols[Sc](Some(3), None) + ) ) ) test("descNullsLast") - checker( @@ -508,6 +540,14 @@ trait OptionalTests extends ScalaSqlSuite { OptCols[Sc](Some(1), Some(2)), OptCols[Sc](None, None), OptCols[Sc](None, Some(4)) + ), + moreValues = Seq( + Seq( + OptCols[Sc](Some(3), None), + OptCols[Sc](Some(1), Some(2)), + OptCols[Sc](None, None), + OptCols[Sc](None, Some(4)) + ) ) ) test("descNullsFirst") - checker( @@ -534,6 +574,14 @@ trait OptionalTests extends ScalaSqlSuite { OptCols[Sc](None, Some(4)), OptCols[Sc](Some(3), None), OptCols[Sc](Some(1), Some(2)) + ), + moreValues = Seq( + Seq( + OptCols[Sc](None, Some(4)), + OptCols[Sc](None, None), + OptCols[Sc](Some(3), None), + OptCols[Sc](Some(1), Some(2)) + ) ) ) } From a24e84e9c9611540be326242d575f107bf995993 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Tue, 10 Sep 2024 15:29:44 +0800 Subject: [PATCH 12/18] Fix LIMIT and OFFSET for MS SQL --- scalasql/query/src/CompoundSelect.scala | 11 +- scalasql/src/dialects/MsSqlDialect.scala | 29 ++- .../test/src/query/CompoundSelectTests.scala | 173 ++++++++++++------ 3 files changed, 157 insertions(+), 56 deletions(-) diff --git a/scalasql/query/src/CompoundSelect.scala b/scalasql/query/src/CompoundSelect.scala index b3ce890..206527e 100644 --- a/scalasql/query/src/CompoundSelect.scala +++ b/scalasql/query/src/CompoundSelect.scala @@ -112,7 +112,7 @@ object CompoundSelect { // columns are duplicates or not, and thus what final set of rows is returned lazy val preserveAll = query.compoundOps.exists(_.op != "UNION ALL") - def render(liveExprs: LiveExprs) = { + protected def prerender(liveExprs: LiveExprs) = { val innerLiveExprs = if (preserveAll) LiveExprs.none else liveExprs.map(_ ++ newReferencedExpressions) @@ -138,7 +138,14 @@ object CompoundSelect { SqlStr.join(compoundStrs) } - lhsStr + compound + sortOpt + limitOpt + offsetOpt + (lhsStr, compound, sortOpt, limitOpt, offsetOpt) + } + + def render(liveExprs: LiveExprs) = { + prerender(liveExprs) match { + case (lhsStr, compound, sortOpt, limitOpt, offsetOpt) => + lhsStr + compound + sortOpt + limitOpt + offsetOpt + } } def orderToSqlStr(newCtx: Context) = CompoundSelect.orderToSqlStr(query.orderBy, newCtx, gap = true) diff --git a/scalasql/src/dialects/MsSqlDialect.scala b/scalasql/src/dialects/MsSqlDialect.scala index 1c49cf4..e1901f6 100644 --- a/scalasql/src/dialects/MsSqlDialect.scala +++ b/scalasql/src/dialects/MsSqlDialect.scala @@ -7,6 +7,7 @@ import scalasql.core.SqlStr.{Renderable, SqlStringSyntax} import scalasql.operations.{ConcatOps, MathOps, TrimOps} import java.time.{Instant, LocalDateTime, OffsetDateTime} +import scalasql.core.LiveExprs trait MsSqlDialect extends Dialect { protected def dialectCastParams = false @@ -166,7 +167,11 @@ object MsSqlDialect extends MsSqlDialect { where, groupBy0 ) - with Select[Q, R] + with Select[Q, R] { + override def take(n: Int): scalasql.query.Select[Q,R] = throw new Exception(".take must follow .sortBy") + + override def drop(n: Int): scalasql.query.Select[Q,R] = throw new Exception(".drop must follow .sortBy") + } class CompoundSelect[Q, R]( lhs: scalasql.query.SimpleSelect[Q, R], @@ -177,6 +182,11 @@ object MsSqlDialect extends MsSqlDialect { )(implicit qr: Queryable.Row[Q, R]) extends scalasql.query.CompoundSelect(lhs, compoundOps, orderBy, limit, offset) with Select[Q, R] { + override def take(n: Int): scalasql.query.Select[Q, R] = copy( + limit = Some(limit.fold(n)(math.min(_, n))), + offset = offset.orElse(Some(0)) + ) + protected override def selectRenderer(prevContext: Context): SubqueryRef.Wrapped.Renderer = new CompoundSelectRenderer(this, prevContext) } @@ -185,9 +195,22 @@ object MsSqlDialect extends MsSqlDialect { query: scalasql.query.CompoundSelect[Q, R], prevContext: Context ) extends scalasql.query.CompoundSelect.Renderer(query, prevContext) { + override lazy val limitOpt = SqlStr.flatten(SqlStr.opt(query.limit) { limit => + sql" FETCH FIRST $limit ROWS ONLY" + }) - override lazy val limitOpt = SqlStr - .flatten(CompoundSelectRendererForceLimit.limitToSqlStr(query.limit, query.offset)) + override lazy val offsetOpt = SqlStr.flatten( + SqlStr.opt(query.offset.orElse(Option.when(query.limit.nonEmpty)(0))) { offset => + sql" OFFSET $offset ROWS" + } + ) + + override def render(liveExprs: LiveExprs): SqlStr = { + prerender(liveExprs) match { + case (lhsStr, compound, sortOpt, limitOpt, offsetOpt) => + lhsStr + compound + sortOpt + offsetOpt + limitOpt + } + } override def orderToSqlStr(newCtx: Context) = { SqlStr.optSeq(query.orderBy) { orderBys => diff --git a/scalasql/test/src/query/CompoundSelectTests.scala b/scalasql/test/src/query/CompoundSelectTests.scala index 7a98434..2139729 100644 --- a/scalasql/test/src/query/CompoundSelectTests.scala +++ b/scalasql/test/src/query/CompoundSelectTests.scala @@ -50,7 +50,10 @@ trait CompoundSelectTests extends ScalaSqlSuite { test("sortLimit") - checker( query = Text { Product.select.sortBy(_.price).map(_.name).take(2) }, - sql = "SELECT product0.name AS res FROM product product0 ORDER BY product0.price LIMIT ?", + sqls = Seq( + "SELECT product0.name AS res FROM product product0 ORDER BY product0.price LIMIT ?", + "SELECT product0.name AS res FROM product product0 ORDER BY product0.price OFFSET ? ROWS FETCH FIRST ? ROWS ONLY" + ), value = Seq("Cookie", "Socks"), docs = """ ScalaSql also supports various combinations of `.take` and `.drop`, translating to SQL @@ -61,14 +64,18 @@ trait CompoundSelectTests extends ScalaSqlSuite { query = Text { Product.select.sortBy(_.price).map(_.name).drop(2) }, sqls = Seq( "SELECT product0.name AS res FROM product product0 ORDER BY product0.price OFFSET ?", - "SELECT product0.name AS res FROM product product0 ORDER BY product0.price LIMIT ? OFFSET ?" + "SELECT product0.name AS res FROM product product0 ORDER BY product0.price LIMIT ? OFFSET ?", + "SELECT product0.name AS res FROM product product0 ORDER BY product0.price OFFSET ? ROWS" ), value = Seq("Face Mask", "Skate Board", "Guitar", "Camera") ) test("sortLimitTwiceHigher") - checker( query = Text { Product.select.sortBy(_.price).map(_.name).take(2).take(3) }, - sql = "SELECT product0.name AS res FROM product product0 ORDER BY product0.price LIMIT ?", + sqls = Seq( + "SELECT product0.name AS res FROM product product0 ORDER BY product0.price LIMIT ?", + "SELECT product0.name AS res FROM product product0 ORDER BY product0.price OFFSET ? ROWS FETCH FIRST ? ROWS ONLY" + ), value = Seq("Cookie", "Socks"), docs = """ Note that `.drop` and `.take` follow Scala collections' semantics, so calling e.g. `.take` @@ -79,48 +86,68 @@ trait CompoundSelectTests extends ScalaSqlSuite { test("sortLimitTwiceLower") - checker( query = Text { Product.select.sortBy(_.price).map(_.name).take(2).take(1) }, - sql = "SELECT product0.name AS res FROM product product0 ORDER BY product0.price LIMIT ?", + sqls = Seq( + "SELECT product0.name AS res FROM product product0 ORDER BY product0.price LIMIT ?", + "SELECT product0.name AS res FROM product product0 ORDER BY product0.price OFFSET ? ROWS FETCH FIRST ? ROWS ONLY" + ), value = Seq("Cookie") ) test("sortLimitOffset") - checker( query = Text { Product.select.sortBy(_.price).map(_.name).drop(2).take(2) }, - sql = + sqls = Seq( "SELECT product0.name AS res FROM product product0 ORDER BY product0.price LIMIT ? OFFSET ?", + "SELECT product0.name AS res FROM product product0 ORDER BY product0.price OFFSET ? ROWS FETCH FIRST ? ROWS ONLY" + ), value = Seq("Face Mask", "Skate Board") ) test("sortLimitOffsetTwice") - checker( query = Text { Product.select.sortBy(_.price).map(_.name).drop(2).drop(2).take(1) }, - sql = + sqls = Seq( "SELECT product0.name AS res FROM product product0 ORDER BY product0.price LIMIT ? OFFSET ?", + "SELECT product0.name AS res FROM product product0 ORDER BY product0.price OFFSET ? ROWS FETCH FIRST ? ROWS ONLY" + ), value = Seq("Guitar") ) test("sortOffsetLimit") - checker( query = Text { Product.select.sortBy(_.price).map(_.name).drop(2).take(2) }, - sql = + sqls = Seq( "SELECT product0.name AS res FROM product product0 ORDER BY product0.price LIMIT ? OFFSET ?", + "SELECT product0.name AS res FROM product product0 ORDER BY product0.price OFFSET ? ROWS FETCH FIRST ? ROWS ONLY" + ), value = Seq("Face Mask", "Skate Board") ) test("sortLimitOffset") - checker( query = Text { Product.select.sortBy(_.price).map(_.name).take(2).drop(1) }, - sql = + sqls = Seq( "SELECT product0.name AS res FROM product product0 ORDER BY product0.price LIMIT ? OFFSET ?", + "SELECT product0.name AS res FROM product product0 ORDER BY product0.price OFFSET ? ROWS FETCH FIRST ? ROWS ONLY" + ), value = Seq("Socks") ) } test("distinct") - checker( query = Text { Purchase.select.sortBy(_.total).desc.take(3).map(_.shippingInfoId).distinct }, - sql = """ - SELECT DISTINCT subquery0.res AS res - FROM (SELECT purchase0.shipping_info_id AS res - FROM purchase purchase0 - ORDER BY purchase0.total DESC - LIMIT ?) subquery0 - """, + sqls = Seq( + """ + SELECT DISTINCT subquery0.res AS res + FROM (SELECT purchase0.shipping_info_id AS res + FROM purchase purchase0 + ORDER BY purchase0.total DESC + LIMIT ?) subquery0 + """, + """ + SELECT DISTINCT subquery0.res AS res + FROM (SELECT purchase0.shipping_info_id AS res + FROM purchase purchase0 + ORDER BY purchase0.total DESC + OFFSET ? ROWS FETCH FIRST ? ROWS ONLY) subquery0 + """ + ), value = Seq(1, 2), normalize = (x: Seq[Int]) => x.sorted, docs = """ @@ -134,15 +161,26 @@ trait CompoundSelectTests extends ScalaSqlSuite { Product.crossJoin().filter(_.id === p.productId).map(_.name) } }, - sql = """ - SELECT product1.name AS res - FROM (SELECT purchase0.product_id AS product_id, purchase0.total AS total - FROM purchase purchase0 - ORDER BY total DESC - LIMIT ?) subquery0 - CROSS JOIN product product1 - WHERE (product1.id = subquery0.product_id) - """, + sqls = Seq( + """ + SELECT product1.name AS res + FROM (SELECT purchase0.product_id AS product_id, purchase0.total AS total + FROM purchase purchase0 + ORDER BY total DESC + LIMIT ?) subquery0 + CROSS JOIN product product1 + WHERE (product1.id = subquery0.product_id) + """, + """ + SELECT product1.name AS res + FROM (SELECT purchase0.product_id AS product_id, purchase0.total AS total + FROM purchase purchase0 + ORDER BY total DESC + OFFSET ? ROWS FETCH FIRST ? ROWS ONLY) subquery0 + CROSS JOIN product product1 + WHERE (product1.id = subquery0.product_id) + """ + ), value = Seq("Camera", "Face Mask", "Guitar"), normalize = (x: Seq[String]) => x.sorted, docs = """ @@ -155,13 +193,22 @@ trait CompoundSelectTests extends ScalaSqlSuite { test("sumBy") - checker( query = Text { Purchase.select.sortBy(_.total).desc.take(3).sumBy(_.total) }, - sql = """ - SELECT SUM(subquery0.total) AS res - FROM (SELECT purchase0.total AS total - FROM purchase purchase0 - ORDER BY total DESC - LIMIT ?) subquery0 - """, + sqls = Seq( + """ + SELECT SUM(subquery0.total) AS res + FROM (SELECT purchase0.total AS total + FROM purchase purchase0 + ORDER BY total DESC + LIMIT ?) subquery0 + """, + """ + SELECT SUM(subquery0.total) AS res + FROM (SELECT purchase0.total AS total + FROM purchase purchase0 + ORDER BY total DESC + OFFSET ? ROWS FETCH FIRST ? ROWS ONLY) subquery0 + """ + ), value = 11788.0, normalize = (x: Double) => x.round.toDouble ) @@ -174,13 +221,22 @@ trait CompoundSelectTests extends ScalaSqlSuite { .take(3) .aggregate(p => (p.sumBy(_.total), p.avgBy(_.total))) }, - sql = """ - SELECT SUM(subquery0.total) AS res_0, AVG(subquery0.total) AS res_1 - FROM (SELECT purchase0.total AS total - FROM purchase purchase0 - ORDER BY total DESC - LIMIT ?) subquery0 - """, + sqls = Seq( + """ + SELECT SUM(subquery0.total) AS res_0, AVG(subquery0.total) AS res_1 + FROM (SELECT purchase0.total AS total + FROM purchase purchase0 + ORDER BY total DESC + LIMIT ?) subquery0 + """, + """ + SELECT SUM(subquery0.total) AS res_0, AVG(subquery0.total) AS res_1 + FROM (SELECT purchase0.total AS total + FROM purchase purchase0 + ORDER BY total DESC + OFFSET ? ROWS FETCH FIRST ? ROWS ONLY) subquery0 + """ + ), value = (11788.0, 3929.0), normalize = (x: (Double, Double)) => (x._1.round.toDouble, x._2.round.toDouble) ) @@ -325,19 +381,34 @@ trait CompoundSelectTests extends ScalaSqlSuite { .drop(4) .take(4) }, - sql = """ - SELECT LOWER(product0.name) AS res - FROM product product0 - UNION ALL - SELECT LOWER(buyer0.name) AS res - FROM buyer buyer0 - UNION - SELECT LOWER(product0.kebab_case_name) AS res - FROM product product0 - ORDER BY res - LIMIT ? - OFFSET ? - """, + sqls = Seq( + """ + SELECT LOWER(product0.name) AS res + FROM product product0 + UNION ALL + SELECT LOWER(buyer0.name) AS res + FROM buyer buyer0 + UNION + SELECT LOWER(product0.kebab_case_name) AS res + FROM product product0 + ORDER BY res + LIMIT ? + OFFSET ? + """, + """ + SELECT LOWER(product0.name) AS res + FROM product product0 + UNION ALL + SELECT LOWER(buyer0.name) AS res + FROM buyer buyer0 + UNION + SELECT LOWER(product0.kebab_case_name) AS res + FROM product product0 + ORDER BY res + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY + """ + ), value = Seq("guitar", "james bond", "li haoyi", "skate board") ) } From 9530c475d30f9edabe05dba515b33fc1c1cd9ad6 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Tue, 10 Sep 2024 16:11:33 +0800 Subject: [PATCH 13/18] Enable .take without .drop for MS SQL Use `SELECT TOP(?) ...` for MS SQL when there's no offset. --- scalasql/src/dialects/MsSqlDialect.scala | 4 +++- .../test/src/dialects/MsSqlDialectTests.scala | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/scalasql/src/dialects/MsSqlDialect.scala b/scalasql/src/dialects/MsSqlDialect.scala index e1901f6..0fb86f8 100644 --- a/scalasql/src/dialects/MsSqlDialect.scala +++ b/scalasql/src/dialects/MsSqlDialect.scala @@ -168,7 +168,9 @@ object MsSqlDialect extends MsSqlDialect { groupBy0 ) with Select[Q, R] { - override def take(n: Int): scalasql.query.Select[Q,R] = throw new Exception(".take must follow .sortBy") + override def take(n: Int): scalasql.query.Select[Q,R] = { + selectWithExprPrefix(true, _ => sql"TOP($n)") + } override def drop(n: Int): scalasql.query.Select[Q,R] = throw new Exception(".drop must follow .sortBy") } diff --git a/scalasql/test/src/dialects/MsSqlDialectTests.scala b/scalasql/test/src/dialects/MsSqlDialectTests.scala index 39931da..badccee 100644 --- a/scalasql/test/src/dialects/MsSqlDialectTests.scala +++ b/scalasql/test/src/dialects/MsSqlDialectTests.scala @@ -1,11 +1,26 @@ package scalasql.dialects import scalasql._ +import sourcecode.Text import utest._ import utils.MsSqlSuite trait MsSqlDialectTests extends MsSqlSuite { def description = "Operations specific to working with Microsoft SQL Databases" - def tests = Tests {} + def tests = Tests { + + test("top") - checker( + query = Buyer.select.take(0), + sql = """ + SELECT TOP(?) buyer0.id AS id, buyer0.name AS name, buyer0.date_of_birth AS date_of_birth + FROM buyer buyer0 + """, + value = Seq[Buyer[Sc]](), + docs = """ + For ScalaSql's Microsoft SQL dialect provides, the `.take(n)` operator translates + into a SQL `TOP(n)` clause + """ + ) + } } From c2dba92976ad43e0878894d0b2727341a0103ea6 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Tue, 10 Sep 2024 16:45:09 +0800 Subject: [PATCH 14/18] Fix tests involving .take and .drop for MS SQL --- scalasql/test/src/query/FlatJoinTests.scala | 52 ++- scalasql/test/src/query/SelectTests.scala | 31 +- scalasql/test/src/query/SubQueryTests.scala | 297 ++++++++++++------ scalasql/test/src/query/UpdateJoinTests.scala | 23 ++ 4 files changed, 289 insertions(+), 114 deletions(-) diff --git a/scalasql/test/src/query/FlatJoinTests.scala b/scalasql/test/src/query/FlatJoinTests.scala index 455cb6f..fd5c9d8 100644 --- a/scalasql/test/src/query/FlatJoinTests.scala +++ b/scalasql/test/src/query/FlatJoinTests.scala @@ -267,22 +267,42 @@ trait FlatJoinTests extends ScalaSqlSuite { si <- ShippingInfo.select.sortBy(_.id).asc.take(1).crossJoin() } yield (b.name, si.shippingDate) }, - sql = """ - SELECT - subquery0.name AS res_0, - subquery1.shipping_date AS res_1 - FROM - (SELECT buyer0.id AS id, buyer0.name AS name - FROM buyer buyer0 - ORDER BY id ASC - LIMIT ?) subquery0 - CROSS JOIN (SELECT - shipping_info1.id AS id, - shipping_info1.shipping_date AS shipping_date - FROM shipping_info shipping_info1 - ORDER BY id ASC - LIMIT ?) subquery1 - """, + sqls = Seq( + """ + SELECT + subquery0.name AS res_0, + subquery1.shipping_date AS res_1 + FROM + (SELECT buyer0.id AS id, buyer0.name AS name + FROM buyer buyer0 + ORDER BY id ASC + LIMIT ?) subquery0 + CROSS JOIN (SELECT + shipping_info1.id AS id, + shipping_info1.shipping_date AS shipping_date + FROM shipping_info shipping_info1 + ORDER BY id ASC + LIMIT ?) subquery1 + """, + """ + SELECT + subquery0.name AS res_0, + subquery1.shipping_date AS res_1 + FROM + (SELECT buyer0.id AS id, buyer0.name AS name + FROM buyer buyer0 + ORDER BY id ASC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) subquery0 + CROSS JOIN (SELECT + shipping_info1.id AS id, + shipping_info1.shipping_date AS shipping_date + FROM shipping_info shipping_info1 + ORDER BY id ASC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) subquery1 + """ + ), value = Seq( ("James Bond", LocalDate.parse("2010-02-03")) ), diff --git a/scalasql/test/src/query/SelectTests.scala b/scalasql/test/src/query/SelectTests.scala index b7c2a52..46c483f 100644 --- a/scalasql/test/src/query/SelectTests.scala +++ b/scalasql/test/src/query/SelectTests.scala @@ -236,15 +236,28 @@ trait SelectTests extends ScalaSqlSuite { ) ) }, - sql = """ - SELECT - product0.name AS res_0, - (SELECT purchase1.total AS res - FROM purchase purchase1 - WHERE (purchase1.product_id = product0.id) - ORDER BY res DESC - LIMIT ?) AS res_1 - FROM product product0""", + sqls = Seq( + """ + SELECT + product0.name AS res_0, + (SELECT purchase1.total AS res + FROM purchase purchase1 + WHERE (purchase1.product_id = product0.id) + ORDER BY res DESC + LIMIT ?) AS res_1 + FROM product product0 + """, + """ + SELECT + product0.name AS res_0, + (SELECT purchase1.total AS res + FROM purchase purchase1 + WHERE (purchase1.product_id = product0.id) + ORDER BY res DESC + OFFSET ? ROWS FETCH FIRST ? ROWS ONLY) AS res_1 + FROM product product0 + """ + ), value = Seq( ("Face Mask", 888.0), ("Guitar", 900.0), diff --git a/scalasql/test/src/query/SubQueryTests.scala b/scalasql/test/src/query/SubQueryTests.scala index ee3a762..16f3bc0 100644 --- a/scalasql/test/src/query/SubQueryTests.scala +++ b/scalasql/test/src/query/SubQueryTests.scala @@ -19,15 +19,28 @@ trait SubQueryTests extends ScalaSqlSuite { .join(Product.select.sortBy(_.price).desc.take(1))(_.productId `=` _.id) .map { case (purchase, product) => purchase.total } }, - sql = """ + sqls = Seq( + """ SELECT purchase0.total AS res FROM purchase purchase0 JOIN (SELECT product1.id AS id, product1.price AS price FROM product product1 ORDER BY price DESC - LIMIT ?) subquery1 + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) subquery1 ON (purchase0.product_id = subquery1.id) - """, + """, + """ + SELECT purchase0.total AS res + FROM purchase purchase0 + JOIN (SELECT product1.id AS id, product1.price AS price + FROM product product1 + ORDER BY price DESC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) subquery1 + ON (purchase0.product_id = subquery1.id) + """ + ), value = Seq(10000.0), docs = """ A ScalaSql `.join` referencing a `.select` translates straightforwardly @@ -41,14 +54,25 @@ trait SubQueryTests extends ScalaSqlSuite { case (product, purchase) => purchase.total } }, - sql = """ - SELECT purchase1.total AS res - FROM (SELECT product0.id AS id, product0.price AS price - FROM product product0 - ORDER BY price DESC - LIMIT ?) subquery0 - JOIN purchase purchase1 ON (subquery0.id = purchase1.product_id) - """, + sqls = Seq( + """ + SELECT purchase1.total AS res + FROM (SELECT product0.id AS id, product0.price AS price + FROM product product0 + ORDER BY price DESC + LIMIT ?) subquery0 + JOIN purchase purchase1 ON (subquery0.id = purchase1.product_id) + """, + """ + SELECT purchase1.total AS res + FROM (SELECT product0.id AS id, product0.price AS price + FROM product product0 + ORDER BY price DESC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) subquery0 + JOIN purchase purchase1 ON (subquery0.id = purchase1.product_id) + """ + ), value = Seq(10000.0), docs = """ Some sequences of operations cannot be expressed as a single SQL query, @@ -68,25 +92,48 @@ trait SubQueryTests extends ScalaSqlSuite { .join(Purchase.select.sortBy(_.count).desc.take(3))(_.id `=` _.productId) .map { case (product, purchase) => (product.name, purchase.count) } }, - sql = """ - SELECT - subquery0.name AS res_0, - subquery1.count AS res_1 - FROM (SELECT - product0.id AS id, - product0.name AS name, - product0.price AS price - FROM product product0 - ORDER BY price DESC - LIMIT ?) subquery0 - JOIN (SELECT - purchase1.product_id AS product_id, - purchase1.count AS count - FROM purchase purchase1 - ORDER BY count DESC - LIMIT ?) subquery1 - ON (subquery0.id = subquery1.product_id) - """, + sqls = Seq( + """ + SELECT + subquery0.name AS res_0, + subquery1.count AS res_1 + FROM (SELECT + product0.id AS id, + product0.name AS name, + product0.price AS price + FROM product product0 + ORDER BY price DESC + LIMIT ?) subquery0 + JOIN (SELECT + purchase1.product_id AS product_id, + purchase1.count AS count + FROM purchase purchase1 + ORDER BY count DESC + LIMIT ?) subquery1 + ON (subquery0.id = subquery1.product_id) + """, + """ + SELECT + subquery0.name AS res_0, + subquery1.count AS res_1 + FROM (SELECT + product0.id AS id, + product0.name AS name, + product0.price AS price + FROM product product0 + ORDER BY price DESC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) subquery0 + JOIN (SELECT + purchase1.product_id AS product_id, + purchase1.count AS count + FROM purchase purchase1 + ORDER BY count DESC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) subquery1 + ON (subquery0.id = subquery1.product_id) + """ + ), value = Seq(("Camera", 10)), docs = """ This example shows a ScalaSql query that results in a subquery in both @@ -98,17 +145,32 @@ trait SubQueryTests extends ScalaSqlSuite { query = Text { Product.select.sortBy(_.price).desc.take(4).sortBy(_.price).asc.take(2).map(_.name) }, - sql = """ - SELECT subquery0.name AS res - FROM (SELECT - product0.name AS name, - product0.price AS price - FROM product product0 - ORDER BY price DESC - LIMIT ?) subquery0 - ORDER BY subquery0.price ASC - LIMIT ? - """, + sqls = Seq( + """ + SELECT subquery0.name AS res + FROM (SELECT + product0.name AS name, + product0.price AS price + FROM product product0 + ORDER BY price DESC + LIMIT ?) subquery0 + ORDER BY subquery0.price ASC + LIMIT ? + """, + """ + SELECT subquery0.name AS res + FROM (SELECT + product0.name AS name, + product0.price AS price + FROM product product0 + ORDER BY price DESC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) subquery0 + ORDER BY subquery0.price ASC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY + """ + ), value = Seq("Face Mask", "Skate Board"), docs = """ Performing multiple sorts with `.take`s in between is also something @@ -121,17 +183,31 @@ trait SubQueryTests extends ScalaSqlSuite { query = Text { Purchase.select.sortBy(_.count).take(5).groupBy(_.productId)(_.sumBy(_.total)) }, - sql = """ - SELECT subquery0.product_id AS res_0, SUM(subquery0.total) AS res_1 - FROM (SELECT - purchase0.product_id AS product_id, - purchase0.count AS count, - purchase0.total AS total - FROM purchase purchase0 - ORDER BY count - LIMIT ?) subquery0 - GROUP BY subquery0.product_id - """, + sqls = Seq( + """ + SELECT subquery0.product_id AS res_0, SUM(subquery0.total) AS res_1 + FROM (SELECT + purchase0.product_id AS product_id, + purchase0.count AS count, + purchase0.total AS total + FROM purchase purchase0 + ORDER BY count + LIMIT ?) subquery0 + GROUP BY subquery0.product_id + """, + """ + SELECT subquery0.product_id AS res_0, SUM(subquery0.total) AS res_1 + FROM (SELECT + purchase0.product_id AS product_id, + purchase0.count AS count, + purchase0.total AS total + FROM purchase purchase0 + ORDER BY count + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) subquery0 + GROUP BY subquery0.product_id + """ + ), value = Seq((1, 44.4), (2, 900.0), (3, 15.7), (4, 493.8), (5, 10000.0)), normalize = (x: Seq[(Int, Double)]) => x.sorted ) @@ -241,16 +317,25 @@ trait SubQueryTests extends ScalaSqlSuite { .take(2) .unionAll(Product.select.map(_.kebabCaseName.toLowerCase)) }, - sql = """ - SELECT subquery0.res AS res - FROM (SELECT - LOWER(buyer0.name) AS res + sqls = Seq( + """ + SELECT subquery0.res AS res + FROM (SELECT + LOWER(buyer0.name) AS res + FROM buyer buyer0 + LIMIT ?) subquery0 + UNION ALL + SELECT LOWER(product0.kebab_case_name) AS res + FROM product product0 + """, + """ + SELECT TOP(?) LOWER(buyer0.name) AS res FROM buyer buyer0 - LIMIT ?) subquery0 - UNION ALL - SELECT LOWER(product0.kebab_case_name) AS res - FROM product product0 - """, + UNION ALL + SELECT LOWER(product0.kebab_case_name) AS res + FROM product product0 + """ + ), value = Seq("james bond", "叉烧包", "face-mask", "guitar", "socks", "skate-board", "camera", "cookie") ) @@ -261,16 +346,25 @@ trait SubQueryTests extends ScalaSqlSuite { .map(_.name.toLowerCase) .unionAll(Product.select.map(_.kebabCaseName.toLowerCase).take(2)) }, - sql = """ - SELECT LOWER(buyer0.name) AS res - FROM buyer buyer0 - UNION ALL - SELECT subquery0.res AS res - FROM (SELECT - LOWER(product0.kebab_case_name) AS res + sqls = Seq( + """ + SELECT LOWER(buyer0.name) AS res + FROM buyer buyer0 + UNION ALL + SELECT subquery0.res AS res + FROM (SELECT + LOWER(product0.kebab_case_name) AS res + FROM product product0 + LIMIT ?) subquery0 + """, + """ + SELECT LOWER(buyer0.name) AS res + FROM buyer buyer0 + UNION ALL + SELECT TOP(?) LOWER(product0.kebab_case_name) AS res FROM product product0 - LIMIT ?) subquery0 - """, + """ + ), value = Seq("james bond", "叉烧包", "li haoyi", "face-mask", "guitar") ) @@ -351,26 +445,51 @@ trait SubQueryTests extends ScalaSqlSuite { .toExpr } }, - sql = """ - SELECT - buyer0.name AS res_0, - (SELECT - (SELECT - (SELECT product3.price AS res - FROM product product3 - WHERE (product3.id = purchase2.product_id) + sqls = Seq( + """ + SELECT + buyer0.name AS res_0, + (SELECT + (SELECT + (SELECT product3.price AS res + FROM product product3 + WHERE (product3.id = purchase2.product_id) + ORDER BY res DESC + LIMIT ?) AS res + FROM purchase purchase2 + WHERE (purchase2.shipping_info_id = shipping_info1.id) + ORDER BY res DESC + LIMIT ?) AS res + FROM shipping_info shipping_info1 + WHERE (shipping_info1.buyer_id = buyer0.id) ORDER BY res DESC - LIMIT ?) AS res - FROM purchase purchase2 - WHERE (purchase2.shipping_info_id = shipping_info1.id) - ORDER BY res DESC - LIMIT ?) AS res - FROM shipping_info shipping_info1 - WHERE (shipping_info1.buyer_id = buyer0.id) - ORDER BY res DESC - LIMIT ?) AS res_1 - FROM buyer buyer0 - """, + LIMIT ?) AS res_1 + FROM buyer buyer0 + """, + """ + SELECT + buyer0.name AS res_0, + (SELECT + (SELECT + (SELECT product3.price AS res + FROM product product3 + WHERE (product3.id = purchase2.product_id) + ORDER BY res DESC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) AS res + FROM purchase purchase2 + WHERE (purchase2.shipping_info_id = shipping_info1.id) + ORDER BY res DESC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) AS res + FROM shipping_info shipping_info1 + WHERE (shipping_info1.buyer_id = buyer0.id) + ORDER BY res DESC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) AS res_1 + FROM buyer buyer0 + """ + ), value = Seq( ("James Bond", 1000.0), ("叉烧包", 300.0), diff --git a/scalasql/test/src/query/UpdateJoinTests.scala b/scalasql/test/src/query/UpdateJoinTests.scala index 3091cd2..b633153 100644 --- a/scalasql/test/src/query/UpdateJoinTests.scala +++ b/scalasql/test/src/query/UpdateJoinTests.scala @@ -120,6 +120,18 @@ trait UpdateJoinTests extends ScalaSqlSuite { LIMIT ?) subquery0 ON (buyer.id = subquery0.buyer_id) SET buyer.date_of_birth = subquery0.shipping_date WHERE (buyer.name = ?) + """, + """ + UPDATE buyer SET date_of_birth = subquery0.shipping_date + FROM (SELECT + shipping_info0.id AS id, + shipping_info0.buyer_id AS buyer_id, + shipping_info0.shipping_date AS shipping_date + FROM shipping_info shipping_info0 + ORDER BY id ASC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) subquery0 + WHERE (buyer.id = subquery0.buyer_id) AND (buyer.name = ?) """ ), value = 1, @@ -166,6 +178,17 @@ trait UpdateJoinTests extends ScalaSqlSuite { LIMIT ?) subquery0 ON (buyer.id = subquery0.buyer_id) SET buyer.date_of_birth = ? WHERE (buyer.name = ?) + """, + """ + UPDATE buyer SET date_of_birth = ? + FROM (SELECT + shipping_info0.id AS id, + shipping_info0.buyer_id AS buyer_id + FROM shipping_info shipping_info0 + ORDER BY id ASC + OFFSET ? ROWS + FETCH FIRST ? ROWS ONLY) subquery0 + WHERE (buyer.id = subquery0.buyer_id) AND (buyer.name = ?) """ ), value = 1 From ec7931cefd82a923dcda827b8bce8cb5d14962a1 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Tue, 10 Sep 2024 17:34:59 +0800 Subject: [PATCH 15/18] Fix MS SQL numeric ops --- scalasql/src/dialects/MsSqlDialect.scala | 13 +++++++++ .../src/operations/DbNumericOpsTests.scala | 27 ++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/scalasql/src/dialects/MsSqlDialect.scala b/scalasql/src/dialects/MsSqlDialect.scala index 0fb86f8..623e9da 100644 --- a/scalasql/src/dialects/MsSqlDialect.scala +++ b/scalasql/src/dialects/MsSqlDialect.scala @@ -45,6 +45,10 @@ trait MsSqlDialect extends Dialect { ): MsSqlDialect.ExprStringLikeOps[geny.Bytes] = new MsSqlDialect.ExprStringLikeOps(v) + override implicit def ExprNumericOpsConv[T: Numeric: TypeMapper]( + v: Expr[T] + ): MsSqlDialect.ExprNumericOps[T] = new MsSqlDialect.ExprNumericOps(v) + override implicit def TableOpsConv[V[_[_]]](t: Table[V]): scalasql.dialects.TableOps[V] = new MsSqlDialect.TableOps(t) @@ -101,6 +105,15 @@ object MsSqlDialect extends MsSqlDialect { def reverse: Expr[T] = Expr { implicit ctx => sql"REVERSE($v)" } } + class ExprNumericOps[T: Numeric: TypeMapper](protected val v: Expr[T]) + extends operations.ExprNumericOps[T](v) { + override def %[V: Numeric](x: Expr[V]): Expr[T] = Expr { implicit ctx => sql"$v % $x" } + + override def mod[V: Numeric](x: Expr[V]): Expr[T] = Expr { implicit ctx => sql"$v % $x" } + + override def ceil: Expr[T] = Expr { implicit ctx => sql"CEILING($v)" } + } + class TableOps[V[_[_]]](t: Table[V]) extends scalasql.dialects.TableOps[V](t) { protected override def joinableToSelect: Select[V[Expr], V[Sc]] = { diff --git a/scalasql/test/src/operations/DbNumericOpsTests.scala b/scalasql/test/src/operations/DbNumericOpsTests.scala index 26d0547..6f2c9e3 100644 --- a/scalasql/test/src/operations/DbNumericOpsTests.scala +++ b/scalasql/test/src/operations/DbNumericOpsTests.scala @@ -16,7 +16,14 @@ trait ExprNumericOpsTests extends ScalaSqlSuite { test("divide") - checker(query = Expr(6) / Expr(2), sql = "SELECT (? / ?) AS res", value = 3) - test("modulo") - checker(query = Expr(6) % Expr(2), sql = "SELECT MOD(?, ?) AS res", value = 0) + test("modulo") - checker( + query = Expr(6) % Expr(2), + sqls = Seq( + "SELECT MOD(?, ?) AS res", + "SELECT ? % ? AS res" + ), + value = 0 + ) test("bitwiseAnd") - checker( query = Expr(6) & Expr(2), @@ -49,9 +56,23 @@ trait ExprNumericOpsTests extends ScalaSqlSuite { test("abs") - checker(query = Expr(-4).abs, sql = "SELECT ABS(?) AS res", value = 4) - test("mod") - checker(query = Expr(8).mod(Expr(3)), sql = "SELECT MOD(?, ?) AS res", value = 2) + test("mod") - checker( + query = Expr(8).mod(Expr(3)), + sqls = Seq( + "SELECT MOD(?, ?) AS res", + "SELECT ? % ? AS res" + ), + value = 2 + ) - test("ceil") - checker(query = Expr(4.3).ceil, sql = "SELECT CEIL(?) AS res", value = 5.0) + test("ceil") - checker( + query = Expr(4.3).ceil, + sqls = Seq( + "SELECT CEIL(?) AS res", + "SELECT CEILING(?) AS res" + ), + value = 5.0 + ) test("floor") - checker(query = Expr(4.7).floor, sql = "SELECT FLOOR(?) AS res", value = 4.0) From 1850923c394eae2b1562df3fbcf2d1ede2620332 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Thu, 12 Sep 2024 12:10:11 +0800 Subject: [PATCH 16/18] Lint --- scalasql/src/dialects/MsSqlDialect.scala | 36 ++++++++++++++++-------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/scalasql/src/dialects/MsSqlDialect.scala b/scalasql/src/dialects/MsSqlDialect.scala index 623e9da..1c34ac3 100644 --- a/scalasql/src/dialects/MsSqlDialect.scala +++ b/scalasql/src/dialects/MsSqlDialect.scala @@ -1,7 +1,16 @@ package scalasql.dialects import scalasql.query.{AscDesc, GroupBy, Join, Nulls, OrderBy, SubqueryRef, Table} -import scalasql.core.{Aggregatable, Context, DbApi, DialectTypeMappers, Expr, Queryable, TypeMapper, SqlStr} +import scalasql.core.{ + Aggregatable, + Context, + DbApi, + DialectTypeMappers, + Expr, + Queryable, + TypeMapper, + SqlStr +} import scalasql.{Sc, operations} import scalasql.core.SqlStr.{Renderable, SqlStringSyntax} import scalasql.operations.{ConcatOps, MathOps, TrimOps} @@ -64,12 +73,12 @@ object MsSqlDialect extends MsSqlDialect { extends scalasql.operations.DbApiOps(dialect) with ConcatOps with MathOps { - override def ln[T: Numeric](v: Expr[T]): Expr[Double] = Expr { implicit ctx => sql"LOG($v)" } + override def ln[T: Numeric](v: Expr[T]): Expr[Double] = Expr { implicit ctx => sql"LOG($v)" } - override def atan2[T: Numeric](v: Expr[T], y: Expr[T]): Expr[Double] = Expr { implicit ctx => - sql"ATN2($v, $y)" - } - } + override def atan2[T: Numeric](v: Expr[T], y: Expr[T]): Expr[Double] = Expr { implicit ctx => + sql"ATN2($v, $y)" + } + } class ExprAggOps[T](v: Aggregatable[Expr[T]]) extends scalasql.operations.ExprAggOps[T](v) { def mkString(sep: Expr[String] = null)(implicit tm: TypeMapper[T]): Expr[String] = { @@ -181,12 +190,14 @@ object MsSqlDialect extends MsSqlDialect { groupBy0 ) with Select[Q, R] { - override def take(n: Int): scalasql.query.Select[Q,R] = { - selectWithExprPrefix(true, _ => sql"TOP($n)") - } + override def take(n: Int): scalasql.query.Select[Q, R] = { + selectWithExprPrefix(true, _ => sql"TOP($n)") + } - override def drop(n: Int): scalasql.query.Select[Q,R] = throw new Exception(".drop must follow .sortBy") - } + override def drop(n: Int): scalasql.query.Select[Q, R] = throw new Exception( + ".drop must follow .sortBy" + ) + } class CompoundSelect[Q, R]( lhs: scalasql.query.SimpleSelect[Q, R], @@ -237,7 +248,8 @@ object MsSqlDialect extends MsSqlDialect { case (Some(AscDesc.Asc), None | Some(Nulls.First)) => sql"$exprStr ASC" case (Some(AscDesc.Desc), Some(Nulls.First)) => sql"IIF($exprStr IS NULL, 0, 1), $exprStr DESC" - case (Some(AscDesc.Asc), Some(Nulls.Last)) => sql"IIF($exprStr IS NULL, 1, 0), $exprStr ASC" + case (Some(AscDesc.Asc), Some(Nulls.Last)) => + sql"IIF($exprStr IS NULL, 1, 0), $exprStr ASC" case (Some(AscDesc.Desc), None | Some(Nulls.Last)) => sql"$exprStr DESC" case (None, None) => exprStr case (None, Some(Nulls.First)) => sql"IIF($exprStr IS NULL, 0, 1), $exprStr" From 5aafe6eacbd8bddd1a46868a2e14e892e40a0b50 Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Thu, 12 Sep 2024 12:12:17 +0800 Subject: [PATCH 17/18] Wait for mssql container startup to complete The built-in LogMessageWaitStrategy doesn't work. --- scalasql/test/src/example/MsSqlExample.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scalasql/test/src/example/MsSqlExample.scala b/scalasql/test/src/example/MsSqlExample.scala index 9879447..56d3b85 100644 --- a/scalasql/test/src/example/MsSqlExample.scala +++ b/scalasql/test/src/example/MsSqlExample.scala @@ -3,6 +3,7 @@ package scalasql.example import org.testcontainers.containers.MSSQLServerContainer import scalasql.Table import scalasql.MsSqlDialect._ +import scala.util.control.Breaks.{break, breakable} object MsSqlExample { case class ExampleProduct[T[_]]( @@ -20,6 +21,13 @@ object MsSqlExample { mssql.acceptLicense() mssql.addEnv("MSSQL_COLLATION", "Latin1_General_100_CI_AS_SC_UTF8") mssql.start() + + breakable { + while (true) { + if (mssql.getLogs().contains("The default collation was successfully changed.")) break() + } + } + mssql } From 7aae613cac4674c3f96e1b8fd476e21e396d636d Mon Sep 17 00:00:00 2001 From: Kien Dang Date: Thu, 12 Sep 2024 13:33:43 +0800 Subject: [PATCH 18/18] Fix wrongly edited test case --- scalasql/test/src/query/SubQueryTests.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scalasql/test/src/query/SubQueryTests.scala b/scalasql/test/src/query/SubQueryTests.scala index 16f3bc0..b61e252 100644 --- a/scalasql/test/src/query/SubQueryTests.scala +++ b/scalasql/test/src/query/SubQueryTests.scala @@ -26,8 +26,7 @@ trait SubQueryTests extends ScalaSqlSuite { JOIN (SELECT product1.id AS id, product1.price AS price FROM product product1 ORDER BY price DESC - OFFSET ? ROWS - FETCH FIRST ? ROWS ONLY) subquery1 + LIMIT ?) subquery1 ON (purchase0.product_id = subquery1.id) """, """