跳转到正文

sql-memory.ts ​

源文件: examples/_shared/tools/storage/sql-memory.ts · 下载原文件 · 示例使用说明

ts
import { randomUUID } from "node:crypto";
import { MemoryError, type MemoryItem, type MemoryStore, type MemorySearchProvider } from "@codesoul-co/ditto/worker/memory";

export type Row = { id: string; memory_key: string | null; content: string; metadata: string | null };
export type Rows = (sql: string, values?: unknown[]) => Promise<Row[]>;
export interface SqlDatabase {
  dialect: "sqlite" | "postgres" | "mysql";
  rows: Rows;
  transaction<T>(operation: (rows: Rows) => Promise<T>): Promise<T>;
  close(): Promise<void>;
}

/** Application adapter: parameterized SQL and explicit JSON mapping, no ORM. */
export function createSqlMemoryStore(database: SqlDatabase, table: string, idempotentKeys = false): MemoryStore & MemorySearchProvider {
  if (!/^[a-z][a-z0-9_]*$/.test(table)) throw new Error("Invalid example table name");
  const lock = database.dialect === "sqlite" ? "" : " FOR UPDATE";
  const map = (row: Row): MemoryItem => ({ id: row.id, content: JSON.parse(row.content),
    ...(row.memory_key === null ? {} : { key: row.memory_key }),
    ...(row.metadata === null ? {} : { metadata: JSON.parse(row.metadata) }),
  });
  const json = (value: unknown) => {
    const encoded = JSON.stringify(value);
    if (encoded === undefined) throw new MemoryError("INVALID_INPUT", "SQL example requires JSON content");
    return encoded;
  };
  const list = (values: readonly string[]) => values.map(() => "?").join(",");
  const filter = (value?: Record<string, unknown>): { sql: string; values: unknown[] } => {
    if (!value || !Object.keys(value).length) return { sql: "1=1", values: [] };
    const clauses: string[] = [], values: unknown[] = [];
    for (const [key, v] of Object.entries(value)) {
      if (!["key", "namespace", "kind"].includes(key) || typeof v !== "string") throw new MemoryError("INVALID_INPUT", "SQL filter supports string key, namespace and kind");
      const field = key === "key" ? "memory_key" : database.dialect === "postgres" ? `(metadata::jsonb ->> '${key}')` : database.dialect === "mysql" ? `JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.${key}'))` : `json_extract(metadata, '$.${key}')`;
      clauses.push(`${field} = ?`); values.push(v);
    }
    return { sql: clauses.join(" AND "), values };
  };
  return {
    async get(input) {
      const clauses: string[] = [], values: unknown[] = [];
      if (input.ids?.length) { clauses.push(`id IN (${list(input.ids)})`); values.push(...input.ids); }
      if (input.keys?.length) { clauses.push(`memory_key IN (${list(input.keys)})`); values.push(...input.keys); }
      return clauses.length ? (await database.rows(`SELECT * FROM ${table} WHERE ${clauses.join(" OR ")} ORDER BY id`, values)).map(map) : [];
    },
    async query(input) {
      if (input.orderBy?.some(order => order.field !== "id" || (order.direction ?? "asc") !== "asc")) {
        throw new MemoryError("INVALID_INPUT", "SQL example supports ORDER BY id ASC");
      }
      const where = filter(input.filter), limit = input.limit ?? 100;
      if (limit === 0) return { items: [] };
      const rows = await database.rows(`SELECT * FROM ${table} WHERE ${where.sql}${input.cursor ? " AND id > ?" : ""} ORDER BY id LIMIT ?`,
        [...where.values, ...(input.cursor ? [input.cursor] : []), limit + 1]);
      const items = rows.slice(0, limit).map(map);
      return { items, ...(rows.length > limit ? { nextCursor: items.at(-1)!.id } : {}) };
    },
    async search(input) {
      if (typeof input.query !== "string" || (input.strategy && input.strategy !== "keyword") || Object.keys(input.options ?? {}).length) {
        throw new MemoryError("INVALID_INPUT", "SQL example supports string keyword queries without options");
      }
      const where = filter(input.filter);
      const query = input.query.toLowerCase().replace(/[!%_]/g, char => `!${char}`);
      const rows = await database.rows(`SELECT * FROM ${table} WHERE ${where.sql} AND LOWER(content) LIKE ? ESCAPE '!' ORDER BY id LIMIT ?`,
        [...where.values, `%${query}%`, input.limit ?? 10]);
      return rows.map(row => ({ memory: map(row) })); // Database substring matching, not vector ranking.
    },
    async write(input) {
      const items = input.memories.map(memory => ({ ...memory, id: randomUUID() }));
      return database.transaction(async rows => {
        const saved: MemoryItem[] = [];
        for (const item of items) {
          if (idempotentKeys && item.key) {
            const [existing] = await rows(`SELECT * FROM ${table} WHERE memory_key = ?${lock}`, [item.key]);
            if (existing) {
              if (existing.content !== json(item.content) || existing.metadata !== (item.metadata === undefined ? null : json(item.metadata))) throw new MemoryError("INVALID_INPUT", "Memory key conflicts with existing content");
              saved.push(map(existing)); continue;
            }
          }
          await rows(`INSERT INTO ${table} (id, memory_key, content, metadata) VALUES (?, ?, ?, ?)`,
            [item.id, item.key ?? null, json(item.content), item.metadata === undefined ? null : json(item.metadata)]);
          saved.push(item);
        }
        return saved;
      });
    },
    async update(input) {
      if (!input.memories.length) return [];
      return database.transaction(async rows => {
        const ids = input.memories.map(item => item.id);
        const existing = new Map((await rows(`SELECT * FROM ${table} WHERE id IN (${list(ids)}) ORDER BY id${lock}`, ids)).map(row => [row.id, map(row)]));
        if (existing.size !== ids.length) throw new MemoryError("NOT_FOUND", "Update target does not exist");
        const items = input.memories.map(change => ({ ...existing.get(change.id)!, ...change }));
        for (const item of items) await rows(`UPDATE ${table} SET content = ?, metadata = ? WHERE id = ?`,
          [json(item.content), item.metadata === undefined ? null : json(item.metadata), item.id]);
        return items;
      });
    },
    async delete(input) {
      if (!input.ids.length) return { deleted: [] };
      return database.transaction(async rows => {
        const existing = await rows(`SELECT * FROM ${table} WHERE id IN (${list(input.ids)}) ORDER BY id${lock}`, [...input.ids]);
        const ids = existing.map(row => row.id);
        if (ids.length) await rows(`DELETE FROM ${table} WHERE id IN (${list(ids)})`, ids);
        return { deleted: ids };
      });
    },
  };
}

Ditto · @codesoul-co/ditto · Node.js 24+