Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom JdbcEncoder when creating sql segment #131

Conversation

andrzejressel
Copy link

This PR allows JdbcEncoder to be used in sql interpolator using Param.Nested

Because they are a lot of issues regarding JdbcEncoder/Setter priority, I've added comment for users that want custom encoding for already existing types.

Fixes #83
/claim #83

import scala.language.implicitConversions

package object jdbc {
package object jdbc extends LowPriorityImplicits1 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not great practice to use package objects because of their removal in Scala 3.

I think we can solve this problem in another way:

By using JdbcEncoder, and deleting Setter.

In order to do this, we would enhance JdbcDecoder with two new methods like so:

def unsafeEncode(columIndex: Int, ps: PreparedStatement, value: A): Int
def unsafeEncodeNull(columIndex: Int, ps: PreparedStatement): Int

and implement them for all existing JdbcEncoder.

These are essentially Setter#setValue / setNull, except the functions return the output index, because it may require more than 1 index to write the full value.

What do you think?

Copy link
Author

@andrzejressel andrzejressel May 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if JdbcEncode and Setter could be combined under one sealed trait.

From what I understand Setter is a bit safer since it uses PreparedStatement.

I will try some experiments today.

EDIT: All right, I get the unsafeEncode now, it's like in Decoder:

trait JdbcDecoder[+A] { self =>
  def unsafeDecode(columIndex: Int, rs: ResultSet): (Int, A)
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All right, I've combined Setter into JdbcDecoder and I think it looks pretty good.

It's not finished - there is some refactoring to do. But you can already check if the idea is fine in your opinion.

def encode(value: A): SqlFragment
val setter: Option[Setter[A]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, I would inline Setter, as there is no need for a separate type class.

Second, why make it Option? Are there some cases that have no well-defined implementation?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the one from issue does not:

  implicit val byteArrayjdbcEncoder: JdbcEncoder[Array[Byte]] =
    value => s"FROM_BASE64('${encoder.encodeToString(value)}')"

I mean - setter can be set as required, but encoder won't be so simple.

Maybe it's better - after all these kind of encoders are open to SQL injection attacks.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So after seeing support for iterable I had an epiphany how it may work - I hope API is now what you wanted.

…encoder_in_segment

# Conflicts:
#	core/src/main/scala/zio/jdbc/ZConnection.scala
override def sql(a: B): String = "?"
}

def sql(value: A): String
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be implemented by calling encode and then rendering that SqlFragment to a SQL string.

@@ -84,6 +83,8 @@ sealed trait SqlFragment { self =>
def notIn[B](b: B, bs: B*)(implicit encoder: JdbcEncoder[B]): SqlFragment =
notIn(b +: bs)

def and(): SqlFragment = self ++ SqlFragment.and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def and(): SqlFragment = self ++ SqlFragment.and
def and: SqlFragment = self ++ SqlFragment.and

def apply[A](
onEncode: A => SqlFragment,
onSql: => String,
onValue: (PreparedStatement, Int, A) => Unit,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct, it must return an Int for the next index, to capture how many placeholders it used in the prepared statement

tuple => JdbcEncoder[A]().encode(tuple._1) ++ SqlFragment.comma ++ JdbcEncoder[B]().encode(tuple._2)
override def unsafeSetValue(ps: PreparedStatement, index: Int, value: A): Int = {
onValue(ps, index, value)
index + 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can see this method falsely claims it uses 1 placeholder, but in fact, onValue could consume many placeholders. Hence the need to change the type of onValue to be accurate.

) ++ SqlFragment.comma ++ JdbcEncoder[C]().encode(tuple._3)
override def unsafeSetNull(ps: PreparedStatement, index: Int): Int = {
onNull(ps, index)
index + 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, onNull must return an int, because what if we are talking about a composite JdbEncoder, e.g. Person(name, age), and then when you set that to null, you actually have to set 2 placeholders to null.

implicit def singleParamEncoder[A: SqlFragment.Setter]: JdbcEncoder[A] = value => sql"$value"
def apply[A](
onEncode: A => SqlFragment,
onSql: => String,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best to generalize this to take the A:

Suggested change
onSql: => String,
onSql: A => String,

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or better yet, better to derive the implementation and make it final in the trait so users don't have to specify this (because you can go from A to SqlFragment, and then from SqlFragment to String, automatically).

@jdegoes
Copy link
Member

jdegoes commented Jun 13, 2023

@andrzejressel Please re-open if you have the desire to push this one forward, and thanks for your prototyping work here!

@jdegoes jdegoes closed this Jun 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support JdbcEncoder when using Segment.Param
2 participants