diff --git a/build.sc b/build.sc index ce5623bd..36011720 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" ) diff --git a/scalasql/query/src/CompoundSelect.scala b/scalasql/query/src/CompoundSelect.scala index b3ce890c..206527e2 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 new file mode 100644 index 00000000..1c34ac3c --- /dev/null +++ b/scalasql/src/dialects/MsSqlDialect.scala @@ -0,0 +1,266 @@ +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.{Sc, operations} +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 + + 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 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) + + override implicit def ExprBlobOpsConv( + v: Expr[geny.Bytes] + ): 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) + + 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 { + 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] = { + 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)" } + } + + 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]] = { + 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] { + 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" + ) + } + + 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] { + 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) + } + + 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(SqlStr.opt(query.limit) { limit => + sql" FETCH FIRST $limit ROWS ONLY" + }) + + 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 => + 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/src/package.scala b/scalasql/src/package.scala index 7a04ee2e..c018dd20 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 } diff --git a/scalasql/test/resources/mssql-customer-schema.sql b/scalasql/test/resources/mssql-customer-schema.sql new file mode 100644 index 00000000..46ec2b0f --- /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 8b906f39..50cac5fd 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/ExampleTests.scala b/scalasql/test/src/ExampleTests.scala index cecc902e..bf703dff 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/datatypes/OptionalTests.scala b/scalasql/test/src/datatypes/OptionalTests.scala index 065506b2..7d72367e 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( @@ -407,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 """ @@ -423,6 +436,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( @@ -430,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( @@ -444,6 +470,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( @@ -451,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( @@ -472,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( @@ -493,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( @@ -507,6 +562,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( @@ -514,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)) + ) ) ) } diff --git a/scalasql/test/src/dialects/MsSqlDialectTests.scala b/scalasql/test/src/dialects/MsSqlDialectTests.scala new file mode 100644 index 00000000..badccee9 --- /dev/null +++ b/scalasql/test/src/dialects/MsSqlDialectTests.scala @@ -0,0 +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 { + + 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 + """ + ) + } +} diff --git a/scalasql/test/src/example/MsSqlExample.scala b/scalasql/test/src/example/MsSqlExample.scala new file mode 100644 index 00000000..56d3b85d --- /dev/null +++ b/scalasql/test/src/example/MsSqlExample.scala @@ -0,0 +1,83 @@ +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[_]]( + 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.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 + } + + 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")) + } + } +} diff --git a/scalasql/test/src/operations/DbAggOpsTests.scala b/scalasql/test/src/operations/DbAggOpsTests.scala index cd49e6b0..781189d1 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 532a5c81..e23122cb 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") ) @@ -22,20 +26,24 @@ 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 ) 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 ??? ) 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 @@ -62,7 +70,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 +80,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 +90,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/DbMathOpsTests.scala b/scalasql/test/src/operations/DbMathOpsTests.scala index 6212a876..1bd76aec 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( diff --git a/scalasql/test/src/operations/DbNumericOpsTests.scala b/scalasql/test/src/operations/DbNumericOpsTests.scala index 26d05474..6f2c9e32 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) diff --git a/scalasql/test/src/operations/DbStringOpsTests.scala b/scalasql/test/src/operations/DbStringOpsTests.scala index 436207ae..f5bda8ae 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" ) @@ -22,20 +26,24 @@ 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 ) 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 ??? ) 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 ) @@ -73,7 +81,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 +91,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 +101,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/CompoundSelectTests.scala b/scalasql/test/src/query/CompoundSelectTests.scala index 7a984349..21397298 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") ) } diff --git a/scalasql/test/src/query/FlatJoinTests.scala b/scalasql/test/src/query/FlatJoinTests.scala index 455cb6fe..fd5c9d89 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 9ead3168..46c483f2 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), @@ -551,6 +564,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 +616,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/SubQueryTests.scala b/scalasql/test/src/query/SubQueryTests.scala index ee3a762a..b61e2523 100644 --- a/scalasql/test/src/query/SubQueryTests.scala +++ b/scalasql/test/src/query/SubQueryTests.scala @@ -19,7 +19,8 @@ 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 @@ -27,7 +28,18 @@ trait SubQueryTests extends ScalaSqlSuite { ORDER BY price DESC LIMIT ?) 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 +53,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 +91,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 +144,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 +182,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 +316,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 +345,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 +444,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 3091cd28..b6331536 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 diff --git a/scalasql/test/src/query/WithCteTests.scala b/scalasql/test/src/query/WithCteTests.scala index 93c837e1..f4fb13b3 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"), diff --git a/scalasql/test/src/utils/ScalaSqlSuite.scala b/scalasql/test/src/utils/ScalaSqlSuite.scala index 7f84c2bb..7721da4c 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() +}