Adds performance and flexibility by exposing Raw methods

This commit is contained in:
Vitor Pamplona
2025-12-31 13:39:17 -05:00
parent 4f7972df90
commit 6713c3bd50
2 changed files with 147 additions and 90 deletions
@@ -23,8 +23,6 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.database.Cursor import android.database.Cursor
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import com.vitorpamplona.quartz.nip01Core.core.Event 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.OptimizedJsonMapper
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.store.sqlite.sql.where import com.vitorpamplona.quartz.nip01Core.store.sqlite.sql.where
@@ -36,47 +34,66 @@ class QueryBuilder(
val fts: FullTextSearchModule, val fts: FullTextSearchModule,
val hasher: (db: SQLiteDatabase) -> TagNameValueHasher, val hasher: (db: SQLiteDatabase) -> TagNameValueHasher,
) { ) {
fun planQuery( // ------------
filter: Filter, // Main methods
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())
}
}
fun <T : Event> query( fun <T : Event> query(
filter: Filter, filter: Filter,
db: SQLiteDatabase, db: SQLiteDatabase,
): List<T> { ): List<T> = db.runQuery(toSql(filter, hasher(db)))
val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db))
return if (rowIdSubQuery == null) {
db.runQuery(makeEverythingQuery())
} else {
db.runQuery(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args)
}
}
fun <T : Event> query( fun <T : Event> query(
filter: Filter, filter: Filter,
db: SQLiteDatabase, db: SQLiteDatabase,
onEach: (T) -> Unit, onEach: (T) -> Unit,
) { ) = db.runQuery(toSql(filter, hasher(db)), onEach)
val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db))
return if (rowIdSubQuery == null) { fun <T : Event> query(
db.runQuery(makeEverythingQuery(), onEach = onEach) filters: List<Filter>,
} else { db: SQLiteDatabase,
db.runQuery(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args, onEach) ): List<T> = db.runQuery(toSql(filters, hasher(db)))
}
fun <T : Event> query(
filters: List<Filter>,
db: SQLiteDatabase,
onEach: (T) -> Unit,
) = db.runQuery(toSql(filters, hasher(db)), onEach)
// ---------------------------
// Raw methods for performance
// ---------------------------
fun rawQuery(
filter: Filter,
db: SQLiteDatabase,
): List<RawEvent> = 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<Filter>,
db: SQLiteDatabase,
): List<RawEvent> = db.runRawQuery(toSql(filters, hasher(db)))
fun rawQuery(
filters: List<Filter>,
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( fun planQuery(
@@ -84,36 +101,45 @@ class QueryBuilder(
hasher: TagNameValueHasher, hasher: TagNameValueHasher,
db: SQLiteDatabase, db: SQLiteDatabase,
): String { ): String {
val rowIdSubQuery = unionSubqueriesIfNeeded(filters, hasher) val query = toSql(filters, hasher)
return db.explainQuery(query.sql, query.args.toTypedArray())
}
return if (rowIdSubQuery == null) { fun toSql(
val query = makeEverythingQuery() filter: Filter,
db.explainQuery(query) hasher: TagNameValueHasher,
): QuerySpec {
val rowIdSubqueries = prepareRowIDSubQueries(filter, hasher)
return if (rowIdSubqueries == null) {
QuerySpec(
makeEverythingQuery(),
emptyList(),
)
} else { } else {
val query = makeQueryIn(rowIdSubQuery.sql) QuerySpec(
db.explainQuery(query, rowIdSubQuery.args.toTypedArray()) makeQueryIn(rowIdSubqueries.sql),
rowIdSubqueries.args,
)
} }
} }
fun <T : Event> query( fun toSql(
filters: List<Filter>, filters: List<Filter>,
db: SQLiteDatabase, hasher: TagNameValueHasher,
): List<T> { ): QuerySpec {
val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db)) ?: return db.runQuery(makeEverythingQuery()) val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher)
return db.runQuery(makeQueryIn(rowIdSubqueries.sql), rowIdSubqueries.args)
}
fun <T : Event> query( return if (rowIdSubqueries == null) {
filters: List<Filter>, QuerySpec(
db: SQLiteDatabase, makeEverythingQuery(),
onEach: (T) -> Unit, emptyList(),
) { )
val rowIdSubqueries = unionSubqueriesIfNeeded(filters, hasher(db))
if (rowIdSubqueries == null) {
db.runQuery(makeEverythingQuery(), onEach = onEach)
} else { } 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 ORDER BY created_at DESC, id
""".trimIndent() """.trimIndent()
private fun <T : Event> SQLiteDatabase.runQuery( private fun <T : Event> SQLiteDatabase.runQuery(query: QuerySpec): List<T> =
sql: String, rawQuery(query.sql, query.args.toTypedArray()).use { cursor ->
args: List<String> = emptyList(),
): List<T> =
rawQuery(sql, args.toTypedArray()).use { cursor ->
ArrayList<T>(cursor.count).apply { ArrayList<T>(cursor.count).apply {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
add(cursor.toEvent()) add(cursor.toEvent())
@@ -141,16 +164,33 @@ class QueryBuilder(
} }
} }
private fun SQLiteDatabase.runRawQuery(query: QuerySpec): List<RawEvent> =
rawQuery(query.sql, query.args.toTypedArray()).use { cursor ->
ArrayList<RawEvent>(cursor.count).apply {
while (cursor.moveToNext()) {
add(cursor.toRawEvent())
}
}
}
private inline fun <T : Event> SQLiteDatabase.runQuery( private inline fun <T : Event> SQLiteDatabase.runQuery(
sql: String, query: QuerySpec,
args: List<String> = emptyList(),
onEach: (T) -> Unit, onEach: (T) -> Unit,
) = rawQuery(sql, args.toTypedArray()).use { cursor -> ) = rawQuery(query.sql, query.args.toTypedArray()).use { cursor ->
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
onEach(cursor.toEvent()) 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 <T : Event> Cursor.toEvent() = private fun <T : Event> Cursor.toEvent() =
EventFactory.create<T>( EventFactory.create<T>(
getString(0).intern(), getString(0).intern(),
@@ -162,27 +202,6 @@ class QueryBuilder(
getString(6), 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 <T : Event> toEvent() =
EventFactory.create<T>(
id.intern(),
pubKey.intern(),
createdAt,
kind,
OptimizedJsonMapper.fromJsonToTagArray(jsonTags),
content,
sig,
)
}
private fun Cursor.toRawEvent() = private fun Cursor.toRawEvent() =
RawEvent( RawEvent(
getString(0), getString(0),
@@ -271,7 +290,7 @@ class QueryBuilder(
fun unionSubqueriesIfNeeded( fun unionSubqueriesIfNeeded(
filters: List<Filter>, filters: List<Filter>,
hasher: TagNameValueHasher, hasher: TagNameValueHasher,
): RowIdSubQuery? { ): QuerySpec? {
val inner = val inner =
filters.mapNotNull { filter -> filters.mapNotNull { filter ->
prepareRowIDSubQueries(filter, hasher) prepareRowIDSubQueries(filter, hasher)
@@ -282,7 +301,7 @@ class QueryBuilder(
return if (inner.size == 1) { return if (inner.size == 1) {
inner.first() inner.first()
} else { } else {
RowIdSubQuery( QuerySpec(
sql = inner.joinToString("\n UNION\n ") { "SELECT row_id FROM (${it.sql})" }, sql = inner.joinToString("\n UNION\n ") { "SELECT row_id FROM (${it.sql})" },
args = inner.flatMap { it.args }, args = inner.flatMap { it.args },
) )
@@ -306,7 +325,7 @@ class QueryBuilder(
fun prepareRowIDSubQueries( fun prepareRowIDSubQueries(
filter: Filter, filter: Filter,
hasher: TagNameValueHasher, hasher: TagNameValueHasher,
): RowIdSubQuery? { ): QuerySpec? {
if (!filter.isFilledFilter()) return null if (!filter.isFilledFilter()) return null
val mustJoinSearch = (filter.search != null) val mustJoinSearch = (filter.search != null)
@@ -457,10 +476,10 @@ class QueryBuilder(
clause.conditions 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 sql: String,
val args: List<String>, val args: List<String>,
) )
@@ -27,10 +27,13 @@ import android.database.sqlite.SQLiteOpenHelper
import androidx.core.database.sqlite.transaction import androidx.core.database.sqlite.transaction
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey 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.core.isEphemeral
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.store.IEventStore import com.vitorpamplona.quartz.nip01Core.store.IEventStore
import com.vitorpamplona.quartz.nip40Expiration.isExpired import com.vitorpamplona.quartz.nip40Expiration.isExpired
import com.vitorpamplona.quartz.utils.EventFactory
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -187,6 +190,20 @@ class SQLiteEventStore(
onEach: (T) -> Unit, onEach: (T) -> Unit,
) = queryBuilder.query(filters, readableDatabase, onEach) ) = queryBuilder.query(filters, readableDatabase, onEach)
fun rawQuery(filter: Filter): List<RawEvent> = queryBuilder.rawQuery(filter, readableDatabase)
fun rawQuery(filters: List<Filter>): List<RawEvent> = queryBuilder.rawQuery(filters, readableDatabase)
fun rawQuery(
filter: Filter,
onEach: (RawEvent) -> Unit,
) = queryBuilder.rawQuery(filter, readableDatabase, onEach)
fun rawQuery(
filters: List<Filter>,
onEach: (RawEvent) -> Unit,
) = queryBuilder.rawQuery(filters, readableDatabase, onEach)
fun count(filter: Filter): Int = queryBuilder.count(filter, readableDatabase) fun count(filter: Filter): Int = queryBuilder.count(filter, readableDatabase)
fun count(filters: List<Filter>): Int = queryBuilder.count(filters, readableDatabase) fun count(filters: List<Filter>): Int = queryBuilder.count(filters, readableDatabase)
@@ -203,3 +220,24 @@ class SQLiteEventStore(
fun deleteExpiredEvents() = expirationModule.deleteExpiredEvents(writableDatabase) 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 <T : Event> toEvent() =
EventFactory.create<T>(
id.intern(),
pubKey.intern(),
createdAt,
kind,
OptimizedJsonMapper.fromJsonToTagArray(jsonTags),
content,
sig,
)
}