diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index 0d3a5bb79..94713826c 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -23,8 +23,6 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite import android.database.Cursor import android.database.sqlite.SQLiteDatabase import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.store.sqlite.sql.where @@ -36,47 +34,66 @@ class QueryBuilder( val fts: FullTextSearchModule, val hasher: (db: SQLiteDatabase) -> TagNameValueHasher, ) { - fun planQuery( - filter: Filter, - hasher: TagNameValueHasher, - db: SQLiteDatabase, - ): String { - val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher) - - return if (rowIdSubQuery == null) { - val query = makeEverythingQuery() - db.explainQuery(query) - } else { - val query = makeQueryIn(rowIdSubQuery.sql) - db.explainQuery(query, rowIdSubQuery.args.toTypedArray()) - } - } - + // ------------ + // Main methods + // ------------ fun query( filter: Filter, db: SQLiteDatabase, - ): List { - val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) - - return if (rowIdSubQuery == null) { - db.runQuery(makeEverythingQuery()) - } else { - db.runQuery(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args) - } - } + ): List = db.runQuery(toSql(filter, hasher(db))) fun query( filter: Filter, db: SQLiteDatabase, onEach: (T) -> Unit, - ) { - val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) + ) = db.runQuery(toSql(filter, hasher(db)), onEach) - return if (rowIdSubQuery == null) { - db.runQuery(makeEverythingQuery(), onEach = onEach) - } else { - db.runQuery(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args, onEach) - } + fun query( + filters: List, + db: SQLiteDatabase, + ): List = db.runQuery(toSql(filters, hasher(db))) + + fun query( + filters: List, + db: SQLiteDatabase, + onEach: (T) -> Unit, + ) = db.runQuery(toSql(filters, hasher(db)), onEach) + + // --------------------------- + // Raw methods for performance + // --------------------------- + fun rawQuery( + filter: Filter, + db: SQLiteDatabase, + ): List = db.runRawQuery(toSql(filter, hasher(db))) + + fun rawQuery( + filter: Filter, + db: SQLiteDatabase, + onEach: (RawEvent) -> Unit, + ) = db.runRawQuery(toSql(filter, hasher(db)), onEach) + + fun rawQuery( + filters: List, + db: SQLiteDatabase, + ): List = db.runRawQuery(toSql(filters, hasher(db))) + + fun rawQuery( + filters: List, + db: SQLiteDatabase, + onEach: (RawEvent) -> Unit, + ) = db.runRawQuery(toSql(filters, hasher(db)), onEach) + + // ----------- + // Debug Tools + // ----------- + fun planQuery( + filter: Filter, + hasher: TagNameValueHasher, + db: SQLiteDatabase, + ): String { + val query = toSql(filter, hasher) + return db.explainQuery(query.sql, query.args.toTypedArray()) } fun planQuery( @@ -84,36 +101,45 @@ class QueryBuilder( hasher: TagNameValueHasher, db: SQLiteDatabase, ): String { - val rowIdSubQuery = unionSubqueriesIfNeeded(filters, hasher) + val query = toSql(filters, hasher) + return db.explainQuery(query.sql, query.args.toTypedArray()) + } - return if (rowIdSubQuery == null) { - val query = makeEverythingQuery() - db.explainQuery(query) + fun toSql( + filter: Filter, + hasher: TagNameValueHasher, + ): QuerySpec { + val rowIdSubqueries = prepareRowIDSubQueries(filter, hasher) + + return if (rowIdSubqueries == null) { + QuerySpec( + makeEverythingQuery(), + emptyList(), + ) } else { - val query = makeQueryIn(rowIdSubQuery.sql) - db.explainQuery(query, rowIdSubQuery.args.toTypedArray()) + QuerySpec( + makeQueryIn(rowIdSubqueries.sql), + rowIdSubqueries.args, + ) } } - fun query( + fun toSql( filters: List, - db: SQLiteDatabase, - ): List { - val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) ?: return db.runQuery(makeEverythingQuery()) - return db.runQuery(makeQueryIn(rowIdSubqueries.sql), rowIdSubqueries.args) - } + hasher: TagNameValueHasher, + ): QuerySpec { + val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher) - fun query( - filters: List, - db: SQLiteDatabase, - onEach: (T) -> Unit, - ) { - val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) - - if (rowIdSubqueries == null) { - db.runQuery(makeEverythingQuery(), onEach = onEach) + return if (rowIdSubqueries == null) { + QuerySpec( + makeEverythingQuery(), + emptyList(), + ) } else { - db.runQuery(makeQueryIn(rowIdSubqueries.sql), rowIdSubqueries.args, onEach) + QuerySpec( + makeQueryIn(rowIdSubqueries.sql), + rowIdSubqueries.args, + ) } } @@ -129,11 +155,8 @@ class QueryBuilder( ORDER BY created_at DESC, id """.trimIndent() - private fun SQLiteDatabase.runQuery( - sql: String, - args: List = emptyList(), - ): List = - rawQuery(sql, args.toTypedArray()).use { cursor -> + private fun SQLiteDatabase.runQuery(query: QuerySpec): List = + rawQuery(query.sql, query.args.toTypedArray()).use { cursor -> ArrayList(cursor.count).apply { while (cursor.moveToNext()) { add(cursor.toEvent()) @@ -141,16 +164,33 @@ class QueryBuilder( } } + private fun SQLiteDatabase.runRawQuery(query: QuerySpec): List = + rawQuery(query.sql, query.args.toTypedArray()).use { cursor -> + ArrayList(cursor.count).apply { + while (cursor.moveToNext()) { + add(cursor.toRawEvent()) + } + } + } + private inline fun SQLiteDatabase.runQuery( - sql: String, - args: List = emptyList(), + query: QuerySpec, onEach: (T) -> Unit, - ) = rawQuery(sql, args.toTypedArray()).use { cursor -> + ) = rawQuery(query.sql, query.args.toTypedArray()).use { cursor -> while (cursor.moveToNext()) { onEach(cursor.toEvent()) } } + private inline fun SQLiteDatabase.runRawQuery( + query: QuerySpec, + onEach: (RawEvent) -> Unit, + ) = rawQuery(query.sql, query.args.toTypedArray()).use { cursor -> + while (cursor.moveToNext()) { + onEach(cursor.toRawEvent()) + } + } + private fun Cursor.toEvent() = EventFactory.create( getString(0).intern(), @@ -162,27 +202,6 @@ class QueryBuilder( getString(6), ) - class RawEvent( - val id: HexKey, - val pubKey: HexKey, - val createdAt: Long, - val kind: Kind, - val jsonTags: String, - val content: String, - val sig: HexKey, - ) { - fun toEvent() = - EventFactory.create( - id.intern(), - pubKey.intern(), - createdAt, - kind, - OptimizedJsonMapper.fromJsonToTagArray(jsonTags), - content, - sig, - ) - } - private fun Cursor.toRawEvent() = RawEvent( getString(0), @@ -271,7 +290,7 @@ class QueryBuilder( fun unionSubqueriesIfNeeded( filters: List, hasher: TagNameValueHasher, - ): RowIdSubQuery? { + ): QuerySpec? { val inner = filters.mapNotNull { filter -> prepareRowIDSubQueries(filter, hasher) @@ -282,7 +301,7 @@ class QueryBuilder( return if (inner.size == 1) { inner.first() } else { - RowIdSubQuery( + QuerySpec( sql = inner.joinToString("\n UNION\n ") { "SELECT row_id FROM (${it.sql})" }, args = inner.flatMap { it.args }, ) @@ -306,7 +325,7 @@ class QueryBuilder( fun prepareRowIDSubQueries( filter: Filter, hasher: TagNameValueHasher, - ): RowIdSubQuery? { + ): QuerySpec? { if (!filter.isFilledFilter()) return null val mustJoinSearch = (filter.search != null) @@ -457,10 +476,10 @@ class QueryBuilder( clause.conditions } - return RowIdSubQuery("$projection WHERE $whereClause", clause.args) + return QuerySpec("$projection WHERE $whereClause", clause.args) } - data class RowIdSubQuery( + data class QuerySpec( val sql: String, val args: List, ) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt index c51d50753..f7faf6792 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -27,10 +27,13 @@ import android.database.sqlite.SQLiteOpenHelper import androidx.core.database.sqlite.transaction import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind +import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.core.isEphemeral import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip40Expiration.isExpired +import com.vitorpamplona.quartz.utils.EventFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -187,6 +190,20 @@ class SQLiteEventStore( onEach: (T) -> Unit, ) = queryBuilder.query(filters, readableDatabase, onEach) + fun rawQuery(filter: Filter): List = queryBuilder.rawQuery(filter, readableDatabase) + + fun rawQuery(filters: List): List = queryBuilder.rawQuery(filters, readableDatabase) + + fun rawQuery( + filter: Filter, + onEach: (RawEvent) -> Unit, + ) = queryBuilder.rawQuery(filter, readableDatabase, onEach) + + fun rawQuery( + filters: List, + onEach: (RawEvent) -> Unit, + ) = queryBuilder.rawQuery(filters, readableDatabase, onEach) + fun count(filter: Filter): Int = queryBuilder.count(filter, readableDatabase) fun count(filters: List): Int = queryBuilder.count(filters, readableDatabase) @@ -203,3 +220,24 @@ class SQLiteEventStore( fun deleteExpiredEvents() = expirationModule.deleteExpiredEvents(writableDatabase) } + +class RawEvent( + val id: HexKey, + val pubKey: HexKey, + val createdAt: Long, + val kind: Kind, + val jsonTags: String, + val content: String, + val sig: HexKey, +) { + fun toEvent() = + EventFactory.create( + id.intern(), + pubKey.intern(), + createdAt, + kind, + OptimizedJsonMapper.fromJsonToTagArray(jsonTags), + content, + sig, + ) +}