diff --git a/Readme.md b/Readme.md index 68ab346..898e15a 100644 --- a/Readme.md +++ b/Readme.md @@ -178,8 +178,6 @@ object Person extends KeyedCrudBase[Int, Person] { override def key: KeyColumnPath = cols.id override def keyOf(value: Person): Int = value.id - - override lazy val tabular: SqlTabular[Person] = summon } println(s"All Persons: ${Person.findAll()}") @@ -189,6 +187,47 @@ Person.update(Person(6, "Franziska")) println(Person.findByKey(6)) // Person(6, Franziska) ``` +### Remarks + +You can not use a directly derived `SqlTabular` in a nested class. The Scala Compiler +may crash with + +``` +assertion failed: missing outer accessor in [Outer class Name] +``` + +You can work around, by defering the initialization of SqlTabular. + +Instead of + +```scala +class Outer { + case class Foo( + // Fields + ) derives SqlTabular + + object Foo extends KeyedCrudBase[Int, Foo] +} +``` + +write + +```scala +class Outer { + case class Foo( + // Fields + ) + + given fooTabular: SqlTabular[Foo] = SqlTabular.derived + + object Foo extends KeyedCrudBase[Int, Foo] +} + +Scala bug is https://github.com/scala/scala3/issues/22704 + +``` + + ## Named Tuples ```scala 3 diff --git a/src/main/scala/usql/dao/Crd.scala b/src/main/scala/usql/dao/Crd.scala index 5149816..becd3ce 100644 --- a/src/main/scala/usql/dao/Crd.scala +++ b/src/main/scala/usql/dao/Crd.scala @@ -48,17 +48,14 @@ trait KeyedCrud[T] extends Crd[T] { } /** Implementation of Crd for Tabular data. */ -abstract class CrdBase[T] extends Crd[T] { +abstract class CrdBase[T](using _tabular: => SqlTabular[T]) extends Crd[T] { protected given pf: RowEncoder[T] = tabular.rowEncoder protected given rd: RowDecoder[T] = tabular.rowDecoder - /** - * Define the referenced tabular, usually implemented using `summon`. We would like to have it as a parameter, but - * this leads to this error https://github.com/scala/scala3/issues/22704 even when using lazy parameters. - */ - lazy val tabular: SqlTabular[T] + /** The tabular instance. */ + def tabular: SqlTabular[T] = _tabular /** Gives access to an aliased view. */ def alias(name: String): Alias[T] = tabular.alias(name) @@ -104,7 +101,9 @@ abstract class CrdBase[T] extends Crd[T] { } /** Implementation of KeyedCrd for KeyedTabular data. */ -abstract class KeyedCrudBase[K, T](using keyDataType: DataType[K]) extends CrdBase[T] with KeyedCrud[T] { +abstract class KeyedCrudBase[K, T](using keyDataType: DataType[K], _tabular: => SqlTabular[T]) + extends CrdBase[T] + with KeyedCrud[T] { override type Key = K diff --git a/src/main/scala/usql/dao/Rep.scala b/src/main/scala/usql/dao/Rep.scala index 0a468f4..8d5f86c 100644 --- a/src/main/scala/usql/dao/Rep.scala +++ b/src/main/scala/usql/dao/Rep.scala @@ -1,8 +1,10 @@ package usql.dao -import usql.dao.Rep.{IsNumber, IsString, SqlRep} -import usql.{DataType, SqlInterpolationParameter, SqlParameters, Sql, UnOption, sql} +import usql.dao.Rep.{IsNumber, IsOrdered, IsString, SqlRep} +import usql.{DataType, Sql, SqlInterpolationParameter, SqlParameters, UnOption, sql} +import java.sql.Timestamp +import java.time.Instant import scala.annotation.unused import scala.language.implicitConversions @@ -18,19 +20,19 @@ trait Rep[T] { SqlRep(sql"${toInterpolationParameter} <> ${rep.toInterpolationParameter}") } - def <(using IsNumber[T])(rep: Rep[T]): Rep[Boolean] = { + def <(using IsOrdered[T])(rep: Rep[T]): Rep[Boolean] = { SqlRep(sql"${toInterpolationParameter} < ${rep.toInterpolationParameter}") } - def >(using IsNumber[T])(rep: Rep[T]): Rep[Boolean] = { + def >(using IsOrdered[T])(rep: Rep[T]): Rep[Boolean] = { SqlRep(sql"${toInterpolationParameter} > ${rep.toInterpolationParameter}") } - def <=(using IsNumber[T])(rep: Rep[T]): Rep[Boolean] = { + def <=(using IsOrdered[T])(rep: Rep[T]): Rep[Boolean] = { SqlRep(sql"${toInterpolationParameter} <= ${rep.toInterpolationParameter}") } - def >=(using IsNumber[T])(rep: Rep[T]): Rep[Boolean] = { + def >=(using IsOrdered[T])(rep: Rep[T]): Rep[Boolean] = { SqlRep(sql"${toInterpolationParameter} >= ${rep.toInterpolationParameter}") } @@ -136,7 +138,16 @@ object Rep { given double: IsNumber[Double] with {} given bigDecimal: IsNumber[BigDecimal] with {} - given opt[T](using IsNumber[T]): IsNumber[Option[T]] with {} + given opt[T: IsNumber]: IsNumber[Option[T]] with {} + } + + trait IsOrdered[T] + object IsOrdered { + given num[T: IsNumber]: IsOrdered[T] with {} + given instant: IsOrdered[Instant] with {} + given string: IsOrdered[String] with {} + given timestamp: IsOrdered[Timestamp] with {} + given opt[T: IsOrdered]: IsOrdered[Option[T]] with {} } /** T is a string SQL type. */ diff --git a/src/test/scala/com/example/example.sc b/src/test/scala/com/example/example.sc index 465fb22..25459b0 100644 --- a/src/test/scala/com/example/example.sc +++ b/src/test/scala/com/example/example.sc @@ -80,8 +80,6 @@ object Person extends KeyedCrudBase[Int, Person] { override def key: KeyColumnPath = cols.id override def keyOf(value: Person): Int = value.id - - override lazy val tabular: SqlTabular[Person] = summon } println(s"All Persons: ${Person.findAll()}") diff --git a/src/test/scala/usql/AutoGeneratedUpdateTest.scala b/src/test/scala/usql/AutoGeneratedUpdateTest.scala index 0b37825..c069f26 100644 --- a/src/test/scala/usql/AutoGeneratedUpdateTest.scala +++ b/src/test/scala/usql/AutoGeneratedUpdateTest.scala @@ -16,12 +16,12 @@ class AutoGeneratedUpdateTest extends TestBaseWithH2 { case class Tenant( id: Int, name: Option[String] - ) derives SqlTabular + ) + + given tenantSqlTabular: SqlTabular[Tenant] = SqlTabular.derived object Tenant extends KeyedCrudBase[Int, Tenant] { override def key: KeyColumnPath = cols.id - - override lazy val tabular: SqlTabular[Tenant] = summon } it should "be possible to insert values" in { diff --git a/src/test/scala/usql/dao/KeyedCrudBaseTest.scala b/src/test/scala/usql/dao/KeyedCrudBaseTest.scala index 47863ed..caac867 100644 --- a/src/test/scala/usql/dao/KeyedCrudBaseTest.scala +++ b/src/test/scala/usql/dao/KeyedCrudBaseTest.scala @@ -23,8 +23,6 @@ class KeyedCrudBaseTest extends TestBaseWithH2 { object UserCrd extends KeyedCrudBase[Int, User] { override def key: KeyColumnPath = cols.id - - override lazy val tabular: SqlTabular[User] = summon } val sample1 = User(1, Some("Alice"), Some(42)) diff --git a/src/test/scala/usql/dao/QueryBuilderTest.scala b/src/test/scala/usql/dao/QueryBuilderTest.scala index b50094c..b89d00d 100644 --- a/src/test/scala/usql/dao/QueryBuilderTest.scala +++ b/src/test/scala/usql/dao/QueryBuilderTest.scala @@ -35,33 +35,33 @@ class QueryBuilderTest extends TestBaseWithH2 { id: Int, name: String, age: Option[Int] = None - ) derives SqlTabular + ) + + given personSqlTabular: SqlTabular[Person] = SqlTabular.derived object Person extends KeyedCrudBase[Int, Person] { override def key: KeyColumnPath = cols.id - - override lazy val tabular: SqlTabular[Person] = summon } case class Permission( id: Int, name: String - ) derives SqlTabular + ) + + given permissionSqlTabular: SqlTabular[Permission] = SqlTabular.derived object Permission extends KeyedCrudBase[Int, Permission] { override def key: KeyColumnPath = cols.id - - override lazy val tabular: SqlTabular[Permission] = summon } case class PersonPermission( personId: Int, permissionId: Int - ) derives SqlTabular + ) - object PersonPermission extends CrdBase[PersonPermission] { - override lazy val tabular: SqlTabular[PersonPermission] = summon - } + given personPermissionSqlTabular: SqlTabular[PersonPermission] = SqlTabular.derived + + object PersonPermission extends CrdBase[PersonPermission] trait EnvWithSamples { val alice = Person(1, "Alice", Some(42)) @@ -214,12 +214,12 @@ class QueryBuilderTest extends TestBaseWithH2 { name: String, @ColumnGroup coordinate: Coordinate, @ColumnGroup secondaryCoordinate: Option[Coordinate] = None - ) derives SqlTabular + ) + + given locationSqlTabular: SqlTabular[Location] = SqlTabular.derived object Location extends KeyedCrudBase[Int, Location] { override def key: KeyColumnPath = cols.id - - override lazy val tabular: SqlTabular[Location] = summon } it should "work with joins on tables with embedded column groups" in new EnvWithSamples { diff --git a/src/test/scala/usql/dao/RepTest.scala b/src/test/scala/usql/dao/RepTest.scala index 0ed8902..0660963 100644 --- a/src/test/scala/usql/dao/RepTest.scala +++ b/src/test/scala/usql/dao/RepTest.scala @@ -20,7 +20,9 @@ class RepTest extends TestBaseWithH2 { price: Int, discount: Option[Int] = None, description: Option[String] = None - ) derives SqlTabular + ) + + given fileItemTabular: SqlTabular[Item] = SqlTabular.derived object Item extends KeyedCrudBase[Int, Item] { override def key: KeyColumnPath = cols.id @@ -43,31 +45,31 @@ class RepTest extends TestBaseWithH2 { ) val cases: Seq[FilterCase] = Seq( - FilterCase("addition", i => i.price + Rep.raw(50) > Rep.raw(150), Seq("Gadget")), - FilterCase("subtraction", i => i.price - i.price === Rep.raw(0), Seq("Widget", "Gadget", "Thingamajig")), - FilterCase("multiplication", i => i.price * Rep.raw(2) > Rep.raw(150), Seq("Widget", "Gadget")), - FilterCase("division", i => i.price / Rep.raw(10) >= Rep.raw(10), Seq("Widget", "Gadget")), - FilterCase("modulo", i => i.price % Rep.raw(100) === Rep.raw(0), Seq("Widget", "Gadget")), - FilterCase("unary negation", i => -i.price < Rep.raw(-50), Seq("Widget", "Gadget")), + FilterCase("addition", i => i.price + 50 > 150, Seq("Gadget")), + FilterCase("subtraction", i => i.price - i.price === 0, Seq("Widget", "Gadget", "Thingamajig")), + FilterCase("multiplication", i => i.price * 2 > 150, Seq("Widget", "Gadget")), + FilterCase("division", i => i.price / 10 >= 10, Seq("Widget", "Gadget")), + FilterCase("modulo", i => i.price % 100 === 0, Seq("Widget", "Gadget")), + FilterCase("unary negation", i => -i.price < -50, Seq("Widget", "Gadget")), FilterCase("LIKE on string", _.name.like("W%"), Seq("Widget")), FilterCase("LIKE on optional string", _.description.like("%gadget%"), Seq("Gadget")), FilterCase("IN clause", _.price.in(Seq(100, 200)), Seq("Widget", "Gadget")), FilterCase("IN clause empty", _.price.in(Seq.empty[Int]), Seq.empty), FilterCase("IN on optional column", _.discount.in(Seq(10)), Seq("Widget")), - FilterCase("BETWEEN", _.price.between(Rep.raw(50), Rep.raw(150)), Seq("Widget", "Thingamajig")), + FilterCase("BETWEEN", _.price.between(50, 150), Seq("Widget", "Thingamajig")), FilterCase( "combined arithmetic and LIKE", - i => i.price * Rep.raw(2) > Rep.raw(100) && i.name.like("G%"), + i => i.price * 2 > 100 && i.name.like("G%"), Seq("Gadget") ), FilterCase( "precedence of chained arithmetic", - i => (i.price + Rep.raw(50)) * Rep.raw(2) > Rep.raw(150), + i => (i.price + 50) * 2 > 150, Seq("Widget", "Gadget", "Thingamajig") ), FilterCase( "precedence of multiply before add", - i => i.price + Rep.raw(50) * Rep.raw(2) > Rep.raw(150), + i => i.price + 50 * 2 > 150, Seq("Widget", "Gadget") ) ) @@ -81,7 +83,7 @@ class RepTest extends TestBaseWithH2 { it should "support string concatenation" in new Env { val results = Item.query - .filter(i => (i.name ++ Rep.raw(" item")).like("%Widget item%")) + .filter(i => (i.name ++ " item").like("%Widget item%")) .map(_.name) .all() results shouldBe Seq("Widget") @@ -90,7 +92,7 @@ class RepTest extends TestBaseWithH2 { it should "support arithmetic on optional columns" in new Env { val results = Item.query .filter(_.discount.isNotNull) - .filter(i => i.discount + i.discount > Rep.rawOpt(12)) + .filter(i => i.discount + i.discount > 12) .map(_.name) .all() results should contain theSameElementsAs Seq("Widget") diff --git a/src/test/scala/usql/dao/SimpleJoinTest.scala b/src/test/scala/usql/dao/SimpleJoinTest.scala index 409d25b..b2b8c5d 100644 --- a/src/test/scala/usql/dao/SimpleJoinTest.scala +++ b/src/test/scala/usql/dao/SimpleJoinTest.scala @@ -24,23 +24,23 @@ class SimpleJoinTest extends TestBaseWithH2 { id: Int, name: String, levelId: Option[Int] = None - ) derives SqlTabular + ) + + given personSqlTabular: SqlTabular[Person] = SqlTabular.derived object Person extends KeyedCrudBase[Int, Person] { override def key: KeyColumnPath = cols.id - - override lazy val tabular: SqlTabular[Person] = summon } case class Level( id: Int, levelName: String - ) derives SqlTabular + ) + + given levelSqlTabular: SqlTabular[Level] = SqlTabular.derived object Level extends KeyedCrudBase[Int, Level] { override def key: KeyColumnPath = cols.id - - override lazy val tabular: SqlTabular[Level] = summon } trait Env { diff --git a/src/test/scala/usql/dao/SqlCrdBaseTest.scala b/src/test/scala/usql/dao/SqlCrdBaseTest.scala index e7477ed..5cb02b7 100644 --- a/src/test/scala/usql/dao/SqlCrdBaseTest.scala +++ b/src/test/scala/usql/dao/SqlCrdBaseTest.scala @@ -22,11 +22,11 @@ class SqlCrdBaseTest extends TestBaseWithH2 { |); |""".stripMargin - case class Coordinate(id: Int, x: Int, y: Int) derives SqlTabular + case class Coordinate(id: Int, x: Int, y: Int) - object CoordinateCrd extends CrdBase[Coordinate] { - override lazy val tabular: SqlTabular[Coordinate] = summon - } + given coordinateSqlTabular: SqlTabular[Coordinate] = SqlTabular.derived + + object CoordinateCrd extends CrdBase[Coordinate] val sample = Coordinate(0, 5, 6) val samples = Seq( @@ -61,12 +61,12 @@ class SqlCrdBaseTest extends TestBaseWithH2 { from: SubCoord, @ColumnGroup(ColumnGroupMapping.Pattern("%c_to")) to: SubCoord - ) derives SqlTabular + ) + + given withSubCoordsSqlTabular: SqlTabular[WithSubCoords] = SqlTabular.derived object WithSubCoords extends KeyedCrudBase[Int, WithSubCoords] { override def key: KeyColumnPath = cols.id - - override lazy val tabular: SqlTabular[WithSubCoords] = summon } it should "work for nested columns" in { diff --git a/src/test/scala/usql/dao/SqlFieldedTest.scala b/src/test/scala/usql/dao/SqlFieldedTest.scala index ceef33f..1a9a3fe 100644 --- a/src/test/scala/usql/dao/SqlFieldedTest.scala +++ b/src/test/scala/usql/dao/SqlFieldedTest.scala @@ -8,7 +8,9 @@ class SqlFieldedTest extends TestBase { case class Coordinate( x: Int, y: Int - ) derives SqlFielded + ) + + given coordinateSqlTabular: SqlTabular[Coordinate] = SqlTabular.derived @TableName("test_person") case class Person( @@ -20,12 +22,12 @@ class SqlFieldedTest extends TestBase { coordinate: Coordinate, @ColumnGroup ocoordinate: Option[Coordinate] = None - ) derives SqlTabular + ) + + given personSqlTabular: SqlTabular[Person] = SqlTabular.derived object Person extends KeyedCrudBase[Int, Person] { override def key: KeyColumnPath = cols.id - - override lazy val tabular: SqlTabular[Person] = summon } it should "work" in {