From c3113f455280b49a3d7e79a7c4c1f5cfc7d9b042 Mon Sep 17 00:00:00 2001 From: Miha-x64 Date: Mon, 2 Mar 2020 00:09:24 +0300 Subject: [PATCH] SQL #32: JDBC and SQLite templates, blocking eager fetching --- .../net/aquadc/persistence/android/sql.kt | 18 +- .../net/aquadc/persistence/VarFuncImpl.kt | 48 ++++ .../net/aquadc/persistence/sql/RealDao.kt | 6 +- .../aquadc/persistence/sql/RealTransaction.kt | 2 +- .../net/aquadc/persistence/sql/Table.kt | 2 +- .../persistence/sql/async/collection.kt | 23 -- .../persistence/sql/blocking/Blocking.kt | 211 ++++++++++++++++++ .../sql/blocking/BlockingSession.kt | 46 ---- .../persistence/sql/blocking/JdbcSession.kt | 66 +++++- .../sql/blocking/LowLevelSession.kt | 42 +--- .../persistence/sql/blocking/SqliteSession.kt | 107 ++++++--- .../net/aquadc/persistence/sql/delegates.kt | 14 +- .../net/aquadc/persistence/sql/selections.kt | 4 +- .../kotlin/net/aquadc/persistence/sql/sql.kt | 84 +------ .../net/aquadc/persistence/sql/template.kt | 119 ++++++++++ .../kotlin/net/aquadc/persistence/sql/util.kt | 11 + .../aquadc/persistence/sql/TemplatesTest.kt | 85 +++++++ .../net/aquadc/persistence/sql/database.kt | 47 ++-- 18 files changed, 686 insertions(+), 249 deletions(-) create mode 100644 persistence/src/main/kotlin/net/aquadc/persistence/VarFuncImpl.kt delete mode 100644 sql/src/main/kotlin/net/aquadc/persistence/sql/async/collection.kt create mode 100644 sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Blocking.kt delete mode 100644 sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/BlockingSession.kt create mode 100644 sql/src/main/kotlin/net/aquadc/persistence/sql/template.kt create mode 100644 sql/src/test/kotlin/net/aquadc/persistence/sql/TemplatesTest.kt diff --git a/android-bindings/src/test/kotlin/net/aquadc/persistence/android/sql.kt b/android-bindings/src/test/kotlin/net/aquadc/persistence/android/sql.kt index 124fc1dd..0afb748c 100644 --- a/android-bindings/src/test/kotlin/net/aquadc/persistence/android/sql.kt +++ b/android-bindings/src/test/kotlin/net/aquadc/persistence/android/sql.kt @@ -8,8 +8,10 @@ import net.aquadc.persistence.sql.QueryBuilderTests import net.aquadc.persistence.sql.Session import net.aquadc.persistence.sql.SqlPropTest import net.aquadc.persistence.sql.blocking.SqliteSession +import net.aquadc.persistence.sql.TemplatesTest import net.aquadc.persistence.sql.TestTables import net.aquadc.persistence.sql.dialect.sqlite.SqliteDialect +import net.aquadc.persistence.sql.blocking.Blocking import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before @@ -67,7 +69,6 @@ class SqlPropRoboTest : SqlPropTest() { } - @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE) class EmbedRelationsRoboTest : EmbedRelationsTest() { @@ -82,7 +83,6 @@ class EmbedRelationsRoboTest : EmbedRelationsTest() { } } - @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE) class QueryBuilderRoboTests : QueryBuilderTests() { @@ -96,3 +96,17 @@ class QueryBuilderRoboTests : QueryBuilderTests() { db.close() } } + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class TemplatesRoboTests : TemplatesTest() { + private lateinit var db: SQLiteDatabase + override lateinit var session: Session> + @Before fun init() { + db = sqliteDb() + session = SqliteSession(db) + } + @After fun close() { + db.close() + } +} diff --git a/persistence/src/main/kotlin/net/aquadc/persistence/VarFuncImpl.kt b/persistence/src/main/kotlin/net/aquadc/persistence/VarFuncImpl.kt new file mode 100644 index 00000000..ae597370 --- /dev/null +++ b/persistence/src/main/kotlin/net/aquadc/persistence/VarFuncImpl.kt @@ -0,0 +1,48 @@ +package net.aquadc.persistence + +import androidx.annotation.RestrictTo + +// TODO: looks like I should create a separate utility library for such things +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +abstract class VarFuncImpl : + () -> R + , (T) -> R + , (T, T) -> R + , (T, T, T) -> R + , (T, T, T, T) -> R + , (T, T, T, T, T) -> R + , (T, T, T, T, T, T) -> R + , (T, T, T, T, T, T, T) -> R + , (T, T, T, T, T, T, T, T) -> R +{ + + abstract fun invokeUnchecked(vararg arg: T): R + + override fun invoke(): R = + invokeUnchecked() + + override fun invoke(p1: T): R = + invokeUnchecked(p1) + + override fun invoke(p1: T, p2: T): R = + invokeUnchecked(p1, p2) + + override fun invoke(p1: T, p2: T, p3: T): R = + invokeUnchecked(p1, p2, p3) + + override fun invoke(p1: T, p2: T, p3: T, p4: T): R = + invokeUnchecked(p1, p2, p3, p4) + + override fun invoke(p1: T, p2: T, p3: T, p4: T, p5: T): R = + invokeUnchecked(p1, p2, p3, p4, p5) + + override fun invoke(p1: T, p2: T, p3: T, p4: T, p5: T, p6: T): R = + invokeUnchecked(p1, p2, p3, p4, p5, p6) + + override fun invoke(p1: T, p2: T, p3: T, p4: T, p5: T, p6: T, p7: T): R = + invokeUnchecked(p1, p2, p3, p4, p5, p6, p7) + + override fun invoke(p1: T, p2: T, p3: T, p4: T, p5: T, p6: T, p7: T, p8: T): R = + invokeUnchecked(p1, p2, p3, p4, p5, p6, p7, p8) + +} diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/RealDao.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/RealDao.kt index 2445c624..e3132b3a 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/RealDao.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/RealDao.kt @@ -23,15 +23,11 @@ import java.util.concurrent.CopyOnWriteArraySet internal class RealDao, ID : IdBound, REC : Record, STMT>( private val session: Session<*>, - private val lowSession: LowLevelSession, + private val lowSession: LowLevelSession, private val table: Table, private val dialect: Dialect ) : Dao { - // helpers for Sessions - - internal val selectStatements = ThreadLocal>() - // these three are guarded by RW lock internal var insertStatement: STMT? = null internal val updateStatements = New.map() diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/RealTransaction.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/RealTransaction.kt index a64b23fa..349e062a 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/RealTransaction.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/RealTransaction.kt @@ -15,7 +15,7 @@ import java.util.BitSet ) internal class RealTransaction( private val session: Session<*>, - private val lowSession: LowLevelSession<*> + private val lowSession: LowLevelSession<*, *> ) : Transaction { private var thread: Thread? = Thread.currentThread() // null means that this transaction has ended diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/Table.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/Table.kt index e2169e70..776b747f 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/Table.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/Table.kt @@ -133,7 +133,7 @@ private constructor( ss.colCount = outColumns.size - start outRecipe.add(Nesting.StructEnd) - check(outDelegates?.put(path, Embedded>( + check(outDelegates?.put(path, Embedded( outColumns.subList(start, outColumns.size).array(), // ArrayList$SubList checks for concurrent modifications and cannot be passed as is outRecipe.subList(recipeStart, outRecipe.size).array() diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/async/collection.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/async/collection.kt deleted file mode 100644 index 2da6dae6..00000000 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/async/collection.kt +++ /dev/null @@ -1,23 +0,0 @@ -package net.aquadc.persistence.sql.async - - -interface AsyncIterator { - suspend operator fun next(): T - suspend operator fun hasNext(): Boolean -} -interface AsyncIterable { - operator fun iterator(): AsyncIterator -} -interface AsyncCollection : Iterable { - suspend /*val*/ fun size(): Int - suspend /*operator*/ fun contains(element: @UnsafeVariance E): Boolean - suspend fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean -} -interface AsyncList { - suspend /*operator*/ fun get(index: Int): E - suspend fun indexOf(element: @UnsafeVariance E): Int - suspend fun lastIndexOf(element: @UnsafeVariance E): Int -// fun listIterator(): ListIterator -// fun listIterator(index: Int): ListIterator -// suspend fun subList(fromIndex: Int, toIndex: Int): List -} diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Blocking.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Blocking.kt new file mode 100644 index 00000000..e2c3535a --- /dev/null +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/Blocking.kt @@ -0,0 +1,211 @@ +@file:Suppress("NOTHING_TO_INLINE") +package net.aquadc.persistence.sql.blocking + +import net.aquadc.persistence.sql.BindBy +import net.aquadc.persistence.sql.Fetch +import net.aquadc.persistence.sql.Table +import net.aquadc.persistence.sql.inflate +import net.aquadc.persistence.sql.row +import net.aquadc.persistence.struct.Schema +import net.aquadc.persistence.struct.StoredNamedLens +import net.aquadc.persistence.struct.StructSnapshot +import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.SimpleNullable +import java.io.InputStream +import java.sql.ResultSet +import java.sql.SQLFeatureNotSupportedException + +/** + * SQL session tied to blocking API with cursors of type [CUR]. + */ +interface Blocking { + // Android SQLite API has special methods for single-cell selections + fun cell(query: String, argumentTypes: Array>, arguments: Array, type: DataType): T + + fun select(query: String, argumentTypes: Array>, arguments: Array, expectedCols: Int): CUR + + fun sizeHint(cursor: CUR): Int + fun next(cursor: CUR): Boolean + + fun cellAt(cursor: CUR, col: Int, type: DataType): T + fun rowByName(cursor: CUR, columns: Array>): Array + fun rowByPosition(cursor: CUR, columns: Array>): Array +} + +object Eager { + inline fun cell(returnType: DataType.Simple): Fetch, R> = + FetchCellEagerly(returnType) + + inline fun cell(returnType: SimpleNullable): Fetch, R?> = + FetchCellEagerly(returnType) + + inline fun col(elementType: DataType.Simple): Fetch, List> = + FetchColEagerly(elementType) + + inline fun col(elementType: SimpleNullable): Fetch, List> = + FetchColEagerly(elementType) + + inline fun > struct(table: Table, bindBy: BindBy): Fetch, StructSnapshot> = + FetchStructEagerly(table, bindBy) + + inline fun > structList(table: Table, bindBy: BindBy): Fetch, List>> = + FetchStructListEagerly(table, bindBy) + + inline fun cellByteStream(): Fetch, InputStream> = + InputStreamFromResultSet // ^^^^^^^^^ JDBC-only. Not supported by Android SQLite +} + +@PublishedApi internal class FetchCellEagerly( + private val rt: DataType +) : Fetch, R> { + + override fun fetch( + from: Blocking, query: String, argumentTypes: Array>, arguments: Array + ): R = + from.cell(query, argumentTypes, arguments, rt) +} + +@PublishedApi internal class FetchColEagerly( + private val et: DataType +) : Fetch, List> { + + override fun fetch( + from: Blocking, query: String, argumentTypes: Array>, arguments: Array + ): List { + val cur = from.select(query, argumentTypes, arguments, 1) + try { + return if (from.next(cur)) { + val first = from.cellAt(cur, 0, et) + if (from.next(cur)) { + ArrayList(from.sizeHint(cur).let { if (it < 0) 10 else it }).also { + it.add(first) + do it.add(from.cellAt(cur, 0, et)) while (from.next(cur)) + } + } else listOf(first) + } else emptyList() + } finally { + cur.close() + } + } +} + +@PublishedApi internal class FetchStructEagerly, CUR : AutoCloseable>( + private val table: Table, + private val bindBy: BindBy +) : Fetch, StructSnapshot> { + + override fun fetch( + from: Blocking, query: String, argumentTypes: Array>, arguments: Array + ): StructSnapshot { + val cur = from.select(query, argumentTypes, arguments, table.columns.size) + try { + check(from.next(cur)) + val values = from.row(cur, table.columnsMappedToFields, bindBy) + check(!from.next(cur)) // single row expected + inflate(table.recipe, values, 0, 0, 0) + @Suppress("UNCHECKED_CAST") + return values[0] as StructSnapshot + } finally { + cur.close() + } + } +} + +@PublishedApi internal class FetchStructListEagerly>( + private val table: Table, + private val bindBy: BindBy +) : Fetch, List>> { + + override fun fetch( + from: Blocking, query: String, argumentTypes: Array>, arguments: Array + ): List> { + val cols = table.columnsMappedToFields + val recipe = table.recipe + + val cur = from.select(query, argumentTypes, arguments, cols.size) + try { + return if (from.next(cur)) { + val first = from.mapRow(cur, cols, recipe) + if (from.next(cur)) { + ArrayList>(from.sizeHint(cur).let { if (it < 0) 10 else it }).also { + it.add(first) + do it.add(from.mapRow(cur, cols, recipe)) while (from.next(cur)) + } + } else listOf(first) + } else emptyList() + } finally { + cur.close() + } + } + + private fun Blocking.mapRow(cur: CUR, cols: Array>, recipe: Array): StructSnapshot { + val firstValues = row(cur, cols, bindBy); inflate(recipe, firstValues, 0, 0, 0) + @Suppress("UNCHECKED_CAST") + return firstValues[0] as StructSnapshot + } +} + +@PublishedApi internal object InputStreamFromResultSet : Fetch, InputStream> { + + override fun fetch( + from: Blocking, query: String, argumentTypes: Array>, arguments: Array + ): InputStream = + from.select(query, argumentTypes, arguments, 1).let { + check(it.next()) + try { + it.getBlob(0).binaryStream // Postgres-JDBC supports this, SQLite-JDBC doesn't + } catch (e: SQLFeatureNotSupportedException) { + it.getBinaryStream(0) // this is typically just in-memory :'( + } + } +} + +//fun lazyValue(): FetchValue> = TODO() +//fun , D> lazyStruct(): FetchStruct, LazyList>> = TODO() + +//fun observableValue(/*todo dependencies*/): FetchValue, Property>> = TODO() +//fun , D, ID : IdBound> observableStruct(idName: String, idType: DataType.Simple/*todo dependencies*/): FetchStruct, DiffProperty>, D>> = TODO() + +/*fun CoroutineScope.asyncValue(): FetchValue, Deferred>> { + launch { } +} +fun , D> CoroutineScope.asyncStruct(): FetchStruct>, Deferred>>> = TODO()*/ + +/*fun cellCallback(cb: (T) -> Unit): FetchValue = TODO() +fun colCallback(cb: (List) -> Unit): FetchValue = TODO() +fun , D> rowCallback(cb: (Struct) -> Unit): FetchStruct = TODO() +fun , D> gridCallback(cb: (List>) -> Unit): FetchStruct = TODO()*/ + +/* +fun CoroutineScope.lazyAsyncValue(): FetchValue, AsyncList> = TODO() +fun , D> CoroutineScope.lazyAsyncStruct(): FetchStruct, AsyncList>> = TODO() + +class ListChanges, ID : IdBound>( + val oldIds: List, // List could wrap IntArray, for example. Array can't + val newIds: List, + val changes: Map> +) + +interface AsyncStruct> + +interface AsyncIterator { + suspend operator fun next(): T + suspend operator fun hasNext(): Boolean +} +interface AsyncIterable { + operator fun iterator(): AsyncIterator +} +interface AsyncCollection : Iterable { + suspend /*val*/ fun size(): Int + suspend /*operator*/ fun contains(element: @UnsafeVariance E): Boolean + suspend fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean +} +interface AsyncList { + suspend /*operator*/ fun get(index: Int): E + suspend fun indexOf(element: @UnsafeVariance E): Int + suspend fun lastIndexOf(element: @UnsafeVariance E): Int +// fun listIterator(): ListIterator +// fun listIterator(index: Int): ListIterator +// suspend fun subList(fromIndex: Int, toIndex: Int): List +} + */ diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/BlockingSession.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/BlockingSession.kt deleted file mode 100644 index 5953c9c0..00000000 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/BlockingSession.kt +++ /dev/null @@ -1,46 +0,0 @@ -@file:JvmName("Blocking") -package net.aquadc.persistence.sql.blocking - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import net.aquadc.persistence.sql.AsyncStruct -import net.aquadc.persistence.sql.FetchStruct -import net.aquadc.persistence.sql.FetchValue -import net.aquadc.persistence.sql.IdBound -import net.aquadc.persistence.sql.LazyList -import net.aquadc.persistence.sql.async.AsyncList -import net.aquadc.persistence.struct.Schema -import net.aquadc.persistence.struct.Struct -import net.aquadc.persistence.type.DataType -import net.aquadc.properties.Property -import net.aquadc.properties.diff.DiffProperty -import net.aquadc.properties.persistence.PropertyStruct - -/** - * Provides the same functionality as [net.aquadc.persistence.sql.Selection] - * but tied to blocking API and not tied to any SQL query. - */ -interface BlockingSession { - -} - -fun eagerValue(): FetchValue> = TODO() -fun , D> eagerStruct(): FetchStruct, List>> = TODO() - -fun lazyValue(): FetchValue> = TODO() -fun , D> lazyStruct(): FetchStruct, LazyList>> = TODO() - -fun observableValue(/*todo dependencies*/): FetchValue, Property>> = TODO() -fun , D, ID : IdBound> observableStruct(idName: String, idType: DataType.Simple/*todo dependencies*/): FetchStruct, DiffProperty>, D>> = TODO() - -fun CoroutineScope.asyncValue(): FetchValue, Deferred>> = TODO() -fun , D> CoroutineScope.asyncStruct(): FetchStruct>, Deferred>>> = TODO() - -fun CoroutineScope.lazyAsyncValue(): FetchValue, AsyncList> = TODO() -fun , D> CoroutineScope.lazyAsyncStruct(): FetchStruct, AsyncList>> = TODO() - -fun cellCallback(cb: (T) -> Unit): FetchValue = TODO() -fun colCallback(cb: (List) -> Unit): FetchValue = TODO() -fun , D> rowCallback(cb: (Struct) -> Unit): FetchStruct = TODO() -fun , D> gridCallback(cb: (List>) -> Unit): FetchStruct = TODO() - diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/JdbcSession.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/JdbcSession.kt index f9794611..6f8c9700 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/JdbcSession.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/JdbcSession.kt @@ -3,16 +3,17 @@ package net.aquadc.persistence.sql.blocking import net.aquadc.persistence.array import net.aquadc.persistence.sql.Dao import net.aquadc.persistence.sql.ExperimentalSql +import net.aquadc.persistence.sql.Fetch import net.aquadc.persistence.sql.IdBound import net.aquadc.persistence.sql.NoOrder import net.aquadc.persistence.sql.Order import net.aquadc.persistence.sql.RealDao import net.aquadc.persistence.sql.RealTransaction import net.aquadc.persistence.sql.Record -import net.aquadc.persistence.sql.Selection import net.aquadc.persistence.sql.Session import net.aquadc.persistence.sql.Table import net.aquadc.persistence.sql.Transaction +import net.aquadc.persistence.sql.VarFunc import net.aquadc.persistence.sql.WhereCondition import net.aquadc.persistence.sql.bindInsertionParams import net.aquadc.persistence.sql.bindQueryParams @@ -26,6 +27,7 @@ import net.aquadc.persistence.struct.Struct import net.aquadc.persistence.struct.approxType import net.aquadc.persistence.type.DataType import net.aquadc.persistence.type.i64 +import org.intellij.lang.annotations.Language import java.sql.Connection import java.sql.PreparedStatement import java.sql.ResultSet @@ -42,7 +44,7 @@ import kotlin.concurrent.getOrSet class JdbcSession( @JvmField @JvmSynthetic internal val connection: Connection, @JvmField @JvmSynthetic internal val dialect: Dialect -) : Session { +) : Session> { init { connection.autoCommit = false @@ -64,7 +66,9 @@ class JdbcSession( // transactional things, guarded by write-lock @JvmField @JvmSynthetic internal var transaction: RealTransaction? = null - private val lowLevel: LowLevelSession = object : LowLevelSession() { + @JvmField @JvmSynthetic internal val selectStatements = ThreadLocal>() + + private val lowLevel: LowLevelSession = object : LowLevelSession() { override fun , ID : IdBound> insert(table: Table, data: Struct): ID { val dao = getDao(table) @@ -147,8 +151,7 @@ class JdbcSession( if (columns == null) dialect.selectCountQuery(table, condition) else dialect.selectQuery(table, columns, condition, order) - return getDao(table) - .selectStatements + return selectStatements .getOrSet(::HashMap) .getOrPut(query) { connection.prepareStatement(query) } .also { stmt -> @@ -237,9 +240,11 @@ class JdbcSession( } @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST") - private fun DataType.get(resultSet: ResultSet, index: Int): T { - val i = 1 + index + private /*wannabe inline*/ fun DataType.get(resultSet: ResultSet, index: Int): T { + return get1indexed(resultSet, 1 + index) + } + private fun DataType.get1indexed(resultSet: ResultSet, i: Int): T { return flattened { isNullable, simple -> val v = when (simple.kind) { DataType.Simple.Kind.Bool -> resultSet.getBoolean(i) @@ -259,6 +264,43 @@ class JdbcSession( } } + override fun cell( + query: String, argumentTypes: Array>, arguments: Array, type: DataType + ): T { + val rs = select(query, argumentTypes, arguments, 1) + try { + check(rs.next()) + val value = type.get(rs, 0) + check(!rs.next()) + return value + } finally { + rs.close() + } + } + + override fun select( + query: String, argumentTypes: Array>, arguments: Array, expectedCols: Int + ): ResultSet = selectStatements + .getOrSet(::HashMap) + .getOrPut(query) { connection.prepareStatement(query) } + .also { stmt -> + for (idx in argumentTypes.indices) { + (argumentTypes[idx] as DataType).bind(stmt, idx, arguments[idx]) + } + } + .executeQuery() + override fun sizeHint(cursor: ResultSet): Int = -1 + override fun next(cursor: ResultSet): Boolean = cursor.next() + override fun cellAt(cursor: ResultSet, col: Int, type: DataType): T = type.get(cursor, col) + override fun rowByName(cursor: ResultSet, columns: Array>): Array = + Array(columns.size) { idx -> + val col = columns[idx] + col.type.get1indexed(cursor, cursor.findColumn(col.name)) + } + override fun rowByPosition(cursor: ResultSet, columns: Array>): Array = + Array(columns.size) { idx -> + columns[idx].type.get(cursor, idx) + } } @@ -278,7 +320,7 @@ class JdbcSession( dao.dump(" ", sb) sb.append(" select statements (for current thread)\n") - dao.selectStatements.get()?.keys?.forEach { sql -> + selectStatements.get()?.keys?.forEach { sql -> sb.append(' ').append(sql).append("\n") } @@ -292,7 +334,11 @@ class JdbcSession( } } - override fun rawQuery(query: String, vararg arguments: Any): Selection = - BlockingSelection(lowLevel, query, arguments) + override fun rawQuery( + @Language("SQL") query: String, + argumentTypes: Array>, + fetch: Fetch, R> + ): VarFunc = + BlockingQuery(lowLevel, query, argumentTypes, fetch) } diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/LowLevelSession.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/LowLevelSession.kt index 4ab3bf8d..378612d0 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/LowLevelSession.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/LowLevelSession.kt @@ -1,58 +1,40 @@ package net.aquadc.persistence.sql.blocking -import net.aquadc.persistence.sql.BindBy +import net.aquadc.persistence.VarFuncImpl import net.aquadc.persistence.sql.ColCond -import net.aquadc.persistence.sql.FetchStruct -import net.aquadc.persistence.sql.FetchValue +import net.aquadc.persistence.sql.Fetch import net.aquadc.persistence.sql.IdBound -import net.aquadc.persistence.sql.ListChanges import net.aquadc.persistence.sql.Order +import net.aquadc.persistence.sql.VarFunc import net.aquadc.persistence.sql.RealDao import net.aquadc.persistence.sql.RealTransaction import net.aquadc.persistence.sql.Record -import net.aquadc.persistence.sql.Selection import net.aquadc.persistence.sql.Session import net.aquadc.persistence.sql.Table import net.aquadc.persistence.sql.WhereCondition -import net.aquadc.persistence.struct.FldSet import net.aquadc.persistence.struct.Lens import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.struct.StoredNamedLens import net.aquadc.persistence.struct.Struct import net.aquadc.persistence.type.DataType -import net.aquadc.persistence.type.SimpleNullable import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.getOrSet -internal class BlockingSelection( - private val session: BlockingSession, +internal class BlockingQuery( + private val session: Blocking, private val query: String, - private val arguments: Array -) : Selection { + private val argumentTypes: Array>, + private val fetch: Fetch, R> +) : VarFuncImpl(), VarFunc { - override fun cell(type: DataType.Simple, fetch: FetchValue): R = - fetch.cell(session, query, arguments, type) - - override fun cell(type: SimpleNullable, fetch: FetchValue): R = - fetch.cell(session, query, arguments, type) - - override fun col(type: DataType.Simple, fetch: FetchValue): R = - fetch.col(session, query, arguments, type) - - override fun col(type: SimpleNullable, fetch: FetchValue): R = - fetch.col(session, query, arguments, type) - - override fun , R> row(schema: S, bindBy: BindBy, fetch: FetchStruct, R, *>): R = - fetch.row(session, query, arguments, schema, bindBy) - - override fun , R, ID : IdBound> grid(schema: S, bindBy: BindBy, fetch: FetchStruct, *, R>): R = - fetch.grid(session, query, arguments, schema, bindBy) + override fun invokeUnchecked(vararg arg: Any): R = + fetch.fetch(session, query, argumentTypes, arg) } -internal abstract class LowLevelSession : BlockingSession { +internal abstract class LowLevelSession : Blocking { abstract fun , ID : IdBound> insert(table: Table, data: Struct): ID /** [columns] : [values] is a map */ @@ -104,7 +86,7 @@ internal abstract class LowLevelSession : BlockingSession { } -internal fun Session<*>.createTransaction(lock: ReentrantReadWriteLock, lowLevel: LowLevelSession<*>): RealTransaction { +internal fun Session<*>.createTransaction(lock: ReentrantReadWriteLock, lowLevel: LowLevelSession<*, *>): RealTransaction { val wLock = lock.writeLock() check(!wLock.isHeldByCurrentThread) { "Thread ${Thread.currentThread()} is already in a transaction" } wLock.lock() diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/SqliteSession.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/SqliteSession.kt index d49cf936..2a02053b 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/SqliteSession.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/blocking/SqliteSession.kt @@ -3,23 +3,26 @@ package net.aquadc.persistence.sql.blocking import android.database.Cursor import android.database.sqlite.SQLiteCursor +import android.database.sqlite.SQLiteCursorDriver import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteProgram +import android.database.sqlite.SQLiteQuery import android.database.sqlite.SQLiteQueryBuilder import android.database.sqlite.SQLiteStatement import net.aquadc.persistence.array import net.aquadc.persistence.sql.Dao import net.aquadc.persistence.sql.ExperimentalSql +import net.aquadc.persistence.sql.Fetch import net.aquadc.persistence.sql.IdBound import net.aquadc.persistence.sql.NoOrder import net.aquadc.persistence.sql.Order import net.aquadc.persistence.sql.RealDao import net.aquadc.persistence.sql.RealTransaction import net.aquadc.persistence.sql.Record -import net.aquadc.persistence.sql.Selection import net.aquadc.persistence.sql.Session import net.aquadc.persistence.sql.Table import net.aquadc.persistence.sql.Transaction +import net.aquadc.persistence.sql.VarFunc import net.aquadc.persistence.sql.WhereCondition import net.aquadc.persistence.sql.bindInsertionParams import net.aquadc.persistence.sql.bindQueryParams @@ -33,6 +36,7 @@ import net.aquadc.persistence.struct.Struct import net.aquadc.persistence.struct.approxType import net.aquadc.persistence.type.DataType import net.aquadc.persistence.type.i64 +import org.intellij.lang.annotations.Language import java.util.concurrent.locks.ReentrantReadWriteLock /** @@ -42,7 +46,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock @ExperimentalSql class SqliteSession( @JvmSynthetic @JvmField internal val connection: SQLiteDatabase -) : Session { +) : Session> { @JvmSynthetic internal val lock = ReentrantReadWriteLock() @@ -58,7 +62,7 @@ class SqliteSession( // transactional things, guarded by write-lock @JvmSynthetic @JvmField internal var transaction: RealTransaction? = null - @JvmSynthetic @JvmField internal val lowLevel = object : LowLevelSession() { + @JvmSynthetic @JvmField internal val lowLevel = object : LowLevelSession() { override fun , ID : IdBound> insert(table: Table, data: Struct): ID { val dao = getDao(table) @@ -140,9 +144,10 @@ class SqliteSession( columns: Array>?, condition: WhereCondition, order: Array> - ): Cursor { - val sql = with(SqliteDialect) { - SQLiteQueryBuilder.buildQueryString( // fixme: building SQL myself may save some allocations + ): Cursor = connection.rawQueryWithFactory( + CurFac(condition, table, null, null), + with(SqliteDialect) { SQLiteQueryBuilder.buildQueryString( + // fixme: building SQL myself could save some allocations /*distinct=*/false, table.name, if (columns == null) arrayOf("COUNT(*)") else columns.mapIndexedToArray { _, col -> col.name }, @@ -151,23 +156,37 @@ class SqliteSession( /*having=*/null, if (order.isEmpty()) null else StringBuilder().appendOrderClause(order).toString(), /*limit=*/null - ) - } - - // a workaround for binding BLOBS, as suggested in https://stackoverflow.com/a/23159664/3050249 - return connection.rawQueryWithFactory( - { _, masterQuery, editTable, query -> - bindQueryParams(condition, table) { type, idx, value -> + ) }, + /*selectionArgs=*/null, + SQLiteDatabase.findEditTable(table.name), // TODO: whether it is necessary? + /*cancellationSignal=*/null + ) + + // a workaround for binding BLOBs, as suggested in https://stackoverflow.com/a/23159664/3050249 + private inner class CurFac>( + private val condition: WhereCondition?, + private val table: Table?, + private val argumentTypes: Array>?, + private val arguments: Array? + ) : SQLiteDatabase.CursorFactory { + + override fun newCursor(db: SQLiteDatabase?, masterQuery: SQLiteCursorDriver?, editTable: String?, query: SQLiteQuery): Cursor { + when { + condition != null -> + bindQueryParams(condition, table!!) { type, idx, value -> type.bind(query, idx, value) } + argumentTypes != null -> arguments!!.let { args -> + argumentTypes.forEachIndexed { idx, type -> + (type as DataType).bind(query, idx, args[idx]) + } + } + else -> + throw AssertionError() + } - SQLiteCursor(masterQuery, editTable, query) - }, - sql, - /*selectionArgs=*/null, - SQLiteDatabase.findEditTable(table.name), - /*cancellationSignal=*/null - ) + return SQLiteCursor(masterQuery, editTable, query) + } } override fun , ID : IdBound, T> fetchSingle( @@ -273,6 +292,47 @@ class SqliteSession( return toByte() } + override fun cell(query: String, argumentTypes: Array>, arguments: Array, type: DataType): T { + val cur = select(query, argumentTypes, arguments, 1) + try { + check(cur.moveToFirst()) + val value = type.get(cur, 0) + check(!cur.moveToNext()) + return value + } finally { + cur.close() + } + } + override fun select(query: String, argumentTypes: Array>, arguments: Array, expectedCols: Int): Cursor = + connection.rawQueryWithFactory( + CurFac(null, null, argumentTypes, arguments), + query, + null, null, null + ) + override fun sizeHint(cursor: Cursor): Int = cursor.count + override fun next(cursor: Cursor): Boolean = cursor.moveToNext() + override fun cellAt(cursor: Cursor, col: Int, type: DataType): T = type.get(cursor, col) + override fun rowByName(cursor: Cursor, columns: Array>): Array = + Array(columns.size) { idx -> + val col = columns[idx] + val index = try { + cursor.getDamnColumnIndex(col.name) // TODO: could subclass SQLiteCursor and attach IntArray + } catch (e: Exception) { // Robolectric doesn't have 'Available columns' part + throw IllegalArgumentException("column '${col.name}' does not exist. " + + "Available columns: ${cursor.columnNames?.contentToString()}") + } + col.type.get(cursor, index) + } + override fun rowByPosition(cursor: Cursor, columns: Array>): Array = + Array(columns.size) { idx -> + columns[idx].type.get(cursor, idx) + } + private fun Cursor.getDamnColumnIndex(name: String): Int { // native `getColumnIndex` wrecks labels with '.'! + val columnNames = columnNames!! + val idx = columnNames.indexOfFirst { it.equals(name, ignoreCase = true) } + if (idx < 0) error { "$name !in ${columnNames.contentToString()}" } + return idx + } } @@ -295,9 +355,6 @@ class SqliteSession( dao.dump(" ", sb) sb.append(" select statements (for current thread)\n") - dao.selectStatements.get()?.keys?.forEach { sql -> - sb.append(' ').append(sql).append("\n") - } arrayOf( "insert statements" to dao.insertStatement, @@ -309,8 +366,8 @@ class SqliteSession( } } - override fun rawQuery(query: String, vararg arguments: Any): Selection = - BlockingSelection(lowLevel, query, arguments) + override fun rawQuery(@Language("SQL") query: String, argumentTypes: Array>, fetch: Fetch, R>): VarFunc = + BlockingQuery(lowLevel, query, argumentTypes, fetch) } diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/delegates.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/delegates.kt index 4efed1cd..0649c84b 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/delegates.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/delegates.kt @@ -12,10 +12,10 @@ import net.aquadc.persistence.struct.StoredNamedLens */ internal interface SqlPropertyDelegate, ID : IdBound> { fun fetch( - session: Session<*>, lowSession: LowLevelSession<*>, table: Table, field: FieldDef, id: ID + session: Session<*>, lowSession: LowLevelSession<*, *>, table: Table, field: FieldDef, id: ID ): T fun update( - session: Session<*>, lowSession: LowLevelSession<*>, table: Table, field: FieldDef, id: ID, + session: Session<*>, lowSession: LowLevelSession<*, *>, table: Table, field: FieldDef, id: ID, previous: T, update: T ) } @@ -23,25 +23,25 @@ internal interface SqlPropertyDelegate, ID : IdBound> { internal class Simple, ID : IdBound> : SqlPropertyDelegate { override fun fetch( - session: Session<*>, lowSession: LowLevelSession<*>, table: Table, field: FieldDef, id: ID + session: Session<*>, lowSession: LowLevelSession<*, *>, table: Table, field: FieldDef, id: ID ): T = lowSession.fetchSingle(table, field, id) override fun update( - session: Session<*>, lowSession: LowLevelSession<*>, table: Table, field: FieldDef, id: ID, + session: Session<*>, lowSession: LowLevelSession<*, *>, table: Table, field: FieldDef, id: ID, previous: T, update: T ): Unit = lowSession.update(table, id, field, update) } -internal class Embedded, ID : IdBound, TSCH : Schema>( +internal class Embedded, ID : IdBound>( private val columns: Array>, private val recipe: Array // contains a single start-end pair with (flattened) nesting inside ) : SqlPropertyDelegate { override fun fetch( - session: Session<*>, lowSession: LowLevelSession<*>, table: Table, field: FieldDef, id: ID + session: Session<*>, lowSession: LowLevelSession<*, *>, table: Table, field: FieldDef, id: ID ): T { val values = lowSession.fetch(table, columns, id) inflate(recipe, values, 0, 0, 0) @@ -49,7 +49,7 @@ internal class Embedded, ID : IdBound, TSCH : Schema>( } override fun update( - session: Session<*>, lowSession: LowLevelSession<*>, table: Table, field: FieldDef, id: ID, previous: T, update: T + session: Session<*>, lowSession: LowLevelSession<*, *>, table: Table, field: FieldDef, id: ID, previous: T, update: T ): Unit = lowSession.update( table, id, columns, // TODO don't allocate this array, bind args directly instead diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/selections.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/selections.kt index 56595698..cfb20b8a 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/selections.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/selections.kt @@ -6,7 +6,7 @@ import net.aquadc.persistence.struct.Schema internal class PrimaryKeys, ID : IdBound>( private val table: Table, - private val lowSession: LowLevelSession<*> + private val lowSession: LowLevelSession<*, *> ) : (ConditionAndOrder) -> Array { override fun invoke(cor: ConditionAndOrder): Array = @@ -16,7 +16,7 @@ internal class PrimaryKeys, ID : IdBound>( internal class Count, ID : IdBound>( private val table: Table, - private val lowSession: LowLevelSession<*> + private val lowSession: LowLevelSession<*, *> ): (WhereCondition) -> Long { override fun invoke(condition: WhereCondition): Long = diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/sql.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/sql.kt index 4285f012..491d805e 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/sql.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/sql.kt @@ -1,26 +1,21 @@ @file:UseExperimental(ExperimentalContracts::class) +@file:Suppress("NOTHING_TO_INLINE") package net.aquadc.persistence.sql -import kotlinx.coroutines.CoroutineScope -import net.aquadc.persistence.sql.blocking.BlockingSession -import net.aquadc.persistence.sql.blocking.asyncStruct -import net.aquadc.persistence.sql.blocking.asyncValue import net.aquadc.persistence.struct.FieldDef import net.aquadc.persistence.struct.FieldSet -import net.aquadc.persistence.struct.FldSet import net.aquadc.persistence.struct.PartialStruct import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.struct.Struct import net.aquadc.persistence.struct.forEach import net.aquadc.persistence.struct.intersectMutable import net.aquadc.persistence.type.DataType -import net.aquadc.persistence.type.SimpleNullable -import net.aquadc.persistence.type.string import net.aquadc.properties.Property import net.aquadc.properties.TransactionalProperty import net.aquadc.properties.internal.ManagedProperty import net.aquadc.properties.internal.Manager import org.intellij.lang.annotations.Language +import java.io.Closeable import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract @@ -57,80 +52,13 @@ interface Session { */ fun beginTransaction(): Transaction - fun rawQuery(@Language("SQL") query: String, vararg arguments: Any): Selection - // ^^^^^^^^^^^^^^^^ add Database Navigator to IntelliJ for SQL highlighting in String literals + fun rawQuery(@Language("SQL") query: String, argumentTypes: Array>, fetch: Fetch): VarFunc + // ^^^^^^^^^^^^^^^^ add Database Navigator to IntelliJ for SQL highlighting in String literals } -interface Selection { - fun cell(type: DataType.Simple, fetch: FetchValue): R - fun cell(type: SimpleNullable, fetch: FetchValue): R +interface LazyList : List, Closeable // by GreenDAO - fun - col(type: DataType.Simple, fetch: FetchValue): R - fun - col(type: SimpleNullable, fetch: FetchValue): R - - fun , R> - row(schema: SCH, bindBy: BindBy, fetch: FetchStruct, R, *>): R - - fun , R, ID : IdBound> - grid(schema: SCH, bindBy: BindBy, fetch: FetchStruct, *, R>): R -} - -// I have absolutely no idea how to represent primitive list changes so there's no D parameter in the first interface: -interface FetchValue { - fun cell(from: SRC, query: String, arguments: Array, type: DataType): VAL - fun col(from: SRC, query: String, arguments: Array, type: DataType): LST -} -interface FetchStruct, ID, D, STR, LST> { - fun row(from: SRC, query: String, arguments: Array, schema: SCH, bindBy: BindBy): STR - fun grid(from: SRC, query: String, arguments: Array, schema: SCH, bindBy: BindBy): LST -} - -enum class BindBy { - Name, - Position, -} -class ListChanges, ID : IdBound>( - val oldIds: List, // List could wrap IntArray, for example. Array can't - val newIds: List, - val changes: Map> -) - -interface LazyList : List -interface AsyncStruct> // TODO - -// TODO move these to tests -object User : Schema() { - val Name = "nam" let string - val Email = "email" let string -} -suspend fun CoroutineScope.smpl(s: Session) { - val name = s - .rawQuery("SELECT nam FROM users LIMIT 1") - .cell(string, asyncValue()) - .await() - - val names = s - .rawQuery("SELECT nam FROM users") - .col(string, asyncValue()) - .await() - - val explicit = s - .rawQuery("SELECT nam, email FROM users LIMIT 1") - .row(User, BindBy.Name, asyncStruct>()) - - val struct = s - .rawQuery("SELECT nam, email FROM users LIMIT 1") - .row(User, BindBy.Name, asyncStruct>()) - .await() - - val structs = s - .rawQuery("SELECT nam, email FROM users") - .grid(User, BindBy.Name, asyncStruct>()) - .await() -} /** * Represents a database session specialized for a certain [Table]. @@ -138,7 +66,7 @@ suspend fun CoroutineScope.smpl(s: Session) { */ interface Dao, ID : IdBound, REC : Record> : Manager { fun find(id: ID /* TODO fields to prefetch */): REC? - fun select(condition: WhereCondition, order: Array>/* TODO: prefetch */): Property> // TODO FetchStruct | group by | having + fun select(condition: WhereCondition, order: Array>/* TODO: prefetch */): Property> // TODO Fetch | group by | having // todo joins fun count(condition: WhereCondition): Property // why do they have 'out' variance? Because we want to use a single WhereCondition when there's no condition diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/template.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/template.kt new file mode 100644 index 00000000..e960a245 --- /dev/null +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/template.kt @@ -0,0 +1,119 @@ +@file:JvmName("SqlTemplate") +@file:Suppress( + "NOTHING_TO_INLINE", // just path-through + unchecked cast functions, nothing to outline + "UNCHECKED_CAST" +) +package net.aquadc.persistence.sql + +import net.aquadc.persistence.VarFuncImpl +import net.aquadc.persistence.type.DataType +import org.intellij.lang.annotations.Language + +/** + * A function of unknown arity. + * Implementors should also ~~implement [Function0]..[Function8]~~ + * **inherit from [VarFuncImpl]** __until KT-24067 fixed__. + */ +interface VarFunc { + fun invokeUnchecked(vararg arg: T): R +} + +interface Fetch { + fun fetch(from: SRC, query: String, argumentTypes: Array>, arguments: Array): R +} + +enum class BindBy { + Name, + Position, +} + +// Because of KT-24067 I can't just cast Query to (...) -> R, so let's cast to VariadicConsumer +inline fun Session.query( + @Language("SQL") query: String, + fetch: Fetch +): () -> R = + rawQuery(query, emptyArray(), fetch) as VarFuncImpl + +inline fun Session.query( + @Language("SQL") query: String, + type: DataType.Simple, + fetch: Fetch +): (T) -> R = + rawQuery(query, arrayOf(type), fetch) as VarFuncImpl + +inline fun Session.query( + @Language("SQL") query: String, + type1: DataType.Simple, + type2: DataType.Simple, + fetch: Fetch +): (T1, T2) -> R = + rawQuery(query, arrayOf(type1, type2), fetch) as VarFuncImpl + +inline fun Session.query( + @Language("SQL") query: String, + type1: DataType.Simple, + type2: DataType.Simple, + type3: DataType.Simple, + fetch: Fetch +): (T1, T2, T3) -> R = + rawQuery(query, arrayOf(type1, type2, type3), fetch) as VarFuncImpl + +inline fun Session.query( + @Language("SQL") query: String, + type1: DataType.Simple, + type2: DataType.Simple, + type3: DataType.Simple, + type4: DataType.Simple, + fetch: Fetch +): (T1, T2, T3, T4) -> R = + rawQuery(query, arrayOf(type1, type2, type3, type4), fetch) as VarFuncImpl + +inline fun Session.query( + @Language("SQL") query: String, + type1: DataType.Simple, + type2: DataType.Simple, + type3: DataType.Simple, + type4: DataType.Simple, + type5: DataType.Simple, + fetch: Fetch +): (T1, T2, T3, T4, T5) -> R = + rawQuery(query, arrayOf(type1, type2, type3, type4, type5), fetch) as VarFuncImpl + +inline fun Session.query( + @Language("SQL") query: String, + type1: DataType.Simple, + type2: DataType.Simple, + type3: DataType.Simple, + type4: DataType.Simple, + type5: DataType.Simple, + type6: DataType.Simple, + fetch: Fetch +): (T1, T2, T3, T4, T5) -> R = + rawQuery(query, arrayOf(type1, type2, type3, type4, type5, type6), fetch) as VarFuncImpl + +inline fun Session.query( + @Language("SQL") query: String, + type1: DataType.Simple, + type2: DataType.Simple, + type3: DataType.Simple, + type4: DataType.Simple, + type5: DataType.Simple, + type6: DataType.Simple, + type7: DataType.Simple, + fetch: Fetch +): (T1, T2, T3, T4, T5, T7) -> R = + rawQuery(query, arrayOf(type1, type2, type3, type4, type5, type6, type7), fetch) as VarFuncImpl + +inline fun Session.query( + @Language("SQL") query: String, + type1: DataType.Simple, + type2: DataType.Simple, + type3: DataType.Simple, + type4: DataType.Simple, + type5: DataType.Simple, + type6: DataType.Simple, + type7: DataType.Simple, + type8: DataType.Simple, + fetch: Fetch +): (T1, T2, T3, T4, T5, T7, T8) -> R = + rawQuery(query, arrayOf(type1, type2, type3, type4, type5, type6, type7, type8), fetch) as VarFuncImpl diff --git a/sql/src/main/kotlin/net/aquadc/persistence/sql/util.kt b/sql/src/main/kotlin/net/aquadc/persistence/sql/util.kt index d26947ff..8deb1802 100644 --- a/sql/src/main/kotlin/net/aquadc/persistence/sql/util.kt +++ b/sql/src/main/kotlin/net/aquadc/persistence/sql/util.kt @@ -2,13 +2,16 @@ package net.aquadc.persistence.sql import net.aquadc.persistence.New +import net.aquadc.persistence.sql.blocking.Blocking import net.aquadc.persistence.struct.FieldDef import net.aquadc.persistence.struct.FieldSet import net.aquadc.persistence.struct.Lens import net.aquadc.persistence.struct.PartialStruct import net.aquadc.persistence.struct.Schema import net.aquadc.persistence.struct.StoredLens +import net.aquadc.persistence.struct.StoredNamedLens import net.aquadc.persistence.struct.Struct +import net.aquadc.persistence.struct.StructSnapshot import net.aquadc.persistence.struct.allFieldSet import net.aquadc.persistence.struct.contains import net.aquadc.persistence.struct.indexOf @@ -140,6 +143,7 @@ internal inline fun Array.mapIndexedToArray(transform: (Int, T /** * Transforms flat column values to in-memory instance. + * Puts the resulting [StructSnapshot] into [mutColumnValues] at [_dstPos]. */ @Suppress("UPPER_BOUND_VIOLATED") internal fun inflate( @@ -315,3 +319,10 @@ private inline fun flattenFieldValues( ) private inline operator fun FieldSet, *>?.contains(field: FieldDef<*, *, *>): Boolean = this != null && this.contains>(field as FieldDef, *, *>) + +internal fun Blocking.row( + cursor: CUR, columns: Array>, bindBy: BindBy +): Array = when (bindBy) { + BindBy.Name -> rowByName(cursor, columns) + BindBy.Position -> rowByPosition(cursor, columns) +} diff --git a/sql/src/test/kotlin/net/aquadc/persistence/sql/TemplatesTest.kt b/sql/src/test/kotlin/net/aquadc/persistence/sql/TemplatesTest.kt new file mode 100644 index 00000000..2b2aed1d --- /dev/null +++ b/sql/src/test/kotlin/net/aquadc/persistence/sql/TemplatesTest.kt @@ -0,0 +1,85 @@ +package net.aquadc.persistence.sql + +import net.aquadc.persistence.extended.Tuple +import net.aquadc.persistence.extended.build +import net.aquadc.persistence.sql.blocking.Blocking +import net.aquadc.persistence.sql.blocking.Eager.cell +import net.aquadc.persistence.sql.blocking.Eager.col +import net.aquadc.persistence.sql.blocking.Eager.struct +import net.aquadc.persistence.sql.blocking.Eager.structList +import net.aquadc.persistence.struct.Struct +import net.aquadc.persistence.type.DataType +import net.aquadc.persistence.type.i32 +import net.aquadc.persistence.type.string +import org.junit.Assert.assertEquals +import org.junit.Test + + +open class TemplatesTest { + + open val session: Session> get() = jdbcSession + + @Test fun cell() { + val session = session as Session> + val kek = session.query("SELECT ? || 'kek'", string, cell(string)) + assertEquals("lolkek", kek("lol")) + } + + @Test fun col() { + val session = session as Session> + val one = session.query("SELECT 1", col(i32)) + assertEquals(listOf(1), one()) + } + + @Test fun row() { + val session = session as Session> + val sumAndMul = session.query( + "SELECT ? + ?, ? * ?", i32, i32, i32, i32, + struct, Int, DataType.Simple>>(projection(Tuple("f", i32, "s", i32)), BindBy.Position) + ) + assertEquals( + Pair(84, 48), + sumAndMul(80, 4, 6, 8).let { Pair(it[it.schema.First], it[it.schema.Second]) } + ) + } + + @Test fun join() { + val session = session as Session> + val johnPk = session.withTransaction { + val john = insert(UserTable, User.build("John", "john@doe.com")) + insert(ContactTable, Contact.build("@johnDoe", john.primaryKey)) + john.primaryKey + } + + val userAndContact = Tuple("u", User, "c", Contact) + // ^^^^^^^^^^^^^^ should inline this variable after inference fix + val joined = projection(userAndContact) { arrayOf( + Relation.Embedded(NestingCase, First) + , Relation.Embedded(NestingCase, Second) + ) } + + val userContact = session.query( + "SELECT u._id as 'u.id', u.name as 'u.name', u.email as 'u.email'," + + "c._id as 'c.id', c.value as 'c.value', c.user_id as 'c.user_id' " + + "FROM users u INNER JOIN contacts c ON u._id = c.user_id WHERE u.name = ? LIMIT 1", string, + struct, String, DataType.Simple>>, Tuple, String, DataType.Simple>, Struct, Long, DataType.Simple>>, Tuple, Long, DataType.Simple>>>(joined, BindBy.Name) + ) + val contact = userContact("John") + val expectedJohn = joined.schema.build( + User.build("John", "john@doe.com"), + Contact.build("@johnDoe", johnPk) + ) + assertEquals(expectedJohn, contact) + + val userContacts = session.query( + "SELECT u._id as 'u.id', u.name as 'u.name', u.email as 'u.email'," + + "c._id as 'c.id', c.value as 'c.value', c.user_id as 'c.user_id' " + + "FROM users u INNER JOIN contacts c ON u._id = c.user_id WHERE u.name = ? AND u.email LIKE (? || '%') LIMIT 1", string, string, + structList, String, DataType.Simple>>, Tuple, String, DataType.Simple>, Struct, Long, DataType.Simple>>, Tuple, Long, DataType.Simple>>>(joined, BindBy.Name) + ) + val contacts = userContacts("John", "john") + assertEquals(listOf(expectedJohn), contacts) + } + +} + diff --git a/sql/src/test/kotlin/net/aquadc/persistence/sql/database.kt b/sql/src/test/kotlin/net/aquadc/persistence/sql/database.kt index cb844cc6..80e8910c 100644 --- a/sql/src/test/kotlin/net/aquadc/persistence/sql/database.kt +++ b/sql/src/test/kotlin/net/aquadc/persistence/sql/database.kt @@ -36,7 +36,7 @@ object WithNested : Schema() { } val TableWithEmbed = tableOf(WithNested, "with_nested", "_id", i64) { arrayOf( - Relation.Embedded(SnakeCase, WithNested.Nested) + Relation.Embedded(SnakeCase, Nested) ) } @@ -46,8 +46,8 @@ object DeeplyNested : Schema() { } val TableWithDeepEmbed = tableOf(DeeplyNested, "deep", "_id", i64) { arrayOf( - Relation.Embedded(SnakeCase, DeeplyNested.Nested), - Relation.Embedded(SnakeCase, DeeplyNested.Nested / WithNested.Nested) + Relation.Embedded(SnakeCase, Nested), + Relation.Embedded(SnakeCase, Nested / WithNested.Nested) ) } @@ -62,13 +62,13 @@ val goDeeper = Tuple3( val WeNeedToGoDeeper = tableOf(goDeeper, "deeper", "_id", i64) { arrayOf( - Relation.Embedded(SnakeCase, goDeeper.First) - , Relation.Embedded(SnakeCase, goDeeper.First / DeeplyNested.Nested) - , Relation.Embedded(SnakeCase, goDeeper.First / DeeplyNested.Nested / WithNested.Nested) - , Relation.Embedded(SnakeCase, goDeeper.Second) - , Relation.Embedded(SnakeCase, goDeeper.Second / WithNullableNested.Nested, "fieldSet") - , Relation.Embedded(SnakeCase, goDeeper.Third, "which") - , Relation.Embedded(SnakeCase, goDeeper.Third % goDeeper.Third.type.schema.Second, "fieldSet") + Relation.Embedded(SnakeCase, First) + , Relation.Embedded(SnakeCase, First / DeeplyNested.Nested) + , Relation.Embedded(SnakeCase, First / DeeplyNested.Nested / WithNested.Nested) + , Relation.Embedded(SnakeCase, Second) + , Relation.Embedded(SnakeCase, Second / WithNullableNested.Nested, "fieldSet") + , Relation.Embedded(SnakeCase, Third, "which") + , Relation.Embedded(SnakeCase, Third % goDeeper.Third.type.schema.Second, "fieldSet") ) } @@ -80,7 +80,7 @@ object WithNullableNested : Schema() { } val TableWithNullableEmbed = tableOf(WithNullableNested, "with_nullable_nested", "_id", i64) { arrayOf( - Relation.Embedded(SnakeCase, WithNullableNested.Nested, "nested_fields") + Relation.Embedded(SnakeCase, Nested, "nested_fields") ) } @@ -90,7 +90,7 @@ object WithPartialNested : Schema() { } val TableWithPartialEmbed = tableOf(WithPartialNested, "with_partial_nested", "_id", i64) { arrayOf( - Relation.Embedded(SnakeCase, WithPartialNested.Nested, "nested_fields") + Relation.Embedded(SnakeCase, Nested, "nested_fields") ) } @@ -100,9 +100,9 @@ object WithEverything : Schema() { } val TableWithEverything = tableOf(WithEverything, "with_everything", "_id", i64) { arrayOf( - Relation.Embedded(SnakeCase, WithEverything.Nest1, "fields"), - Relation.Embedded(SnakeCase, WithEverything.Nest1 / WithPartialNested.Nested, "fields"), - Relation.Embedded(SnakeCase, WithEverything.Nest2) + Relation.Embedded(SnakeCase, Nest1, "fields"), + Relation.Embedded(SnakeCase, Nest1 / WithPartialNested.Nested, "fields"), + Relation.Embedded(SnakeCase, Nest2) ) } @@ -110,17 +110,26 @@ val stringOrNullableString = either("a", string, "b", nullable(string)) val schemaWithNullableEither = Tuple("1", stringOrNullableString, "2", nullable(stringOrNullableString)) val TableWithNullableEither = tableOf(schemaWithNullableEither, "tableContainingEither","_id", i64) { arrayOf( - Relation.Embedded(SnakeCase, schemaWithNullableEither.First, "which"), - Relation.Embedded(SnakeCase, schemaWithNullableEither.Second, "whetherAndWhich") + Relation.Embedded(SnakeCase, First, "which"), + Relation.Embedded(SnakeCase, Second, "whetherAndWhich") ) } +// for Templates test +val User = Tuple("name", string, "email", string) +val UserTable = tableOf(User, "users", "_id", i64) + +val Contact = Tuple("value", string, "user_id", i64) +val ContactTable = tableOf(Contact, "contacts", "_id", i64) + + val TestTables = arrayOf( SomeTable, TableWithId, TableWithEmbed, TableWithDeepEmbed, WeNeedToGoDeeper, - TableWithNullableEmbed, TableWithPartialEmbed, TableWithEverything, TableWithNullableEither + TableWithNullableEmbed, TableWithPartialEmbed, TableWithEverything, TableWithNullableEither, + UserTable, ContactTable ) -val jdbcSession by lazy { // init only when requested, unused in Rololectric tests +val jdbcSession by lazy { // init only when requested, unused in Robolectric tests JdbcSession(DriverManager.getConnection("jdbc:sqlite::memory:").also { conn -> val stmt = conn.createStatement() TestTables.forEach {