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( class EventIndexesModule(
val fts: FullTextSearchModule, val fts: FullTextSearchModule,
val tagIndexStrategy: IndexingStrategy = IndexingStrategy(),
) { ) {
fun create(db: SQLiteDatabase) { fun create(db: SQLiteDatabase) {
db.execSQL( db.execSQL(
@@ -126,14 +127,14 @@ class EventIndexesModule(
} }
val headerId = stmt.executeInsert() val headerId = stmt.executeInsert()
val tagsToIndex = event.indexableTags() val tagsToIndex = event.tags.filter(tagIndexStrategy::shouldIndex)
val reuseStatements = mutableMapOf<Int, SQLiteStatement>() val reuseStatements = mutableMapOf<Int, SQLiteStatement>()
for (chunk in tagsToIndex.chunked(300)) { for (chunk in tagsToIndex.chunked(300)) {
if (chunk.isNotEmpty()) { if (chunk.isNotEmpty()) {
val stmtTags = val stmtTags =
reuseStatements.get(chunk.size - 1) ?: run { reuseStatements[chunk.size - 1] ?: run {
val sql = val sql =
buildString { buildString {
append(sqlInsertTags) append(sqlInsertTags)
@@ -161,13 +162,11 @@ class EventIndexesModule(
return headerId return headerId
} }
fun Event.indexableTags(): List<Tag> { /**
val indexableTagNames = extraIndexableTagNames() * By default, we index all tags that have a single letter name and some value
return if (indexableTagNames.isNotEmpty()) { */
tags.filter { it.size >= 2 && (it[0].length == 1 || it[0] in indexableTagNames) } class IndexingStrategy {
} else { fun shouldIndex(tag: Tag) = tag.size >= 2 && tag[0].length == 1
tags.filter { it.size >= 2 && it[0].length == 1 }
}
} }
fun planQuery(filter: Filter): String { fun planQuery(filter: Filter): String {
@@ -23,16 +23,24 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.content.Context import android.content.Context
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventIndexesModule.IndexingStrategy
class EventStore( class EventStore(
context: Context, context: Context,
dbName: String? = "events.db", dbName: String? = "events.db",
val relayUrl: String? = "wss://quartz.local", 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) 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(filter: Filter) = store.query(filter)
fun query(filters: List<Filter>) = store.query(filters) 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.HexKey
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.sqlite.EventIndexesModule.IndexingStrategy
import com.vitorpamplona.quartz.nip40Expiration.isExpired import com.vitorpamplona.quartz.nip40Expiration.isExpired
class SQLiteEventStore( class SQLiteEventStore(
val context: Context, val context: Context,
val dbName: String? = "events.db", val dbName: String? = "events.db",
val relayUrl: String? = null, val relayUrl: String? = null,
val tagIndexStrategy: IndexingStrategy = IndexingStrategy(),
) : SQLiteOpenHelper(context, dbName, null, DATABASE_VERSION) { ) : SQLiteOpenHelper(context, dbName, null, DATABASE_VERSION) {
companion object { companion object {
const val DATABASE_VERSION = 1 const val DATABASE_VERSION = 1
} }
val fullTextSearchModule = FullTextSearchModule() val fullTextSearchModule = FullTextSearchModule()
val eventIndexModule = EventIndexesModule(fullTextSearchModule) val eventIndexModule = EventIndexesModule(fullTextSearchModule, tagIndexStrategy)
val replaceableModule = ReplaceableModule() val replaceableModule = ReplaceableModule()
val addressableModule = AddressableModule() val addressableModule = AddressableModule()
@@ -87,21 +89,52 @@ class SQLiteEventStore(
context.deleteDatabase(dbName) 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 { fun insertEvent(event: Event): Boolean {
if (event.isExpired()) throw SQLiteConstraintException("blocked: Cannot insert an expired event") if (event.isExpired()) throw SQLiteConstraintException("blocked: Cannot insert an expired event")
if (event.kind.isEphemeral()) return false if (event.kind.isEphemeral()) return false
val db = writableDatabase val db = writableDatabase
db.transaction { db.transaction {
val headerId = eventIndexModule.insert(event, db) innerInsertEvent(event, this)
deletionModule.insert(event, headerId, db)
expirationModule.insert(event, headerId, db)
fullTextSearchModule.insert(event, headerId, db)
rightToVanishModule.insert(event, relayUrl, headerId, db)
} }
return true 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(filter: Filter): List<Event> = eventIndexModule.query(filter, readableDatabase)
fun query(filters: List<Filter>): List<Event> = eventIndexModule.query(filters, readableDatabase) fun query(filters: List<Filter>): List<Event> = eventIndexModule.query(filters, readableDatabase)
@@ -44,8 +44,6 @@ open class Event(
*/ */
open fun isContentEncoded() = false open fun isContentEncoded() = false
open fun extraIndexableTagNames() = emptySet<String>()
open fun countMemory(): Int = open fun countMemory(): Int =
7 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) 7 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit)
12 + // createdAt + kind 12 + // createdAt + kind