Skip to content

Commit

Permalink
Refactor encoders and renderers for SQL statements
Browse files Browse the repository at this point in the history
Replaced `ValueRenderer` with `ValueEncoder` for consistency and added a `ValueEncoderRegistry` to manage encoders. Updated `SimpleStatement` and `ExtendedStatement` to support these changes, enhancing the flexibility for SQL parameter encoding. Removed redundant date period functions from `decode.kt`.
  • Loading branch information
smyrgeorge committed Oct 6, 2024
1 parent 0c201cf commit 4a54d63
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package io.github.smyrgeorge.sqlx4k

import io.github.smyrgeorge.sqlx4k.impl.SimpleStatement
import io.github.smyrgeorge.sqlx4k.impl.statement.SimpleStatement
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlin.reflect.KClass

/**
Expand Down Expand Up @@ -50,10 +54,11 @@ interface Statement {
* - Numeric and boolean values are converted to their string representation using `toString()`.
* - For other types, it attempts to use a custom renderer. If no renderer is found, it throws a [SQLError].
*
* @param encoders A map of encoders for specific types, used to encode values into a SQL-compatible format.
* @return A string representation of the receiver suitable for database operations.
* @throws SQLError if the type of the receiver is unsupported and no appropriate renderer is found.
*/
fun Any?.renderValue(): String {
fun Any?.encodeValue(encoders: ValueEncoderRegistry): String {
return when (this) {
null -> "null"
is String -> {
Expand All @@ -64,77 +69,88 @@ interface Statement {
}

is Boolean, is Number -> toString()
is LocalDate, is LocalTime, is LocalDateTime, is Instant -> toString()
else -> {
val error = SQLError(
code = SQLError.Code.NamedParameterTypeNotSupported,
message = "Could not map named parameter of type ${this::class.simpleName}"
)

val renderer = ValueRenderers.get(this::class) ?: error.ex()
renderer.render(this).renderValue()
val encoder = encoders.get(this::class) ?: error.ex()
encoder.encode(this).encodeValue(encoders)
}
}
}

/**
* An interface for rendering values of type `T` into a format suitable for
* An interface for encoding values of type `T` into a format suitable for
* usage in database statements. Implementations of this interface will define
* how to convert a value of type `T` into a type that can be safely and
* correctly used within a SQL statement.
*
* @param T The type of the value to be rendered.
*/
interface ValueRenderer<T> {
fun render(value: T): Any
interface ValueEncoder<T> {
fun encode(value: T): Any
}

/**
* A singleton class responsible for managing a collection of `ValueRenderer` instances.
* A singleton class responsible for managing a collection of `ValueEncoder` instances.
* Each renderer is associated with a specific data type and is used to convert that type
* into a format suitable for use in database statements.
*/
@Suppress("unused", "UNCHECKED_CAST")
class ValueRenderers {
companion object {
private val renderers: MutableMap<KClass<*>, ValueRenderer<*>> = mutableMapOf()
class ValueEncoderRegistry {
private val encoders: MutableMap<KClass<*>, ValueEncoder<*>> = mutableMapOf()

/**
* Retrieves a `ValueRenderer` associated with the specified type.
*
* @param type The `KClass` of the type for which to get the renderer.
* @return The `ValueRenderer` instance associated with the specified type, or null if none is found.
*/
fun get(type: KClass<*>): ValueRenderer<Any>? =
renderers[type] as ValueRenderer<Any>?
/**
* Retrieves a `ValueRenderer` associated with the specified type.
*
* @param type The `KClass` of the type for which to get the renderer.
* @return The `ValueRenderer` instance associated with the specified type, or null if none is found.
*/
fun get(type: KClass<*>): ValueEncoder<Any>? =
encoders[type] as ValueEncoder<Any>?

/**
* Registers a `ValueRenderer` for a specified type.
*
* @param type The `KClass` of the type for which to register the renderer.
* @param renderer The `ValueRenderer` instance to be associated with the specified type.
*/
fun register(type: KClass<*>, renderer: ValueRenderer<*>) {
renderers[type] = renderer
}
/**
* Registers a `ValueEncoder` for a specific type within the `ValueEncoderRegistry`.
*
* @param type The `KClass` of the type for which the encoder is being registered.
* @param renderer The `ValueEncoder` instance to associate with the specified type.
* @return The `ValueEncoderRegistry` instance after the encoder has been registered.
*/
fun register(type: KClass<*>, renderer: ValueEncoder<*>): ValueEncoderRegistry {
encoders[type] = renderer
return this
}

/**
* Unregisters the `ValueRenderer` associated with a specified type.
*
* @param type The `KClass` of the type for which to unregister the renderer.
*/
fun unregister(type: KClass<*>) {
renderers.remove(type)
}
/**
* Unregisters a `ValueEncoder` for the specified type.
*
* @param type The `KClass` of the type for which the encoder should be unregistered.
* @return The `ValueEncoderRegistry` instance after the encoder has been removed.
*/
fun unregister(type: KClass<*>): ValueEncoderRegistry {
encoders.remove(type)
return this
}

companion object {
val EMPTY = ValueEncoderRegistry()
}
}

companion object {
/**
* Creates and returns a new `SimpleStatement` based on the provided SQL string.
* Creates a new `Statement` instance with the given SQL string and an optional `ValueEncoderRegistry`.
*
* @param sql The SQL statement as a string.
* @return The constructed `SimpleStatement` instance.
* @param sql The SQL string to be used in the statement.
* @param encoders The `ValueEncoderRegistry` to be used for encoding values, default is `ValueEncoderRegistry.EMPTY`.
* @return The newly created `Statement` instance with the specified SQL and encoders.
*/
fun create(sql: String): Statement = SimpleStatement(sql)
fun create(
sql: String,
encoders: ValueEncoderRegistry = ValueEncoderRegistry.EMPTY
): Statement = SimpleStatement(sql, encoders)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ package io.github.smyrgeorge.sqlx4k.impl.extensions

import io.github.smyrgeorge.sqlx4k.ResultSet
import io.github.smyrgeorge.sqlx4k.SQLError
import kotlinx.datetime.DatePeriod
import kotlinx.datetime.DateTimePeriod
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
Expand Down Expand Up @@ -53,7 +51,3 @@ fun ResultSet.Row.Column.asLocalDateTime(): LocalDateTime = LocalDateTime.parse(
fun ResultSet.Row.Column.asLocalDateTimeOrNull(): LocalDateTime? = asStringOrNull()?.let { LocalDateTime.parse(it.fixTime()) }
fun ResultSet.Row.Column.asInstant(): Instant = Instant.parse(asString())
fun ResultSet.Row.Column.asInstantOrNull(): Instant? = asStringOrNull()?.let { Instant.parse(it) }
fun ResultSet.Row.Column.asDatePeriod(): DatePeriod = DatePeriod.parse(asString())
fun ResultSet.Row.Column.asDatePeriodOrNull(): DatePeriod? = asStringOrNull()?.let { DatePeriod.parse(it) }
fun ResultSet.Row.Column.asDateTimePeriod(): DateTimePeriod = DateTimePeriod.parse(asString())
fun ResultSet.Row.Column.asDateTimePeriodOrNull(): DateTimePeriod? = asStringOrNull()?.let { DateTimePeriod.parse(it) }
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package io.github.smyrgeorge.sqlx4k.impl
package io.github.smyrgeorge.sqlx4k.impl.statement

import io.github.smyrgeorge.sqlx4k.SQLError
import io.github.smyrgeorge.sqlx4k.Statement.ValueEncoderRegistry

/**
* A subclass of `SimpleStatement` that extends its capabilities to handle PostgreSQL specific
* positional parameters using the dollar-sign notation (e.g., `$1`, `$2`).
* An extension of the `SimpleStatement` class that adds support for positional parameter binding
* and custom SQL statement rendering.
*
* @property sql The SQL query that may contain PostgreSQL specific positional parameters.
* @property sql The SQL statement to be executed.
* @property encoders A `ValueEncoderRegistry` used for encoding values.
* @constructor Creates an `ExtendedStatement` with the given SQL string and value encoder registry.
*/
@Suppress("unused")
class ExtendedStatement(
private val sql: String
) : SimpleStatement(sql) {
private val sql: String,
private val encoders: ValueEncoderRegistry = ValueEncoderRegistry.EMPTY
) : SimpleStatement(sql, encoders) {

private val pgParameters: List<Int> by lazy {
extractPgParameters(sql)
Expand Down Expand Up @@ -74,7 +78,7 @@ class ExtendedStatement(
message = "Value for positional parameter index '$index' was not supplied."
).ex()
}
val value = pgParametersValues[index].renderValue()
val value = pgParametersValues[index].encodeValue(encoders)
res = res.replace("\$${index + 1}", value)
}
return res
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
package io.github.smyrgeorge.sqlx4k.impl
package io.github.smyrgeorge.sqlx4k.impl.statement

import io.github.smyrgeorge.sqlx4k.SQLError
import io.github.smyrgeorge.sqlx4k.Statement
import io.github.smyrgeorge.sqlx4k.Statement.ValueEncoderRegistry

/**
* Represents a single SQL statement that supports both positional and named parameters.
* Represents a simplified SQL statement allowing the binding of values to named and positional parameters.
*
* @property sql The SQL statement as a string.
* This class provides mechanisms to bind values to both named and positional parameters
* within an SQL statement. The SQL statement can then be rendered with the bound values substituted in place.
*
* @constructor Creates a [SimpleStatement] instance with the given SQL string.
* @property sql The SQL string containing the statement.
* @property encoders A registry for encoding values to be inserted into the SQL statement.
*/
@Suppress("unused")
open class SimpleStatement(
private val sql: String
private val sql: String,
private val encoders: ValueEncoderRegistry = ValueEncoderRegistry.EMPTY
) : Statement {

private val namedParameters: Set<String> by lazy {
Expand Down Expand Up @@ -96,7 +103,7 @@ open class SimpleStatement(
message = "Value for positional parameter index '$index' was not supplied."
).ex()
}
val value = positionalParametersValues[index].renderValue()
val value = positionalParametersValues[index].encodeValue(encoders)
val range = positionalParametersRegex.find(res)?.range ?: SQLError(
code = SQLError.Code.PositionalParameterValueNotSupplied,
message = "Value for positional parameter index '$index' was not supplied."
Expand Down Expand Up @@ -125,7 +132,7 @@ open class SimpleStatement(
message = "Value for named parameter '$name' was not supplied."
).ex()
}
val value = namedParametersValues[name].renderValue()
val value = namedParametersValues[name].encodeValue(encoders)
res = res.replace(":$name", value)
}
return res
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,18 @@ class StatementTest {
@Suppress("unused")
class Test(val id: Int)

class TestRenderer : Statement.ValueRenderer<Test> {
override fun render(value: Test): Any {
class TestEncoder : Statement.ValueEncoder<Test> {
override fun encode(value: Test): Any {
return value.id
}
}

Statement.ValueRenderers.register(Test::class, TestRenderer())
val encoders = Statement
.ValueEncoderRegistry()
.register(Test::class, TestEncoder())

val sql = "select * from sqlx4k where id = :id"
val res = Statement.create(sql)
val res = Statement.create(sql, encoders)
.bind("id", Test(65))
.render()
assertThat(res).contains("id = 65")
Expand Down

0 comments on commit 4a54d63

Please sign in to comment.