diff --git a/core/src/main/kotlin/de/mineking/database/Conditions.kt b/core/src/main/kotlin/de/mineking/database/Conditions.kt index f981e8d..f18cf02 100644 --- a/core/src/main/kotlin/de/mineking/database/Conditions.kt +++ b/core/src/main/kotlin/de/mineking/database/Conditions.kt @@ -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) = Where(this + " in (" + nodes.join() + ")") +fun Node.isIn(nodes: Collection) = isIn(nodes.toTypedArray()) + infix fun Node.isGreaterThan(other: Node) = Where(this + " > " + other) infix fun Node.isGreaterThanOrEqual(other: Node) = Where(this + " >= " + other) diff --git a/core/src/main/kotlin/de/mineking/database/TypeMapper.kt b/core/src/main/kotlin/de/mineking/database/TypeMapper.kt index 5790b40..84cc839 100644 --- a/core/src/main/kotlin/de/mineking/database/TypeMapper.kt +++ b/core/src/main/kotlin/de/mineking/database/TypeMapper.kt @@ -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 @@ -47,6 +51,19 @@ interface TypeMapper { 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 : TypeMapper { @@ -75,22 +92,21 @@ inline fun typeMapper( } override fun extract(column: DirectColumnData<*, *>?, type: KType, context: ReadContext, name: String): T = context.read(name, extractor) + + override fun toString() = typeOf().toString() } inline fun 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()) } ) = typeMapper(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 typeMapper( temporary: TypeMapper, @@ -105,4 +121,23 @@ inline fun 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().toString() +} + +inline fun binaryTypeMapper( + dataType: DataType, + crossinline parser: (ByteArray) -> T?, + crossinline formatter: (T?) -> ByteArray +): TypeMapper = object : TypeMapper { + override fun accepts(manager: DatabaseConnection, property: KProperty<*>?, type: KType): Boolean = type.isSubtypeOf(typeOf()) + 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 } \ No newline at end of file diff --git a/sqlite/build.gradle.kts b/sqlite/build.gradle.kts index c1be031..2060af1 100644 --- a/sqlite/build.gradle.kts +++ b/sqlite/build.gradle.kts @@ -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") } \ No newline at end of file diff --git a/sqlite/src/main/kotlin/de/mineking/database/vendors/SQLiteTable.kt b/sqlite/src/main/kotlin/de/mineking/database/vendors/SQLiteTable.kt index b84cfb9..f3f0466 100644 --- a/sqlite/src/main/kotlin/de/mineking/database/vendors/SQLiteTable.kt +++ b/sqlite/src/main/kotlin/de/mineking/database/vendors/SQLiteTable.kt @@ -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 @@ -44,39 +49,207 @@ class SQLiteTable( } } } + 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>, prefix: Array = emptyArray()): List { + 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 { - TODO("Not yet implemented") + fun createColumnList(columns: Collection>, prefix: Array = emptyArray()): List> { + if (columns.isEmpty()) return emptyList() + return columns + .filterIsInstance>() + .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 { + override val instance: () -> T = this@SQLiteTable.instance + override fun 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 select(target: Node, type: KType, where: Where, order: Order?, limit: Int?, offset: Int?): QueryResult { - TODO("Not yet implemented") + val column = target.columnContext(structure) + val mapper = structure.manager.getTypeMapper(type, column?.column?.getRootColumn()?.property) ?: throw IllegalArgumentException("No suitable TypeMapper found") + + fun createColumnList(columns: List>, prefix: Array = emptyArray()): List> { + if (columns.isEmpty()) return emptyList() + return columns + .filterIsInstance>() + .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 { + override fun execute(handler: (ResultIterable) -> 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 createArgument(column: ColumnData) = 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 createResult(function: () -> T): UpdateResult { + return try { + UpdateResult(function(), null, uniqueViolation = false, notNullViolation = false) + } catch (e: UnableToExecuteStatementException) { + val sqlException = e.cause as SQLiteException + val result = UpdateResult(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 { - 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 { - TODO("Not yet implemented") + override fun update(column: String, value: Node, where: Where): UpdateResult { + 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 { - 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 { 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() + } } } \ No newline at end of file diff --git a/sqlite/src/main/kotlin/de/mineking/database/vendors/SQLiteTypes.kt b/sqlite/src/main/kotlin/de/mineking/database/vendors/SQLiteTypes.kt index 70cf24e..32f9150 100644 --- a/sqlite/src/main/kotlin/de/mineking/database/vendors/SQLiteTypes.kt +++ b/sqlite/src/main/kotlin/de/mineking/database/vendors/SQLiteTypes.kt @@ -1,29 +1,177 @@ package de.mineking.database.vendors import de.mineking.database.* +import org.jdbi.v3.core.argument.Argument +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.sql.Blob +import java.sql.Date import java.sql.ResultSet +import java.sql.Timestamp +import java.time.* import kotlin.reflect.KProperty import kotlin.reflect.KType import kotlin.reflect.jvm.jvmErasure object SQLiteMappers { - val ANY = typeMapper(DataType.VALUE, { set, name -> error("No suitable TypeMapper for insertion found") }, { value, statement, pos -> statement.setObject(pos, value) }) + val ANY = typeMapper(DataType.VALUE, { _, _ -> error("No suitable TypeMapper for insertion found") }, { value, statement, pos -> statement.setObject(pos, value) }) - val INTEGER = typeMapper(SQLiteType.INTEGER, ResultSet::getInt) - val BOOLEAN = typeMapper(SQLiteType.INTEGER, { set, name -> set.getInt(name) > 0 }) + val BOOLEAN = nullSafeTypeMapper(SQLiteType.INTEGER, { set, name -> set.getInt(name) > 0 }, { value, statement, position -> statement.setInt(position, if (value) 1 else 0) }) + val BYTE_ARRAy = typeMapper(SQLiteType.BLOB, { set, name -> set.getBytes(name) }) + val BLOB = typeMapper(SQLiteType.BLOB, { set, name -> set.getBlob(name) }) + + val SHORT = nullSafeTypeMapper(SQLiteType.INTEGER, ResultSet::getShort) + val INTEGER = nullSafeTypeMapper(SQLiteType.INTEGER, ResultSet::getInt) + val LONG = nullSafeTypeMapper(SQLiteType.INTEGER, ResultSet::getLong) + + val FLOAT = nullSafeTypeMapper(SQLiteType.REAL, ResultSet::getFloat) + val DOUBLE = nullSafeTypeMapper(SQLiteType.REAL, ResultSet::getDouble) val STRING = typeMapper(SQLiteType.TEXT, ResultSet::getString) val ENUM = object : TypeMapper?, String?> { override fun accepts(manager: DatabaseConnection, property: KProperty<*>?, type: KType): Boolean = type.jvmErasure.java.isEnum + override fun getType(column: ColumnData<*, *>?, table: TableStructure<*>, property: KProperty<*>?, type: KType): DataType = SQLiteType.TEXT override fun format(column: ColumnData<*, *>?, table: TableStructure<*>, type: KType, value: Enum<*>?): String? = value?.name override fun extract(column: DirectColumnData<*, *>?, type: KType, context: ReadContext, name: String): String? = STRING.extract(column, type, context, name) - override fun parse(column: DirectColumnData<*, *>?, type: KType, value: String?, context: ReadContext, name: String): Enum<*>? = value?.let { name -> type.jvmErasure.java.enumConstants.map { it as Enum<*> }.filter { it.name == name }.first() } + override fun parse(column: DirectColumnData<*, *>?, type: KType, value: String?, context: ReadContext, name: String): Enum<*>? = value?.let { name -> type.jvmErasure.java.enumConstants.map { it as Enum<*> }.first { it.name == name } } + } + + val INSTANT = typeMapper(SQLiteType.INTEGER, { set, name -> set.getTimestamp(name).toInstant() }, { value, statement, position -> statement.setTimestamp(position, value?.let { Timestamp.from(it) }) }) + val LOCAL_DATE_TIME = typeMapper(SQLiteType.INTEGER, { set, name -> set.getTimestamp(name).toLocalDateTime() }, { value, statement, position -> statement.setTimestamp(position, value?.let { Timestamp.valueOf(it) }) }) + val LOCAL_DATE = typeMapper(SQLiteType.INTEGER, { set, name -> set.getDate(name).toLocalDate() }, { value, statement, position -> statement.setDate(position, value?.let { Date.valueOf(it) }) }) + + val ARRAY = object : TypeMapper { + fun Any.asArray(): Array<*> = when (this) { + is Array<*> -> this + is Collection<*> -> this.toTypedArray() + else -> error("Invalid type") + } + + fun Collection<*>.createArray(component: KType) = (this as java.util.Collection<*>).toArray { java.lang.reflect.Array.newInstance(component.jvmErasure.java, it) as Array<*> } + + override fun accepts(manager: DatabaseConnection, property: KProperty<*>?, type: KType): Boolean = type.isArray() + override fun getType(column: ColumnData<*, *>?, table: TableStructure<*>, property: KProperty<*>?, type: KType): DataType = SQLiteType.BLOB + + override fun initialize(column: DirectColumnData, type: KType) { + val component = type.component() + val componentMapper = column.table.manager.getTypeMapper(component, column.property) ?: throw IllegalArgumentException("No TypeMapper found for $component") + + componentMapper.initialize(column, component) + } + + override fun format(column: ColumnData<*, *>?, table: TableStructure<*>, type: KType, value: Any?): ByteArray { + if (value == null) return ByteArray(0) + + val component = type.component() + val mapper = table.manager.getTypeMapper(component, if (column is DirectColumnData) column.property else null) ?: throw IllegalArgumentException("No TypeMapper found for $component") + + val result = ByteArrayOutputStream() + val array = value.asArray() + + ObjectOutputStream(result).use { stream -> + stream.writeInt(array.size) + array.forEach { + val bytes = mapper.writeToBinary(column, table, component, it) + stream.writeInt(bytes.size) + stream.write(bytes) + } + } + + return result.toByteArray() + } + + 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): Any? { + if (value.isEmpty()) return null + + val component = type.component() + val mapper = context.table.manager.getTypeMapper(component, if (column is DirectColumnData) column.property else null) ?: throw IllegalArgumentException("No TypeMapper found for $component") + + val array = ObjectInputStream(ByteArrayInputStream(value)).use { stream -> + val size = stream.readInt() + (1..size) + .map { stream.readNBytes(stream.readInt()) } + .toTypedArray() + } + + if (column?.reference == null) return type.createCollection(array.map { mapper.readFromBinary(column, component, it, context, name) }.createArray(component)) + else { + @Suppress("UNCHECKED_CAST") + val key = column.reference!!.structure.getKeys()[0] as ColumnData + + val ids = array.map { key.mapper.readFromBinary(column, key.type, it, context, name) } + val rows = column.reference!!.select(where = property(key.name).isIn(ids.map { value(it, key.type) })).list().associateBy { key.get(it) } + println(rows) + + return type.createCollection(ids.map { rows[it] }.asArray()) + } + } + + 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 } - val BLOB = typeMapper(SQLiteType.BLOB, { set, name -> set.getBytes(name) }) + val REFERENCE = object : TypeMapper { + override fun accepts(manager: DatabaseConnection, property: KProperty<*>?, type: KType): Boolean = property?.hasDatabaseAnnotation() == true && !type.isArray() + + override fun initialize(column: DirectColumnData, type: KType) { + val table = column.property.getDatabaseAnnotation()?.table ?: throw IllegalArgumentException("No table specified") + + val reference = column.table.manager.getCachedTable(table) + column.reference = reference + + require(reference.structure.getKeys().size == 1) { "Can only reference a table with exactly one key" } + } + + override fun getType(column: ColumnData<*, *>?, table: TableStructure<*>, property: KProperty<*>?, type: KType): DataType { + require(column is DirectColumnData) { "Something went really wrong" } + + val key = column.reference!!.structure.getKeys().first() + return key.mapper.getType(column, table, property, key.type) + } + + @Suppress("UNCHECKED_CAST") + override fun format(column: ColumnData<*, *>?, table: TableStructure<*>, type: KType, value: Any?): Any? { + require(column is DirectColumnData) { "Cannot use references for virtual columns" } + + val reference = column.reference!! as Table + val key = reference.structure.getKeys().first() as DirectColumnData + + return value?.let { key.mapper.format(column, reference.structure, key.type, key.get(it )) } + } + + @Suppress("UNCHECKED_CAST") + override fun createArgument(column: ColumnData<*, *>?, table: TableStructure<*>, type: KType, value: Any?): Argument { + require(column is DirectColumnData) { "Cannot use references for virtual columns" } + + val reference = column.reference!! as Table + val key = reference.structure.getKeys().first() as DirectColumnData + + return key.mapper.write(key, reference.structure, key.type, value) + } + + override fun extract(column: DirectColumnData<*, *>?, type: KType, context: ReadContext, name: String): Any? = null + + @Suppress("UNCHECKED_CAST") + override fun parse(column: DirectColumnData<*, *>?, type: KType, value: Any?, context: ReadContext, name: String): Any? { + require(column != null) { "Cannot parse reference without column context" } + + val reference = column.reference!! as Table + val key = reference.structure.getKeys().first() as DirectColumnData + + val context = context.nest(column.name, column.reference!!.implementation) + if (key.mapper.read(column, key.type, context, key.name) == null) return null + + column.reference!!.implementation.parseResult(context) + + return context.instance + } + } } enum class SQLiteType(override val sqlName: String) : DataType { diff --git a/sqlite/src/test/kotlin/setup/Objects.kt b/sqlite/src/test/kotlin/setup/Objects.kt new file mode 100644 index 0000000..f298b69 --- /dev/null +++ b/sqlite/src/test/kotlin/setup/Objects.kt @@ -0,0 +1,13 @@ +package setup + +import de.mineking.database.AutoGenerate +import de.mineking.database.Column +import de.mineking.database.Key +import de.mineking.database.Unique + +data class UserDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Unique @Column val email: String = "", + @Column val name: String = "", + @Column val age: Int = 0 +) \ No newline at end of file diff --git a/sqlite/src/test/kotlin/setup/Utils.kt b/sqlite/src/test/kotlin/setup/Utils.kt new file mode 100644 index 0000000..008628e --- /dev/null +++ b/sqlite/src/test/kotlin/setup/Utils.kt @@ -0,0 +1,23 @@ +package setup + +import de.mineking.database.Table +import mu.KotlinLogging +import org.jdbi.v3.core.statement.SqlLogger +import org.jdbi.v3.core.statement.StatementContext + +object ConsoleSqlLogger : SqlLogger { + val logger = KotlinLogging.logger {} + + override fun logBeforeExecution(context: StatementContext?) { + logger.info(context!!.renderedSql) + logger.info(context.binding.toString()) + + println() + } +} + +fun Table<*>.recreate() { + createTable() + dropTable() + createTable() +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/general/Delete.kt b/sqlite/src/test/kotlin/tests/sqlite/general/Delete.kt new file mode 100644 index 0000000..73acbfe --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/general/Delete.kt @@ -0,0 +1,44 @@ +package tests.sqlite.general + +import de.mineking.database.isBetween +import de.mineking.database.property +import de.mineking.database.value +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.UserDao +import setup.recreate +import kotlin.test.assertEquals + +class DeleteTest { + val connection = SQLiteConnection("test.db") + val table = connection.getTable(name = "basic_test") { UserDao() } + + val users = listOf( + UserDao(name = "Tom", email = "tom@example.com", age = 12), + UserDao(name = "Alex", email = "alex@example.com", age = 23), + UserDao(name = "Bob", email = "bob@example.com", age = 50), + UserDao(name = "Eve", email = "eve@example.com", age = 42), + UserDao(name = "Max", email = "max@example.com", age = 20) + ) + + init { + table.recreate() + + users.forEach { table.insert(it) } + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun deleteAll() { + assertEquals(5, table.delete()) + assertEquals(0, table.selectRowCount()) + } + + @Test + fun deleteCondition() { + assertEquals(2, table.delete(where = property("age").isBetween(value(18), value(25)))) + assertEquals(3, table.selectRowCount()) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/general/Insert.kt b/sqlite/src/test/kotlin/tests/sqlite/general/Insert.kt new file mode 100644 index 0000000..f12f978 --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/general/Insert.kt @@ -0,0 +1,62 @@ +package tests.sqlite.general + +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.UserDao +import setup.recreate +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class InsertTest { + val connection = SQLiteConnection("test.db") + val table = connection.getTable(name = "basic_test") { UserDao() } + + val users = listOf( + UserDao(name = "Tom", email = "tom@example.com", age = 12), + UserDao(name = "Alex", email = "alex@example.com", age = 23), + UserDao(name = "Bob", email = "bob@example.com", age = 50), + UserDao(name = "Eve", email = "eve@example.com", age = 42), + UserDao(name = "Max", email = "max@example.com", age = 20) + ) + + init { + table.recreate() + + users.forEach { table.insert(it) } + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun checkIds() { + assertContentEquals(1..5, users.map { it.id }) + } + + @Test + fun insert() { + val obj = UserDao(name = "Test", email = "test@example.com", age = 50) + val result = table.insert(obj) + + assertTrue(result.isSuccess()) + assertEquals(obj, result.value) + assertEquals(6, obj.id) + } + + @Test + fun insertCollision() { + fun checkResult(obj: UserDao) { + val old = obj.copy() + val result = table.insert(obj) + + assertTrue(result.isError()) + assertTrue(result.uniqueViolation) + + assertEquals(old.id, obj.id) + } + + checkResult(UserDao(name = "Test", email = "tom@example.com", age = 50)) + checkResult(UserDao(id = 1, name = "Test", email = "test@example.com", age = 50)) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/general/Select.kt b/sqlite/src/test/kotlin/tests/sqlite/general/Select.kt new file mode 100644 index 0000000..7c0e074 --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/general/Select.kt @@ -0,0 +1,120 @@ +package tests.sqlite.general + +import de.mineking.database.* +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.UserDao +import setup.recreate +import kotlin.test.assertContains +import kotlin.test.assertEquals + +class SelectTest { + val connection = SQLiteConnection("test.db") + val table = connection.getTable(name = "basic_test") { UserDao() } + + val users = listOf( + UserDao(name = "Tom", email = "tom@example.com", age = 12), + UserDao(name = "Alex", email = "alex@example.com", age = 23), + UserDao(name = "Bob", email = "bob@example.com", age = 50), + UserDao(name = "Eve", email = "eve@example.com", age = 42), + UserDao(name = "Max", email = "max@example.com", age = 20) + ) + + init { + table.recreate() + + users.forEach { table.insert(it) } + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun rowCount() { + assertEquals(5, table.selectRowCount()) + } + + @Test + fun selectAll() { + assertEquals(5, table.select().list().size) + } + + @Test + fun selectBetween() { + assertEquals(2, table.selectRowCount(where = property("age").isBetween(value(18), value(25)))) + assertEquals(2, table.select(where = property("age").isBetween(value(18), value(25))).list().size) + } + + @Test + fun selectSingle() { + val result = table.select(where = property("name") isEqualTo value("Max")).list() + + assertEquals(1, result.size) + + assertEquals("Max", result.first().name) + assertEquals(20, result.first().age) + } + + @Test + fun selectSpecifiedColumns() { + val result = table.select("email", "name", where = property("name") isEqualTo value("Max")).list() + + assertEquals(1, result.size) + + assertEquals("Max", result.first().name) + assertEquals(0, result.first().age) //Age has the default value (0) + } + + @Test + fun selectColumn() { + val result = table.select(property("age"), where = property("name") isEqualTo value("Max")).list() + + assertEquals(1, result.size) + assertEquals(20, result.first()) + } + + @Test + fun selectComplex() { + assertEquals(42, table.select(value(42), where = property("name") isEqualTo value("Max")).first()) + + assertEquals(21, table.select(property("age") + " + 1", where = property("name") isEqualTo value("Max")).first()) + assertEquals(40, table.select(property("age") + " * 2", where = property("name") isEqualTo value("Max")).first()) + + assertEquals("MAX", table.select(upperCase(property("name")), where = property("name") isEqualTo value("Max")).first()) + } + + @Test + fun limit() { + val result = table.select(property("name"), limit = 2).list() + + assertEquals(2, result.size) + assertContains(result, "Tom") + assertContains(result, "Alex") + } + + @Test + fun offset() { + val result = table.select(property("name"), limit = 1, offset = 1).list() + + assertEquals(1, result.size) + assertContains(result, "Alex") + } + + @Test + fun order() { + val result1 = table.select(property("name"), order = ascendingBy("id")).list() + + assertEquals("Tom", result1[0]) + assertEquals("Max", result1[4]) + + val result2 = table.select(property("name"), order = descendingBy("id")).list() + + assertEquals("Max", result2[0]) + assertEquals("Tom", result2[4]) + + val result3 = table.select(property("name"), order = ascendingBy("name")).list() + + assertEquals("Alex", result3[0]) + assertEquals("Tom", result3[4]) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/general/Update.kt b/sqlite/src/test/kotlin/tests/sqlite/general/Update.kt new file mode 100644 index 0000000..6854802 --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/general/Update.kt @@ -0,0 +1,57 @@ +package tests.sqlite.general + +import de.mineking.database.isEqualTo +import de.mineking.database.property +import de.mineking.database.select +import de.mineking.database.value +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.UserDao +import setup.recreate +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class UpdateTest { + val connection = SQLiteConnection("test.db") + val table = connection.getTable(name = "basic_test") { UserDao() } + + val users = listOf( + UserDao(name = "Tom", email = "tom@example.com", age = 12), + UserDao(name = "Alex", email = "alex@example.com", age = 23), + UserDao(name = "Bob", email = "bob@example.com", age = 50), + UserDao(name = "Eve", email = "eve@example.com", age = 42), + UserDao(name = "Max", email = "max@example.com", age = 20) + ) + + init { + table.recreate() + + users.forEach { table.insert(it) } + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun updateName() { + assertEquals(1, table.update("name", value("Test"), where = property("id") isEqualTo value(1)).value) + assertEquals("Test", table.select(property("name"), where = property("id") isEqualTo value(1)).first()) + } + + @Test + fun updateConflict() { + val result = table.update("email", value("max@example.com"), where = property("id") isEqualTo value(1)) + + assertTrue(result.isError()) + assertTrue(result.uniqueViolation) + } + + @Test + fun updateObject() { + val firstUser = users[0].copy(name = "Test") + val result = table.update(firstUser) + + assertTrue(result.isSuccess()) + assertEquals(firstUser, result.value) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/reference/DeletedReference.kt b/sqlite/src/test/kotlin/tests/sqlite/reference/DeletedReference.kt new file mode 100644 index 0000000..e8fc5b9 --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/reference/DeletedReference.kt @@ -0,0 +1,58 @@ +package tests.sqlite.reference + +import de.mineking.database.AutoGenerate +import de.mineking.database.Column +import de.mineking.database.Key +import de.mineking.database.Reference +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.UserDao +import setup.recreate +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +data class DeletedReferenceDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Reference("basic_test") @Column val user: UserDao = UserDao(), + @Reference("basic_test") @Column val users: List = emptyList() +) + +class DeletedReferenceTest { + val connection = SQLiteConnection("test.db") + val userTable = connection.getTable(name = "basic_test") { UserDao() } + val referenceTable = connection.getTable(name = "deleted_reference_test") { DeletedReferenceDao() } + + val users = listOf( + UserDao(name = "Tom", email = "tom@example.com", age = 12), + UserDao(name = "Alex", email = "alex@example.com", age = 23), + UserDao(name = "Bob", email = "bob@example.com", age = 50), + UserDao(name = "Eve", email = "eve@example.com", age = 42), + UserDao(name = "Max", email = "max@example.com", age = 20) + ) + + init { + userTable.recreate() + referenceTable.recreate() + + users.forEach { userTable.insert(it) } + + referenceTable.insert(DeletedReferenceDao(user = users[0], users = users.reversed() + null)) + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @BeforeEach + fun delete() { + userTable.delete(users[0]) + userTable.delete(users[1]) + } + + @Test + fun selectAll() { + val result = referenceTable.select().first() + assertEquals(null, result.user as UserDao?) + assertContentEquals(listOf("Max", "Eve", "Bob", null, null, null), result.users.map { it?.name }) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/reference/Reference.kt b/sqlite/src/test/kotlin/tests/sqlite/reference/Reference.kt new file mode 100644 index 0000000..9d7e68a --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/reference/Reference.kt @@ -0,0 +1,109 @@ +package tests.sqlite.reference + +import de.mineking.database.* +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.recreate +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +data class PublisherDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Column val name: String = "" +) + +data class AuthorDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Column val name: String = "", + @Reference("publisher_test") @Column val publisher: PublisherDao = PublisherDao() +) + +data class BookDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Column val title: String = "", + @Column val year: Int = 0, + @Reference(table = "author_test") @Column val author: AuthorDao = AuthorDao(), + @Reference(table = "publisher_test") @Column val publisher: PublisherDao? = PublisherDao() +) + +class ReferenceTest { + val connection = SQLiteConnection("test.db") + val publisherTable = connection.getTable(name = "publisher_test") { PublisherDao() } + val authorTable = connection.getTable(name = "author_test") { AuthorDao() } + val bookTable = connection.getTable(name = "book_test") { BookDao() } + + val publisherA = PublisherDao(name = "A") + val publisherB = PublisherDao(name = "B") + + val shakespeare = AuthorDao(name = "William Shakespeare", publisher = publisherA) + val tolkien = AuthorDao(name = "J.R.R. Tolkien", publisher = publisherB) + + val hamlet = BookDao(title = "Hamlet", year = 1601, author = shakespeare, publisher = null) + val romeoAndJulia = BookDao(title = "Romeo and Julia", year = 1595, author = shakespeare, publisher = publisherA) + + val hobbit = BookDao(title = "The Hobbit", year = 1937, author = tolkien, publisher = publisherA) + val lotr = BookDao(title = "The Lord of the Rings", year = 1949, author = tolkien, publisher = publisherB) + val silmarillion = BookDao(title = "Silmarillion", year = 1977, author = tolkien, publisher = publisherB) + + init { + publisherTable.recreate() + authorTable.recreate() + bookTable.recreate() + + publisherTable.insert(publisherA) + publisherTable.insert(publisherB) + + authorTable.insert(shakespeare) + authorTable.insert(tolkien) + + bookTable.insert(hamlet) + bookTable.insert(romeoAndJulia) + + bookTable.insert(hobbit) + bookTable.insert(lotr) + bookTable.insert(silmarillion) + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun selectAll() { + val result = bookTable.select().list() + + assertEquals(5, result.size) + + assertContentEquals(listOf(hamlet, romeoAndJulia, hobbit, lotr, silmarillion), result) + } + + @Test + fun selectSingleReference() { + assertEquals(2, bookTable.select(where = property("author->name") isEqualTo value("William Shakespeare")).list().size) + assertEquals(3, bookTable.select(where = property("author->name") isEqualTo value("J.R.R. Tolkien")).list().size) + } + + @Test + fun selectDoubleReference() { + assertEquals(2, bookTable.select(where = property("author->publisher->name") isEqualTo value("A")).list().size) + assertEquals(3, bookTable.select(where = property("author->publisher->name") isEqualTo value("B")).list().size) + } + + @Test + fun selectSingle() { + val result = bookTable.select(upperCase(property("title")), where = property("publisher") isNotEqualTo property("author->publisher")).list() + + assertEquals(1, result.size) + assertEquals("THE HOBBIT", result.first()) + } + + @Test + fun selectReference() { + assertEquals(tolkien, bookTable.select(property("author"), where = property("title") isEqualTo value("The Hobbit")).first()) + } + + @Test + fun updateReference() { + bookTable.update("publisher", value(publisherB), where = property("title") isEqualTo value("The Hobbit")) + assertEquals(publisherB, bookTable.select(property("publisher"), where = property("title") isEqualTo value("The Hobbit")).first()) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/reference/ReferenceArray.kt b/sqlite/src/test/kotlin/tests/sqlite/reference/ReferenceArray.kt new file mode 100644 index 0000000..59916dc --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/reference/ReferenceArray.kt @@ -0,0 +1,68 @@ +package tests.sqlite.reference + +import de.mineking.database.* +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.recreate +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +data class ReferenceArrayDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Reference("book_test") @Column val books: List = emptyList() +) + +class ReferenceArrayTest { + val connection = SQLiteConnection("test.db") + val publisherTable = connection.getTable(name = "publisher_test") { PublisherDao() } + val authorTable = connection.getTable(name = "author_test") { AuthorDao() } + val bookTable = connection.getTable(name = "book_test") { BookDao() } + val referenceTable = connection.getTable(name = "reference_array_test") { ReferenceArrayDao() } + + val publisherA = PublisherDao(name = "A") + val publisherB = PublisherDao(name = "B") + + val shakespeare = AuthorDao(name = "William Shakespeare", publisher = publisherA) + val tolkien = AuthorDao(name = "J.R.R. Tolkien", publisher = publisherB) + + val hamlet = BookDao(title = "Hamlet", year = 1601, author = shakespeare, publisher = null) + val romeoAndJulia = BookDao(title = "Romeo and Julia", year = 1595, author = shakespeare, publisher = publisherA) + + val hobbit = BookDao(title = "The Hobbit", year = 1937, author = tolkien, publisher = publisherA) + val lotr = BookDao(title = "The Lord of the Rings", year = 1949, author = tolkien, publisher = publisherB) + val silmarillion = BookDao(title = "Silmarillion", year = 1977, author = tolkien, publisher = publisherB) + + init { + publisherTable.recreate() + authorTable.recreate() + bookTable.recreate() + referenceTable.recreate() + + publisherTable.insert(publisherA) + publisherTable.insert(publisherB) + + authorTable.insert(shakespeare) + authorTable.insert(tolkien) + + bookTable.insert(hamlet) + bookTable.insert(romeoAndJulia) + + bookTable.insert(hobbit) + bookTable.insert(lotr) + bookTable.insert(silmarillion) + + referenceTable.insert(ReferenceArrayDao(books = listOf(hamlet, romeoAndJulia, null, hamlet))) + referenceTable.insert(ReferenceArrayDao(books = listOf(hobbit, lotr, silmarillion, lotr, hobbit, hamlet))) + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun selectAll() { + val result = referenceTable.select().list() + + assertContentEquals(listOf(hamlet, romeoAndJulia, null, hamlet), result[0].books) + assertContentEquals(listOf(hobbit, lotr, silmarillion, lotr, hobbit, hamlet), result[1].books) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/specific/Array.kt b/sqlite/src/test/kotlin/tests/sqlite/specific/Array.kt new file mode 100644 index 0000000..997a2d1 --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/specific/Array.kt @@ -0,0 +1,52 @@ +package tests.sqlite.specific + +import de.mineking.database.* +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.recreate +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +data class ArrayDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Column val a: Int = 0, + @Column val stringList: List = emptyList(), + @Column val arrayList: Array> = emptyArray() +) + +class ArrayTest { + val connection = SQLiteConnection("test.db") + val table = connection.getTable(name = "array_test") { ArrayDao() } + + init { + table.recreate() + + table.insert(ArrayDao( + a = 0, + stringList = listOf("a", "b", "c"), + arrayList = arrayOf(listOf("a", "b"), listOf("c", "d"), listOf("e")) + )) + + table.insert(ArrayDao( + a = 5, + stringList = listOf("d", "e", "f"), + arrayList = emptyArray() + )) + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun selectAll() { + val result = table.select().first() + + assertEquals(3, result.stringList.size) + assertContentEquals(listOf("a", "b", "c"), result.stringList) + + assertEquals(3, result.arrayList.size) + assertArrayEquals(arrayOf(listOf("a", "b"), listOf("c", "d"), listOf("e")), result.arrayList) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/specific/Date.kt b/sqlite/src/test/kotlin/tests/sqlite/specific/Date.kt new file mode 100644 index 0000000..7be96ce --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/specific/Date.kt @@ -0,0 +1,40 @@ +package tests.sqlite.specific + +import de.mineking.database.* +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.recreate +import java.time.Instant +import java.time.LocalDate +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import kotlin.test.assertEquals + +data class DateDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Column val time: Instant = Instant.MIN, + @Column val date: LocalDate = LocalDate.MIN, +) + +class DateTest { + val connection = SQLiteConnection("test.db") + val table = connection.getTable(name = "date_test") { DateDao() } + + val time = Instant.now() + val date = LocalDate.now() + + init { + table.recreate() + + table.insert(DateDao(time = time, date = date)) + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun selectAll() { + assertEquals(time.truncatedTo(ChronoUnit.MILLIS), table.select(property("time")).first().truncatedTo(ChronoUnit.MILLIS)) + assertEquals(date, table.select(property("date")).first()) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/specific/Enum.kt b/sqlite/src/test/kotlin/tests/sqlite/specific/Enum.kt new file mode 100644 index 0000000..305eea9 --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/specific/Enum.kt @@ -0,0 +1,47 @@ +package tests.sqlite.specific + +import de.mineking.database.* +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.recreate +import java.util.* +import kotlin.reflect.full.createType +import kotlin.test.assertContains +import kotlin.test.assertEquals + +enum class TestEnum { A, B, C } +data class EnumDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Column val single: TestEnum = TestEnum.A, + @Column val multi: EnumSet = EnumSet.noneOf(TestEnum::class.java) +) + +class EnumTest { + val connection = SQLiteConnection("test.db") + val table = connection.getTable(name = "enum_test") { EnumDao() } + + init { + table.recreate() + + table.insert(EnumDao(single = TestEnum.A, multi = EnumSet.of(TestEnum.A, TestEnum.C))) + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun selectAll() { + val result = table.select().first() + + assertEquals(TestEnum.A, result.single) + + assertEquals(2, result.multi.size) + assertContains(result.multi, TestEnum.A) + assertContains(result.multi, TestEnum.C) + } + + @Test + fun selectSingle() { + assertEquals(TestEnum.A, table.select(property("single")).first()) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/specific/Null.kt b/sqlite/src/test/kotlin/tests/sqlite/specific/Null.kt new file mode 100644 index 0000000..6e4c2b7 --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/specific/Null.kt @@ -0,0 +1,66 @@ +package tests.sqlite.specific + +import de.mineking.database.* +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.recreate +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +data class NullDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Column val test: String? = null, + @Column val name: String = "" +) + +class NullTest { + val connection = SQLiteConnection("test.db") + val table = connection.getTable(name = "null_test") { NullDao() } + + init { + table.recreate() + + table.insert(NullDao(test = "abc", name = "not-null")) + table.insert(NullDao(test = null, name = "null")) + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun selectAll() { + val result = table.select(property("test")).list() + + assertEquals(2, result.size) + + assertEquals("abc", result[0]) + assertEquals(null, result[1]) + } + + @Test + fun selectIsNull() { + assertEquals("not-null", table.select(property("name"), where = property("test").isNotNull()).first()) + assertEquals("null", table.select(property("name"), where = property("test").isNull()).first()) + } + + @Test + fun updateNullError() { + fun checkResult(result: UpdateResult<*>) { + assertTrue(result.isError()) + assertTrue(result.notNullViolation) + } + + checkResult(table.update("name", value(null), where = property("id") isEqualTo value(1))) + checkResult(table.update("name", nullValue(), where = property("id") isEqualTo value(1))) + } + + @Test + fun updateNull() { + val result = table.update("test", nullValue(), where = property("id") isEqualTo value(1)) + + assertTrue(result.isSuccess()) + assertEquals(1, result.value) + + assertEquals(null, table.select(property("test"), where = property("id") isEqualTo value(1)).first()) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/specific/Numeric.kt b/sqlite/src/test/kotlin/tests/sqlite/specific/Numeric.kt new file mode 100644 index 0000000..54a5430 --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/specific/Numeric.kt @@ -0,0 +1,41 @@ +package tests.sqlite.specific + +import de.mineking.database.AutoGenerate +import de.mineking.database.Column +import de.mineking.database.Key +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.recreate +import java.math.BigDecimal +import java.math.BigInteger +import kotlin.test.assertEquals + +data class NumericDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Column val short: Short = 0, + @Column val int: Int = 0, + @Column val long: Long = 0, + @Column val float: Float = 0F, + @Column val double: Double = 0.0, +) + +class NumericTest { + val connection = SQLiteConnection("test.db") + val table = connection.getTable(name = "numeric_test") { NumericDao() } + + init { + table.recreate() + + table.insert(NumericDao(short = Short.MAX_VALUE, int = Int.MAX_VALUE, long = Long.MAX_VALUE, float = Float.MAX_VALUE, double = Double.MAX_VALUE)) + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun select() { + //The main purpose here is to verify that reading works without exceptions because of type problems + val result = table.select().first() + assertEquals(Double.MAX_VALUE, result.double) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/table/CustomTable.kt b/sqlite/src/test/kotlin/tests/sqlite/table/CustomTable.kt new file mode 100644 index 0000000..8228843 --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/table/CustomTable.kt @@ -0,0 +1,54 @@ +package tests.sqlite.table + +import de.mineking.database.Table +import de.mineking.database.isEqualTo +import de.mineking.database.property +import de.mineking.database.value +import de.mineking.database.vendors.SQLiteConnection +import setup.ConsoleSqlLogger +import setup.UserDao +import setup.recreate +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +interface UserTable : Table { + fun createUser(name: String, email: String, age: Int): UserDao = insert(UserDao(name = name, email = email, age = age)).getOrThrow() + fun getUserByEmail(email: String): UserDao? = select(where = property("email") isEqualTo value(email)).findFirst() + + fun updateName(email: String, name: String) = (update("name", value(name), where = property("email") isEqualTo value(email)).value ?: 0) > 0 +} + +class CustomTableTest { + val connection = SQLiteConnection("test.db") + val table = connection.getTable<_, UserTable>(name = "basic_test") { UserDao() } + + init { + table.recreate() + + table.createUser(name = "Tom", email = "tom@example.com", age = 12) + table.createUser(name = "Alex", email = "alex@example.com", age = 23) + table.createUser(name = "Bob", email = "bob@example.com", age = 50) + table.createUser(name = "Eve", email = "eve@example.com", age = 42) + table.createUser(name = "Max", email = "max@example.com", age = 20) + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun getByEmail() { + assertEquals("Tom", table.getUserByEmail("tom@example.com")?.name) + } + + @Test + fun updateName() { + assertTrue(table.updateName("tom@example.com", "Test")) + assertEquals("Test", table.getUserByEmail("tom@example.com")?.name) + } + + @Test + fun notUpdated() { + assertFalse(table.updateName("test@example.com", "Test")) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/table/DataObject.kt b/sqlite/src/test/kotlin/tests/sqlite/table/DataObject.kt new file mode 100644 index 0000000..f4f3961 --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/table/DataObject.kt @@ -0,0 +1,62 @@ +package tests.sqlite.table + +import de.mineking.database.* +import de.mineking.database.vendors.SQLiteConnection +import setup.ConsoleSqlLogger +import setup.recreate +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +data class DataObjectReferenceDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Column val reference: Int = 0 +) + +data class DataObjectDao( + val main: DataObjectTest, + @AutoGenerate @Key @Column val id: Int = 0, + @Column val name: String = "" +) : DataObject { + override fun getTable(): Table = main.table +} + +class DataObjectTest { + val connection = SQLiteConnection("test.db") + val referenceTable = connection.getTable(name = "data_object_reference_test") { DataObjectReferenceDao() } + val table = connection.getTable(name = "data_object_test") { DataObjectDao(this) } + + val references = arrayListOf() + + init { + table.recreate() + referenceTable.recreate() + + val a = table.insert(DataObjectDao(this, name = "A")).value!! + val b = table.insert(DataObjectDao(this, name = "B")).value!! + + references += referenceTable.insert(DataObjectReferenceDao(reference = a.id)).value!! + references += referenceTable.insert(DataObjectReferenceDao(reference = b.id)).value!! + references += referenceTable.insert(DataObjectReferenceDao(reference = b.id)).value!! + references += referenceTable.insert(DataObjectReferenceDao(reference = a.id)).value!! + references += referenceTable.insert(DataObjectReferenceDao(reference = b.id)).value!! + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun selectAll() { + val result = table.select().list() + + assertEquals(2, result.size) + assertEquals(2, result[0].selectReferring(referenceTable, "reference").list().size) + assertEquals(3, result[1].selectReferring(referenceTable, "reference").list().size) + } + + @Test + fun update() { + assertTrue(referenceTable.update(references[1].copy(reference = 1)).isSuccess()) + + assertEquals(3, table.select(where = property("name") isEqualTo value("A")).first().selectReferring(referenceTable, "reference").list().size) + } +} \ No newline at end of file diff --git a/sqlite/src/test/kotlin/tests/sqlite/table/Unique.kt b/sqlite/src/test/kotlin/tests/sqlite/table/Unique.kt new file mode 100644 index 0000000..a66e916 --- /dev/null +++ b/sqlite/src/test/kotlin/tests/sqlite/table/Unique.kt @@ -0,0 +1,43 @@ +package tests.sqlite.table + +import de.mineking.database.AutoGenerate +import de.mineking.database.Column +import de.mineking.database.Key +import de.mineking.database.Unique +import de.mineking.database.vendors.SQLiteConnection +import org.junit.jupiter.api.Test +import setup.ConsoleSqlLogger +import setup.recreate +import kotlin.test.assertTrue + +data class UniqueDao( + @AutoGenerate @Key @Column val id: Int = 0, + @Unique("a") @Column val a: String = "", + @Unique("b") @Column val b: String = "" +) + +class UniqueTest { + val connection = SQLiteConnection("test.db") + val table = connection.getTable(name = "complex_unique_test") { UniqueDao() } + + init { + table.recreate() + + table.insert(UniqueDao(a = "a", b = "b")) + + connection.driver.setSqlLogger(ConsoleSqlLogger) + } + + @Test + fun simpleUnique() { + assertTrue(table.insert(UniqueDao(a = "b", b = "a")).isSuccess()) + + val result1 = table.insert(UniqueDao(a = "a", b = "a")) + assertTrue(result1.isError()) + assertTrue(result1.uniqueViolation) + + val result2 = table.insert(UniqueDao(a = "b", b = "b")) + assertTrue(result2.isError()) + assertTrue(result2.uniqueViolation) + } +} \ No newline at end of file