{"version":3,"sources":["../src/query/dialect/vector/sqlite-vec-strategy.ts"],"names":["assertFiniteEmbedding","sql","vectorPhysicalName","quoteIdentifier","VECTOR_CONTRIBUTION_PREFIX","vectorScoreExpression","conditions","vectorMinScoreCondition","vectorPageClause"],"mappings":"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","file":"chunk-CBHZX3G5.cjs","sourcesContent":["/**\n * sqlite-vec strategy — real `vec0` KNN for local SQLite.\n *\n * This upgrades `createLocalSqliteBackend` (better-sqlite3 + the sqlite-vec\n * extension) from the legacy brute-force-on-a-shared-table model to genuine\n * approximate-nearest-neighbor search on per-`(nodeKind, fieldPath)` storage.\n *\n * ## Storage choice: a `vec0` virtual table per field\n *\n * Each slot is backed by its own `vec0` virtual table:\n *\n * ```sql\n * CREATE VIRTUAL TABLE tg_vec_<kind>_<field> USING vec0(\n *   node_id TEXT PRIMARY KEY,\n *   graph_id TEXT PARTITION KEY,\n *   +created_at TEXT,\n *   +updated_at TEXT,\n *   embedding float[<N>] distance_metric=<cosine|l2>\n * );\n * ```\n *\n * Verified against the installed `sqlite-vec` `v0.1.9`:\n *\n * - **vec0 over a plain `vec_f32` BLOB column.** The shipped option (a) — a\n *   `vec0` virtual table — is chosen over option (b) — a regular table whose\n *   `vec_f32` BLOB column is brute-forced — because vec0 is the *only* form\n *   that gives local SQLite a real ANN index (today's code only ever\n *   brute-forces). vec0 indexes inline (the virtual table *is* the index), so\n *   there is no separate `CREATE INDEX` step.\n * - **KNN with partition-correct filtering.** `WHERE embedding MATCH vec_f32(q)\n *   AND k = ? AND graph_id = ?` returns the nearest `k`, ordered by `distance`,\n *   scoped to one graph — a near vector in another `graph_id` partition does\n *   not leak. This is strictly better than libSQL's table-global `vector_top_k`\n *   (which over-fetches for multi-graph correctness): vec0's `PARTITION KEY`\n *   makes the per-graph KNN exact.\n * - **One table serves both read paths.** The same `embedding` column answers\n *   the compiler's brute-force `distanceExpression` (`vec_distance_cosine` /\n *   `vec_distance_l2` over the column, `ORDER BY … LIMIT`, no `MATCH`), so the\n *   `.where(field.similarTo(...))` CTE path needs no second structure.\n * - **`k` and `LIMIT` are mutually exclusive** in a vec0 `MATCH` query, so the\n *   ANN path uses `k = <limit>` with no `LIMIT`, and the brute-force path uses\n *   `LIMIT <limit>` with no `MATCH`.\n * - **No `ON CONFLICT` / `INSERT OR REPLACE`** on a vec0 primary key, so upsert\n *   is `DELETE` + `INSERT` (atomic under the caller's outer transaction),\n *   mirroring the FTS5 strategy.\n *\n * Metric support is `cosine` + `l2` (no `inner_product` — sqlite-vec has no\n * `vec_distance_ip`), advertised as data on `capabilities`. The table's\n * `distance_metric` is fixed at creation from the slot's metric, alongside its\n * fixed dimension `N`.\n *\n * The libSQL and pgvector siblings implement the same `VectorStrategy`\n * contract for their engines.\n */\nimport { type StrategyTableContribution } from \"../../../backend/table-contribution\";\nimport {\n  type DeleteEmbeddingParams,\n  type UpsertEmbeddingBatchParams,\n  type UpsertEmbeddingParams,\n  type VectorCapabilities,\n  type VectorMetric,\n  type VectorSearchParams,\n} from \"../../../backend/types\";\nimport { sql, type SqlFragment } from \"../../sql-fragment\";\nimport {\n  assertFiniteEmbedding,\n  quoteIdentifier,\n  VECTOR_CONTRIBUTION_PREFIX,\n  vectorMinScoreCondition,\n  vectorPhysicalName,\n  vectorScoreExpression,\n  type VectorSlot,\n  type VectorStrategy,\n} from \"../vector-strategy\";\nimport { vectorPageClause } from \"./pagination\";\n\n/** Physical-name prefix for the per-field `vec0` virtual table. */\nconst TABLE_PREFIX = \"tg_vec\";\n\n/**\n * sqlite-vec stores each dimension as a 32-bit float; `float[N]` accepts\n * large `N`, but we advertise the same conservative ceiling the legacy\n * SQLite path used so capability discovery is stable across the swap.\n */\nconst SQLITE_VEC_MAX_DIMENSIONS = 8000;\n\nconst SQLITE_VEC_CAPABILITIES: VectorCapabilities = {\n  supported: true,\n  metrics: [\"cosine\", \"l2\"],\n  // vec0 always indexes inline, so the portable ANN intent (\"hnsw\", the\n  // `embedding()` default) is honored by real KNN; \"none\" stays brute-force.\n  // Both map to the same vec0 table — only `buildSearch` differs.\n  indexTypes: [\"hnsw\", \"none\"],\n  maxDimensions: SQLITE_VEC_MAX_DIMENSIONS,\n  // vec0's KNN accepts primary-key `IN (SELECT …)` pushdown (verified on\n  // sqlite-vec v0.1.9), so the candidate filter constrains the KNN itself and\n  // a filtered search returns `k` live rows — no over-fetch, no scan bound, no\n  // under-fill. The only bundled engine that can promise a full page.\n  filteredApproximateSearch: {\n    mode: \"filter-pushdown\",\n    guaranteesFullPage: true,\n  },\n  // A vec0 KNN query is `WHERE embedding MATCH ? AND k = ?` — `k` is the only\n  // knob, and it IS the page size, not a frontier around it. sqlite-vec\n  // exposes no session setting, no query parameter, and no index option that\n  // widens the candidate list (verified against v0.1.9's vec0 query surface),\n  // so a per-search `efSearch` has nothing to bind to and is refused rather\n  // than accepted and dropped.\n  searchFrontierTuning: {\n    tunable: false,\n    reason:\n      \"a vec0 KNN takes only `k` (the page size); sqlite-vec exposes no ANN frontier parameter\",\n  },\n};\n\n/** Whether a slot's declared index type maps to vec0's `MATCH … k =` KNN. */\nfunction usesAnnIndex(slot: VectorSlot): boolean {\n  return slot.indexType === \"hnsw\";\n}\n\n/** vec0 `distance_metric` token accepted in the virtual-table definition. */\nfunction sqliteVecMetricToken(metric: VectorMetric): string {\n  switch (metric) {\n    case \"cosine\": {\n      return \"cosine\";\n    }\n    case \"l2\": {\n      return \"l2\";\n    }\n    case \"inner_product\": {\n      throw new Error(\n        \"sqlite-vec does not support inner_product. Use 'cosine' or 'l2'.\",\n      );\n    }\n    default: {\n      const _exhaustive: never = metric;\n      throw new Error(`Unsupported vector metric: ${String(_exhaustive)}`);\n    }\n  }\n}\n\n/**\n * Formats an embedding as the bound JSON argument to `vec_f32(...)`. The\n * value rides as a parameter (not interpolated) — `vec_f32` parses the JSON\n * array into the packed float blob. Finiteness is validated first so a\n * NaN/Infinity names its index before JSON.stringify masks it as `null`.\n */\nfunction vecF32Literal(\n  embedding: readonly number[],\n  name: string,\n): SqlFragment {\n  assertFiniteEmbedding(embedding, name);\n  return sql`vec_f32(${JSON.stringify(embedding)})`;\n}\n\nfunction distanceExpression(\n  embeddingColumn: SqlFragment,\n  queryEmbedding: readonly number[],\n  metric: VectorMetric,\n): SqlFragment {\n  const query = vecF32Literal(queryEmbedding, \"queryEmbedding\");\n  switch (metric) {\n    case \"cosine\": {\n      return sql`vec_distance_cosine(${embeddingColumn}, ${query})`;\n    }\n    case \"l2\": {\n      return sql`vec_distance_l2(${embeddingColumn}, ${query})`;\n    }\n    case \"inner_product\": {\n      throw new Error(\n        \"sqlite-vec does not support inner_product. Use 'cosine' or 'l2'.\",\n      );\n    }\n    default: {\n      const _exhaustive: never = metric;\n      throw new Error(`Unsupported vector metric: ${String(_exhaustive)}`);\n    }\n  }\n}\n\nfunction tableName(\n  graphId: string,\n  nodeKind: string,\n  fieldPath: string,\n): string {\n  return vectorPhysicalName(TABLE_PREFIX, graphId, nodeKind, fieldPath);\n}\n\nexport const sqliteVecStrategy: VectorStrategy = {\n  name: \"sqlite-vec\",\n  capabilities: SQLITE_VEC_CAPABILITIES,\n\n  tableName,\n\n  ownedTables(slot): readonly StrategyTableContribution[] {\n    const table = tableName(slot.graphId, slot.nodeKind, slot.fieldPath);\n    const name = quoteIdentifier(table);\n    const metric = sqliteVecMetricToken(slot.metric);\n\n    // vec0 virtual tables cannot be modeled as a Drizzle table, so this is a\n    // raw-ddl contribution — emitted verbatim and invisible to drizzle-kit.\n    // `graph_id` is a PARTITION KEY so per-graph KNN is exact; `created_at` /\n    // `updated_at` are auxiliary (`+`) columns (stored, not filtered on). The\n    // virtual table *is* the index — no separate CREATE INDEX is emitted.\n    return [\n      {\n        scope: \"graph\",\n        logicalName: `${VECTOR_CONTRIBUTION_PREFIX}:${slot.nodeKind}.${slot.fieldPath}`,\n        owner: \"sqlite-vec\",\n        tableName: table,\n        createDdl: [\n          `CREATE VIRTUAL TABLE IF NOT EXISTS ${name} USING vec0(\n  node_id TEXT PRIMARY KEY,\n  graph_id TEXT PARTITION KEY,\n  +created_at TEXT,\n  +updated_at TEXT,\n  embedding float[${slot.dimensions}] distance_metric=${metric}\n);`,\n        ],\n        runtimeEnsure: true,\n      },\n    ];\n  },\n\n  buildUpsert(\n    slot,\n    params: UpsertEmbeddingParams,\n    timestamp,\n  ): readonly SqlFragment[] {\n    const table = sql.identifier(\n      tableName(slot.graphId, slot.nodeKind, slot.fieldPath),\n    );\n    const value = vecF32Literal(params.embedding, \"embedding\");\n    // vec0 rejects ON CONFLICT / INSERT OR REPLACE on its primary key —\n    // emulate upsert with DELETE + INSERT, atomic under the caller's\n    // outer transaction (mirrors the FTS5 fulltext strategy).\n    return [\n      sql`\n        DELETE FROM ${table}\n        WHERE \"node_id\" = ${params.nodeId} AND \"graph_id\" = ${params.graphId}\n      `,\n      sql`\n        INSERT INTO ${table} (\"node_id\", \"graph_id\", \"created_at\", \"updated_at\", \"embedding\")\n        VALUES (${params.nodeId}, ${params.graphId}, ${timestamp}, ${timestamp}, ${value})\n      `,\n    ];\n  },\n\n  buildUpsertBatch(\n    slot,\n    params: UpsertEmbeddingBatchParams,\n    timestamp,\n  ): readonly SqlFragment[] {\n    const table = sql.identifier(\n      tableName(slot.graphId, slot.nodeKind, slot.fieldPath),\n    );\n    // Same DELETE + INSERT upsert emulation as buildUpsert, in multi-row\n    // form: one IN-list delete, one multi-row insert.\n    const nodeIds = sql.join(\n      params.rows.map((row) => sql`${row.nodeId}`),\n      sql`, `,\n    );\n    const valueRows = sql.join(\n      params.rows.map(\n        (row) =>\n          sql`(${row.nodeId}, ${params.graphId}, ${timestamp}, ${timestamp}, ${vecF32Literal(row.embedding, \"embedding\")})`,\n      ),\n      sql`, `,\n    );\n    return [\n      sql`\n        DELETE FROM ${table}\n        WHERE \"graph_id\" = ${params.graphId} AND \"node_id\" IN (${nodeIds})\n      `,\n      sql`\n        INSERT INTO ${table} (\"node_id\", \"graph_id\", \"created_at\", \"updated_at\", \"embedding\")\n        VALUES ${valueRows}\n      `,\n    ];\n  },\n\n  buildDelete(slot, params: DeleteEmbeddingParams): readonly SqlFragment[] {\n    const table = sql.identifier(\n      tableName(slot.graphId, slot.nodeKind, slot.fieldPath),\n    );\n    return [\n      sql`\n        DELETE FROM ${table}\n        WHERE \"node_id\" = ${params.nodeId} AND \"graph_id\" = ${params.graphId}\n      `,\n    ];\n  },\n\n  buildDeleteBatch(\n    slot,\n    params: Omit<DeleteEmbeddingParams, \"nodeId\"> &\n      Readonly<{ nodeIds: readonly string[] }>,\n  ): readonly SqlFragment[] {\n    if (params.nodeIds.length === 0) return [];\n    const table = sql.identifier(\n      tableName(slot.graphId, slot.nodeKind, slot.fieldPath),\n    );\n    return [\n      sql`DELETE FROM ${table} WHERE \"graph_id\" = ${params.graphId} AND \"node_id\" IN (${sql.join(\n        params.nodeIds.map((nodeId) => sql`${nodeId}`),\n        sql`, `,\n      )})`,\n    ];\n  },\n\n  // vec0's KNN is brute force in C — exact by construction (the\n  // \"index\" is a partitioned scan, not a graph) — and the non-indexed\n  // fallback below is a plain SQL scan. The compiler may therefore\n  // route the NON-approximate `.similarTo()` branch through this form.\n  searchIsExact: true,\n\n  buildSearch(\n    slot,\n    params: VectorSearchParams,\n    candidates?: SqlFragment,\n  ): SqlFragment {\n    const table = sql.identifier(\n      tableName(slot.graphId, slot.nodeKind, slot.fieldPath),\n    );\n    const embeddingColumn = sql`${table}.\"embedding\"`;\n    const distance = distanceExpression(\n      embeddingColumn,\n      params.queryEmbedding,\n      params.metric,\n    );\n    const score = vectorScoreExpression(distance, params.metric);\n\n    if (usesAnnIndex(slot)) {\n      // vec0 KNN. `k` and `LIMIT` are mutually exclusive in a MATCH query, so\n      // bound `k = limit` and omit LIMIT. `graph_id` filters by the partition\n      // key (exact, no recall loss). Order by vec0's own `distance` column —\n      // the recomputed `distance` expression is reused only for score/minScore\n      // so the math matches the brute-force path and the shared helpers.\n      const query = vecF32Literal(params.queryEmbedding, \"queryEmbedding\");\n      // `k` covers the requested page: vec0 has no OFFSET inside a MATCH\n      // query, so fetch `limit + offset` neighbors and page in a wrapper.\n      const knnK = params.limit + (params.offset ?? 0);\n      const conditions: SqlFragment[] = [\n        sql`${table}.\"embedding\" MATCH ${query}`,\n        sql`k = ${knnK}`,\n        sql`${table}.\"graph_id\" = ${params.graphId}`,\n      ];\n      if (params.minScore !== undefined) {\n        conditions.push(\n          vectorMinScoreCondition(distance, params.metric, params.minScore),\n        );\n      }\n      // vec0 KNN accepts primary-key `IN (SELECT ...)` pushdown (verified on\n      // sqlite-vec v0.1.9): the filter constrains the KNN candidate set\n      // itself, so `k` live results come back — exact, no over-fetch.\n      if (candidates !== undefined) {\n        conditions.push(sql`${table}.\"node_id\" IN (${candidates})`);\n      }\n      const knnBody = sql`\n        SELECT ${table}.\"node_id\" AS node_id, ${score} AS score\n        FROM ${table}\n        WHERE ${sql.join(conditions, sql` AND `)}\n        ORDER BY distance ASC\n      `;\n      if (params.offset === undefined || params.offset === 0) {\n        return knnBody;\n      }\n      // MATERIALIZED fences the page wrapper: SQLite would otherwise\n      // flatten the subquery and push the outer LIMIT into the vec0 MATCH\n      // query, which rejects `k = ?` and LIMIT together.\n      return sql`\n        WITH knn_page AS MATERIALIZED (${knnBody})\n        SELECT node_id, score FROM knn_page\n        LIMIT ${params.limit} OFFSET ${params.offset}\n      `;\n    }\n\n    // Brute-force scan: no MATCH, so LIMIT is allowed (and required).\n    const conditions: SqlFragment[] = [\n      sql`${table}.\"graph_id\" = ${params.graphId}`,\n    ];\n    if (candidates !== undefined) {\n      conditions.push(sql`${table}.\"node_id\" IN (${candidates})`);\n    }\n    if (params.minScore !== undefined) {\n      conditions.push(\n        vectorMinScoreCondition(distance, params.metric, params.minScore),\n      );\n    }\n    const pageClause = vectorPageClause(params.limit, params.offset);\n    return sql`\n      SELECT ${table}.\"node_id\" AS node_id, ${score} AS score\n      FROM ${table}\n      WHERE ${sql.join(conditions, sql` AND `)}\n      ORDER BY ${distance} ASC\n      ${pageClause}\n    `;\n  },\n\n  distanceExpression(embeddingColumn, queryEmbedding, metric) {\n    return distanceExpression(embeddingColumn, queryEmbedding, metric);\n  },\n\n  // vec0 indexes inline (the virtual table is the index), so there is no\n  // standalone ANN index to create or drop — both return undefined.\n  buildCreateIndex(): SqlFragment | undefined {\n    return undefined;\n  },\n\n  buildDropIndex(): SqlFragment | undefined {\n    return undefined;\n  },\n\n  buildDropStorage(slot): readonly string[] {\n    const table = quoteIdentifier(\n      tableName(slot.graphId, slot.nodeKind, slot.fieldPath),\n    );\n    // Dropping the vec0 virtual table removes its backing shadow tables too.\n    return [`DROP TABLE IF EXISTS ${table}`];\n  },\n};\n"]}