Offers support for external transactions

Offers support for indexing strategies for tags
This commit is contained in:
Vitor Pamplona
2025-12-09 17:15:22 -05:00
parent bcebac529b
commit d0efe26ce7
4 changed files with 56 additions and 18 deletions
@@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.utils.EventFactory
class EventIndexesModule(
val fts: FullTextSearchModule,
val tagIndexStrategy: IndexingStrategy = IndexingStrategy(),
) {
fun create(db: SQLiteDatabase) {
db.execSQL(
@@ -126,14 +127,14 @@ class EventIndexesModule(
}
val headerId = stmt.executeInsert()
val tagsToIndex = event.indexableTags()
val tagsToIndex = event.tags.filter(tagIndexStrategy::shouldIndex)
val reuseStatements = mutableMapOf<Int, SQLiteStatement>()
for (chunk in tagsToIndex.chunked(300)) {
if (chunk.isNotEmpty()) {
val stmtTags =
reuseStatements.get(chunk.size - 1) ?: run {
reuseStatements[chunk.size - 1] ?: run {
val sql =
buildString {
append(sqlInsertTags)
@@ -161,13 +162,11 @@ class EventIndexesModule(
return headerId
}
fun Event.indexableTags(): List<Tag> {
val indexableTagNames = extraIndexableTagNames()
return if (indexableTagNames.isNotEmpty()) {
tags.filter { it.size >= 2 && (it[0].length == 1 || it[0] in indexableTagNames) }
} else {
tags.filter { it.size >= 2 && it[0].length == 1 }
}
/**
* By default, we index all tags that have a single letter name and some value
*/
class IndexingStrategy {
fun shouldIndex(tag: Tag) = tag.size >= 2 && tag[0].length == 1
}
fun planQuery(filter: Filter): String {
@@ -23,16 +23,24 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.content.Context
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventIndexesModule.IndexingStrategy
class EventStore(
context: Context,
dbName: String? = "events.db",
val relayUrl: String? = "wss://quartz.local",
val tagIndexStrategy: IndexingStrategy = IndexingStrategy(),
) {
val store = SQLiteEventStore(context, dbName, relayUrl)
val store = SQLiteEventStore(context, dbName, relayUrl, tagIndexStrategy)
fun insert(event: Event) = store.insertEvent(event)
interface ITransaction {
fun insert(event: Event): Boolean
}
fun transaction(body: ITransaction.() -> Unit) = store.transaction(body)
fun query(filter: Filter) = store.query(filter)
fun query(filters: List<Filter>) = store.query(filters)
@@ -29,19 +29,21 @@ import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.isEphemeral
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventIndexesModule.IndexingStrategy
import com.vitorpamplona.quartz.nip40Expiration.isExpired
class SQLiteEventStore(
val context: Context,
val dbName: String? = "events.db",
val relayUrl: String? = null,
val tagIndexStrategy: IndexingStrategy = IndexingStrategy(),
) : SQLiteOpenHelper(context, dbName, null, DATABASE_VERSION) {
companion object {
const val DATABASE_VERSION = 1
}
val fullTextSearchModule = FullTextSearchModule()
val eventIndexModule = EventIndexesModule(fullTextSearchModule)
val eventIndexModule = EventIndexesModule(fullTextSearchModule, tagIndexStrategy)
val replaceableModule = ReplaceableModule()
val addressableModule = AddressableModule()
@@ -87,21 +89,52 @@ class SQLiteEventStore(
context.deleteDatabase(dbName)
}
private fun innerInsertEvent(
event: Event,
db: SQLiteDatabase,
) {
val headerId = eventIndexModule.insert(event, db)
deletionModule.insert(event, headerId, db)
expirationModule.insert(event, headerId, db)
fullTextSearchModule.insert(event, headerId, db)
rightToVanishModule.insert(event, relayUrl, headerId, db)
}
fun insertEvent(event: Event): Boolean {
if (event.isExpired()) throw SQLiteConstraintException("blocked: Cannot insert an expired event")
if (event.kind.isEphemeral()) return false
val db = writableDatabase
db.transaction {
val headerId = eventIndexModule.insert(event, db)
deletionModule.insert(event, headerId, db)
expirationModule.insert(event, headerId, db)
fullTextSearchModule.insert(event, headerId, db)
rightToVanishModule.insert(event, relayUrl, headerId, db)
innerInsertEvent(event, this)
}
return true
}
inner class Transaction(
val db: SQLiteDatabase,
) : EventStore.ITransaction {
override fun insert(event: Event): Boolean {
if (event.isExpired()) throw SQLiteConstraintException("blocked: Cannot insert an expired event")
if (event.kind.isEphemeral()) return false
innerInsertEvent(event, db)
return true
}
}
fun transaction(body: Transaction.() -> Unit) {
val db = writableDatabase
db.beginTransaction()
try {
with(Transaction(db)) {
body()
}
} finally {
db.endTransaction()
}
}
fun query(filter: Filter): List<Event> = eventIndexModule.query(filter, readableDatabase)
fun query(filters: List<Filter>): List<Event> = eventIndexModule.query(filters, readableDatabase)