Skip to content

Commit

Permalink
Add basic SQLite support (#2)
Browse files Browse the repository at this point in the history
* Remove todo

* Implement SQLiteTable

* Add unimplemented upsert

* Use DatabaseConnection#execute
  • Loading branch information
MineKing9534 authored Oct 18, 2024
1 parent 4443805 commit 155d70f
Show file tree
Hide file tree
Showing 22 changed files with 1,339 additions and 22 deletions.
3 changes: 3 additions & 0 deletions core/src/main/kotlin/de/mineking/database/Conditions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ infix fun Node.isLike(other: String) = Where(this + " like '" + other + "'")
infix fun Node.matches(other: String) = Where(this + " ~ '" + other + "'")
infix fun Node.contains(other: Node) = Where(other + " = any(" + this + ")")

fun Node.isIn(nodes: Array<Node>) = Where(this + " in (" + nodes.join() + ")")
fun Node.isIn(nodes: Collection<Node>) = isIn(nodes.toTypedArray())

infix fun Node.isGreaterThan(other: Node) = Where(this + " > " + other)
infix fun Node.isGreaterThanOrEqual(other: Node) = Where(this + " >= " + other)

Expand Down
47 changes: 41 additions & 6 deletions core/src/main/kotlin/de/mineking/database/TypeMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package de.mineking.database

import org.jdbi.v3.core.argument.Argument
import org.jdbi.v3.core.statement.StatementContext
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.Types
Expand Down Expand Up @@ -47,6 +51,19 @@ interface TypeMapper<T, D> {

fun write(column: ColumnData<*, *>?, table: TableStructure<*>, type: KType, value: T): Argument = createArgument(column, table, type, format(column, table, type, value))
fun read(column: DirectColumnData<*, *>?, type: KType, context: ReadContext, name: String): T = parse(column, type, extract(column, type, context, name), context, name)

fun writeToBinary(column: ColumnData<*, *>?, table: TableStructure<*>, type: KType, value: T): ByteArray = toBinary(column, table, type, format(column, table, type, value))
fun toBinary(column: ColumnData<*, *>?, table: TableStructure<*>, type: KType, value: D): ByteArray {
val output = ByteArrayOutputStream()

ObjectOutputStream(output).use { it.writeObject(value) }

return output.toByteArray()
}

@Suppress("UNCHECKED_CAST")
fun fromBinary(column: DirectColumnData<*, *>?, type: KType, value: ByteArray, context: ReadContext, name: String): D = ObjectInputStream(ByteArrayInputStream(value)).use { it.readObject() } as D
fun readFromBinary(column: DirectColumnData<*, *>?, type: KType, value: ByteArray, context: ReadContext, name: String): T = parse(column, type, fromBinary(column, type, value, context, name), context, name)
}

interface SimpleTypeMapper<T> : TypeMapper<T, T> {
Expand Down Expand Up @@ -75,22 +92,21 @@ inline fun <reified T> typeMapper(
}

override fun extract(column: DirectColumnData<*, *>?, type: KType, context: ReadContext, name: String): T = context.read(name, extractor)

override fun toString() = typeOf<T>().toString()
}

inline fun <reified T> nullSafeTypeMapper(
dataType: DataType,
noinline extractor: (ResultSet, String) -> T?,
crossinline inserter: (T?, PreparedStatement, Int) -> Unit = { value, statement, position ->
if (value == null) statement.setNull(position, Types.NULL)
else statement.setObject(position, value)
},
noinline extractor: (ResultSet, String) -> T,
crossinline inserter: (T, PreparedStatement, Int) -> Unit = { value, statement, position -> statement.setObject(position, value) },
crossinline acceptor: (KType) -> Boolean = { it.isSubtypeOf(typeOf<T?>()) }
) = typeMapper<T?>(dataType, { set, name ->
val temp = extractor(set, name)

if (set.wasNull()) null
else temp
}, inserter, acceptor)
}, { value, statement, position -> if (value == null) statement.setNull(position, Types.NULL) else inserter(value, statement, position) }, acceptor)

