Skip to content

Commit

Permalink
Spec changes
Browse files Browse the repository at this point in the history
  • Loading branch information
AugustNagro committed Dec 2, 2024
1 parent 3f8a6a8 commit 0a89c57
Showing 1 changed file with 49 additions and 44 deletions.
93 changes: 49 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Yet another database client for Scala. No dependencies, high productivity.
## Installing

```
"com.augustnagro" %% "magnum" % "1.3.0"
"com.augustnagro" %% "magnum" % "2.0.0"
```

Magnum requires Scala >= 3.3.0
Expand Down Expand Up @@ -218,7 +218,7 @@ The optional `@Id` annotation denotes the table's primary key. Not setting `@Id`
It is a best practice to extend ImmutableRepo to encapsulate your SQL in repositories. This way, it's easier to maintain since they're grouped together.

```scala
class UserRepo extends ImmutableRepo[User, Long]:
object UserRepo extends ImmutableRepo[User, Long]:
def firstNamesForLast(lastName: String)(using DbCon): Vector[String] =
sql"""
SELECT DISTINCT first_name
Expand All @@ -229,18 +229,22 @@ class UserRepo extends ImmutableRepo[User, Long]:
// other User-related queries here
```

If you don't want to expose all the ImmutableRepo methods, use a Scala 3 [exports clause](https://docs.scala-lang.org/scala3/reference/other-new-features/export.html):

```scala
object UserRepo:
private val repo = ImmutableRepo[User, Long]

export repo.{count, existsById}

// other User-related queries here
```

### Repositories

The `Repo` class auto-generates the following methods at compile-time:
The `Repo` class extends ImmutableRepo and also defines:

```scala
def count(using DbCon): Long
def existsById(id: ID)(using DbCon): Boolean
def findAll(using DbCon): Vector[E]
def findAll(spec: Spec[E])(using DbCon): Vector[E]
def findById(id: ID)(using DbCon): Option[E]
def findAllById(ids: Iterable[ID])(using DbCon): Vector[E]

def delete(entity: E)(using DbCon): Unit
def deleteById(id: ID)(using DbCon): Unit
def truncate()(using DbCon): Unit
Expand Down Expand Up @@ -275,7 +279,7 @@ val countAfterUpdate = transact(xa):
It is a best practice to encapsulate your SQL in repositories.

```scala
class UserRepo extends Repo[User, User, Long]
object UserRepo extends Repo[User, User, Long]
```

Also note that Repo extends ImmutableRepo. Some databases cannot support every method, and will throw UnsupportedOperationException.
Expand Down Expand Up @@ -315,18 +319,9 @@ val newUser: User = transact(xa):
Specifications help you write safe, dynamic queries.
An example use-case would be a search results page that allows users to sort and filter the paginated data.

1. If you need to perform joins to get the data needed, first create a database view.
2. Next, create an entity class that derives DbCodec.
3. Finally, use the Spec class to create a specification.

Here's an example:

```scala
val partialName = "Ja"
val lastNameOpt = Option("Brown")
val searchDate = OffsetDateTime.now.minusDays(2)
val idPosition = 42L

val spec = Spec[User]
.where(sql"first_name ILIKE '$partialName%'")
.where(lastNameOpt.map(ln => sql"last_name = $ln").getOrElse(sql""))
Expand All @@ -335,9 +330,14 @@ val spec = Spec[User]
.limit(10)

val users: Vector[User] = userRepo.findAll(spec)

