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

Fix JdbcSqliteDriver url parsing when choosing ConnectionManager type #4656

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.squareup.sqldelight.driver.test

import app.cash.sqldelight.Query
import app.cash.sqldelight.db.AfterVersion
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlCursor
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.db.SqlSchema
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull

/**
* Test for SQLite ephemeral database configurations
* */
abstract class EphemeralTest {

protected enum class Type {
IN_MEMORY,
NAMED,
TEMPORARY,
}

protected val schema = object : SqlSchema<QueryResult.Value<Unit>> {
override val version: Long = 1

override fun create(driver: SqlDriver): QueryResult.Value<Unit> {
driver.execute(
null,
"""
CREATE TABLE test (
id INTEGER NOT NULL PRIMARY KEY,
value TEXT NOT NULL
);
""".trimIndent(),
0,
)
return QueryResult.Unit
}

override fun migrate(
driver: SqlDriver,
oldVersion: Long,
newVersion: Long,
vararg callbacks: AfterVersion,
): QueryResult.Value<Unit> {
// No-op.
return QueryResult.Unit
}
}

private val mapper = { cursor: SqlCursor ->
TestData(
cursor.getLong(0)!!,
cursor.getString(1)!!,
)
}

protected abstract fun setupDatabase(type: Type): SqlDriver

@Test
fun inMemoryCreatesIndependentDatabase() {
val data1 = TestData(1, "val1")
val driver1 = setupDatabase(Type.IN_MEMORY)
driver1.insertTestData(data1)
assertEquals(data1, driver1.testDataQuery().executeAsOne())

val driver2 = setupDatabase(Type.IN_MEMORY)
assertNull(driver2.testDataQuery().executeAsOneOrNull())
driver1.close()
driver2.close()
}

@Test
fun temporaryCreatesIndependentDatabase() {
val data1 = TestData(1, "val1")
val driver1 = setupDatabase(Type.TEMPORARY)
driver1.insertTestData(data1)
assertEquals(data1, driver1.testDataQuery().executeAsOne())

val driver2 = setupDatabase(Type.TEMPORARY)
assertNull(driver2.testDataQuery().executeAsOneOrNull())
driver1.close()
driver2.close()
}

@Test
fun namedCreatesSharedDatabase() {
val data1 = TestData(1, "val1")
val driver1 = setupDatabase(Type.NAMED)
driver1.insertTestData(data1)
assertEquals(data1, driver1.testDataQuery().executeAsOne())

val driver2 = setupDatabase(Type.NAMED)
assertEquals(data1, driver2.testDataQuery().executeAsOne())
driver1.close()
assertEquals(data1, driver2.testDataQuery().executeAsOne())
driver2.close()

val driver3 = setupDatabase(Type.NAMED)
assertNull(driver3.testDataQuery().executeAsOneOrNull())
driver3.close()
}

private fun SqlDriver.insertTestData(testData: TestData) {
execute(1, "INSERT INTO test VALUES (?, ?)", 2) {
bindLong(0, testData.id)
bindString(1, testData.value)
}
}

private fun SqlDriver.testDataQuery(): Query<TestData> {
return object : Query<TestData>(mapper) {
override fun <R> execute(mapper: (SqlCursor) -> QueryResult<R>): QueryResult<R> {
return executeQuery(0, "SELECT * FROM test", mapper, 0, null)
}

override fun addListener(listener: Listener) {
addListener("test", listener = listener)
}

override fun removeListener(listener: Listener) {
removeListener("test", listener = listener)
}
}
}

private data class TestData(val id: Long, val value: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import app.cash.sqldelight.Query
import app.cash.sqldelight.driver.jdbc.ConnectionManager
import app.cash.sqldelight.driver.jdbc.ConnectionManager.Transaction
import app.cash.sqldelight.driver.jdbc.JdbcDriver
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver.Companion.IN_MEMORY
import java.sql.Connection
import java.sql.DriverManager
import java.sql.PreparedStatement
Expand All @@ -14,8 +13,24 @@ import kotlin.concurrent.getOrSet
@Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE")
class JdbcSqliteDriver constructor(
/**
* Database connection URL in the form of `jdbc:sqlite:path` where `path` is either blank
* (creating an in-memory database) or a path to a file.
* Database connection URL in the form of `jdbc:sqlite:path?key1=value1&...` where:
* - `jdbc:sqlite:` is the prefix which instructs [DriverManager] to open a connection
* using the provided [org.sqlite.JDBC] Driver.
* - `path` is a file path which instructs sqlite *where* it should open the database
* connection.
* - `?key1=value1&...` is an optional query string which instruct sqlite *how* it
* should open the connection.
*
* Examples:
* - `jdbc:sqlite:/path/to/myDatabase.db` opens a database connection, writing changes
* to the filesystem at the specified `path`.
* - `jdbc:sqlite:` (i.e. an empty path) will create a temporary database whereby the
* temp file is deleted upon connection closure.
* - `jdbc:sqlite::memory:` will create a purely in-memory database.
* - `jdbc:sqlite:file:memdb1?mode=memory&cache=shared` will create a named in-memory
* database which can be shared across connections until all are closed.
*
* [sqlite.org/inmemorydb](https://www.sqlite.org/inmemorydb.html)
*/
url: String,
properties: Properties = Properties(),
Expand Down Expand Up @@ -51,9 +66,17 @@ class JdbcSqliteDriver constructor(
}
}

private fun connectionManager(url: String, properties: Properties) = when (url) {
IN_MEMORY -> InMemoryConnectionManager(properties)
else -> ThreadedConnectionManager(url, properties)
private fun connectionManager(url: String, properties: Properties): ConnectionManager {
val path = url.substringBefore('?').substringAfter("jdbc:sqlite:")

return when {
path.isEmpty() ||
path == ":memory:" ||
path == "file::memory:" ||
path.startsWith(":resource:") ||
url.contains("mode=memory") -> StaticConnectionManager(url, properties)
else -> ThreadedConnectionManager(url, properties)
}
}

private abstract class JdbcSqliteDriverConnectionManager : ConnectionManager {
Expand All @@ -70,11 +93,12 @@ private abstract class JdbcSqliteDriverConnectionManager : ConnectionManager {
}
}

private class InMemoryConnectionManager(
private class StaticConnectionManager(
url: String,
properties: Properties,
) : JdbcSqliteDriverConnectionManager() {
override var transaction: Transaction? = null
private val connection: Connection = DriverManager.getConnection(IN_MEMORY, properties)
private val connection: Connection = DriverManager.getConnection(url, properties)

override fun getConnection() = connection
override fun closeConnection(connection: Connection) = Unit
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.squareup.sqldelight.driver.sqlite

import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import com.squareup.sqldelight.driver.test.EphemeralTest

class SqliteEphemeralTest : EphemeralTest() {
override fun setupDatabase(type: Type): SqlDriver {
val suffix = when (type) {
Type.IN_MEMORY -> ":memory:"
Type.NAMED -> "file:memdb1?mode=memory&cache=shared"
Type.TEMPORARY -> ""
}

return JdbcSqliteDriver("jdbc:sqlite:$suffix", schema = schema)
}
}