inline fun <reified T, reified D> typeMapper(
temporary: TypeMapper<D, *>,
Expand All @@ -105,4 +121,23 @@ inline fun <reified T, reified D> typeMapper(

override fun extract(column: DirectColumnData<*, *>?, type: KType, context: ReadContext, name: String): D = temporary.read(column, type, context, name)
override fun parse(column: DirectColumnData<*, *>?, type: KType, value: D, context: ReadContext, name: String): T = parser(value)

override fun toString() = typeOf<T>().toString()
}

inline fun <reified T> binaryTypeMapper(
dataType: DataType,
crossinline parser: (ByteArray) -> T?,
crossinline formatter: (T?) -> ByteArray
): TypeMapper<T?, ByteArray> = object : TypeMapper<T?, ByteArray> {
override fun accepts(manager: DatabaseConnection, property: KProperty<*>?, type: KType): Boolean = type.isSubtypeOf(typeOf<T>())
override fun getType(column: ColumnData<*, *>?, table: TableStructure<*>, property: KProperty<*>?, type: KType): DataType = dataType

override fun format(column: ColumnData<*, *>?, table: TableStructure<*>, type: KType, value: T?): ByteArray = formatter(value)

override fun extract(column: DirectColumnData<*, *>?, type: KType, context: ReadContext, name: String): ByteArray = context.read(name, ResultSet::getBytes)
override fun parse(column: DirectColumnData<*, *>?, type: KType, value: ByteArray, context: ReadContext, name: String): T? = parser(value)

override fun toBinary(column: ColumnData<*, *>?, table: TableStructure<*>, type: KType, value: ByteArray): ByteArray = value
override fun fromBinary(column: DirectColumnData<*, *>?, type: KType, value: ByteArray, context: ReadContext, name: String): ByteArray = value
}
3 changes: 1 addition & 2 deletions sqlite/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ dependencies {
implementation(project(":core"))
implementation("org.jdbi:jdbi3-core:3.45.4")
implementation("org.jdbi:jdbi3-kotlin:3.45.4")
implementation("org.xerial:sqlite-jdbc:3.46.1.0")

implementation(kotlin("reflect"))
implementation("com.google.code.gson:gson:2.10.1")

testImplementation("org.xerial:sqlite-jdbc:3.46.1.0")

testImplementation("ch.qos.logback:logback-classic:1.5.8")
implementation("io.github.microutils:kotlin-logging-jvm:2.0.11")
}
191 changes: 182 additions & 9 deletions sqlite/src/main/kotlin/de/mineking/database/vendors/SQLiteTable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ package de.mineking.database.vendors
import de.mineking.database.*
import de.mineking.database.vendors.SQLiteConnection.Companion.logger
import org.jdbi.v3.core.kotlin.useHandleUnchecked
import org.jdbi.v3.core.result.ResultIterable
import org.jdbi.v3.core.statement.UnableToExecuteStatementException
import org.jdbi.v3.core.statement.Update
import org.sqlite.SQLiteErrorCode
import org.sqlite.SQLiteException
import kotlin.reflect.KClass
import kotlin.reflect.KType

Expand Down Expand Up @@ -44,39 +49,207 @@ class SQLiteTable<T: Any>(
} }
}


override fun selectRowCount(where: Where): Int {
TODO("Not yet implemented")
val sql = """
select count(*) from ${ structure.name }
${ createJoinList(structure.columns).joinToString(" ") }
${ where.format(structure) }
""".trim().replace("\\s+".toRegex(), " ")

return structure.manager.execute { it.createQuery(sql)
.bindMap(where.values(structure))
.mapTo(Int::class.java)
.first()
}
}

private fun createJoinList(columns: Collection<DirectColumnData<*, *>>, prefix: Array<String> = emptyArray()): List<String> {
val temp = columns.filter { it.reference != null }.filter { !it.type.isArray() }

if (temp.isEmpty()) return emptyList()
return temp.flatMap { listOf("""
left join ${ it.reference!!.structure.name }
as "${ (prefix + it.name).joinToString(".") }"
on ${ (
unsafeNode("\"${(prefix + it.name).joinToString(".")}\".\"${it.reference!!.structure.getKeys().first().name}\"")
isEqualTo
unsafeNode("\"${prefix.joinToString(".").takeIf { it.isNotBlank() } ?: structure.name}\".\"${it.name}\"")
).get(structure) }
""") + createJoinList(it.reference!!.structure.columns, prefix + it.name) }
}

