Skip to content

Suggestion: Support for in-line row mapper #4

Description

@arvearve

In many cases it would be nice to have an overload for queryOf<T>() / actionReturing<T>() that takes an extractor function / row mapper, e.g.

data class Statement {
    // ...
    inline fun <reified T> queryOf((ResultSet) -> T?): Query<T> = TODO()
}

Some times it’s nice to be allowed to access “under the hood”

If i understand correctly this would let you bypass the kotlinx.serialization loop and would be useful in complex cases where you would otherwise have to tailor your entity data classes a lot in order to make things work. E.g. custom Serializers, @contextual and @SerialName annotations,

Example envisioned usage:

// Business layer model
data class Member(val id: String, val name: String?, val createdAt: ZonedDateTime)

// No need for @Serializable entity class with @Contextual on dates, etc.
val query = Sql("...").queryOf<Member> { row -> Member(
    row.int("id"), 
    row.stringOrNull("name"), 
    row.zonedDateTime("created_at")
  )
}
query.runOn(db)

This suggestion is inspired by Kotliquery's implementation.

References:

Activity

  1. changed the title [-]Support for in-line row mapper[/-] [+]Suggestion: Support for in-line row mapper[/+] on Nov 6, 2024
  2. deusaquilus commented on Dec 19, 2024

    @deusaquilus
    Member

    Yup. Good feature idea.

  3. deusaquilus commented on Dec 20, 2024

    @deusaquilus
    Member

    @arvearve If you're willing to use kotlin serialization you can build a custom serializer like this:

    data class CustomPerson(val name: String, val age: Int)
    
    object CustomPersonSerializer : KSerializer<CustomPerson> {
      override val descriptor =
        buildClassSerialDescriptor("CustomPerson") {
          element("name", serialDescriptor<String>())
          element("age", serialDescriptor<Int>())
        }
    
      override fun serialize(encoder: Encoder, value: CustomPerson) = 
        TODO("Not used by Terpal-SQL so don't care")
    
      override fun deserialize(decoder: Decoder): CustomPerson =
        decoder.decodeStructure(descriptor) {
          CustomPerson(
            decodeStringElement(descriptor, 0),
            decodeIntElement(descriptor, 1)
          )
        }
    }
    
    // Then invoke it like this:
    val people = Sql("SELECT name, age FROM people").queryOf<CustomPerson>(CustomPersonSerializer).runOn(driver)
    println(people)

    This is obviously not the nicest API but its roughly what JetBrains is expecting everyone to use when they want to build custom composite serializer.

    I have purposely not built a cross-database row-abstraction because there are ultimately too many nasty gotchas in the process (e.g. some row-implementations have metadata, others don't. Some row-implementations have async callbacks, etc...).

    What I am interested in is exploring some kind of way to make writing this deserialize function in a more palatable way which is what I'm exploring now. Which API would you prefer, this one?

    Sql("...").queryOf<Person>(
      shape = {
        fieldString("name")
        fieldInt("age")
      },
      run = {
        CustomPerson(readString(), readInt())
      }
    )

    Or this one?

    Sql("...").queryOf<Person>(
      { fields -> 
        fields.string("name")
        fields.int("age")
      },
      { read -> 
        CustomPerson(read.string(), read.int())
      }
    )
  4. arvearve commented on Jan 6, 2025

    @arvearve
    Author

    Hi. Personally I think the 2nd variant looks better 👍. However, it still feels quite like code repetition for the caller.
    I can sort of see how for example async callbacks might make this unavoidable though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions