Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()}")
Expand All @@ -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
Expand Down
13 changes: 6 additions & 7 deletions src/main/scala/usql/dao/Crd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
25 changes: 18 additions & 7 deletions src/main/scala/usql/dao/Rep.scala
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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}")
}

Expand Down Expand Up @@ -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. */
Expand Down
2 changes: 0 additions & 2 deletions src/test/scala/com/example/example.sc
Original file line number Diff line number Diff line change
Expand Up @@ -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()}")
Expand Down
6 changes: 3 additions & 3 deletions src/test/scala/usql/AutoGeneratedUpdateTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 0 additions & 2 deletions src/test/scala/usql/dao/KeyedCrudBaseTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
26 changes: 13 additions & 13 deletions src/test/scala/usql/dao/QueryBuilderTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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 {
Expand Down
28 changes: 15 additions & 13 deletions src/test/scala/usql/dao/RepTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
)
)
Expand All @@ -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")
Expand All @@ -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")
Expand Down
12 changes: 6 additions & 6 deletions src/test/scala/usql/dao/SimpleJoinTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 7 additions & 7 deletions src/test/scala/usql/dao/SqlCrdBaseTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 6 additions & 4 deletions src/test/scala/usql/dao/SqlFieldedTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 {
Expand Down
Loading