private fun createSelect(columns: String, where: Where, order: Order?, limit: Int?, offset: Int?): String = """
select $columns
from ${ structure.name }
${ createJoinList(structure.columns.reversed()).joinToString(" ") }
${ where.format(structure) }
${ order?.format() ?: "" }
${ limit?.let { "limit $it" } ?: "" }
${ offset?.let { "offset $it" } ?: "" }
""".trim().replace("\\s+".toRegex(), " ")

override fun select(vararg columns: String, where: Where, order: Order?, limit: Int?, offset: Int?): QueryResult<T> {
TODO("Not yet implemented")
fun createColumnList(columns: Collection<ColumnData<*, *>>, prefix: Array<String> = emptyArray()): List<Pair<String, String>> {
if (columns.isEmpty()) return emptyList()
return columns
.filterIsInstance<DirectColumnData<*, *>>()
.filter { it.reference != null }
.filter { !it.type.isArray() }
.flatMap { createColumnList(it.reference!!.structure.getAllColumns(), prefix + it.name) } +
columns.map { (prefix.joinToString(".").takeIf { it.isNotBlank() } ?: structure.name) to it.name }
}

val columnList = createColumnList(
if (columns.isEmpty()) structure.getAllColumns()
else columns.map { parseColumnSpecification(it, structure).column }.toSet()
)

val sql = createSelect(columnList.joinToString { "\"${it.first}\".\"${it.second}\" as \"${it.first}.${it.second}\"" }, where, order, limit, offset)
return object : RowQueryResult<T> {
override val instance: () -> T = this@SQLiteTable.instance
override fun <O> execute(handler: ((T) -> Boolean) -> O): O = structure.manager.execute { it.createQuery(sql)
.bindMap(where.values(structure))
.execute { stmt, _ ->
val statement = stmt.get()
val set = statement.resultSet

handler { parseResult(ReadContext(it, structure, set, columnList.map { "${ it.first }.${ it.second }" })) }
}
}
}
}

override fun <C> select(target: Node, type: KType, where: Where, order: Order?, limit: Int?, offset: Int?): QueryResult<C> {
TODO("Not yet implemented")
val column = target.columnContext(structure)
val mapper = structure.manager.getTypeMapper<C, Any>(type, column?.column?.getRootColumn()?.property) ?: throw IllegalArgumentException("No suitable TypeMapper found")

fun createColumnList(columns: List<ColumnData<*, *>>, prefix: Array<String> = emptyArray()): List<Pair<String, String>> {
if (columns.isEmpty()) return emptyList()
return columns
.filterIsInstance<DirectColumnData<*, *>>()
.filter { it.reference != null }
.flatMap { createColumnList(it.reference!!.structure.getAllColumns(), prefix + it.name
) } +
(columns + columns.flatMap { if (it is DirectColumnData) it.getChildren() else emptyList() }).map { (prefix.joinToString(".").takeIf { it.isNotBlank() } ?: structure.name) to it.name }
}

val columnList = createColumnList(column?.column?.let { listOf(it) } ?: emptyList())

val sql = createSelect((columnList.map { "\"${ it.first }\".\"${ it.second }\" as \"${ it.first }.${ it.second }\"" } + "(${ target.format(structure) }) as \"value\"").joinToString(), where, order, limit, offset)
return object : ValueQueryResult<C> {
override fun <O> execute(handler: (ResultIterable<C>) -> O): O = structure.manager.execute { handler(it.createQuery(sql)
.bindMap(target.values(structure, column?.column))
.bindMap(where.values(structure))
.map { set, _ -> mapper.read(column?.column?.getRootColumn(), type, ReadContext(it, structure, set, columnList.map { "${ it.first }.${ it.second }" } + "value", autofillPrefix = { it != "value" }, shouldRead = false), "value") }
) }
}
}