def partialName = "Ja"
def lastNameOpt = Option("Brown")
def searchDate = OffsetDateTime.now.minusDays(2)
def idPosition = 42L
```

Note that both [seek pagination](https://blog.jooq.org/faster-sql-paging-with-jooq-using-the-seek-method/) and offset pagination is supported.
Note that both [seek pagination](https://blog.jooq.org/faster-sql-paging-with-jooq-using-the-seek-method/) and offset pagination is supported. If you need to use joins to select the columns, use the `Spec.prefix` method.

### Scala 3 Enum & NewType Support

Expand Down Expand Up @@ -403,7 +403,7 @@ A common problem when writing SQL queries is that they're difficult to refactor.

There's also lots of repetition when writing SQL. Magnum's repositories help scrap the boilerplate, but writing `SELECT a, b, c, d, ...` for a large table quickly gets tiring.

To help with this, Magnum offers a `TableInfo` class to enable 'future-proof' queries. An important caveat is that these queries are harder to copy/paste into SQL editors like PgAdmin or DbBeaver.
To help with this, Magnum offers a `TableInfo` class to enable 'future-proof' queries. An important caveat is that these queries are harder to copy/paste into SQL editors like PgAdmin or DbBeaver (of course, you can still find them in [DEBUG logs](#logging-sql-queries))

Here's some examples:

Expand All @@ -415,29 +415,26 @@ case class UserCreator(firstName: String, age: Int) derives DbCodec
@Table(PostgresDbType, SqlNameMapper.CamelToSnakeCase)
case class User(id: Long, firstName: String, age: Int) derives DbCodec

object User:
val Table = TableInfo[UserCreator, User, Long]
object UserSql:
private val u = TableInfo[UserCreator, User, Long]

def allUsers(using DbCon): Vector[User] =
val u = User.Table
// equiv to
// SELECT id, first_name, age FROM user
sql"SELECT ${u.all} FROM $u".query[User].run()
def allUsers(using DbCon): Vector[User] =
// equiv to
// SELECT id, first_name, age FROM user
sql"SELECT ${u.all} FROM $u".query[User].run()

def firstNamesForLast(lastName: String)(using DbCon): Vector[String] =
val u = User.Table
// equiv to
// SELECT DISTINCT first_name FROM user WHERE last_name = ?
sql"""
SELECT DISTINCT ${u.firstName} FROM $u
WHERE ${u.lastName} = $lastName
""".query[String].run()
def firstNamesForLast(lastName: String)(using DbCon): Vector[String] =
// equiv to
// SELECT DISTINCT first_name FROM user WHERE last_name = ?
sql"""
SELECT DISTINCT ${u.firstName} FROM $u
WHERE ${u.lastName} = $lastName
""".query[String].run()

def insertOrIgnore(creator: UserCreator)(using DbCon): Unit =
val u = User.Table
// equiv to
// INSERT OR IGNORE INTO user (first_name, age) VALUES (?, ?)
sql"INSERT OR IGNORE INTO $u ${u.insertCols} VALUES ($creator)".update.run()
def insertOrIgnore(creator: UserCreator)(using DbCon): Unit =
// equiv to
// INSERT OR IGNORE INTO user (first_name, age) VALUES (?, ?)
sql"INSERT OR IGNORE INTO $u ${u.insertCols} VALUES ($creator)".update.run()
```

It's important that `val Table = TableInfo[X, Y, Z]` is not explicitly typed, otherwise its structural typing will be destroyed.
Expand Down Expand Up @@ -562,6 +559,16 @@ case class Address(
zipCode: String,
country: String
) derives DbCodec

def companyInfo(companyName: String)(using DbCon): Vector[(Company, Address)] =
val c = TableInfo[Company, Company, String].alias("c")
val a = TableInfo[Address, Address, Long].alias("a")
sql"""
SELECT ${c.all}, ${a.all}"
FROM $c
JOIN $a ON ${a.id} = ${c.addressId}
WHERE ${c.name} = $companyName
""".query[(Company, Address)].run()
```

#### UUID DbCodec doesn't work for my database
Expand All @@ -580,7 +587,5 @@ case class Person(@Id id: Long, name: String, tracking_id: Option[UUID]) derives
```

## Todo
* JSON / XML support
* Support MSSql
* Cats Effect & ZIO modules
* Explicit Nulls support

0 comments on commit 0a89c57

Please sign in to comment.