Skip to content

Commit

Permalink
Implement named parameters (#17)
Browse files Browse the repository at this point in the history
* Implement named parameters

* Refactor Driver Interface.

* Add NamedParameters to README.md

* Stop usage of generic exception

* Expose SqlParameterSource in execute()

* Bump version
  • Loading branch information
moreirasantos authored Jul 29, 2023
1 parent 0d4f22f commit 69010cd
Show file tree
Hide file tree
Showing 15 changed files with 856 additions and 28 deletions.
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@
[![Maven Central](https://img.shields.io/maven-central/v/io.github.moreirasantos/pgkn)](https://central.sonatype.com/artifact/io.github.moreirasantos/pgkn/)
[![Kotlin](https://img.shields.io/badge/kotlin-1.9.0-blue.svg?logo=kotlin)](http://kotlinlang.org)


# pgkn
PostgreSQL Kotlin/Native Driver

## Usage
```
// Show full structure of a kotlin native project
implementation("io.github.moreirasantos:pgkn:1.0.0")
```
```
```kotlin
fun main() {
val driver = PostgresDriver(
host = "host.docker.internal",
Expand Down Expand Up @@ -44,3 +42,19 @@ fun main() {
}
}
```
## Features
## Named Parameters
```kotlin
driver.execute(
"select name from my_table where name = :one OR email = :other",
mapOf("one" to "your_name", "other" to "[email protected]")
) { it.getString(0) }
```
Named Parameters provides an alternative to the traditional syntax using `?` to specify parameters.
Under the hood, it substitutes the named parameters to a query placeholder.

In JDBC, the placeholder would be `?` but with libpq, we will pass `$1`, `$2`, etc as stated here:
[31.3.1. Main Functions - PQexecParams](https://www.postgresql.org/docs/9.5/libpq-exec.html)

This feature implementation tries to follow Spring's `NamedParameterJdbcTemplate` as close as possible.
[NamedParameterJdbcTemplate](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.html)
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ plugins {
}

group = "io.github.moreirasantos"
version = "1.0.1"
version = "1.0.2"

repositories {
mavenCentral()
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
package io.github.moreirasantos.pgkn

import kotlinx.cinterop.*
import libpq.*
import io.github.moreirasantos.pgkn.paramsource.MapSqlParameterSource
import io.github.moreirasantos.pgkn.paramsource.SqlParameterSource
import io.github.moreirasantos.pgkn.resultset.PostgresResultSet
import io.github.moreirasantos.pgkn.resultset.ResultSet
import io.github.moreirasantos.pgkn.sql.buildValueArray
import io.github.moreirasantos.pgkn.sql.parseSql
import io.github.moreirasantos.pgkn.sql.substituteNamedParameters
import kotlinx.cinterop.*
import libpq.*

/**
* Executes given query with given named parameters.
* If you pass a handler, you will receive a list of result data.
* You can pass an [SqlParameterSource] to register your own Postgres types.
*/
sealed interface PostgresDriver {
fun <T> execute(sql: String, handler: (ResultSet) -> T): List<T>

fun execute(sql: String): Long
fun <T> execute(sql: String, namedParameters: Map<String, Any?> = emptyMap(), handler: (ResultSet) -> T): List<T>
fun <T> execute(sql: String, paramSource: SqlParameterSource, handler: (ResultSet) -> T): List<T>
fun execute(sql: String, namedParameters: Map<String, Any?> = emptyMap()): Long
fun execute(sql: String, paramSource: SqlParameterSource): Long
}

@OptIn(ExperimentalForeignApi::class)
Expand Down Expand Up @@ -45,24 +56,60 @@ private class PostgresDriverImpl(
pgtty = null
).apply { require(ConnStatusType.CONNECTION_OK == PQstatus(this)) }!!

override fun <T> execute(sql: String, handler: (ResultSet) -> T): List<T> = doExecute(sql).let {
val rs = PostgresResultSet(it)
override fun <T> execute(sql: String, namedParameters: Map<String, Any?>, handler: (ResultSet) -> T) =
if (namedParameters.isEmpty()) doExecute(sql).handleResults(handler)
else execute(sql, MapSqlParameterSource(namedParameters), handler)

override fun <T> execute(sql: String, paramSource: SqlParameterSource, handler: (ResultSet) -> T) =
doExecute(sql, paramSource).handleResults(handler)

override fun execute(sql: String, namedParameters: Map<String, Any?>) =
if (namedParameters.isEmpty()) doExecute(sql).returnCount()
else execute(sql, MapSqlParameterSource(namedParameters))

override fun execute(sql: String, paramSource: SqlParameterSource) =
doExecute(sql, paramSource).returnCount()

private fun <T> CPointer<PGresult>.handleResults(handler: (ResultSet) -> T): List<T> {
val rs = PostgresResultSet(this)

val list: MutableList<T> = mutableListOf()
while (rs.next()) {
list.add(handler(rs))
}

PQclear(it)
PQclear(this)
return list
}

override fun execute(sql: String): Long = doExecute(sql).let {
val rows = PQcmdTuples(it)!!.toKString()
PQclear(it)
private fun CPointer<PGresult>.returnCount(): Long {
val rows = PQcmdTuples(this)!!.toKString()
PQclear(this)
return rows.toLongOrNull() ?: 0
}

private fun doExecute(sql: String, paramSource: SqlParameterSource): CPointer<PGresult> {
val parsedSql = parseSql(sql)
val sqlToUse: String = substituteNamedParameters(parsedSql, paramSource)
val params: Array<Any?> = buildValueArray(parsedSql, paramSource)

return memScoped {
PQexecParams(
connection,
command = sqlToUse,
nParams = params.size,
paramValues = createValues(params.size) {
println(params[it]?.toString()?.cstr)
value = params[it]?.toString()?.cstr?.getPointer(this@memScoped)
},
paramLengths = params.map { it?.toString()?.length ?: 0 }.toIntArray().refTo(0),
paramFormats = IntArray(params.size) { TEXT_RESULT_FORMAT }.refTo(0),
paramTypes = parsedSql.parameterNames.map(paramSource::getSqlType).toUIntArray().refTo(0),
resultFormat = TEXT_RESULT_FORMAT
)
}.check()
}

private fun doExecute(sql: String) = memScoped {
PQexecParams(
connection,
Expand All @@ -74,8 +121,7 @@ private class PostgresDriverImpl(
paramTypes = createValues(0) {},
resultFormat = TEXT_RESULT_FORMAT
)
}
.check()
}.check()

private fun CPointer<PGresult>?.check(): CPointer<PGresult> {
val status = PQresultStatus(this)
Expand All @@ -90,5 +136,6 @@ private class PostgresDriverImpl(
private fun CPointer<PGconn>?.error(): String = PQerrorMessage(this)!!.toKString().also { PQfinish(this) }

private const val TEXT_RESULT_FORMAT = 0

@Suppress("UnusedPrivateProperty")
private const val BINARY_RESULT_FORMAT = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.moreirasantos.pgkn.exception

sealed class SQLException(message: String? = null, cause: Throwable? = null) : Exception(message, cause)

class InvalidDataAccessApiUsageException(message: String, cause: Throwable? = null) : SQLException(message, cause)

class AnonymousClassException : SQLException("Class must not be anonymous", null)

class GetColumnValueException(columnIndex: Int) : SQLException("Error getting column $columnIndex value", null)
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package io.github.moreirasantos.pgkn.paramsource

import io.github.moreirasantos.pgkn.exception.AnonymousClassException
import io.github.moreirasantos.pgkn.paramsource.SqlParameterSource.Companion.TYPE_UNKNOWN
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlin.reflect.KClass


/**
* Abstract base class for [SqlParameterSource] implementations.
* Provides registration of SQL types per parameter and a friendly
* [toString] representation.
* Concrete subclasses must implement [hasValue] and [getValue].
*/
abstract class AbstractSqlParameterSource : SqlParameterSource {
private val sqlTypes: MutableMap<String, UInt> = HashMap()
private val typeNames: MutableMap<String, String> = HashMap()

/**
* Register an SQL type for the given parameter.
* @param paramName the name of the parameter
* @param sqlType the SQL type of the parameter
*/
fun registerSqlType(paramName: String, sqlType: UInt) {
sqlTypes[paramName] = sqlType
}

fun registerSqlType(paramName: String, value: Any?) {
registerSqlType(
paramName = paramName,
sqlType = value?.let { oidMap[it::class.simpleName ?: throw AnonymousClassException()] }
?: TYPE_UNKNOWN
)
}

/**
* Register an SQL type for the given parameter.
* @param paramName the name of the parameter
* @param typeName the type name of the parameter
*/
fun registerTypeName(paramName: String, typeName: String) {
typeNames[paramName] = typeName
}

/**
* Return the SQL type for the given parameter, if registered.
* @param paramName the name of the parameter
* @return the SQL type of the parameter,
* or `TYPE_UNKNOWN` if not registered
*/
override fun getSqlType(paramName: String) = sqlTypes[paramName] ?: TYPE_UNKNOWN

/**
* Return the type name for the given parameter, if registered.
* @param paramName the name of the parameter
* @return the type name of the parameter,
* or `null` if not registered
*/
fun getTypeName(paramName: String) = typeNames[paramName]

/**
* Enumerate the parameter names and values with their corresponding SQL type if available,
* or just return the simple `SqlParameterSource` implementation class name otherwise.
* @since 5.2
* @see .getParameterNames
*/
@Suppress("NestedBlockDepth")
override fun toString(): String {
val parameterNames: Array<String>? = parameterNames
return if (parameterNames != null) {
val array = ArrayList<String>(parameterNames.size)
for (parameterName in parameterNames) {
val value = getValue(parameterName)
/*
if (value is SqlParameterValue) {
value = (value as SqlParameterValue?).getValue()
}
*/
var typeName = getTypeName(parameterName)
if (typeName == null) {
val sqlType = getSqlType(parameterName)
if (sqlType != TYPE_UNKNOWN) {
typeName = sqlTypeNames[sqlType]
if (typeName == null) {
typeName = sqlType.toString()
}
}
}
val entry = StringBuilder()
entry.append(parameterName).append('=').append(value)
if (typeName != null) {
entry.append(" (type:").append(typeName).append(')')
}
array.add(entry.toString())
}
array.joinToString(
separator = ", ",
prefix = this::class.simpleName + " {",
postfix = "}"
)
} else {
this::class.simpleName!!
}
}
}

// Full list here: https://jdbc.postgresql.org/documentation/publicapi/constant-values.html
private val oidMap: Map<String, UInt> = hashMapOf(
Boolean::class.namedClassName to 16u,
ByteArray::class.namedClassName to 17u,
Long::class.namedClassName to 20u,
Int::class.namedClassName to 23u,
String::class.namedClassName to 25u,
Double::class.namedClassName to 701u,
LocalDate::class.namedClassName to 1082u,
LocalTime::class.namedClassName to 1083u,
LocalDateTime::class.namedClassName to 1114u,
Instant::class.namedClassName to 1184u,
// intervalOid = 1186u
// uuidOid = 2950u
)

private val sqlTypeNames: Map<UInt, String> = oidMap.entries.associateBy({ it.value }) { it.key }

private val KClass<*>.namedClassName get() = this.simpleName!!
Loading

0 comments on commit 69010cd

Please sign in to comment.