private fun executeUpdate(update: Update, obj: T) {
val columns = structure.getAllColumns()

columns.forEach {
fun <C> createArgument(column: ColumnData<T, C>) = column.mapper.write(column, structure, column.type, column.get(obj))
update.bind(it.name, createArgument(it))
}

return update.execute { stmt, _ ->
val statement = stmt.get()
val set = statement.resultSet

parseResult(ReadContext(obj, structure, set, columns.filter { it.getRootColumn().reference == null }.map { it.name }, autofillPrefix = { false }))
}
}

private fun <T> createResult(function: () -> T): UpdateResult<T> {
return try {
UpdateResult(function(), null, uniqueViolation = false, notNullViolation = false)
} catch (e: UnableToExecuteStatementException) {
val sqlException = e.cause as SQLiteException
val result = UpdateResult<T>(null, sqlException, sqlException.resultCode == SQLiteErrorCode.SQLITE_CONSTRAINT_UNIQUE || sqlException.resultCode == SQLiteErrorCode.SQLITE_CONSTRAINT_PRIMARYKEY, sqlException.resultCode == SQLiteErrorCode.SQLITE_CONSTRAINT_NOTNULL)

if (!result.uniqueViolation && !result.notNullViolation) throw e
result
}
}

override fun update(obj: T): UpdateResult<T> {
TODO("Not yet implemented")
if (obj is DataObject<*>) obj.beforeWrite()
val identity = identifyObject(obj)

val columns = structure.getAllColumns().filter { !it.getRootColumn().key }

val sql = """
update ${ structure.name }
set ${columns.joinToString { "\"${it.name}\" = :${it.name}" }}
${ identity.format(structure) }
returning *
""".trim().replace("\\s+".toRegex(), " ")

return createResult {
structure.manager.execute { executeUpdate(it.createUpdate(sql).bindMap(identity.values(structure)), obj) }
if (obj is DataObject<*>) obj.afterRead()
obj
}
}

override fun update(column: String, value: Node, where: Where): UpdateResult<Int> {
TODO("Not yet implemented")
override fun update(column: String, value: Node, where: Where): UpdateResult<Int > {
val spec = parseColumnSpecification(column, structure)

require(spec.context.isEmpty()) { "Cannot update reference, update in the table directly" }
require(!spec.column.getRootColumn().key) { "Cannot update key" }

val sql = """
update ${ structure.name }
set ${ spec.build(false) } = ${ value.format(structure) }
${ where.format(structure) }
""".trim().replace("\\s+".toRegex(), " ")

return createResult { structure.manager.execute { it.createUpdate(sql)
.bindMap(value.values(structure, spec.column))
.bindMap(where.values(structure))
.execute()
} }
}

override fun insert(obj: T): UpdateResult<T> {
TODO("Not yet implemented")
if (obj is DataObject<*>) obj.beforeWrite()

val columns = structure.getAllColumns().filter {
if (!it.getRootColumn().autogenerate) true
else {
val value = it.get(obj)
value != 0 && value != null
}
}

val sql = """
insert into ${ structure.name }
(${columns.joinToString { "\"${it.name}\"" }})
values(${columns.joinToString { ":${it.name}" }})
returning *
""".trim().replace("\\s+".toRegex(), " ")

return createResult {
structure.manager.execute { executeUpdate(it.createUpdate(sql), obj) }
if (obj is DataObject<*>) obj.afterRead()
obj
}
}

override fun upsert(obj: T): UpdateResult<T> {
TODO("Not yet implemented")
}

/**
* Does not support reference conditions (because postgres doesn't allow join in delete)
*/
override fun delete(where: Where): Int {
TODO("Not yet implemented")
val sql = "delete from ${ structure.name } ${ where.format(structure) }"
return structure.manager.execute { it.createUpdate(sql)
.bindMap(where.values(structure))
.execute()
}
}
}
Loading

0 comments on commit 155d70f

Please sign in to comment.