libsql-search 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -831,6 +831,48 @@ function normalizeVectorDimensions(dimensions) {
831
831
  return dimensions;
832
832
  }
833
833
 
834
+ function isDatabaseAdapter(client) {
835
+ return typeof client === "object" && client !== null && client.libsqlSearchAdapter === true;
836
+ }
837
+ function resolveDatabase(client) {
838
+ if (isDatabaseAdapter(client)) {
839
+ assertCompleteAdapter(client);
840
+ return client;
841
+ }
842
+ return createLibsqlAdapter(client);
843
+ }
844
+ const ADAPTER_METHODS = ["executeDdl", "executeQuery", "executeAtomicWrite"];
845
+ function assertCompleteAdapter(adapter) {
846
+ const missing = ADAPTER_METHODS.filter(
847
+ (method) => typeof adapter[method] !== "function"
848
+ );
849
+ if (missing.length > 0) {
850
+ throw new TypeError(
851
+ `This client is marked as a libsql-search database adapter but is missing ${missing.join(", ")}. The usual cause is two different versions of libsql-search resolved in one dependency tree, so the adapter was built by a different copy of the package than the one calling it. Deduplicate libsql-search, or build the adapter from the same copy you call.`
852
+ );
853
+ }
854
+ }
855
+ function createLibsqlAdapter(client) {
856
+ return {
857
+ libsqlSearchAdapter: true,
858
+ backend: "libsql",
859
+ supportsVectorIndex: true,
860
+ async executeDdl(sql) {
861
+ await client.execute(sql);
862
+ },
863
+ async executeQuery(sql, args) {
864
+ const result = args === void 0 ? await client.execute(sql) : await client.execute({ sql, args });
865
+ return result.rows;
866
+ },
867
+ async executeAtomicWrite(statements) {
868
+ const batch = statements.map(
869
+ (statement) => statement.args === void 0 ? statement.sql : statement
870
+ );
871
+ await client.batch(batch, "write");
872
+ }
873
+ };
874
+ }
875
+
834
876
  class IndexingError extends Error {
835
877
  phase;
836
878
  failures;
@@ -853,6 +895,7 @@ async function indexContent(options) {
853
895
  failurePolicy = "abort",
854
896
  allowEmptyIndex = false
855
897
  } = options;
898
+ const database = resolveDatabase(client);
856
899
  const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
857
900
  let files;
858
901
  try {
@@ -926,7 +969,7 @@ async function indexContent(options) {
926
969
  );
927
970
  }
928
971
  const success = documents.length;
929
- await replaceIndex(client, quotedTableName, documents, failures);
972
+ await replaceIndex(database, quotedTableName, documents, failures);
930
973
  return {
931
974
  success,
932
975
  failed: failures.length,
@@ -1039,8 +1082,8 @@ function fallbackTitle(file) {
1039
1082
  function describeType(value) {
1040
1083
  return Array.isArray(value) ? "array" : typeof value;
1041
1084
  }
1042
- async function replaceIndex(client, quotedTableName, documents, failures) {
1043
- const statements = [`DELETE FROM ${quotedTableName}`];
1085
+ async function replaceIndex(database, quotedTableName, documents, failures) {
1086
+ const statements = [{ sql: `DELETE FROM ${quotedTableName}` }];
1044
1087
  try {
1045
1088
  for (let i = 0; i < documents.length; i++) {
1046
1089
  const document = documents[i];
@@ -1053,7 +1096,7 @@ async function replaceIndex(client, quotedTableName, documents, failures) {
1053
1096
  documents.length = 0;
1054
1097
  }
1055
1098
  try {
1056
- await client.batch(statements, "write");
1099
+ await database.executeAtomicWrite(statements);
1057
1100
  } catch (error) {
1058
1101
  throw new IndexingError(
1059
1102
  `Failed to replace the contents of ${quotedTableName}. The transaction was rolled back and the existing index was left unchanged.`,
@@ -1082,12 +1125,13 @@ function toError(error) {
1082
1125
  return error instanceof Error ? error : new Error(String(error));
1083
1126
  }
1084
1127
  async function createTable(client, tableName = "articles", dimensions = 384) {
1128
+ const database = resolveDatabase(client);
1085
1129
  const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
1086
1130
  const vectorDimensions = normalizeVectorDimensions(dimensions);
1087
1131
  const quotedEmbeddingIndexName = quoteSqlIdentifier(`${tableName}_embedding_idx`, "embedding index name");
1088
1132
  const quotedFolderIndexName = quoteSqlIdentifier(`${tableName}_folder_idx`, "folder index name");
1089
1133
  const quotedSlugIndexName = quoteSqlIdentifier(`${tableName}_slug_idx`, "slug index name");
1090
- await client.execute(`
1134
+ await database.executeDdl(`
1091
1135
  CREATE TABLE IF NOT EXISTS ${quotedTableName} (
1092
1136
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1093
1137
  slug TEXT UNIQUE NOT NULL,
@@ -1100,15 +1144,17 @@ async function createTable(client, tableName = "articles", dimensions = 384) {
1100
1144
  updated_at TEXT NOT NULL
1101
1145
  )
1102
1146
  `);
1103
- await client.execute(`
1104
- CREATE INDEX IF NOT EXISTS ${quotedEmbeddingIndexName}
1105
- ON ${quotedTableName}(libsql_vector_idx(embedding))
1106
- `);
1107
- await client.execute(`
1147
+ if (database.supportsVectorIndex) {
1148
+ await database.executeDdl(`
1149
+ CREATE INDEX IF NOT EXISTS ${quotedEmbeddingIndexName}
1150
+ ON ${quotedTableName}(libsql_vector_idx(embedding))
1151
+ `);
1152
+ }
1153
+ await database.executeDdl(`
1108
1154
  CREATE INDEX IF NOT EXISTS ${quotedFolderIndexName}
1109
1155
  ON ${quotedTableName}(folder)
1110
1156
  `);
1111
- await client.execute(`
1157
+ await database.executeDdl(`
1112
1158
  CREATE INDEX IF NOT EXISTS ${quotedSlugIndexName}
1113
1159
  ON ${quotedTableName}(slug)
1114
1160
  `);
@@ -1131,6 +1177,7 @@ async function search(options) {
1131
1177
  candidates,
1132
1178
  exact = false
1133
1179
  } = options;
1180
+ const database = resolveDatabase(client);
1134
1181
  const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
1135
1182
  const resultLimit = normalizeSearchLimit(limit);
1136
1183
  const embeddingIndexName = validateSqlIdentifier(
@@ -1143,20 +1190,21 @@ async function search(options) {
1143
1190
  intent: embeddingOptions.intent ?? "query"
1144
1191
  });
1145
1192
  const queryVector = JSON.stringify(queryEmbedding);
1146
- const results = exact ? await executeExactSearch(client, quotedTableName, queryVector, resultLimit) : await executeIndexSearch(
1147
- client,
1193
+ const useExactSearch = exact || !database.supportsVectorIndex;
1194
+ const rows = useExactSearch ? await executeExactSearch(database, quotedTableName, queryVector, resultLimit) : await executeIndexSearch(
1195
+ database,
1148
1196
  quotedTableName,
1149
1197
  embeddingIndexName,
1150
1198
  queryVector,
1151
1199
  candidateCount,
1152
1200
  resultLimit
1153
1201
  );
1154
- return results.rows.map(toSearchResult);
1202
+ return rows.map(toSearchResult);
1155
1203
  }
1156
- async function executeIndexSearch(client, quotedTableName, embeddingIndexName, queryVector, candidateCount, resultLimit) {
1204
+ async function executeIndexSearch(database, quotedTableName, embeddingIndexName, queryVector, candidateCount, resultLimit) {
1157
1205
  try {
1158
- return await client.execute({
1159
- sql: `
1206
+ return await database.executeQuery(
1207
+ `
1160
1208
  SELECT
1161
1209
  ${RESULT_COLUMNS},
1162
1210
  vector_distance_cos(a.embedding, vector(:queryVector)) as distance
@@ -1165,20 +1213,20 @@ async function executeIndexSearch(client, quotedTableName, embeddingIndexName, q
1165
1213
  ORDER BY distance, a.id
1166
1214
  LIMIT :resultLimit
1167
1215
  `,
1168
- args: {
1216
+ {
1169
1217
  queryVector,
1170
1218
  indexName: embeddingIndexName,
1171
1219
  candidates: candidateCount,
1172
1220
  resultLimit
1173
1221
  }
1174
- });
1222
+ );
1175
1223
  } catch (error) {
1176
1224
  throw wrapIndexPathError(error, embeddingIndexName);
1177
1225
  }
1178
1226
  }
1179
- async function executeExactSearch(client, quotedTableName, queryVector, resultLimit) {
1180
- return client.execute({
1181
- sql: `
1227
+ async function executeExactSearch(database, quotedTableName, queryVector, resultLimit) {
1228
+ return database.executeQuery(
1229
+ `
1182
1230
  SELECT
1183
1231
  ${RESULT_COLUMNS},
1184
1232
  vector_distance_cos(a.embedding, vector(:queryVector)) as distance
@@ -1187,8 +1235,8 @@ async function executeExactSearch(client, quotedTableName, queryVector, resultLi
1187
1235
  ORDER BY distance, a.id
1188
1236
  LIMIT :resultLimit
1189
1237
  `,
1190
- args: { queryVector, resultLimit }
1191
- });
1238
+ { queryVector, resultLimit }
1239
+ );
1192
1240
  }
1193
1241
  function wrapIndexPathError(error, embeddingIndexName) {
1194
1242
  if (!(error instanceof Error)) {
@@ -1221,13 +1269,14 @@ function toSearchResult(row) {
1221
1269
  };
1222
1270
  }
1223
1271
  async function getAllArticles(client, tableName = "articles") {
1272
+ const database = resolveDatabase(client);
1224
1273
  const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
1225
- const results = await client.execute(`
1274
+ const rows = await database.executeQuery(`
1226
1275
  SELECT id, slug, title, folder, tags, created_at, updated_at
1227
1276
  FROM ${quotedTableName}
1228
1277
  ORDER BY title
1229
1278
  `);
1230
- return results.rows.map((row) => ({
1279
+ return rows.map((row) => ({
1231
1280
  id: row.id,
1232
1281
  slug: row.slug,
1233
1282
  title: row.title,
@@ -1238,20 +1287,21 @@ async function getAllArticles(client, tableName = "articles") {
1238
1287
  }));
1239
1288
  }
1240
1289
  async function getArticleBySlug(client, slug, tableName = "articles") {
1290
+ const database = resolveDatabase(client);
1241
1291
  const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
1242
- const results = await client.execute({
1243
- sql: `
1292
+ const rows = await database.executeQuery(
1293
+ `
1244
1294
  SELECT id, slug, title, content, folder, tags, created_at, updated_at
1245
1295
  FROM ${quotedTableName}
1246
1296
  WHERE slug = ?
1247
1297
  LIMIT 1
1248
1298
  `,
1249
- args: [slug]
1250
- });
1251
- if (results.rows.length === 0) {
1299
+ [slug]
1300
+ );
1301
+ if (rows.length === 0) {
1252
1302
  return null;
1253
1303
  }
1254
- const row = results.rows[0];
1304
+ const row = rows[0];
1255
1305
  return {
1256
1306
  id: row.id,
1257
1307
  slug: row.slug,
@@ -1264,17 +1314,18 @@ async function getArticleBySlug(client, slug, tableName = "articles") {
1264
1314
  };
1265
1315
  }
1266
1316
  async function getArticlesByFolder(client, folder, tableName = "articles") {
1317
+ const database = resolveDatabase(client);
1267
1318
  const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
1268
- const results = await client.execute({
1269
- sql: `
1319
+ const rows = await database.executeQuery(
1320
+ `
1270
1321
  SELECT id, slug, title, folder, tags
1271
1322
  FROM ${quotedTableName}
1272
1323
  WHERE folder = ?
1273
1324
  ORDER BY title
1274
1325
  `,
1275
- args: [folder]
1276
- });
1277
- return results.rows.map((row) => ({
1326
+ [folder]
1327
+ );
1328
+ return rows.map((row) => ({
1278
1329
  id: row.id,
1279
1330
  slug: row.slug,
1280
1331
  title: row.title,
@@ -1283,13 +1334,14 @@ async function getArticlesByFolder(client, folder, tableName = "articles") {
1283
1334
  }));
1284
1335
  }
1285
1336
  async function getFolders(client, tableName = "articles") {
1337
+ const database = resolveDatabase(client);
1286
1338
  const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
1287
- const results = await client.execute(`
1339
+ const rows = await database.executeQuery(`
1288
1340
  SELECT DISTINCT folder
1289
1341
  FROM ${quotedTableName}
1290
1342
  ORDER BY folder
1291
1343
  `);
1292
- return results.rows.map((row) => row.folder);
1344
+ return rows.map((row) => row.folder);
1293
1345
  }
1294
1346
 
1295
1347
  export { DEFAULT_SEARCH_CANDIDATE_MULTIPLIER, IndexingError, MAX_SEARCH_CANDIDATES, MIN_SEARCH_CANDIDATES, createEmbeddingProvider, createTable, generateEmbedding, generateEmbeddings, getAllArticles, getArticleBySlug, getArticlesByFolder, getEmbeddingProviderMetadata, getFolders, indexContent, padEmbedding, prepareTextForEmbedding, search, validateEmbeddingBatch };
package/dist/turso.cjs ADDED
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ function tursoAdapter(database) {
4
+ assertTursoDatabase(database);
5
+ return {
6
+ libsqlSearchAdapter: true,
7
+ backend: "turso",
8
+ /**
9
+ * Turso Database ships the vector functions but no ANN index. This is a
10
+ * design direction rather than a gap: the project shipped SIMD-accelerated
11
+ * exact search instead, and the DiskANN port remains an open backlog item.
12
+ */
13
+ supportsVectorIndex: false,
14
+ async executeDdl(sql) {
15
+ await database.exec(sql);
16
+ },
17
+ async executeQuery(sql, args) {
18
+ const statement = database.prepare(sql);
19
+ try {
20
+ const rows = await (args === void 0 ? statement.all() : statement.all(args));
21
+ return rows;
22
+ } finally {
23
+ closeStatement(statement);
24
+ }
25
+ },
26
+ /**
27
+ * Replace the table contents inside one explicit transaction.
28
+ *
29
+ * `batch()` is deliberately not used. It is not transactional on this
30
+ * backend: a batch that fails part way through leaves the statements before
31
+ * the failure applied, which would silently turn a failed rebuild into a
32
+ * half-destroyed index. `BEGIN IMMEDIATE` takes the write lock up front and
33
+ * `ROLLBACK` restores the previous rows exactly.
34
+ *
35
+ * Statements are prepared once per distinct SQL string and rebound per row.
36
+ * An index rebuild is one `DELETE` followed by N identical `INSERT`s, so
37
+ * this turns N+1 prepares into exactly 2 regardless of corpus size.
38
+ */
39
+ async executeAtomicWrite(statements) {
40
+ const preparedBySql = /* @__PURE__ */ new Map();
41
+ const prepareOnce = (sql) => {
42
+ let prepared = preparedBySql.get(sql);
43
+ if (prepared === void 0) {
44
+ prepared = database.prepare(sql);
45
+ preparedBySql.set(sql, prepared);
46
+ }
47
+ return prepared;
48
+ };
49
+ await database.exec("BEGIN IMMEDIATE");
50
+ try {
51
+ for (const statement of statements) {
52
+ const prepared = prepareOnce(statement.sql);
53
+ await (statement.args === void 0 ? prepared.run() : prepared.run(statement.args));
54
+ }
55
+ await database.exec("COMMIT");
56
+ } catch (error) {
57
+ try {
58
+ await database.exec("ROLLBACK");
59
+ } catch (rollbackError) {
60
+ warnOnFailedRollback(rollbackError);
61
+ }
62
+ throw error;
63
+ } finally {
64
+ for (const prepared of preparedBySql.values()) {
65
+ closeStatement(prepared);
66
+ }
67
+ preparedBySql.clear();
68
+ }
69
+ }
70
+ };
71
+ }
72
+ function closeStatement(statement) {
73
+ try {
74
+ statement.close?.();
75
+ } catch {
76
+ }
77
+ }
78
+ const NO_ACTIVE_TRANSACTION_PATTERN = /no transaction is active/i;
79
+ function warnOnFailedRollback(error) {
80
+ const message = error instanceof Error ? error.message : String(error);
81
+ if (NO_ACTIVE_TRANSACTION_PATTERN.test(message)) {
82
+ return;
83
+ }
84
+ console.warn(
85
+ `[libsql-search] ROLLBACK failed after a failed index replacement: ${message}. This database handle may still be inside an open transaction, in which case a later write on it fails with "cannot start a transaction within a transaction". Reconnect the handle before reusing it.`
86
+ );
87
+ }
88
+ function assertTursoDatabase(database) {
89
+ if (typeof database !== "object" || database === null || typeof database.exec !== "function" || typeof database.prepare !== "function") {
90
+ throw new TypeError(
91
+ "tursoAdapter() expects a connected @tursodatabase/database handle with exec() and prepare() methods. connect() returns a Promise, so remember to await it."
92
+ );
93
+ }
94
+ }
95
+
96
+ exports.tursoAdapter = tursoAdapter;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Database backend boundary.
3
+ *
4
+ * Every SQL call this library makes goes through {@link DatabaseAdapter}. The
5
+ * `@libsql/client` adapter is built here, because that client is the declared
6
+ * peer dependency and the only backend the main entry point knows about. Other
7
+ * backends supply their own adapter from their own entry point, so the main
8
+ * bundle never imports, resolves, or references their package.
9
+ */
10
+
11
+ /**
12
+ * The narrow database contract the indexer, search, and retrieval helpers use.
13
+ *
14
+ * This interface is deliberately self-contained: it names no other type from
15
+ * this package and no type from any backend package. That keeps the declaration
16
+ * emitted into every entry point's `.d.ts` structurally identical, so an adapter
17
+ * built by one entry point is assignable to the parameter types of another
18
+ * without the two entry points sharing a nominal symbol.
19
+ *
20
+ * Do not brand this with a `unique symbol`. Each bundled `.d.ts` declares its
21
+ * own copy of the interface, and a `unique symbol` would make those copies
22
+ * nominally distinct — an adapter from the `libsql-search/turso` entry point
23
+ * would stop being assignable to `SearchOptions['client']`.
24
+ */
25
+ interface DatabaseAdapter {
26
+ /**
27
+ * Marker set by this package's adapter factories.
28
+ *
29
+ * Public functions accept either a raw `@libsql/client` `Client` or an
30
+ * adapter, and this property is how they tell the two apart. It is a plain
31
+ * property rather than structural method sniffing on purpose: guessing a
32
+ * backend from its method names would risk picking the wrong atomicity
33
+ * primitive, and the two backends disagree about which primitive is
34
+ * transactional.
35
+ */
36
+ readonly libsqlSearchAdapter: true;
37
+ /** Which backend this adapter drives. Informational; behavior keys off the capability flags. */
38
+ readonly backend: string;
39
+ /**
40
+ * Whether the backend can build a `libsql_vector_idx()` index and query it
41
+ * through `vector_top_k()`.
42
+ *
43
+ * When false, `createTable()` skips the vector index and `search()` runs the
44
+ * exact full-scan path automatically. Both are required, not optimizations:
45
+ * on a backend without the index, `CREATE INDEX ... libsql_vector_idx(...)`
46
+ * is a parse error and `vector_top_k` is not a table.
47
+ */
48
+ readonly supportsVectorIndex: boolean;
49
+ /** Run a schema statement that returns no rows. */
50
+ executeDdl(sql: string): Promise<void>;
51
+ /**
52
+ * Run one statement and return its rows as plain column-keyed objects.
53
+ *
54
+ * The bind-value union is spelled out inline rather than named, so that this
55
+ * interface stays free of any other symbol. It is narrower than `unknown` on
56
+ * purpose: it is the only compile-time check that a caller is binding
57
+ * something a SQLite driver can actually store.
58
+ */
59
+ executeQuery(sql: string, args?: Readonly<Record<string, string | number | bigint | Uint8Array | null>> | ReadonlyArray<string | number | bigint | Uint8Array | null>): Promise<Array<Record<string, unknown>>>;
60
+ /**
61
+ * Apply every statement atomically: either all of them commit, or none do.
62
+ *
63
+ * The whole "a failed rebuild leaves the previous index intact" guarantee
64
+ * rests on this method. A backend whose batch primitive is not transactional
65
+ * must implement this with an explicit transaction instead.
66
+ */
67
+ executeAtomicWrite(statements: ReadonlyArray<{
68
+ sql: string;
69
+ args?: ReadonlyArray<string | number | bigint | Uint8Array | null>;
70
+ }>): Promise<void>;
71
+ }
72
+
73
+ /**
74
+ * Turso Database adapter — `libsql-search/turso`.
75
+ *
76
+ * **Experimental, and exact-search only.** See `docs/TURSO.md`.
77
+ *
78
+ * This entry point exists so that the main entry point never mentions
79
+ * `@tursodatabase/database`. Nothing here imports it either: the database
80
+ * handle is accepted through the structural {@link TursoDatabase} type, so
81
+ * neither `tsc`, nor Deno, nor a bundler is ever asked to resolve the
82
+ * native package on behalf of a user who does not use it.
83
+ *
84
+ * ```ts
85
+ * import { connect } from '@tursodatabase/database';
86
+ * import { tursoAdapter } from 'libsql-search/turso';
87
+ * import { createTable, indexContent, search } from 'libsql-search';
88
+ *
89
+ * const client = tursoAdapter(await connect('./local.db'));
90
+ *
91
+ * await createTable(client);
92
+ * await indexContent({ client, contentPath: './content' });
93
+ * const results = await search({ client, query: 'vector search' });
94
+ * ```
95
+ *
96
+ * @module libsql-search/turso
97
+ */
98
+
99
+ /**
100
+ * The prepared-statement surface this adapter uses, as
101
+ * `@tursodatabase/database` exposes it.
102
+ *
103
+ * Every method returns a Promise on this backend, but they are typed as
104
+ * possibly-synchronous and always awaited, so a synchronous driver with the
105
+ * same shape works too.
106
+ */
107
+ interface TursoStatement {
108
+ run(args?: unknown): unknown;
109
+ all(args?: unknown): unknown;
110
+ /**
111
+ * Release the native statement. Optional: a driver that manages statement
112
+ * lifetime itself may omit it.
113
+ *
114
+ * Not closing leaks roughly 10 KB of native memory per prepare on
115
+ * `@tursodatabase/database`, which the garbage collector does not reclaim
116
+ * because it is not JavaScript heap. A server calling `search()` per request
117
+ * grows without bound until the process is killed.
118
+ */
119
+ close?(): unknown;
120
+ }
121
+ /**
122
+ * The database-handle surface this adapter uses, matching the object returned
123
+ * by `connect()` from `@tursodatabase/database`.
124
+ *
125
+ * Declared structurally on purpose. Importing the real type would make
126
+ * `@tursodatabase/database` a hard resolution target for anyone who type-checks
127
+ * this package, including the many users who only ever touch `@libsql/client`.
128
+ */
129
+ interface TursoDatabase {
130
+ exec(sql: string): unknown;
131
+ prepare(sql: string): TursoStatement;
132
+ }
133
+ /**
134
+ * Wrap a `@tursodatabase/database` handle so this library's functions can use
135
+ * it.
136
+ *
137
+ * The returned value is passed as `client` to `createTable()`,
138
+ * `indexContent()`, `search()`, and the retrieval helpers.
139
+ *
140
+ * Two behaviors differ from `@libsql/client`, and both are forced by the
141
+ * backend rather than chosen here:
142
+ *
143
+ * - **No ANN vector index.** Turso Database implements the vector *functions*
144
+ * but not `libsql_vector_idx()` or `vector_top_k()`, so `createTable()` skips
145
+ * the vector index and `search()` always runs the exact full scan. Search is
146
+ * therefore O(rows) on this backend.
147
+ * - **Transactions, not batches.** Turso's `batch()` is not atomic — a failing
148
+ * batch can leave earlier statements committed — so the index replacement
149
+ * runs inside an explicit `BEGIN IMMEDIATE` / `COMMIT` transaction, which is.
150
+ * This preserves the guarantee that a failed rebuild leaves the previous
151
+ * index intact.
152
+ */
153
+ declare function tursoAdapter(database: TursoDatabase): DatabaseAdapter;
154
+
155
+ export { tursoAdapter };
156
+ export type { DatabaseAdapter, TursoDatabase, TursoStatement };
@@ -0,0 +1,94 @@
1
+ function tursoAdapter(database) {
2
+ assertTursoDatabase(database);
3
+ return {
4
+ libsqlSearchAdapter: true,
5
+ backend: "turso",
6
+ /**
7
+ * Turso Database ships the vector functions but no ANN index. This is a
8
+ * design direction rather than a gap: the project shipped SIMD-accelerated
9
+ * exact search instead, and the DiskANN port remains an open backlog item.
10
+ */
11
+ supportsVectorIndex: false,
12
+ async executeDdl(sql) {
13
+ await database.exec(sql);
14
+ },
15
+ async executeQuery(sql, args) {
16
+ const statement = database.prepare(sql);
17
+ try {
18
+ const rows = await (args === void 0 ? statement.all() : statement.all(args));
19
+ return rows;
20
+ } finally {
21
+ closeStatement(statement);
22
+ }
23
+ },
24
+ /**
25
+ * Replace the table contents inside one explicit transaction.
26
+ *
27
+ * `batch()` is deliberately not used. It is not transactional on this
28
+ * backend: a batch that fails part way through leaves the statements before
29
+ * the failure applied, which would silently turn a failed rebuild into a
30
+ * half-destroyed index. `BEGIN IMMEDIATE` takes the write lock up front and
31
+ * `ROLLBACK` restores the previous rows exactly.
32
+ *
33
+ * Statements are prepared once per distinct SQL string and rebound per row.
34
+ * An index rebuild is one `DELETE` followed by N identical `INSERT`s, so
35
+ * this turns N+1 prepares into exactly 2 regardless of corpus size.
36
+ */
37
+ async executeAtomicWrite(statements) {
38
+ const preparedBySql = /* @__PURE__ */ new Map();
39
+ const prepareOnce = (sql) => {
40
+ let prepared = preparedBySql.get(sql);
41
+ if (prepared === void 0) {
42
+ prepared = database.prepare(sql);
43
+ preparedBySql.set(sql, prepared);
44
+ }
45
+ return prepared;
46
+ };
47
+ await database.exec("BEGIN IMMEDIATE");
48
+ try {
49
+ for (const statement of statements) {
50
+ const prepared = prepareOnce(statement.sql);
51
+ await (statement.args === void 0 ? prepared.run() : prepared.run(statement.args));
52
+ }
53
+ await database.exec("COMMIT");
54
+ } catch (error) {
55
+ try {
56
+ await database.exec("ROLLBACK");
57
+ } catch (rollbackError) {
58
+ warnOnFailedRollback(rollbackError);
59
+ }
60
+ throw error;
61
+ } finally {
62
+ for (const prepared of preparedBySql.values()) {
63
+ closeStatement(prepared);
64
+ }
65
+ preparedBySql.clear();
66
+ }
67
+ }
68
+ };
69
+ }
70
+ function closeStatement(statement) {
71
+ try {
72
+ statement.close?.();
73
+ } catch {
74
+ }
75
+ }
76
+ const NO_ACTIVE_TRANSACTION_PATTERN = /no transaction is active/i;
77
+ function warnOnFailedRollback(error) {
78
+ const message = error instanceof Error ? error.message : String(error);
79
+ if (NO_ACTIVE_TRANSACTION_PATTERN.test(message)) {
80
+ return;
81
+ }
82
+ console.warn(
83
+ `[libsql-search] ROLLBACK failed after a failed index replacement: ${message}. This database handle may still be inside an open transaction, in which case a later write on it fails with "cannot start a transaction within a transaction". Reconnect the handle before reusing it.`
84
+ );
85
+ }
86
+ function assertTursoDatabase(database) {
87
+ if (typeof database !== "object" || database === null || typeof database.exec !== "function" || typeof database.prepare !== "function") {
88
+ throw new TypeError(
89
+ "tursoAdapter() expects a connected @tursodatabase/database handle with exec() and prepare() methods. connect() returns a Promise, so remember to await it."
90
+ );
91
+ }
92
+ }
93
+
94
+ export { tursoAdapter };