Unverified Commit d302164b authored by Hong Minhee's avatar Hong Minhee
Browse files

Implement list() method in SqliteKvStore



Implement the list() method for prefix scanning in SqliteKvStore.
Uses SQL LIKE pattern matching on JSON-encoded keys.

Co-Authored-By: default avatarClaude Opus 4.5 <noreply@anthropic.com>
parent 150355d2
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -54,6 +54,10 @@ To be released.
 -  Allowed Express 5 in the `express` peer dependency range to support NestJS 11.
    [[#492], [#493] by Cho Hasang]

### @fedify/sqlite

 -  Implemented `list()` method in `SqliteKvStore`.  [[#498]]


[#492]: https://github.com/fedify-dev/fedify/issues/492
[#493]: https://github.com/fedify-dev/fedify/pull/493
+74 −0
Original line number Diff line number Diff line
@@ -310,3 +310,77 @@ test("SqliteKvStore.set() - preserves created timestamp on update", async () =>
    await db.close();
  }
});

test("SqliteKvStore.list()", async () => {
  const { db, store } = getStore();
  try {
    await store.set(["prefix", "a"], "value-a");
    await store.set(["prefix", "b"], "value-b");
    await store.set(["prefix", "nested", "c"], "value-c");
    await store.set(["other", "x"], "value-x");

    const entries: { key: readonly string[]; value: unknown }[] = [];
    for await (const entry of store.list!({ prefix: ["prefix"] })) {
      entries.push({ key: entry.key, value: entry.value });
    }

    assert.strictEqual(entries.length, 3);
    assert(
      entries.some((e) => e.key[1] === "a" && e.value === "value-a"),
    );
    assert(entries.some((e) => e.key[1] === "b"));
    assert(entries.some((e) => e.key[1] === "nested"));
  } finally {
    await store.drop();
    await db.close();
  }
});

test("SqliteKvStore.list() - excludes expired", async () => {
  const { db, tableName, store } = getStore();
  try {
    await store.initialize();
    const now = Temporal.Now.instant().epochMilliseconds;

    // Insert expired entry directly
    db.prepare(`
      INSERT INTO "${tableName}" (key, value, created, expires)
      VALUES (?, ?, ?, ?)
    `).run(
      JSON.stringify(["list-test", "expired"]),
      JSON.stringify("expired-value"),
      now - 1000,
      now - 500,
    );
    await store.set(["list-test", "valid"], "valid-value");

    const entries: { key: readonly string[]; value: unknown }[] = [];
    for await (const entry of store.list!({ prefix: ["list-test"] })) {
      entries.push({ key: entry.key, value: entry.value });
    }

    assert.strictEqual(entries.length, 1);
    assert.deepStrictEqual(entries[0].key, ["list-test", "valid"]);
  } finally {
    await store.drop();
    await db.close();
  }
});

test("SqliteKvStore.list() - single element key", async () => {
  const { db, store } = getStore();
  try {
    await store.set(["a"], "value-a");
    await store.set(["b"], "value-b");

    const entries: { key: readonly string[]; value: unknown }[] = [];
    for await (const entry of store.list!({ prefix: ["a"] })) {
      entries.push({ key: entry.key, value: entry.value });
    }

    assert.strictEqual(entries.length, 1);
  } finally {
    await store.drop();
    await db.close();
  }
});
+40 −1
Original line number Diff line number Diff line
import { type PlatformDatabase, SqliteDatabase } from "#sqlite";
import type { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify";
import type {
  KvKey,
  KvStore,
  KvStoreListEntry,
  KvStoreListOptions,
  KvStoreSetOptions,
} from "@fedify/fedify";
import { getLogger } from "@logtape/logtape";
import { isEqual } from "es-toolkit";
import type { SqliteDatabaseAdapter } from "./adapter.ts";
@@ -212,6 +218,39 @@ export class SqliteKvStore implements KvStore {
    }
  }

  /**
   * {@inheritDoc KvStore.list}
   * @since 1.10.0
   */
  async *list(
    options: KvStoreListOptions,
  ): AsyncIterable<KvStoreListEntry> {
    this.initialize();

    const prefix = options.prefix;
    const now = Temporal.Now.instant().epochMilliseconds;
    // JSON pattern: '["prefix","' matches keys starting with prefix
    const pattern = JSON.stringify(prefix).slice(0, -1) + ",%";
    const exactKey = JSON.stringify(prefix);

    const results = this.#db
      .prepare(`
        SELECT key, value
        FROM "${this.#tableName}"
        WHERE (key LIKE ? ESCAPE '\\' OR key = ?)
          AND (expires IS NULL OR expires > ?)
        ORDER BY key
      `)
      .all(pattern, exactKey, now) as { key: string; value: string }[];

    for (const row of results) {
      yield {
        key: this.#decodeKey(row.key),
        value: this.#decodeValue(row.value),
      };
    }
  }

  /**
   * Creates the table used by the key–value store if it does not already exist.
   * Does nothing if the table already exists.