Faster SQLLite DB with probabilistic hashvalues

This commit is contained in:
Vitor Pamplona
2025-12-22 15:18:59 -05:00
parent 79d6745ee6
commit 01ea413afd
9 changed files with 248 additions and 132 deletions
@@ -79,7 +79,7 @@ class DeletionTest {
db.insert(note1)
fail("Should not be able to insert a deleted event")
} catch (e: SQLiteConstraintException) {
assertEquals("blocked: a deletion event for this event id exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
}
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
@@ -124,7 +124,7 @@ class DeletionTest {
db.insert(note1)
fail("Should not be able to insert a deleted event")
} catch (e: SQLiteConstraintException) {
assertEquals("blocked: a deletion event for this event id exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
}
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
@@ -162,7 +162,7 @@ class DeletionTest {
db.insert(note1)
fail("Should not be able to insert a deleted event")
} catch (e: SQLiteConstraintException) {
assertEquals("blocked: a deletion event for this address exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
assertEquals("blocked: a deletion event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message)
}
db.assertQuery(deletion, Filter(ids = listOf(deletion.id)))
@@ -32,39 +32,23 @@ class AddressableModule : IModule {
""".trimIndent(),
)
// Rejects old addressables
// deletes older addressables when inserting new ones
// if a newer addressable is inserted the unique index
// above will be triggered. Delete cascade will take
// care of the event_tags table
db.execSQL(
"""
CREATE TRIGGER reject_older_addressable_event
CREATE TRIGGER delete_older_addressable_event
BEFORE INSERT ON event_headers
FOR EACH ROW
WHEN (NEW.kind >= 30000 AND NEW.kind < 40000)
BEGIN
-- Check for existing newer record
SELECT RAISE(ABORT, 'duplicate: A newer or equally new record already exists')
WHERE EXISTS (
SELECT 1 FROM event_headers
WHERE
event_headers.created_at >= NEW.created_at AND
event_headers.kind = NEW.kind AND
event_headers.pubkey = NEW.pubkey AND
event_headers.d_tag = NEW.d_tag
);
DELETE FROM event_tags
WHERE event_header_row_id in (
SELECT row_id FROM event_headers
WHERE
event_headers.kind = NEW.kind AND
event_headers.pubkey = NEW.pubkey AND
event_headers.d_tag = NEW.d_tag
);
DELETE FROM event_headers
WHERE
event_headers.kind = NEW.kind AND
event_headers.pubkey = NEW.pubkey AND
event_headers.d_tag = NEW.d_tag;
event_headers.d_tag = NEW.d_tag AND
event_headers.created_at < NEW.created_at;
END;
""".trimIndent(),
)
@@ -34,27 +34,16 @@ class DeletionRequestModule : IModule {
FOR EACH ROW
BEGIN
-- Check for ID-based deletion record
SELECT RAISE(ABORT, 'blocked: a deletion event for this event id exists')
SELECT RAISE(ABORT, 'blocked: a deletion event exists')
WHERE EXISTS (
SELECT 1 FROM event_headers INNER JOIN event_tags ON event_headers.row_id = event_tags.event_header_row_id
SELECT 1 FROM event_headers
INNER JOIN event_tags
ON event_headers.row_id = event_tags.event_header_row_id
WHERE
event_headers.created_at >= NEW.created_at AND
event_headers.kind = 5 AND
event_headers.pubkey = NEW.pubkey AND
event_tags.tag_name = 'e' AND
event_tags.tag_value = NEW.id
);
-- Check for address-based deletion record
SELECT RAISE(ABORT, 'blocked: a deletion event for this address exists')
WHERE EXISTS (
SELECT 1 FROM event_headers INNER JOIN event_tags ON event_headers.row_id = event_tags.event_header_row_id
WHERE
event_headers.created_at >= NEW.created_at AND
event_headers.kind = 5 AND
event_headers.pubkey = NEW.pubkey AND
event_tags.tag_name = 'a' AND
event_tags.tag_value = NEW.kind || ':' || NEW.pubkey || ':' || NEW.d_tag
event_tags.tag_hash IN (NEW.etag_hash, NEW.atag_hash)
);
END;
""".trimIndent(),
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteStatement
import com.vitorpamplona.quartz.nip01Core.core.AddressSerializer
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
@@ -33,8 +33,13 @@ import com.vitorpamplona.quartz.utils.EventFactory
class EventIndexesModule(
val fts: FullTextSearchModule,
val seedModule: SeedModule,
val tagIndexStrategy: IndexingStrategy = IndexingStrategy(),
) : IModule {
private var hasherCache: TagNameValueHasher? = null
fun hasher(db: SQLiteDatabase): TagNameValueHasher = hasherCache ?: TagNameValueHasher(seedModule.getSeed(db)).also { hasherCache = it }
override fun create(db: SQLiteDatabase) {
db.execSQL(
"""
@@ -45,6 +50,8 @@ class EventIndexesModule(
created_at INTEGER NOT NULL,
kind INTEGER NOT NULL,
d_tag TEXT,
etag_hash INTEGER NOT NULL,
atag_hash INTEGER,
tags TEXT NOT NULL,
content TEXT NOT NULL,
sig TEXT NOT NULL
@@ -55,9 +62,8 @@ class EventIndexesModule(
db.execSQL(
"""
CREATE TABLE event_tags (
event_header_row_id INTEGER,
tag_name TEXT NOT NULL,
tag_value TEXT NOT NULL,
event_header_row_id INTEGER NOT NULL,
tag_hash INTEGER NOT NULL,
FOREIGN KEY (event_header_row_id) REFERENCES event_headers(row_id) ON DELETE CASCADE
)
""".trimIndent(),
@@ -66,7 +72,7 @@ class EventIndexesModule(
db.execSQL("CREATE UNIQUE INDEX event_headers_id ON event_headers (id)")
db.execSQL("CREATE INDEX query_by_kind_pubkey_idx ON event_headers (created_at desc, kind, pubkey, d_tag)")
db.execSQL("CREATE INDEX query_by_id_idx ON event_headers (created_at desc, id)")
db.execSQL("CREATE INDEX query_by_tags_idx ON event_tags (tag_name, tag_value)")
db.execSQL("CREATE INDEX query_by_tags_idx ON event_tags (tag_hash)")
// Prevent updates to maintain immutability
db.execSQL(
@@ -100,23 +106,24 @@ class EventIndexesModule(
val sqlInsertHeader =
"""
INSERT INTO event_headers
(id, pubkey, created_at, kind, tags, content, sig, d_tag)
(id, pubkey, created_at, kind, tags, content, sig, d_tag, etag_hash, atag_hash)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?)
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val sqlInsertTags =
"""
INSERT OR ROLLBACK INTO event_tags
(event_header_row_id, tag_name, tag_value)
(event_header_row_id, tag_hash)
VALUES
(?,?,?)
(?,?)
""".trimIndent()
fun insert(
event: Event,
db: SQLiteDatabase,
): Long {
val hasher = hasher(db)
val stmt = db.compileStatement(sqlInsertHeader)
stmt.bindString(1, event.id)
stmt.bindString(2, event.pubKey)
@@ -126,40 +133,24 @@ class EventIndexesModule(
stmt.bindString(6, event.content)
stmt.bindString(7, event.sig)
if (event is AddressableEvent) {
stmt.bindString(8, event.dTag())
val dTag = event.dTag()
stmt.bindString(8, dTag)
stmt.bindLong(9, hasher.hashETag(event.id))
stmt.bindLong(10, hasher.hashATag(AddressSerializer.assemble(event.kind, event.pubKey, dTag)))
} else {
stmt.bindNull(8)
stmt.bindLong(9, hasher.hashETag(event.id))
stmt.bindNull(10)
}
val headerId = stmt.executeInsert()
val tagsToIndex = event.tags.filter(tagIndexStrategy::shouldIndex)
val reuseStatements = mutableMapOf<Int, SQLiteStatement>()
for (chunk in tagsToIndex.chunked(300)) {
if (chunk.isNotEmpty()) {
val stmtTags =
reuseStatements[chunk.size - 1] ?: run {
val sql =
buildString {
append(sqlInsertTags)
repeat(chunk.size - 1) {
append(",(?,?,?)")
}
}
val new = db.compileStatement(sql)
reuseStatements[chunk.size - 1] = new
new
}
var index = 1
chunk.forEach { tag ->
stmtTags.bindLong(index++, headerId)
stmtTags.bindString(index++, tag[0])
stmtTags.bindString(index++, tag[1])
}
val stmtTags = db.compileStatement(sqlInsertTags)
event.tags.forEach { tag ->
if (tagIndexStrategy.shouldIndex(event.kind, tag)) {
stmtTags.bindLong(1, headerId)
stmtTags.bindLong(2, hasher.hash(tag[0], tag[1]))
stmtTags.executeInsert()
}
}
@@ -171,11 +162,17 @@ class EventIndexesModule(
* 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 shouldIndex(
kind: Int,
tag: Tag,
) = tag.size >= 2 && tag[0].length == 1
}
fun planQuery(filter: Filter): String {
val rowIdSubQuery = prepareRowIDSubQueries(filter) ?: return makeEverythingQuery()
fun planQuery(
filter: Filter,
hasher: TagNameValueHasher,
): String {
val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher) ?: return makeEverythingQuery()
return makeQueryIn(rowIdSubQuery.sql)
}
@@ -184,7 +181,7 @@ class EventIndexesModule(
filter: Filter,
db: SQLiteDatabase,
): List<T> {
val rowIdSubQuery = prepareRowIDSubQueries(filter)
val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db))
return if (rowIdSubQuery == null) {
db.runQuery(makeEverythingQuery())
@@ -198,13 +195,16 @@ class EventIndexesModule(
db: SQLiteDatabase,
onEach: (T) -> Unit,
) {
val rowIdSubQuery = prepareRowIDSubQueries(filter) ?: return db.runQueryEmitting(makeEverythingQuery(), onEach = onEach)
val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) ?: return db.runQueryEmitting(makeEverythingQuery(), onEach = onEach)
db.runQueryEmitting(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args, onEach)
}
fun planQuery(filters: List<Filter>): String {
val rowIdSubQueries = filters.mapNotNull { prepareRowIDSubQueries(it) }
fun planQuery(
filters: List<Filter>,
hasher: TagNameValueHasher,
): String {
val rowIdSubQueries = filters.mapNotNull { prepareRowIDSubQueries(it, hasher) }
if (rowIdSubQueries.isEmpty()) return makeEverythingQuery()
val unions = rowIdSubQueries.joinToString(" UNION ") { it.sql }
return makeQueryIn(unions)
@@ -214,7 +214,7 @@ class EventIndexesModule(
filters: List<Filter>,
db: SQLiteDatabase,
): List<T> {
val rowIdSubQueries = filters.mapNotNull { prepareRowIDSubQueries(it) }
val rowIdSubQueries = filters.mapNotNull { prepareRowIDSubQueries(it, hasher(db)) }
if (rowIdSubQueries.isEmpty()) return db.runQuery(makeEverythingQuery())
@@ -229,7 +229,7 @@ class EventIndexesModule(
db: SQLiteDatabase,
onEach: (T) -> Unit,
) {
val rowIdSubQueries = filters.mapNotNull { prepareRowIDSubQueries(it) }
val rowIdSubQueries = filters.mapNotNull { prepareRowIDSubQueries(it, hasher(db)) }
if (rowIdSubQueries.isEmpty()) return db.runQueryEmitting(makeEverythingQuery(), onEach = onEach)
@@ -311,7 +311,7 @@ class EventIndexesModule(
filter: Filter,
db: SQLiteDatabase,
): Int {
val rowIdSubQuery = prepareRowIDSubQueries(filter) ?: return db.countEverything()
val rowIdSubQuery = prepareRowIDSubQueries(filter, hasher(db)) ?: return db.countEverything()
return db.countIn(rowIdSubQuery.sql, rowIdSubQuery.args)
}
@@ -320,7 +320,7 @@ class EventIndexesModule(
filters: List<Filter>,
db: SQLiteDatabase,
): Int {
val rowIdSubQueries = filters.mapNotNull { prepareRowIDSubQueries(it) }
val rowIdSubQueries = filters.mapNotNull { prepareRowIDSubQueries(it, hasher(db)) }
if (rowIdSubQueries.isEmpty()) return db.countEverything()
@@ -353,7 +353,7 @@ class EventIndexesModule(
filter: Filter,
db: SQLiteDatabase,
): Int {
val rowIdQuery = prepareRowIDSubQueries(filter) ?: return 0
val rowIdQuery = prepareRowIDSubQueries(filter, hasher(db)) ?: return 0
return db.runDelete(rowIdQuery.sql, rowIdQuery.args)
}
@@ -361,7 +361,7 @@ class EventIndexesModule(
filters: List<Filter>,
db: SQLiteDatabase,
): Int {
val rowIdSubqueries = filters.mapNotNull { prepareRowIDSubQueries(it) }
val rowIdSubqueries = filters.mapNotNull { prepareRowIDSubQueries(it, hasher(db)) }
if (rowIdSubqueries.isEmpty()) return 0
@@ -379,7 +379,10 @@ class EventIndexesModule(
// ----------------------------
// Inner row id selections
// ----------------------------
fun prepareRowIDSubQueries(filter: Filter): RowIdSubQuery? {
fun prepareRowIDSubQueries(
filter: Filter,
hasher: TagNameValueHasher,
): RowIdSubQuery? {
if (!filter.isFilledFilter()) return null
val hasHeaders =
@@ -453,8 +456,12 @@ class EventIndexesModule(
if (tagName == "d") {
equalsOrIn("event_headers.d_tag", tagValues)
} else {
equals("tag$tagName.tag_name", tagName)
equalsOrIn("tag$tagName.tag_value", tagValues)
equalsOrIn(
"tag$tagName.tag_hash",
tagValues.map {
hasher.hash(tagName, it)
},
)
}
}
@@ -32,37 +32,23 @@ class ReplaceableModule : IModule {
""".trimIndent(),
)
// Rejects old replaceables
// deletes older addressables when inserting new ones
// if a newer addressable is inserted the unique index
// above will be triggered. Delete cascade will take
// care of the event_tags table
db.execSQL(
"""
CREATE TRIGGER reject_older_replaceable_event
CREATE TRIGGER delete_older_replaceable_event
BEFORE INSERT ON event_headers
FOR EACH ROW
WHEN (NEW.kind >= 10000 AND NEW.kind < 20000) OR (NEW.kind IN (0, 3))
BEGIN
-- Check for existing newer record
SELECT RAISE(ABORT, 'duplicate: A newer or equally new record already exists')
WHERE EXISTS (
SELECT 1 FROM event_headers
WHERE
event_headers.created_at >= NEW.created_at AND
event_headers.kind = NEW.kind AND
event_headers.pubkey = NEW.pubkey
);
DELETE FROM event_tags
WHERE event_header_row_id in (
SELECT row_id FROM event_headers
WHERE
event_headers.kind = NEW.kind AND
event_headers.pubkey = NEW.pubkey
);
-- Delete older records if this is the newest
DELETE FROM event_headers
WHERE
event_headers.kind = NEW.kind AND
event_headers.pubkey = NEW.pubkey;
event_headers.pubkey = NEW.pubkey AND
event_headers.created_at < NEW.created_at;
END;
""".trimIndent(),
)
@@ -40,11 +40,12 @@ class SQLiteEventStore(
val tagIndexStrategy: IndexingStrategy = IndexingStrategy(),
) : SQLiteOpenHelper(context, dbName, null, DATABASE_VERSION) {
companion object {
const val DATABASE_VERSION = 1
const val DATABASE_VERSION = 2
}
val seedModule = SeedModule()
val fullTextSearchModule = FullTextSearchModule()
val eventIndexModule = EventIndexesModule(fullTextSearchModule, tagIndexStrategy)
val eventIndexModule = EventIndexesModule(fullTextSearchModule, seedModule, tagIndexStrategy)
val replaceableModule = ReplaceableModule()
val addressableModule = AddressableModule()
@@ -56,6 +57,7 @@ class SQLiteEventStore(
val modules =
listOf(
seedModule,
eventIndexModule,
replaceableModule,
addressableModule,
@@ -91,7 +93,19 @@ class SQLiteEventStore(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int,
) {}
) {
// Handle all intermediate versions
for (version in oldVersion..<newVersion) {
when (version) {
1 -> {
// Upgrade from version 1 to 2
// We changed event_tags to use a probabilistic hash as tag
modules.reversed().forEach { it.drop(db) }
modules.forEach { it.create(db) }
}
}
}
}
fun clearDB() {
val db = writableDatabase
@@ -132,14 +146,10 @@ class SQLiteEventStore(
}
fun transaction(body: Transaction.() -> Unit) {
val db = writableDatabase
db.beginTransaction()
try {
with(Transaction(db)) {
writableDatabase.transaction {
with(Transaction(this)) {
body()
}
} finally {
db.endTransaction()
}
}
@@ -0,0 +1,79 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.store.sqlite
import android.database.sqlite.SQLiteDatabase
import com.vitorpamplona.quartz.utils.RandomInstance
class SeedModule : IModule {
override fun create(db: SQLiteDatabase) {
db.execSQL("CREATE TABLE seeds (seed_value INTEGER)")
val insertSeed = "INSERT INTO seeds (seed_value) VALUES (?)"
val stmt = db.compileStatement(insertSeed)
stmt.bindLong(1, RandomInstance.long())
stmt.executeInsert()
// Prevent updates to maintain immutability
db.execSQL(
"""
CREATE TRIGGER block_insert_seeds
BEFORE INSERT ON seeds
BEGIN
SELECT RAISE(ABORT, 'Inserts are not allowed on this table');
END;
""".trimIndent(),
)
db.execSQL(
"""
CREATE TRIGGER block_update_seeds
BEFORE UPDATE ON seeds
BEGIN
SELECT RAISE(ABORT, 'Inserts are not allowed on this table');
END;
""".trimIndent(),
)
db.execSQL(
"""
CREATE TRIGGER block_delete_seeds
BEFORE DELETE ON seeds
BEGIN
SELECT RAISE(ABORT, 'Inserts are not allowed on this table');
END;
""".trimIndent(),
)
}
fun getSeed(db: SQLiteDatabase): Long =
db.rawQuery("SELECT seed_value FROM seeds LIMIT 1", null).use {
it.moveToFirst()
it.getLong(0)
}
override fun drop(db: SQLiteDatabase) {
db.execSQL("DROP TABLE IF EXISTS seeds")
}
override fun deleteAll(db: SQLiteDatabase) {}
}
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.store.sqlite
import com.vitorpamplona.quartz.nip01Core.hints.bloom.MurmurHash3
/**
* The seed is used to compute the hash of the Key, which
* becomes the seed for the hash of the value
*/
class TagNameValueHasher(
val seed: Long,
) {
val hasher = MurmurHash3()
// small performance improvements on inserting
val eTagHash by lazy {
hasher.hash128x64Half("e".encodeToByteArray(), seed)
}
val aTagHash by lazy {
hasher.hash128x64Half("a".encodeToByteArray(), seed)
}
fun hash(
key: ByteArray,
value: ByteArray,
) = hasher.hash128x64Half(value, hasher.hash128x64Half(key, seed))
/*
* We tried caching these values to avoid recomputation of the hash
* But caching on a LruCache<String, Long> is slower than recomputing
*/
fun hash(
key: String,
value: String,
) = hash(key.encodeToByteArray(), value.encodeToByteArray())
fun hashATag(value: String) = hasher.hash128x64Half(value.encodeToByteArray(), aTagHash)
fun hashETag(value: String) = hasher.hash128x64Half(value.encodeToByteArray(), eTagHash)
}
@@ -25,7 +25,8 @@ import junit.framework.TestCase
import kotlin.test.Test
class EventDbQueryAssemblerTest {
val builder = EventIndexesModule(FullTextSearchModule())
val builder = EventIndexesModule(FullTextSearchModule(), SeedModule())
val hasher = TagNameValueHasher(0L)
val key1 = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d"
val key2 = "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14"
@@ -33,7 +34,7 @@ class EventDbQueryAssemblerTest {
@Test
fun testEmpty() {
val sql = builder.planQuery(Filter())
val sql = builder.planQuery(Filter(), hasher)
TestCase.assertEquals(
"SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers ORDER BY created_at DESC, id",
sql,
@@ -42,7 +43,7 @@ class EventDbQueryAssemblerTest {
@Test
fun testLimit() {
val sql = builder.planQuery(Filter(limit = 10))
val sql = builder.planQuery(Filter(limit = 10), hasher)
TestCase.assertEquals(
"""
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
@@ -63,6 +64,7 @@ class EventDbQueryAssemblerTest {
Filter(authors = listOf(key1), kinds = listOf(1, 1111), search = "keywords", limit = 100),
Filter(kinds = listOf(20), search = "cats", limit = 30),
),
hasher,
)
TestCase.assertEquals(
"""
@@ -77,7 +79,7 @@ class EventDbQueryAssemblerTest {
@Test
fun testIdQuery() {
val sql = builder.planQuery(Filter(ids = listOf(key1)))
val sql = builder.planQuery(Filter(ids = listOf(key1)), hasher)
TestCase.assertEquals(
"""
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
@@ -91,7 +93,7 @@ class EventDbQueryAssemblerTest {
@Test
fun testAuthors() {
val sql = builder.planQuery(Filter(authors = listOf(key1, key2)))
val sql = builder.planQuery(Filter(authors = listOf(key1, key2)), hasher)
TestCase.assertEquals(
"""
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
@@ -105,7 +107,7 @@ class EventDbQueryAssemblerTest {
@Test
fun testAuthorsAndSearch() {
val sql = builder.planQuery(Filter(authors = listOf(key1, key2, key3), search = "keywords"))
val sql = builder.planQuery(Filter(authors = listOf(key1, key2, key3), search = "keywords"), hasher)
TestCase.assertEquals(
"""
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers
@@ -119,7 +121,7 @@ class EventDbQueryAssemblerTest {
@Test
fun testKindAndSearch() {
val sql = builder.planQuery(Filter(kinds = listOf(1, 1111, 10000), search = "keywords"))
val sql = builder.planQuery(Filter(kinds = listOf(1, 1111, 10000), search = "keywords"), hasher)
TestCase.assertEquals(
"""
SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers