From 6bea4991d07f2254b01d65c111cb0e806eb74c2a Mon Sep 17 00:00:00 2001 From: Miguel Date: Fri, 24 Nov 2023 22:35:20 +0000 Subject: [PATCH] Implement Connection Pool. Introduce PostgresDriverUnit. Only restart connection on fatal error. Use K2 compiler and gradle 8.4. Tests now using shared pool. (#30) --- README.md | 19 +- build.gradle.kts | 15 +- gradle.properties | 3 + gradle/wrapper/gradle-wrapper.properties | 3 +- .../moreirasantos/pgkn/PostgresDriver.kt | 93 ++++++++- .../moreirasantos/pgkn/pool/ConnectionPool.kt | 29 +++ .../moreirasantos/pgkn/DriverUnitTest.kt | 20 ++ .../moreirasantos/pgkn/NamedParametersTest.kt | 80 ++++--- .../moreirasantos/pgkn/PostgresDriverTest.kt | 196 +++++++++--------- .../pgkn/testutils/TestDriver.kt | 13 ++ 10 files changed, 322 insertions(+), 149 deletions(-) create mode 100644 src/commonMain/kotlin/io/github/moreirasantos/pgkn/pool/ConnectionPool.kt create mode 100644 src/commonTest/kotlin/io/github/moreirasantos/pgkn/DriverUnitTest.kt create mode 100644 src/commonTest/kotlin/io/github/moreirasantos/pgkn/testutils/TestDriver.kt diff --git a/README.md b/README.md index 2d2e4a2..4dc55ef 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - [![Kotlin Experimental](https://kotl.in/badges/experimental.svg)](https://kotlinlang.org/docs/components-stability.html) [![CI](https://github.com/miguel-moreira/pgkn/actions/workflows/blank.yml/badge.svg?branch=main)](https://github.com/miguel-moreira/pgkn/actions/workflows/blank.yml) [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) @@ -6,12 +5,15 @@ [![Kotlin](https://img.shields.io/badge/kotlin-1.9.0-blue.svg?logo=kotlin)](http://kotlinlang.org) # pgkn + PostgreSQL Kotlin/Native Driver ## Usage + ``` implementation("io.github.moreirasantos:pgkn:1.0.0") ``` + ```kotlin fun main() { val driver = PostgresDriver( @@ -42,14 +44,27 @@ fun main() { } } ``` + ## Features -## Named Parameters + +### Connection Pool + +PGKN has a connection pool, its size being configurable in `PostgresDriver()` - 20 by default. +It will refresh connection units if the query fails fatally, but it still needs more fine-grained status checks. + + +You can also use a single connection with `PostgresDriverUnit`, which currently is not `suspend` +but probably will be in the future. + +### Named Parameters + ```kotlin driver.execute( "select name from my_table where name = :one OR email = :other", mapOf("one" to "your_name", "other" to "your@email.com") ) { 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. diff --git a/build.gradle.kts b/build.gradle.kts index 7c5026a..71e1e32 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,9 +3,10 @@ import com.bmuschko.gradle.docker.tasks.container.DockerRemoveContainer import com.bmuschko.gradle.docker.tasks.container.DockerStartContainer import com.bmuschko.gradle.docker.tasks.image.DockerPullImage import io.gitlab.arturbosch.detekt.Detekt +import org.gradle.api.tasks.testing.logging.TestExceptionFormat plugins { - val kotlinVersion = "1.9.20" + val kotlinVersion = "1.9.21" kotlin("multiplatform") version kotlinVersion id("com.bmuschko.docker-remote-api") version "9.3.7" id("io.gitlab.arturbosch.detekt").version("1.23.0") @@ -13,7 +14,7 @@ plugins { } group = "io.github.moreirasantos" -version = "1.0.2" +version = "1.1.0" repositories { mavenCentral() @@ -45,6 +46,7 @@ kotlin { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("io.github.oshai:kotlin-logging:5.1.0") } } @@ -99,6 +101,15 @@ tasks { } } +tasks.withType { + testLogging { + events("PASSED", "FAILED", "SKIPPED") + exceptionFormat = TestExceptionFormat.FULL + showStandardStreams = true + showStackTraces = true + } +} + detekt { buildUponDefaultConfig = true // preconfigure defaults config.setFrom("$projectDir/config/detekt.yml") diff --git a/gradle.properties b/gradle.properties index 7fc6f1f..b645d60 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,4 @@ kotlin.code.style=official +kotlin.native.binary.sourceInfoType=libbacktrace +kotlin.experimental.tryK2=true +kapt.use.k2=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 17a8ddc..3fa8f86 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/commonMain/kotlin/io/github/moreirasantos/pgkn/PostgresDriver.kt b/src/commonMain/kotlin/io/github/moreirasantos/pgkn/PostgresDriver.kt index a293f89..b4ff003 100644 --- a/src/commonMain/kotlin/io/github/moreirasantos/pgkn/PostgresDriver.kt +++ b/src/commonMain/kotlin/io/github/moreirasantos/pgkn/PostgresDriver.kt @@ -2,6 +2,7 @@ package io.github.moreirasantos.pgkn import io.github.moreirasantos.pgkn.paramsource.MapSqlParameterSource import io.github.moreirasantos.pgkn.paramsource.SqlParameterSource +import io.github.moreirasantos.pgkn.pool.ConnectionPool import io.github.moreirasantos.pgkn.resultset.PostgresResultSet import io.github.moreirasantos.pgkn.resultset.ResultSet import io.github.moreirasantos.pgkn.sql.buildValueArray @@ -16,12 +17,30 @@ import libpq.* * You can pass an [SqlParameterSource] to register your own Postgres types. */ sealed interface PostgresDriver { - fun execute(sql: String, namedParameters: Map = emptyMap(), handler: (ResultSet) -> T): List + suspend fun execute( + sql: String, + namedParameters: Map = emptyMap(), + handler: (ResultSet) -> T + ): List + + suspend fun execute(sql: String, paramSource: SqlParameterSource, handler: (ResultSet) -> T): List + suspend fun execute(sql: String, namedParameters: Map = emptyMap()): Long + suspend fun execute(sql: String, paramSource: SqlParameterSource): Long +} + +sealed interface PostgresDriverUnit { + fun execute( + sql: String, + namedParameters: Map = emptyMap(), + handler: (ResultSet) -> T + ): List + fun execute(sql: String, paramSource: SqlParameterSource, handler: (ResultSet) -> T): List fun execute(sql: String, namedParameters: Map = emptyMap()): Long fun execute(sql: String, paramSource: SqlParameterSource): Long } +@Suppress("LongParameterList") @OptIn(ExperimentalForeignApi::class) fun PostgresDriver( host: String, @@ -29,7 +48,24 @@ fun PostgresDriver( database: String, user: String, password: String, -): PostgresDriver = PostgresDriverImpl( + poolSize: Int = 20 +): PostgresDriver = PostgresDriverPool( + host = host, + port = port, + database = database, + user = user, + password = password, + poolSize = poolSize +) + +@OptIn(ExperimentalForeignApi::class) +fun PostgresDriverUnit( + host: String, + port: Int = 5432, + database: String, + user: String, + password: String +): PostgresDriverUnit = PostgresDriverImpl( host = host, port = port, database = database, @@ -38,15 +74,53 @@ fun PostgresDriver( ) @ExperimentalForeignApi -private class PostgresDriverImpl( +private class PostgresDriverPool( host: String, - port: Int, + port: Int = 5432, database: String, user: String, password: String, + poolSize: Int ) : PostgresDriver { - private val connection = PQsetdbLogin( + private val pool = ConnectionPool((1..poolSize).map { + PostgresDriverImpl( + host = host, + port = port, + database = database, + user = user, + password = password, + ) + }) + + override suspend fun execute( + sql: String, + namedParameters: Map, + handler: (ResultSet) -> T + ) = pool.invoke { it.execute(sql, namedParameters, handler) } + + override suspend fun execute(sql: String, paramSource: SqlParameterSource, handler: (ResultSet) -> T) = + pool.invoke { it.execute(sql, paramSource, handler) } + + override suspend fun execute(sql: String, namedParameters: Map) = + pool.invoke { it.execute(sql, namedParameters) } + + override suspend fun execute(sql: String, paramSource: SqlParameterSource) = + pool.invoke { it.execute(sql, paramSource) } +} + +@ExperimentalForeignApi +private class PostgresDriverImpl( + private val host: String, + private val port: Int, + private val database: String, + private val user: String, + private val password: String, +) : PostgresDriverUnit { + + var connection = initConnection() + + private fun initConnection() = PQsetdbLogin( pghost = host, pgport = port.toString(), dbName = database, @@ -126,14 +200,19 @@ private class PostgresDriverImpl( private fun CPointer?.check(): CPointer { val status = PQresultStatus(this) check(status == PGRES_TUPLES_OK || status == PGRES_COMMAND_OK || status == PGRES_COPY_IN) { - connection.error() + val message = connection.error() + if (status == PGRES_FATAL_ERROR) { + PQfinish(connection) + connection = initConnection() + } + message } return this!! } } @ExperimentalForeignApi -private fun CPointer?.error(): String = PQerrorMessage(this)!!.toKString().also { PQfinish(this) } +private fun CPointer?.error(): String = PQerrorMessage(this)!!.toKString() private const val TEXT_RESULT_FORMAT = 0 diff --git a/src/commonMain/kotlin/io/github/moreirasantos/pgkn/pool/ConnectionPool.kt b/src/commonMain/kotlin/io/github/moreirasantos/pgkn/pool/ConnectionPool.kt new file mode 100644 index 0000000..fc339da --- /dev/null +++ b/src/commonMain/kotlin/io/github/moreirasantos/pgkn/pool/ConnectionPool.kt @@ -0,0 +1,29 @@ +package io.github.moreirasantos.pgkn.pool + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.sync.withPermit + +/** + * Pool with Semaphore/Mutex pattern. + * Using semaphore with max permits being the size of connection pool to throttle. + * Mutex to make sure one connection is not used at the same time- + */ +internal class ConnectionPool(connections: List) { + + private val mutex = Mutex() + private val semaphore = Semaphore(permits = connections.size) + private val connections = connections.toMutableList() + + suspend operator fun invoke(handler: suspend (T) -> U): U { + semaphore.withPermit { + val borrowed = mutex.withLock { connections.removeLast() } + try { + return handler(borrowed) + } finally { + mutex.withLock { connections.add(borrowed) } + } + } + } +} diff --git a/src/commonTest/kotlin/io/github/moreirasantos/pgkn/DriverUnitTest.kt b/src/commonTest/kotlin/io/github/moreirasantos/pgkn/DriverUnitTest.kt new file mode 100644 index 0000000..9cdc7a7 --- /dev/null +++ b/src/commonTest/kotlin/io/github/moreirasantos/pgkn/DriverUnitTest.kt @@ -0,0 +1,20 @@ +package io.github.moreirasantos.pgkn + +import kotlin.test.Test +import kotlin.test.assertEquals + +class DriverUnitTest { + @Test + fun `single driver should work`() { + val driver = PostgresDriverUnit( + host = "localhost", + port = 5678, + database = "postgres", + user = "postgres", + password = "postgres" + ) + assertEquals("echo", driver + .execute("select 'echo'") { it.getString(0) } + .first()) + } +} diff --git a/src/commonTest/kotlin/io/github/moreirasantos/pgkn/NamedParametersTest.kt b/src/commonTest/kotlin/io/github/moreirasantos/pgkn/NamedParametersTest.kt index a055333..e4aacee 100644 --- a/src/commonTest/kotlin/io/github/moreirasantos/pgkn/NamedParametersTest.kt +++ b/src/commonTest/kotlin/io/github/moreirasantos/pgkn/NamedParametersTest.kt @@ -1,17 +1,11 @@ package io.github.moreirasantos.pgkn +import io.github.moreirasantos.pgkn.testutils.TestDriver.driver +import kotlinx.coroutines.runBlocking import kotlin.test.Test import kotlin.test.assertEquals class NamedParametersTest { - val driver = PostgresDriver( - host = "localhost", - port = 5678, - database = "postgres", - user = "postgres", - password = "postgres", - ) - private fun createTable(name: String) = """ create table $name ( @@ -24,52 +18,56 @@ class NamedParametersTest { @Test fun `should select with named params`() { - val t = "named_params" - driver.execute("drop table if exists $t") - driver.execute(createTable(t)) - assertEquals(0, driver.execute("select * from $t") {}.size) + runBlocking { + val t = "named_params" + driver.execute("drop table if exists $t") + driver.execute(createTable(t)) + assertEquals(0, driver.execute("select * from $t") {}.size) - driver.execute("insert into $t(id, name, email, int) values(1, 'john', 'mail@mail.com', 10)") + driver.execute("insert into $t(id, name, email, int) values(1, 'john', 'mail@mail.com', 10)") - assertEquals(listOf("john"), driver.execute( - "select name from $t where name = :one", - mapOf("one" to "john") - ) { it.getString(0) }) + assertEquals(listOf("john"), driver.execute( + "select name from $t where name = :one", + mapOf("one" to "john") + ) { it.getString(0) }) - assertEquals(listOf("john"), driver.execute( - "select name from $t where name = :one OR name = :other", - mapOf("one" to "john", "other" to "another") - ) { it.getString(0) }) + assertEquals(listOf("john"), driver.execute( + "select name from $t where name = :one OR name = :other", + mapOf("one" to "john", "other" to "another") + ) { it.getString(0) }) - assertEquals(emptyList(), driver.execute( - "select name from $t where name = :one", - mapOf("one" to "wrong") - ) { it.getString(0) }) + assertEquals(emptyList(), driver.execute( + "select name from $t where name = :one", + mapOf("one" to "wrong") + ) { it.getString(0) }) - driver.execute("drop table $t") + driver.execute("drop table $t") + } } @Test fun `should update with named params`() { - val t = "named_params_update" - driver.execute("drop table if exists $t") - driver.execute(createTable(t)) - assertEquals(0, driver.execute("select * from $t") {}.size) + runBlocking { + val t = "named_params_update" + driver.execute("drop table if exists $t") + driver.execute(createTable(t)) + assertEquals(0, driver.execute("select * from $t") {}.size) - driver.execute("insert into $t(id, name, email, int) values(1, 'john', 'mail@mail.com', 10)") + driver.execute("insert into $t(id, name, email, int) values(1, 'john', 'mail@mail.com', 10)") - assertEquals( - 1, driver.execute( - "update $t set int = :number where name = :one", - mapOf("one" to "john", "number" to 15) + assertEquals( + 1, driver.execute( + "update $t set int = :number where name = :one", + mapOf("one" to "john", "number" to 15) + ) ) - ) - assertEquals(listOf("john"), driver.execute( - "select name from $t where int = :number", - mapOf("number" to 15) - ) { it.getString(0) }) + assertEquals(listOf("john"), driver.execute( + "select name from $t where int = :number", + mapOf("number" to 15) + ) { it.getString(0) }) - driver.execute("drop table $t") + driver.execute("drop table $t") + } } } diff --git a/src/commonTest/kotlin/io/github/moreirasantos/pgkn/PostgresDriverTest.kt b/src/commonTest/kotlin/io/github/moreirasantos/pgkn/PostgresDriverTest.kt index 521a27c..7c238e0 100644 --- a/src/commonTest/kotlin/io/github/moreirasantos/pgkn/PostgresDriverTest.kt +++ b/src/commonTest/kotlin/io/github/moreirasantos/pgkn/PostgresDriverTest.kt @@ -1,66 +1,66 @@ package io.github.moreirasantos.pgkn -import kotlinx.datetime.* import io.github.moreirasantos.pgkn.resultset.ResultSet -import kotlin.test.Test -import kotlin.test.assertContains -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith +import io.github.moreirasantos.pgkn.testutils.TestDriver.driver +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.* +import kotlin.test.* class PostgresDriverTest { - val driver = PostgresDriver( - host = "localhost", - port = 5678, - database = "postgres", - user = "postgres", - password = "postgres", - ) @Test fun `should fail with non existent table`() { - assertFailsWith { - driver.execute("select * from nonexistent") {} + runBlocking { + assertFailsWith { + driver.execute("select * from nonexistent") {} + } } } + @Test fun `should create and delete table`() { - driver.execute("drop table if exists to_create") - driver.execute("create table to_create(id serial primary key)") + runBlocking { + driver.execute("drop table if exists to_create") + driver.execute("create table to_create(id serial primary key)") - assertEquals(0, driver.execute("select * from to_create") {}.size) + assertEquals(0, driver.execute("select * from to_create") {}.size) - driver.execute("drop table to_create") + driver.execute("drop table to_create") + } } + @Test fun `should insert select update and delete row`() { - val column = "test" - val insertValue = "insert-value" - val updateValue = "update-value" + runBlocking { + val column = "test" + val insertValue = "insert-value" + val updateValue = "update-value" - driver.execute("drop table if exists to_crud") - driver.execute("create table to_crud(id serial primary key, $column varchar (50))") + driver.execute("drop table if exists to_crud") + driver.execute("create table to_crud(id serial primary key, $column varchar (50))") - assertEquals(0, driver.execute("select * from to_crud") {}.size) + assertEquals(0, driver.execute("select * from to_crud") {}.size) - driver.execute("insert into to_crud($column) values('$insertValue')") + driver.execute("insert into to_crud($column) values('$insertValue')") - assertEquals( - insertValue, - driver.execute("select $column from to_crud") { it.getString(0) }.first() - ) + assertEquals( + insertValue, + driver.execute("select $column from to_crud") { it.getString(0) }.first() + ) - assertEquals(1, driver.execute("update to_crud set $column = '$updateValue'")) + assertEquals(1, driver.execute("update to_crud set $column = '$updateValue'")) - assertEquals( - updateValue, - driver.execute("select $column from to_crud") { it.getString(0) }.first() - ) + assertEquals( + updateValue, + driver.execute("select $column from to_crud") { it.getString(0) }.first() + ) - assertEquals(1, driver.execute("delete from to_crud where $column = '$updateValue'")) - assertEquals(0, driver.execute("select * from to_crud") {}.size) - driver.execute("drop table to_crud") + assertEquals(1, driver.execute("delete from to_crud where $column = '$updateValue'")) + assertEquals(0, driver.execute("select * from to_crud") {}.size) + driver.execute("drop table to_crud") + } } private class Holder( @@ -92,69 +92,73 @@ class PostgresDriverTest { private fun Int.toTimeDigit() = toString().padStart(2, '0') + @Test + @Suppress("LongMethod") fun `should select all column types`() { - val now = Clock.System.now().let { Instant.fromEpochSeconds(it.epochSeconds, 0) } - val localDateTime = now.toLocalDateTime(TimeZone.currentSystemDefault()) - - val byteArrayHolder = with("\\xdeadbeef".encodeToByteArray()) { - Holder( - "bytea", - this, "'${this.decodeToString()}'" - ) { it.getBytes(0) } - } - - val list = listOf( - Holder("id", 123L) { it.getLong(0) }, - Holder("name", "name", "'name'") { it.getString(0) }, - Holder("email", "mail@mail.com", "'mail@mail.com'") { it.getString(0) }, - Holder("bool", true) { it.getBoolean(0) }, - Holder("short", Short.MIN_VALUE) { it.getShort(0) }, - Holder("int", 234) { it.getInt(0) }, - Holder("float", 1.23f) { it.getFloat(0) }, - Holder("double", 2.34) { it.getDouble(0) }, - byteArrayHolder, - with(Clock.System.todayIn(TimeZone.currentSystemDefault())) { - Holder("date", this, "'$this'") { it.getDate(0) } - }, - with( - LocalTime(localDateTime.time.hour, localDateTime.time.minute, localDateTime.time.second, 0) - ) { - Holder("time", this, this.let { - "'${it.hour.toTimeDigit()}:${it.minute.toTimeDigit()}:${it.second.toTimeDigit()}'" - }) { it.getTime(0) } - }, - Holder("timestamp", localDateTime, "'$localDateTime'") { it.getLocalDateTime(0) }, - Holder("timestamp_tz", now, "'$now'") { it.getInstant(0) }, - ) - - driver.execute("drop table if exists all_types") - driver.execute(createTable) - assertEquals(0, driver.execute("select * from all_types") {}.size) - - driver.execute( - "insert into all_types(${ - list.joinToString(separator = ", ") { it.column } - }) values(${ - list.joinToString(separator = ", ") { it.insertValue.toString() } - })" - ) - - list - .asSequence() - .filter { it.value !is ByteArray } - .forEach { - assertEquals( - it.value, - driver.execute("select ${it.column} from all_types") { rs -> it.extractor(rs) }.first() - ) + runBlocking { + val now = Clock.System.now().let { Instant.fromEpochSeconds(it.epochSeconds, 0) } + val localDateTime = now.toLocalDateTime(TimeZone.currentSystemDefault()) + + val byteArrayHolder = with("\\xdeadbeef".encodeToByteArray()) { + Holder( + "bytea", + this, "'${this.decodeToString()}'" + ) { it.getBytes(0) } } - with(byteArrayHolder) { - val select = driver.execute("select ${this.column} from all_types") { rs -> this.extractor(rs) } - .first()!! - this.value.forEach { byte -> assertContains(select, byte) } + val list = listOf( + Holder("id", 123L) { it.getLong(0) }, + Holder("name", "name", "'name'") { it.getString(0) }, + Holder("email", "mail@mail.com", "'mail@mail.com'") { it.getString(0) }, + Holder("bool", true) { it.getBoolean(0) }, + Holder("short", Short.MIN_VALUE) { it.getShort(0) }, + Holder("int", 234) { it.getInt(0) }, + Holder("float", 1.23f) { it.getFloat(0) }, + Holder("double", 2.34) { it.getDouble(0) }, + byteArrayHolder, + with(Clock.System.todayIn(TimeZone.currentSystemDefault())) { + Holder("date", this, "'$this'") { it.getDate(0) } + }, + with( + LocalTime(localDateTime.time.hour, localDateTime.time.minute, localDateTime.time.second, 0) + ) { + Holder("time", this, this.let { + "'${it.hour.toTimeDigit()}:${it.minute.toTimeDigit()}:${it.second.toTimeDigit()}'" + }) { it.getTime(0) } + }, + Holder("timestamp", localDateTime, "'$localDateTime'") { it.getLocalDateTime(0) }, + Holder("timestamp_tz", now, "'$now'") { it.getInstant(0) }, + ) + + driver.execute("drop table if exists all_types") + driver.execute(createTable) + assertEquals(0, driver.execute("select * from all_types") {}.size) + + driver.execute( + "insert into all_types(${ + list.joinToString(separator = ", ") { it.column } + }) values(${ + list.joinToString(separator = ", ") { it.insertValue.toString() } + })" + ) + + list + .asSequence() + .filter { it.value !is ByteArray } + .forEach { + assertEquals( + it.value, + driver.execute("select ${it.column} from all_types") { rs -> it.extractor(rs) }.first() + ) + } + + with(byteArrayHolder) { + val select = driver.execute("select ${this.column} from all_types") { rs -> this.extractor(rs) } + .first()!! + this.value.forEach { byte -> assertContains(select, byte) } + } + driver.execute("drop table all_types") } - driver.execute("drop table all_types") } } diff --git a/src/commonTest/kotlin/io/github/moreirasantos/pgkn/testutils/TestDriver.kt b/src/commonTest/kotlin/io/github/moreirasantos/pgkn/testutils/TestDriver.kt new file mode 100644 index 0000000..ea6436c --- /dev/null +++ b/src/commonTest/kotlin/io/github/moreirasantos/pgkn/testutils/TestDriver.kt @@ -0,0 +1,13 @@ +package io.github.moreirasantos.pgkn.testutils + +import io.github.moreirasantos.pgkn.PostgresDriver + +object TestDriver { + val driver = PostgresDriver( + host = "localhost", + port = 5678, + database = "postgres", + user = "postgres", + password = "postgres", + ) +}