diff --git a/scalasql/test/src/ConcreteTestSuites.scala b/scalasql/test/src/ConcreteTestSuites.scala index f259c8e4..1bac8996 100644 --- a/scalasql/test/src/ConcreteTestSuites.scala +++ b/scalasql/test/src/ConcreteTestSuites.scala @@ -11,7 +11,8 @@ import operations.{ ExprBlobOpsTests, ExprMathOpsTests, DbCountOpsTests, - DbCountOpsOptionTests + DbCountOpsOptionTests, + DbCountOpsAdvancedTests } import query.{ InsertTests, @@ -85,6 +86,7 @@ package postgres { object ExprMathOpsTests extends ExprMathOpsTests with PostgresSuite object DbCountOpsTests extends DbCountOpsTests with PostgresSuite object DbCountOpsOptionTests extends DbCountOpsOptionTests with PostgresSuite + object DbCountOpsAdvancedTests extends DbCountOpsAdvancedTests with PostgresSuite object DataTypesTests extends datatypes.DataTypesTests with PostgresSuite @@ -136,6 +138,7 @@ package hikari { object ExprMathOpsTests extends ExprMathOpsTests with HikariSuite object DbCountOpsTests extends DbCountOpsTests with HikariSuite object DbCountOpsOptionTests extends DbCountOpsOptionTests with HikariSuite + object DbCountOpsAdvancedTests extends DbCountOpsAdvancedTests with HikariSuite object DataTypesTests extends datatypes.DataTypesTests with HikariSuite @@ -185,6 +188,7 @@ package mysql { object ExprMathOpsTests extends ExprMathOpsTests with MySqlSuite object DbCountOpsTests extends DbCountOpsTests with MySqlSuite object DbCountOpsOptionTests extends DbCountOpsOptionTests with MySqlSuite + object DbCountOpsAdvancedTests extends DbCountOpsAdvancedTests with MySqlSuite // In MySql, schemas are databases and this requires special treatment not yet implemented here // object SchemaTests extends SchemaTests with MySqlSuite object EscapedTableNameTests extends EscapedTableNameTests with MySqlSuite @@ -235,6 +239,7 @@ package sqlite { // object ExprMathOpsTests extends ExprMathOpsTests with SqliteSuite object DbCountOpsTests extends DbCountOpsTests with SqliteSuite object DbCountOpsOptionTests extends DbCountOpsOptionTests with SqliteSuite + object DbCountOpsAdvancedTests extends DbCountOpsAdvancedTests with SqliteSuite // Sqlite doesn't support schemas // object SchemaTests extends SchemaTests with SqliteSuite object EscapedTableNameTests extends EscapedTableNameTests with SqliteSuite @@ -290,6 +295,7 @@ package h2 { object ExprMathOpsTests extends ExprMathOpsTests with H2Suite object DbCountOpsTests extends DbCountOpsTests with H2Suite object DbCountOpsOptionTests extends DbCountOpsOptionTests with H2Suite + object DbCountOpsAdvancedTests extends DbCountOpsAdvancedTests with H2Suite object DataTypesTests extends datatypes.DataTypesTests with H2Suite object OptionalTests extends datatypes.OptionalTests with H2Suite @@ -341,6 +347,7 @@ package mssql { object ExprMathOpsTests extends ExprMathOpsTests with MsSqlSuite object DbCountOpsTests extends DbCountOpsTests with MsSqlSuite object DbCountOpsOptionTests extends DbCountOpsOptionTests with MsSqlSuite + object DbCountOpsAdvancedTests extends DbCountOpsAdvancedTests with MsSqlSuite object DataTypesTests extends datatypes.DataTypesTests with MsSqlSuite diff --git a/scalasql/test/src/operations/DbCountOpsAdvancedTests.scala b/scalasql/test/src/operations/DbCountOpsAdvancedTests.scala index 1d097f82..72596c24 100644 --- a/scalasql/test/src/operations/DbCountOpsAdvancedTests.scala +++ b/scalasql/test/src/operations/DbCountOpsAdvancedTests.scala @@ -3,308 +3,148 @@ package scalasql.operations import scalasql._ import utest._ import utils.ScalaSqlSuite -import java.time.{LocalDate, LocalDateTime} -import java.util.UUID -import scala.math.BigDecimal import sourcecode.Text +import scalasql.datatypes.OptCols trait DbCountOpsAdvancedTests extends ScalaSqlSuite { - def description = "Advanced COUNT operations with complex types and edge cases" - - // Advanced table with complex types for testing corner cases - case class AdvancedData[T[_]]( - id: T[Int], - uuid: T[UUID], - bigDecimalValue: T[BigDecimal], - timestamp: T[LocalDateTime], - date: T[LocalDate], - booleanFlag: T[Boolean], - optionalString: T[Option[String]], - optionalBigDecimal: T[Option[BigDecimal]], - emptyStringField: T[String], - zeroValue: T[Int], - largeNumber: T[Long] - ) - object AdvancedData extends Table[AdvancedData] + def description = "Advanced COUNT operations with edge cases and expressions" def tests = Tests { test("setup") - checker( query = Text { - AdvancedData.insert.batched( - _.id, - _.uuid, - _.bigDecimalValue, - _.timestamp, - _.date, - _.booleanFlag, - _.optionalString, - _.optionalBigDecimal, - _.emptyStringField, - _.zeroValue, - _.largeNumber - )( - ( - 1, - UUID.fromString("123e4567-e89b-12d3-a456-426614174000"), - BigDecimal("999.99"), - LocalDateTime.of(2024, 1, 1, 12, 0), - LocalDate.of(2024, 1, 1), - true, - Some("test"), - Some(BigDecimal("100.50")), - "", - 0, - 999999999999L - ), - ( - 2, - UUID.fromString("123e4567-e89b-12d3-a456-426614174001"), - BigDecimal("0.01"), - LocalDateTime.of(2024, 1, 2, 13, 30), - LocalDate.of(2024, 1, 2), - false, - None, - None, - "", - 0, - 888888888888L - ), - ( - 3, - UUID.fromString("123e4567-e89b-12d3-a456-426614174002"), - BigDecimal("1000000.00"), - LocalDateTime.of(2024, 1, 3, 14, 45), - LocalDate.of(2024, 1, 3), - true, - Some(""), - Some(BigDecimal("0.0001")), - "not empty", - 1, - 777777777777L - ), - ( - 4, - UUID.fromString("123e4567-e89b-12d3-a456-426614174003"), - BigDecimal("0.00"), - LocalDateTime.of(2024, 1, 4, 15, 0), - LocalDate.of(2024, 1, 4), - false, - Some("duplicate"), - None, - "", - 1, - 666666666666L - ), - ( - 5, - UUID.fromString("123e4567-e89b-12d3-a456-426614174004"), - BigDecimal("0.00"), - LocalDateTime.of(2024, 1, 5, 16, 15), - LocalDate.of(2024, 1, 5), - true, - Some("duplicate"), - Some(BigDecimal("0.00")), - "not empty", - 2, - 555555555555L - ) + OptCols.insert.batched(_.myInt, _.myInt2)( + (Some(1), Some(1)), + (Some(2), None), + (Some(3), Some(3)), + (None, Some(4)), + (Some(5), Some(5)) ) }, value = 5 ) - test("countComplexTypes") - { - test("uuidCount") - checker( - query = Text { AdvancedData.select.countBy(_.uuid) }, - sql = "SELECT COUNT(advanced_data0.uuid) AS res FROM advanced_data advanced_data0", - value = 5 + test("countWithNulls") - { + test("nonNullCount") - checker( + query = Text { OptCols.select.countBy(_.myInt) }, + sql = "SELECT COUNT(opt_cols0.my_int) AS res FROM opt_cols opt_cols0", + value = 4 // NULLs are not counted ) - test("uuidCountDistinct") - checker( - query = Text { AdvancedData.select.countDistinctBy(_.uuid) }, - sql = "SELECT COUNT(DISTINCT advanced_data0.uuid) AS res FROM advanced_data advanced_data0", - value = 5 // All UUIDs are unique + test("nonNullCountDistinct") - checker( + query = Text { OptCols.select.countDistinctBy(_.myInt) }, + sql = "SELECT COUNT(DISTINCT opt_cols0.my_int) AS res FROM opt_cols opt_cols0", + value = 4 // All non-NULL values are unique ) - test("bigDecimalCount") - checker( - query = Text { AdvancedData.select.countBy(_.bigDecimalValue) }, - sql = - "SELECT COUNT(advanced_data0.big_decimal_value) AS res FROM advanced_data advanced_data0", - value = 5 - ) - - test("bigDecimalCountDistinct") - checker( - query = Text { AdvancedData.select.countDistinctBy(_.bigDecimalValue) }, - sql = - "SELECT COUNT(DISTINCT advanced_data0.big_decimal_value) AS res FROM advanced_data advanced_data0", - value = 4 // Two 0.00 values are duplicates + test("secondColumnCount") - checker( + query = Text { OptCols.select.countBy(_.myInt2) }, + sql = "SELECT COUNT(opt_cols0.my_int2) AS res FROM opt_cols opt_cols0", + value = 4 // NULLs are not counted ) - test("dateTimeCount") - checker( - query = Text { AdvancedData.select.countBy(_.timestamp) }, - sql = "SELECT COUNT(advanced_data0.timestamp) AS res FROM advanced_data advanced_data0", - value = 5 - ) - - test("dateCount") - checker( - query = Text { AdvancedData.select.countDistinctBy(_.date) }, - sql = "SELECT COUNT(DISTINCT advanced_data0.date) AS res FROM advanced_data advanced_data0", - value = 5 // All dates are unique + test("secondColumnCountDistinct") - checker( + query = Text { OptCols.select.countDistinctBy(_.myInt2) }, + sql = "SELECT COUNT(DISTINCT opt_cols0.my_int2) AS res FROM opt_cols opt_cols0", + value = 4 // All non-NULL values are unique ) } - test("countWithBooleanExpressions") - { - test("booleanColumnCount") - checker( - query = Text { AdvancedData.select.countBy(_.booleanFlag) }, - sql = "SELECT COUNT(advanced_data0.boolean_flag) AS res FROM advanced_data advanced_data0", - value = 5 - ) - - test("booleanColumnCountDistinct") - checker( - query = Text { AdvancedData.select.countDistinctBy(_.booleanFlag) }, - sql = - "SELECT COUNT(DISTINCT advanced_data0.boolean_flag) AS res FROM advanced_data advanced_data0", - value = 2 // true and false - ) - } - - test("countWithEmptyStringsAndZeros") - { - test("emptyStringCount") - checker( - query = Text { AdvancedData.select.countBy(_.emptyStringField) }, - sql = - "SELECT COUNT(advanced_data0.empty_string_field) AS res FROM advanced_data advanced_data0", - value = 5 // Empty strings are counted (not NULL) - ) - - test("emptyStringCountDistinct") - checker( - query = Text { AdvancedData.select.countDistinctBy(_.emptyStringField) }, - sql = - "SELECT COUNT(DISTINCT advanced_data0.empty_string_field) AS res FROM advanced_data advanced_data0", - value = 2 // Empty string and "not empty" - ) - - test("zeroValueCount") - checker( - query = Text { AdvancedData.select.countBy(_.zeroValue) }, - sql = "SELECT COUNT(advanced_data0.zero_value) AS res FROM advanced_data advanced_data0", - value = 5 // Zero values are counted (not NULL) + test("countWithExpressions") - { + test("countArithmeticExpressions") - checker( + query = Text { Purchase.select.map(p => p.productId * 2).count }, + sql = "SELECT COUNT((purchase0.product_id * ?)) AS res FROM purchase purchase0", + value = 7 // All rows have non-NULL productId ) - test("zeroValueCountDistinct") - checker( - query = Text { AdvancedData.select.countDistinctBy(_.zeroValue) }, + test("countDistinctArithmeticExpressions") - checker( + query = Text { Purchase.select.map(p => p.productId + p.shippingInfoId).countDistinct }, sql = - "SELECT COUNT(DISTINCT advanced_data0.zero_value) AS res FROM advanced_data advanced_data0", - value = 3 // 0, 1, 2 + "SELECT COUNT(DISTINCT (purchase0.product_id + purchase0.shipping_info_id)) AS res FROM purchase purchase0", + value = 6 // All combinations are unique ) } - test("countWithStringExpressions") - { - test("stringConcatCount") - checker( - query = Text { - AdvancedData.select - .filter(_.optionalString.isDefined) - .map(a => a.optionalString.get + "_suffix") - .count - }, + test("countWithModuloOperations") - { + test("moduloCount") - checker( + query = Text { Purchase.select.map(p => p.productId % 2).countDistinct }, sqls = Seq( - """SELECT COUNT((advanced_data0.optional_string || ?)) AS res - FROM advanced_data advanced_data0 - WHERE (advanced_data0.optional_string IS NOT NULL)""", - """SELECT COUNT(CONCAT(advanced_data0.optional_string, ?)) AS res - FROM advanced_data advanced_data0 - WHERE (advanced_data0.optional_string IS NOT NULL)""" + "SELECT COUNT(DISTINCT purchase0.product_id % ?) AS res FROM purchase purchase0", + "SELECT COUNT(DISTINCT MOD(purchase0.product_id, ?)) AS res FROM purchase purchase0" ), - value = 4 // Non-null optional strings + value = 2 // 0 and 1 (even and odd) ) - test("stringLengthCount") - checker( - query = Text { AdvancedData.select.map(_.emptyStringField.length).countDistinct }, - sql = - "SELECT COUNT(DISTINCT LENGTH(advanced_data0.empty_string_field)) AS res FROM advanced_data advanced_data0", - value = 2 // Length 0 and length 9 ("not empty") - ) - } - - test("countWithArithmeticExpressions") - { - test("bigDecimalArithmetic") - checker( - query = Text { AdvancedData.select.map(a => a.bigDecimalValue * 2).countDistinct }, - sql = """SELECT COUNT(DISTINCT (advanced_data0.big_decimal_value * ?)) AS res - FROM advanced_data advanced_data0""", - value = 4 // Distinct doubled values - ) - - test("largeNumberModulo") - checker( - query = Text { AdvancedData.select.map(a => a.largeNumber % 1000000).countDistinct }, - sql = """SELECT COUNT(DISTINCT (advanced_data0.large_number % ?)) AS res - FROM advanced_data advanced_data0""", - value = 5 // All different modulo values + test("moduloWithFilter") - checker( + query = + Text { Purchase.select.filter(_.productId > 2).map(p => p.productId % 3).countDistinct }, + sqls = Seq( + "SELECT COUNT(DISTINCT purchase0.product_id % ?) AS res FROM purchase purchase0 WHERE (purchase0.product_id > ?)", + "SELECT COUNT(DISTINCT MOD(purchase0.product_id, ?)) AS res FROM purchase purchase0 WHERE (purchase0.product_id > ?)" + ), + value = 3 // Different modulo values for productId > 2 ) } - test("countWithComplexGroupBy") - { - test("groupByBooleanWithCountUuid") - checker( - query = Text { AdvancedData.select.groupBy(_.booleanFlag)(agg => agg.countBy(_.uuid)) }, - sql = """SELECT advanced_data0.boolean_flag AS res_0, COUNT(advanced_data0.uuid) AS res_1 - FROM advanced_data advanced_data0 - GROUP BY advanced_data0.boolean_flag""", - value = Seq((false, 2), (true, 3)), - normalize = (x: Seq[(Boolean, Int)]) => x.sortBy(_._1) + test("countWithGroupBy") - { + test("groupByWithCount") - checker( + query = Text { Purchase.select.groupBy(_.shippingInfoId)(agg => agg.countBy(_.productId)) }, + sql = """SELECT purchase0.shipping_info_id AS res_0, COUNT(purchase0.product_id) AS res_1 + FROM purchase purchase0 + GROUP BY purchase0.shipping_info_id""", + value = Seq((1, 3), (2, 2), (3, 2)), + normalize = (x: Seq[(Int, Int)]) => x.sortBy(_._1) ) - test("groupByDateWithCountBigDecimal") - checker( - query = - Text { AdvancedData.select.groupBy(_.date)(agg => agg.countBy(_.optionalBigDecimal)) }, + test("groupByWithCountDistinct") - checker( + query = Text { + Purchase.select.groupBy(_.shippingInfoId)(agg => agg.countDistinctBy(_.productId)) + }, sql = - """SELECT advanced_data0.date AS res_0, COUNT(advanced_data0.optional_big_decimal) AS res_1 - FROM advanced_data advanced_data0 - GROUP BY advanced_data0.date""", - value = Seq( - (LocalDate.of(2024, 1, 1), 1), - (LocalDate.of(2024, 1, 2), 0), // NULL optional value - (LocalDate.of(2024, 1, 3), 1), - (LocalDate.of(2024, 1, 4), 0), // NULL optional value - (LocalDate.of(2024, 1, 5), 1) - ), - normalize = (x: Seq[(LocalDate, Int)]) => x.sortBy(_._1) + """SELECT purchase0.shipping_info_id AS res_0, COUNT(DISTINCT purchase0.product_id) AS res_1 + FROM purchase purchase0 + GROUP BY purchase0.shipping_info_id""", + value = Seq((1, 3), (2, 2), (3, 2)), + normalize = (x: Seq[(Int, Int)]) => x.sortBy(_._1) ) } - test("countWithFilter") - { - test("countWithLargeNumbers") - checker( + test("countWithComplexFilters") - { + test("countWithRangeFilter") - checker( query = Text { - AdvancedData.select - .filter(_.largeNumber > 600000000000L) - .countBy(_.largeNumber) + Purchase.select + .filter(p => p.productId >= 2 && p.productId <= 4) + .countBy(_.total) }, - sql = """SELECT COUNT(advanced_data0.large_number) AS res - FROM advanced_data advanced_data0 - WHERE (advanced_data0.large_number > ?)""", - value = 4 // 4 records with large_number > 600000000000L + sql = """SELECT COUNT(purchase0.total) AS res + FROM purchase purchase0 + WHERE ((purchase0.product_id >= ?) AND (purchase0.product_id <= ?))""", + value = 3 // Purchases with productId in [2,4] ) - test("countWithPrecisionDecimals") - checker( + test("countWithDecimalFilter") - checker( query = Text { - AdvancedData.select - .filter(_.bigDecimalValue > BigDecimal("0.01")) - .countDistinctBy(a => (a.bigDecimalValue * 100).floor) + Purchase.select + .filter(_.total > 100) + .countDistinctBy(_.productId) }, - sql = """SELECT COUNT(DISTINCT FLOOR((advanced_data0.big_decimal_value * ?))) AS res - FROM advanced_data advanced_data0 - WHERE (advanced_data0.big_decimal_value > ?)""", - value = 3 // Different floor values for decimal calculations + sql = """SELECT COUNT(DISTINCT purchase0.product_id) AS res + FROM purchase purchase0 + WHERE (purchase0.total > ?)""", + value = 4 // Distinct productIds for purchases > 100 ) } - test("countWithComplexPredicates") - { - test("countWithDateRange") - checker( + test("countWithAdvancedPredicates") - { + test("countWithComplexFilter") - checker( query = Text { - AdvancedData.select - .filter(a => a.date >= LocalDate.of(2024, 1, 2) && a.date <= LocalDate.of(2024, 1, 4)) - .countBy(_.timestamp) + Purchase.select + .filter(p => p.productId > 1 && p.shippingInfoId <= 2) + .countDistinctBy(_.productId) }, - sql = """SELECT COUNT(advanced_data0.timestamp) AS res - FROM advanced_data advanced_data0 - WHERE ((advanced_data0.date >= ?) AND (advanced_data0.date <= ?))""", - value = 3 // Records for Jan 2, 3, 4 + sql = """SELECT COUNT(DISTINCT purchase0.product_id) AS res + FROM purchase purchase0 + WHERE ((purchase0.product_id > ?) AND (purchase0.shipping_info_id <= ?))""", + value = 4 // Distinct productIds matching filter criteria ) } }