From 26a764a6346e452484ebded4785a142080450608 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 9 Dec 2025 17:15:22 -0500 Subject: [PATCH] Offers support for external transactions Offers support for indexing strategies for tags --- .../store/sqlite/EventIndexesModule.kt | 17 ++++--- .../nip01Core/store/sqlite/EventStore.kt | 8 ++++ .../store/sqlite/SQLiteEventStore.kt | 45 ++++++++++++++++--- .../quartz/nip01Core/core/Event.kt | 2 - 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt index 5f07246a6..0bd3026c2 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -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() 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 { - 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 { diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt index 4794e4c7b..e81ee9ef4 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt @@ -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) 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) = store.query(filters) 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 04cde38c8..b258e10a7 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 @@ -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 = eventIndexModule.query(filter, readableDatabase) fun query(filters: List): List = eventIndexModule.query(filters, readableDatabase) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt index 40656a033..e7a455689 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt @@ -44,8 +44,6 @@ open class Event( */ open fun isContentEncoded() = false - open fun extraIndexableTagNames() = emptySet() - open fun countMemory(): Int = 7 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) 12 + // createdAt + kind