scalaziozio-quill

Use ZIO Quill to conditionally clear or retain a column while updating multiple other columns


I have a ZIO Quill project with a method that updates a bunch of fields in a database, and depending on a flag passed to the method, either clears another specific field or leaves it unchanged.

Here's some sample code that demonstrates the issue:

package com.example.dao

import com.zaxxer.hikari.HikariDataSource
import io.getquill.{PostgresJdbcContext, SnakeCase}

class MyDao(ds: HikariDataSource) {
  lazy val ctx = new PostgresJdbcContext(SnakeCase, ds)
  import ctx._

  def updateValue(input: MyInput, shouldClear: Boolean): Unit =
    if (shouldClear)
      ctx.run(quote {
        query[MyRecord]
          .filter(_.id == lift(input.id))
          .update(
            _.value1 -> lift(input.value1),
            _.value2 -> lift(input.value2),
            _.value3 -> lift(input.value3),
            _.value4 -> lift(input.value4),
            _.value5 -> lift(input.value5),
            _.keepOrClear -> None
          )
      })
    else
      ctx.run(quote {
        query[MyRecord]
          .filter(_.id == lift(input.id))
          .update(
            _.value1 -> lift(input.value1),
            _.value2 -> lift(input.value2),
            _.value3 -> lift(input.value3),
            _.value4 -> lift(input.value4),
            _.value5 -> lift(input.value5)
          )
      })
}

final case class MyRecord(
    id: Int,
    value1: String,
    value2: String,
    value3: String,
    value4: String,
    value5: String,
    keepOrClear: Option[String]
)

final case class MyInput(id: Int, value1: String, value2: String, 
                         value3: String, value4: String, value5: String)

This works just fine, generating the desired SQL:

UPDATE my_record AS x1
SET value1 = ?, value2 = ?, value3 = ?, value4 = ?, value5 = ?,
keep_or_clear = null
WHERE x1.id = ?;

UPDATE my_record AS x8
SET value1 = ?, value2 = ?, value3 = ?, value4 = ?, value5 = ?
WHERE x8.id = ?;

However, the two code paths of the if-else construct are near duplicates of each other. In my actual project, there's enough fields and tangential mapping going on that it's not obvious at a glance that the only thing that differs between the two if-else branches is the presence or absence of _.keepOrClear -> None.

Is there some way to avoid this code duplication in a way that makes it obvious that only the _.keepOrClear -> None is different, and is based on the keepOrClear value? I'd potentially be willing to accept a less optimal SQL output, so long as it was still good enough for my needs, if it removed the duplication.

One thing I tried, which doesn't work and results in a compilation failure, is to set the value to None if the flag is true, or setting it to the existing value if it's false.

def updateValue(input: MyInput, shouldClear: Boolean): Unit =
  ctx.run(quote {
    query[MyRecord]
      .filter(_.id == lift(input.id))
      .update(
        _.value1 -> lift(input.value1),
        _.value2 -> lift(input.value2),
        _.value3 -> lift(input.value3),
        _.value4 -> lift(input.value4),
        _.value5 -> lift(input.value5),
        r => r.keepOrClear -> (if (shouldClear) None else r.keepOrClear)
      )
  })

The error I get is:

/Users/Me/my-project/src/main/scala/com/example/dao/MyDao.scala:20:18: type mismatch;
 found   : Any
 required: Option[String]
          r => r.keepOrClear -> (if (shouldClear) None else r.keepOrClear)

I was hoping this might generate SQL like the following:

UPDATE my_record AS x1 
SET value1 = ?, value2 = ?, value3 = ?, value4 = ?, value5 = ?, 
keep_or_clear = CASE WHEN ? THEN null ELSE x1.keep_or_clear END
WHERE x1.id = ?;

The project is using Scala version 2.13.13 and Quill version 4.8.4.


Solution

  • You need to lift shouldClear and coerce the conditional to a type.

    Thus, doing:

    def updateValue(input: MyInput, shouldClear: Boolean): Unit =
      ctx.run(quote {
        query[MyRecord]
          .filter(_.id == lift(input.id))
          .update(
            _.value1 -> lift(input.value1),
            _.value2 -> lift(input.value2),
            _.value3 -> lift(input.value3),
            _.value4 -> lift(input.value4),
            _.value5 -> lift(input.value5),
            r => r.keepOrClear -> ((if (lift(shouldClear)) None else r.keepOrClear): Option[String])
          )
      })
    

    Results in:

    UPDATE my_record AS x1 SET value1 = ?, value2 = ?, value3 = ?, value4 = ?, value5 = ?, keep_or_clear = CASE WHEN ? THEN null ELSE x1.keep_or_clear END WHERE x1.id = ?