@atscript/db-sqlite 0.1.161 → 0.1.163

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.cjs CHANGED
@@ -310,6 +310,7 @@ const sqliteDialect = {
310
310
  return `"${esc(name)}"`;
311
311
  },
312
312
  unlimitedLimit: "-1",
313
+ stringLiteral: _atscript_db_sql_tools.sqlStringLiteral,
313
314
  toValue: _atscript_db_sql_tools.toSqlValue,
314
315
  toParam(value) {
315
316
  if (value === void 0) return null;
@@ -801,6 +802,21 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
801
802
  supportsNullsPlacement() {
802
803
  return true;
803
804
  }
805
+ /**
806
+ * Locking reads are accepted as a no-op (since 0.1.162): every transaction
807
+ * starts with `BEGIN IMMEDIATE`, which holds the database write lock, so
808
+ * reads inside it are current and no other writer can run until it ends.
809
+ */
810
+ supportsReadLocks() {
811
+ return true;
812
+ }
813
+ /** Every level — a `BEGIN IMMEDIATE` transaction is serializable. @since 0.1.162 */
814
+ isolationLevels() {
815
+ return _atscript_db.ALL_ISOLATION_LEVELS;
816
+ }
817
+ _inherentIsolation() {
818
+ return "serializable";
819
+ }
804
820
  /** Computed view columns and first-row joins. */
805
821
  viewCapabilities() {
806
822
  return _atscript_db.ALL_VIEW_CAPABILITIES;
@@ -1153,8 +1169,25 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
1153
1169
  modifiedCount: result.changes
1154
1170
  };
1155
1171
  }
1172
+ /**
1173
+ * Bounded writes (since 0.1.162) through an ordered, limited rowid subquery
1174
+ * (`rowid IN (SELECT rowid … ORDER BY … LIMIT n)`) — no
1175
+ * `SQLITE_ENABLE_UPDATE_DELETE_LIMIT` build needed.
1176
+ */
1177
+ supportsWriteLimit() {
1178
+ return true;
1179
+ }
1180
+ /** One writer at a time: the rowid pick and the write run as one statement, never interleaved. */
1181
+ writeLimitIsAtomic() {
1182
+ return true;
1183
+ }
1184
+ /** `where` narrowed to a bounded write's rows, or `where` itself when unbounded. */
1185
+ _boundedWhere(where, bound) {
1186
+ if (!bound) return where;
1187
+ return (0, _atscript_db_sql_tools.writeLimitWhere)(sqliteDialect, `"${esc(this.resolveTableName())}"`, ["rowid"], where, bound);
1188
+ }
1156
1189
  async updateMany(filter, data, ops, opts) {
1157
- const where = buildWhere(filter);
1190
+ const where = this._boundedWhere(buildWhere(filter), opts?.limit);
1158
1191
  const versionColumn = this._versionColumnFor(opts);
1159
1192
  const { sql, params } = buildUpdate(this.resolveTableName(), data, where, ops, versionColumn);
1160
1193
  this._log(sql, params);
@@ -1168,10 +1201,10 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
1168
1201
  const where = buildWhere(filter);
1169
1202
  const tableName = this.resolveTableName();
1170
1203
  const versionColumn = this._table.versionColumnPhysical;
1171
- const { sql, params } = buildUpdate(tableName, (0, _atscript_db_sql_tools.fillReplacePayload)(data, (0, _atscript_db_sql_tools.replaceColumnsFor)(this._table.fieldDescriptors, this.nativeDefaultFns()), versionColumn), {
1204
+ const { sql, params } = buildUpdate(tableName, (0, _atscript_db_sql_tools.fillReplacePayload)(data, (0, _atscript_db_sql_tools.replaceColumnsFor)(this._table.fieldDescriptors, this.nativeDefaultFns()), versionColumn, this._table.onUpdateIncPhysical), {
1172
1205
  sql: `rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`,
1173
1206
  params: where.params
1174
- }, void 0, versionColumn, expectedVersion);
1207
+ }, this._onUpdateIncOps(), versionColumn, expectedVersion);
1175
1208
  this._log(sql, params);
1176
1209
  const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
1177
1210
  return {
@@ -1182,7 +1215,7 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
1182
1215
  async replaceMany(filter, data) {
1183
1216
  const where = buildWhere(filter);
1184
1217
  const versionColumn = this._table.versionColumnPhysical;
1185
- const { sql, params } = buildUpdate(this.resolveTableName(), data, where, void 0, versionColumn);
1218
+ const { sql, params } = buildUpdate(this.resolveTableName(), data, where, this._onUpdateIncOps(), versionColumn);
1186
1219
  this._log(sql, params);
1187
1220
  const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
1188
1221
  return {
@@ -1197,8 +1230,8 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
1197
1230
  this._log(sql, where.params);
1198
1231
  return { deletedCount: (await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, where.params)))).changes };
1199
1232
  }
1200
- async deleteMany(filter) {
1201
- const where = buildWhere(filter);
1233
+ async deleteMany(filter, opts) {
1234
+ const where = this._boundedWhere(buildWhere(filter), opts?.limit);
1202
1235
  const { sql, params } = buildDelete(this.resolveTableName(), where);
1203
1236
  this._log(sql, params);
1204
1237
  return { deletedCount: (await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)))).changes };
@@ -1494,30 +1527,33 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
1494
1527
  });
1495
1528
  }
1496
1529
  async syncIndexes() {
1497
- return this._withExclusiveConnection(async () => {
1498
- const tableName = this.resolveTableName();
1499
- await this.syncIndexesWithDiff({
1500
- listExisting: async () => this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => !i.name.startsWith("sqlite_")).map((i) => ({
1501
- name: i.name,
1502
- columns: this.driver.all(`PRAGMA index_info("${esc(i.name)}")`).map((c) => c.name).filter((n) => n !== null)
1503
- })),
1504
- createIndex: async (index) => {
1505
- const unique = index.type === "unique" ? "UNIQUE " : "";
1506
- const cols = index.fields.map((f) => `"${esc(f.name)}" ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
1507
- const sql = `CREATE ${unique}INDEX IF NOT EXISTS "${esc(index.key)}" ON "${esc(tableName)}" (${cols})`;
1508
- this._log(sql);
1509
- this.driver.exec(sql);
1510
- },
1511
- dropIndex: async (name) => {
1512
- const sql = `DROP INDEX IF EXISTS "${esc(name)}"`;
1513
- this._log(sql);
1514
- this.driver.exec(sql);
1515
- },
1516
- shouldSkipType: (type) => type === "fulltext" || type === "geo"
1517
- });
1518
- this._syncFtsIndexes(tableName);
1519
- this._syncVecIndexes(tableName);
1530
+ return this._withExclusiveConnection(() => this._syncIndexes(false));
1531
+ }
1532
+ async _syncIndexes(dryRun) {
1533
+ const tableName = this.resolveTableName();
1534
+ const changes = await this.syncIndexesWithDiff({
1535
+ dryRun,
1536
+ listExisting: async () => this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => !i.name.startsWith("sqlite_")).map((i) => ({
1537
+ name: i.name,
1538
+ columns: this.driver.all(`PRAGMA index_info("${esc(i.name)}")`).map((c) => c.name).filter((n) => n !== null)
1539
+ })),
1540
+ createIndex: async (index) => {
1541
+ const unique = index.type === "unique" ? "UNIQUE " : "";
1542
+ const cols = index.fields.map((f) => `"${esc(f.name)}" ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
1543
+ const sql = `CREATE ${unique}INDEX IF NOT EXISTS "${esc(index.key)}" ON "${esc(tableName)}" (${cols})`;
1544
+ this._log(sql);
1545
+ this.driver.exec(sql);
1546
+ },
1547
+ dropIndex: async (name) => {
1548
+ const sql = `DROP INDEX IF EXISTS "${esc(name)}"`;
1549
+ this._log(sql);
1550
+ this.driver.exec(sql);
1551
+ },
1552
+ skipIndex: (index) => index.type === "fulltext" || index.type === "geo" ? null : void 0
1520
1553
  });
1554
+ changes.push(...await this._syncFtsIndexes(tableName, dryRun));
1555
+ changes.push(...await this._syncVecIndexes(tableName, dryRun));
1556
+ return changes;
1521
1557
  }
1522
1558
  getSearchIndexes() {
1523
1559
  const indexes = [];
@@ -1686,20 +1722,31 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
1686
1722
  /**
1687
1723
  * Creates/drops FTS5 virtual tables and sync triggers to match desired fulltext indexes.
1688
1724
  */
1689
- _syncFtsIndexes(tableName) {
1725
+ async _syncFtsIndexes(tableName, dryRun) {
1726
+ const { changes, apply } = (0, _atscript_db.createChangeRecorder)(dryRun);
1690
1727
  const ftIndexes = this._getFulltextIndexes().filter((idx) => (0, _atscript_db.splitFulltextFields)(idx).text.length > 0);
1691
1728
  const desiredFtsTables = new Set(ftIndexes.map((idx) => this._ftsTableName(idx.name)));
1692
- const existingFts = this.driver.all(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE ?`, [`${tableName}__fts__%`]).filter((r) => r.sql.startsWith("CREATE VIRTUAL TABLE")).map((r) => r.name);
1693
- for (const name of existingFts) if (!desiredFtsTables.has(name)) this._dropFtsTable(name);
1729
+ const existingFts = this._listShadowTables(tableName, "fts");
1730
+ for (const name of existingFts) if (!desiredFtsTables.has(name)) await apply({
1731
+ name,
1732
+ action: "drop"
1733
+ }, name, () => this._dropFtsTable(name));
1694
1734
  const existingSet = new Set(existingFts);
1695
1735
  for (const index of ftIndexes) {
1696
1736
  const ftsTable = this._ftsTableName(index.name);
1697
- if (!existingSet.has(ftsTable)) this._createFtsTable(tableName, ftsTable, index);
1698
- else if (!this._ftsColumnsMatch(ftsTable, index)) {
1737
+ if (!existingSet.has(ftsTable)) await apply({
1738
+ name: ftsTable,
1739
+ action: "add"
1740
+ }, ftsTable, () => this._createFtsTable(tableName, ftsTable, index));
1741
+ else if (!this._ftsColumnsMatch(ftsTable, index)) await apply({
1742
+ name: ftsTable,
1743
+ action: "rebuild"
1744
+ }, ftsTable, () => {
1699
1745
  this._dropFtsTable(ftsTable);
1700
1746
  this._createFtsTable(tableName, ftsTable, index);
1701
- }
1747
+ });
1702
1748
  }
1749
+ return changes;
1703
1750
  }
1704
1751
  /** Whether an existing FTS5 table has exactly the index's text columns, in order. */
1705
1752
  _ftsColumnsMatch(ftsTable, index) {
@@ -1930,21 +1977,29 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
1930
1977
  /**
1931
1978
  * Creates/drops vec0 virtual shadow tables and sync triggers to match desired vector fields.
1932
1979
  */
1933
- _syncVecIndexes(tableName) {
1934
- if (this._vectorFields.size === 0) return;
1980
+ async _syncVecIndexes(tableName, dryRun) {
1981
+ const { changes, apply } = (0, _atscript_db.createChangeRecorder)(dryRun);
1982
+ if (this._vectorFields.size === 0) return changes;
1935
1983
  if (!this._detectVectorSupport()) {
1936
1984
  this._log("[atscript-db-sqlite] sqlite-vec not available, skipping vec0 shadow table sync (vector fields stored as JSON)");
1937
- return;
1985
+ return changes;
1938
1986
  }
1939
1987
  const desiredVecTables = /* @__PURE__ */ new Map();
1940
1988
  for (const [fieldPath, meta] of this._vectorFields.entries()) desiredVecTables.set(this._vecTableName(meta.indexName), {
1941
1989
  field: fieldPath,
1942
1990
  meta
1943
1991
  });
1944
- const existingVec = this.driver.all(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE ?`, [`${tableName}__vec__%`]).filter((r) => r.sql.startsWith("CREATE VIRTUAL TABLE")).map((r) => r.name);
1945
- for (const name of existingVec) if (!desiredVecTables.has(name)) this._dropVecTable(name);
1992
+ const existingVec = this._listShadowTables(tableName, "vec");
1993
+ for (const name of existingVec) if (!desiredVecTables.has(name)) await apply({
1994
+ name,
1995
+ action: "drop"
1996
+ }, name, () => this._dropVecTable(name));
1946
1997
  const existingSet = new Set(existingVec);
1947
- for (const [vecTable, { field, meta }] of desiredVecTables.entries()) if (!existingSet.has(vecTable)) this._createVecTable(tableName, vecTable, field, meta);
1998
+ for (const [vecTable, { field, meta }] of desiredVecTables.entries()) if (!existingSet.has(vecTable)) await apply({
1999
+ name: vecTable,
2000
+ action: "add"
2001
+ }, vecTable, () => this._createVecTable(tableName, vecTable, field, meta));
2002
+ return changes;
1948
2003
  }
1949
2004
  /** Creates a vec0 virtual shadow table with sync triggers and seeds it from existing rows. */
1950
2005
  _createVecTable(tableName, vecTable, field, vec) {
@@ -2034,8 +2089,7 @@ function andWhere(left, right) {
2034
2089
  * PRAGMA returns `'active'` (SQL-quoted), we store `active` (raw). */
2035
2090
  function normalizeSqliteDefault(value) {
2036
2091
  if (value === null) return;
2037
- if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
2038
- return value;
2092
+ return (0, _atscript_db_sql_tools.unquoteSqlStringLiteral)(value) ?? value;
2039
2093
  }
2040
2094
  //#endregion
2041
2095
  //#region src/index.ts
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TJsonCopyTarget, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteManyOptions, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbIsolationLevel, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TIndexChange, TJsonCopyTarget, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
2
2
  import { TMetadataMap } from "@atscript/typescript/utils";
3
3
  import { FilterExpr as FilterExpr$1 } from "@uniqu/core";
4
4
  import { TFilterVisitorOptions, TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
@@ -299,6 +299,15 @@ declare class SqliteAdapter extends BaseDbAdapter {
299
299
  supportsAggregateExpressions(): boolean;
300
300
  /** `$nulls` / `@db.sort.nulls` NULL placement — `NULLS FIRST` / `NULLS LAST`. */
301
301
  supportsNullsPlacement(): boolean;
302
+ /**
303
+ * Locking reads are accepted as a no-op (since 0.1.162): every transaction
304
+ * starts with `BEGIN IMMEDIATE`, which holds the database write lock, so
305
+ * reads inside it are current and no other writer can run until it ends.
306
+ */
307
+ supportsReadLocks(): boolean;
308
+ /** Every level — a `BEGIN IMMEDIATE` transaction is serializable. @since 0.1.162 */
309
+ isolationLevels(): ReadonlySet<TDbIsolationLevel>;
310
+ protected _inherentIsolation(): TDbIsolationLevel;
302
311
  /** Computed view columns and first-row joins. */
303
312
  viewCapabilities(): ReadonlySet<TViewCapability>;
304
313
  /** INSERT text per column signature (rows of one table share a handful of shapes). */
@@ -406,11 +415,21 @@ declare class SqliteAdapter extends BaseDbAdapter {
406
415
  count(query: DbQuery): Promise<number>;
407
416
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
408
417
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
418
+ /**
419
+ * Bounded writes (since 0.1.162) through an ordered, limited rowid subquery
420
+ * (`rowid IN (SELECT rowid … ORDER BY … LIMIT n)`) — no
421
+ * `SQLITE_ENABLE_UPDATE_DELETE_LIMIT` build needed.
422
+ */
423
+ supportsWriteLimit(): boolean;
424
+ /** One writer at a time: the rowid pick and the write run as one statement, never interleaved. */
425
+ writeLimitIsAtomic(): boolean;
426
+ /** `where` narrowed to a bounded write's rows, or `where` itself when unbounded. */
427
+ private _boundedWhere;
409
428
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
410
429
  replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
411
430
  replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
412
431
  deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
413
- deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
432
+ deleteMany(filter: FilterExpr, opts?: TDbDeleteManyOptions): Promise<TDbDeleteResult>;
414
433
  /**
415
434
  * SQLite accepts forward FK references, so a foreign-key cycle is created
416
435
  * inline in any order — `opts.deferForeignKeysTo` is accepted and ignored.
@@ -462,7 +481,8 @@ declare class SqliteAdapter extends BaseDbAdapter {
462
481
  * sync). A virtual table's hidden columns (`hidden` 1) are skipped.
463
482
  */
464
483
  getExistingColumnsForTable(tableName: string): Promise<TExistingColumn[]>;
465
- syncIndexes(): Promise<void>;
484
+ syncIndexes(): Promise<TIndexChange[]>;
485
+ protected _syncIndexes(dryRun: boolean): Promise<TIndexChange[]>;
466
486
  getSearchIndexes(): TSearchIndexInfo[];
467
487
  search(text: string, query: DbQuery, indexName?: string): Promise<Array<Record<string, unknown>>>;
468
488
  searchWithCount(text: string, query: DbQuery, indexName?: string): Promise<{
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TJsonCopyTarget, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteManyOptions, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbIsolationLevel, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TIndexChange, TJsonCopyTarget, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
2
2
  import { TFilterVisitorOptions, TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
3
3
  import { FilterExpr as FilterExpr$1 } from "@uniqu/core";
4
4
  import { TMetadataMap } from "@atscript/typescript/utils";
@@ -299,6 +299,15 @@ declare class SqliteAdapter extends BaseDbAdapter {
299
299
  supportsAggregateExpressions(): boolean;
300
300
  /** `$nulls` / `@db.sort.nulls` NULL placement — `NULLS FIRST` / `NULLS LAST`. */
301
301
  supportsNullsPlacement(): boolean;
302
+ /**
303
+ * Locking reads are accepted as a no-op (since 0.1.162): every transaction
304
+ * starts with `BEGIN IMMEDIATE`, which holds the database write lock, so
305
+ * reads inside it are current and no other writer can run until it ends.
306
+ */
307
+ supportsReadLocks(): boolean;
308
+ /** Every level — a `BEGIN IMMEDIATE` transaction is serializable. @since 0.1.162 */
309
+ isolationLevels(): ReadonlySet<TDbIsolationLevel>;
310
+ protected _inherentIsolation(): TDbIsolationLevel;
302
311
  /** Computed view columns and first-row joins. */
303
312
  viewCapabilities(): ReadonlySet<TViewCapability>;
304
313
  /** INSERT text per column signature (rows of one table share a handful of shapes). */
@@ -406,11 +415,21 @@ declare class SqliteAdapter extends BaseDbAdapter {
406
415
  count(query: DbQuery): Promise<number>;
407
416
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
408
417
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
418
+ /**
419
+ * Bounded writes (since 0.1.162) through an ordered, limited rowid subquery
420
+ * (`rowid IN (SELECT rowid … ORDER BY … LIMIT n)`) — no
421
+ * `SQLITE_ENABLE_UPDATE_DELETE_LIMIT` build needed.
422
+ */
423
+ supportsWriteLimit(): boolean;
424
+ /** One writer at a time: the rowid pick and the write run as one statement, never interleaved. */
425
+ writeLimitIsAtomic(): boolean;
426
+ /** `where` narrowed to a bounded write's rows, or `where` itself when unbounded. */
427
+ private _boundedWhere;
409
428
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
410
429
  replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
411
430
  replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
412
431
  deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
413
- deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
432
+ deleteMany(filter: FilterExpr, opts?: TDbDeleteManyOptions): Promise<TDbDeleteResult>;
414
433
  /**
415
434
  * SQLite accepts forward FK references, so a foreign-key cycle is created
416
435
  * inline in any order — `opts.deferForeignKeysTo` is accepted and ignored.
@@ -462,7 +481,8 @@ declare class SqliteAdapter extends BaseDbAdapter {
462
481
  * sync). A virtual table's hidden columns (`hidden` 1) are skipped.
463
482
  */
464
483
  getExistingColumnsForTable(tableName: string): Promise<TExistingColumn[]>;
465
- syncIndexes(): Promise<void>;
484
+ syncIndexes(): Promise<TIndexChange[]>;
485
+ protected _syncIndexes(dryRun: boolean): Promise<TIndexChange[]>;
466
486
  getSearchIndexes(): TSearchIndexInfo[];
467
487
  search(text: string, query: DbQuery, indexName?: string): Promise<Array<Record<string, unknown>>>;
468
488
  searchWithCount(text: string, query: DbQuery, indexName?: string): Promise<{
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
- import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, defaultFulltextIndex, describeFulltext, fkColumns, searchIndexNotFoundMessage, searchTermInteger, splitFulltextFields, vectorIndexNotFoundMessage } from "@atscript/db";
2
+ import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_ISOLATION_LEVELS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, createChangeRecorder, defaultFulltextIndex, describeFulltext, fkColumns, searchIndexNotFoundMessage, searchTermInteger, splitFulltextFields, vectorIndexNotFoundMessage } from "@atscript/db";
3
3
  import { resolveAggregateSearch } from "@atscript/db/agg";
4
- import { EMPTY_AND, InsertSqlCache, SEARCH_SOURCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, fromSourceSql, geoWindowFromControls, jsonDollarPath, orFragment, orderByList, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, viewReadSource } from "@atscript/db-sql-tools";
4
+ import { EMPTY_AND, InsertSqlCache, SEARCH_SOURCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, fromSourceSql, geoWindowFromControls, jsonDollarPath, orFragment, orderByList, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, viewReadSource, writeLimitWhere } from "@atscript/db-sql-tools";
5
5
  import { bucketer } from "@uniqu/core";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  //#region src/better-sqlite3-driver.ts
@@ -309,6 +309,7 @@ const sqliteDialect = {
309
309
  return `"${esc(name)}"`;
310
310
  },
311
311
  unlimitedLimit: "-1",
312
+ stringLiteral: sqlStringLiteral,
312
313
  toValue: toSqlValue,
313
314
  toParam(value) {
314
315
  if (value === void 0) return null;
@@ -800,6 +801,21 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
800
801
  supportsNullsPlacement() {
801
802
  return true;
802
803
  }
804
+ /**
805
+ * Locking reads are accepted as a no-op (since 0.1.162): every transaction
806
+ * starts with `BEGIN IMMEDIATE`, which holds the database write lock, so
807
+ * reads inside it are current and no other writer can run until it ends.
808
+ */
809
+ supportsReadLocks() {
810
+ return true;
811
+ }
812
+ /** Every level — a `BEGIN IMMEDIATE` transaction is serializable. @since 0.1.162 */
813
+ isolationLevels() {
814
+ return ALL_ISOLATION_LEVELS;
815
+ }
816
+ _inherentIsolation() {
817
+ return "serializable";
818
+ }
803
819
  /** Computed view columns and first-row joins. */
804
820
  viewCapabilities() {
805
821
  return ALL_VIEW_CAPABILITIES;
@@ -1152,8 +1168,25 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1152
1168
  modifiedCount: result.changes
1153
1169
  };
1154
1170
  }
1171
+ /**
1172
+ * Bounded writes (since 0.1.162) through an ordered, limited rowid subquery
1173
+ * (`rowid IN (SELECT rowid … ORDER BY … LIMIT n)`) — no
1174
+ * `SQLITE_ENABLE_UPDATE_DELETE_LIMIT` build needed.
1175
+ */
1176
+ supportsWriteLimit() {
1177
+ return true;
1178
+ }
1179
+ /** One writer at a time: the rowid pick and the write run as one statement, never interleaved. */
1180
+ writeLimitIsAtomic() {
1181
+ return true;
1182
+ }
1183
+ /** `where` narrowed to a bounded write's rows, or `where` itself when unbounded. */
1184
+ _boundedWhere(where, bound) {
1185
+ if (!bound) return where;
1186
+ return writeLimitWhere(sqliteDialect, `"${esc(this.resolveTableName())}"`, ["rowid"], where, bound);
1187
+ }
1155
1188
  async updateMany(filter, data, ops, opts) {
1156
- const where = buildWhere(filter);
1189
+ const where = this._boundedWhere(buildWhere(filter), opts?.limit);
1157
1190
  const versionColumn = this._versionColumnFor(opts);
1158
1191
  const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, ops, versionColumn);
1159
1192
  this._log(sql, params);
@@ -1167,10 +1200,10 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1167
1200
  const where = buildWhere(filter);
1168
1201
  const tableName = this.resolveTableName();
1169
1202
  const versionColumn = this._table.versionColumnPhysical;
1170
- const { sql, params } = buildUpdate$1(tableName, fillReplacePayload(data, replaceColumnsFor(this._table.fieldDescriptors, this.nativeDefaultFns()), versionColumn), {
1203
+ const { sql, params } = buildUpdate$1(tableName, fillReplacePayload(data, replaceColumnsFor(this._table.fieldDescriptors, this.nativeDefaultFns()), versionColumn, this._table.onUpdateIncPhysical), {
1171
1204
  sql: `rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`,
1172
1205
  params: where.params
1173
- }, void 0, versionColumn, expectedVersion);
1206
+ }, this._onUpdateIncOps(), versionColumn, expectedVersion);
1174
1207
  this._log(sql, params);
1175
1208
  const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
1176
1209
  return {
@@ -1181,7 +1214,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1181
1214
  async replaceMany(filter, data) {
1182
1215
  const where = buildWhere(filter);
1183
1216
  const versionColumn = this._table.versionColumnPhysical;
1184
- const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, void 0, versionColumn);
1217
+ const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, this._onUpdateIncOps(), versionColumn);
1185
1218
  this._log(sql, params);
1186
1219
  const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
1187
1220
  return {
@@ -1196,8 +1229,8 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1196
1229
  this._log(sql, where.params);
1197
1230
  return { deletedCount: (await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, where.params)))).changes };
1198
1231
  }
1199
- async deleteMany(filter) {
1200
- const where = buildWhere(filter);
1232
+ async deleteMany(filter, opts) {
1233
+ const where = this._boundedWhere(buildWhere(filter), opts?.limit);
1201
1234
  const { sql, params } = buildDelete$1(this.resolveTableName(), where);
1202
1235
  this._log(sql, params);
1203
1236
  return { deletedCount: (await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)))).changes };
@@ -1493,30 +1526,33 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1493
1526
  });
1494
1527
  }
1495
1528
  async syncIndexes() {
1496
- return this._withExclusiveConnection(async () => {
1497
- const tableName = this.resolveTableName();
1498
- await this.syncIndexesWithDiff({
1499
- listExisting: async () => this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => !i.name.startsWith("sqlite_")).map((i) => ({
1500
- name: i.name,
1501
- columns: this.driver.all(`PRAGMA index_info("${esc(i.name)}")`).map((c) => c.name).filter((n) => n !== null)
1502
- })),
1503
- createIndex: async (index) => {
1504
- const unique = index.type === "unique" ? "UNIQUE " : "";
1505
- const cols = index.fields.map((f) => `"${esc(f.name)}" ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
1506
- const sql = `CREATE ${unique}INDEX IF NOT EXISTS "${esc(index.key)}" ON "${esc(tableName)}" (${cols})`;
1507
- this._log(sql);
1508
- this.driver.exec(sql);
1509
- },
1510
- dropIndex: async (name) => {
1511
- const sql = `DROP INDEX IF EXISTS "${esc(name)}"`;
1512
- this._log(sql);
1513
- this.driver.exec(sql);
1514
- },
1515
- shouldSkipType: (type) => type === "fulltext" || type === "geo"
1516
- });
1517
- this._syncFtsIndexes(tableName);
1518
- this._syncVecIndexes(tableName);
1529
+ return this._withExclusiveConnection(() => this._syncIndexes(false));
1530
+ }
1531
+ async _syncIndexes(dryRun) {
1532
+ const tableName = this.resolveTableName();
1533
+ const changes = await this.syncIndexesWithDiff({
1534
+ dryRun,
1535
+ listExisting: async () => this.driver.all(`PRAGMA index_list("${esc(tableName)}")`).filter((i) => !i.name.startsWith("sqlite_")).map((i) => ({
1536
+ name: i.name,
1537
+ columns: this.driver.all(`PRAGMA index_info("${esc(i.name)}")`).map((c) => c.name).filter((n) => n !== null)
1538
+ })),
1539
+ createIndex: async (index) => {
1540
+ const unique = index.type === "unique" ? "UNIQUE " : "";
1541
+ const cols = index.fields.map((f) => `"${esc(f.name)}" ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
1542
+ const sql = `CREATE ${unique}INDEX IF NOT EXISTS "${esc(index.key)}" ON "${esc(tableName)}" (${cols})`;
1543
+ this._log(sql);
1544
+ this.driver.exec(sql);
1545
+ },
1546
+ dropIndex: async (name) => {
1547
+ const sql = `DROP INDEX IF EXISTS "${esc(name)}"`;
1548
+ this._log(sql);
1549
+ this.driver.exec(sql);
1550
+ },
1551
+ skipIndex: (index) => index.type === "fulltext" || index.type === "geo" ? null : void 0
1519
1552
  });
1553
+ changes.push(...await this._syncFtsIndexes(tableName, dryRun));
1554
+ changes.push(...await this._syncVecIndexes(tableName, dryRun));
1555
+ return changes;
1520
1556
  }
1521
1557
  getSearchIndexes() {
1522
1558
  const indexes = [];
@@ -1685,20 +1721,31 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1685
1721
  /**
1686
1722
  * Creates/drops FTS5 virtual tables and sync triggers to match desired fulltext indexes.
1687
1723
  */
1688
- _syncFtsIndexes(tableName) {
1724
+ async _syncFtsIndexes(tableName, dryRun) {
1725
+ const { changes, apply } = createChangeRecorder(dryRun);
1689
1726
  const ftIndexes = this._getFulltextIndexes().filter((idx) => splitFulltextFields(idx).text.length > 0);
1690
1727
  const desiredFtsTables = new Set(ftIndexes.map((idx) => this._ftsTableName(idx.name)));
1691
- const existingFts = this.driver.all(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE ?`, [`${tableName}__fts__%`]).filter((r) => r.sql.startsWith("CREATE VIRTUAL TABLE")).map((r) => r.name);
1692
- for (const name of existingFts) if (!desiredFtsTables.has(name)) this._dropFtsTable(name);
1728
+ const existingFts = this._listShadowTables(tableName, "fts");
1729
+ for (const name of existingFts) if (!desiredFtsTables.has(name)) await apply({
1730
+ name,
1731
+ action: "drop"
1732
+ }, name, () => this._dropFtsTable(name));
1693
1733
  const existingSet = new Set(existingFts);
1694
1734
  for (const index of ftIndexes) {
1695
1735
  const ftsTable = this._ftsTableName(index.name);
1696
- if (!existingSet.has(ftsTable)) this._createFtsTable(tableName, ftsTable, index);
1697
- else if (!this._ftsColumnsMatch(ftsTable, index)) {
1736
+ if (!existingSet.has(ftsTable)) await apply({
1737
+ name: ftsTable,
1738
+ action: "add"
1739
+ }, ftsTable, () => this._createFtsTable(tableName, ftsTable, index));
1740
+ else if (!this._ftsColumnsMatch(ftsTable, index)) await apply({
1741
+ name: ftsTable,
1742
+ action: "rebuild"
1743
+ }, ftsTable, () => {
1698
1744
  this._dropFtsTable(ftsTable);
1699
1745
  this._createFtsTable(tableName, ftsTable, index);
1700
- }
1746
+ });
1701
1747
  }
1748
+ return changes;
1702
1749
  }
1703
1750
  /** Whether an existing FTS5 table has exactly the index's text columns, in order. */
1704
1751
  _ftsColumnsMatch(ftsTable, index) {
@@ -1929,21 +1976,29 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1929
1976
  /**
1930
1977
  * Creates/drops vec0 virtual shadow tables and sync triggers to match desired vector fields.
1931
1978
  */
1932
- _syncVecIndexes(tableName) {
1933
- if (this._vectorFields.size === 0) return;
1979
+ async _syncVecIndexes(tableName, dryRun) {
1980
+ const { changes, apply } = createChangeRecorder(dryRun);
1981
+ if (this._vectorFields.size === 0) return changes;
1934
1982
  if (!this._detectVectorSupport()) {
1935
1983
  this._log("[atscript-db-sqlite] sqlite-vec not available, skipping vec0 shadow table sync (vector fields stored as JSON)");
1936
- return;
1984
+ return changes;
1937
1985
  }
1938
1986
  const desiredVecTables = /* @__PURE__ */ new Map();
1939
1987
  for (const [fieldPath, meta] of this._vectorFields.entries()) desiredVecTables.set(this._vecTableName(meta.indexName), {
1940
1988
  field: fieldPath,
1941
1989
  meta
1942
1990
  });
1943
- const existingVec = this.driver.all(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE ?`, [`${tableName}__vec__%`]).filter((r) => r.sql.startsWith("CREATE VIRTUAL TABLE")).map((r) => r.name);
1944
- for (const name of existingVec) if (!desiredVecTables.has(name)) this._dropVecTable(name);
1991
+ const existingVec = this._listShadowTables(tableName, "vec");
1992
+ for (const name of existingVec) if (!desiredVecTables.has(name)) await apply({
1993
+ name,
1994
+ action: "drop"
1995
+ }, name, () => this._dropVecTable(name));
1945
1996
  const existingSet = new Set(existingVec);
1946
- for (const [vecTable, { field, meta }] of desiredVecTables.entries()) if (!existingSet.has(vecTable)) this._createVecTable(tableName, vecTable, field, meta);
1997
+ for (const [vecTable, { field, meta }] of desiredVecTables.entries()) if (!existingSet.has(vecTable)) await apply({
1998
+ name: vecTable,
1999
+ action: "add"
2000
+ }, vecTable, () => this._createVecTable(tableName, vecTable, field, meta));
2001
+ return changes;
1947
2002
  }
1948
2003
  /** Creates a vec0 virtual shadow table with sync triggers and seeds it from existing rows. */
1949
2004
  _createVecTable(tableName, vecTable, field, vec) {
@@ -2033,8 +2088,7 @@ function andWhere(left, right) {
2033
2088
  * PRAGMA returns `'active'` (SQL-quoted), we store `active` (raw). */
2034
2089
  function normalizeSqliteDefault(value) {
2035
2090
  if (value === null) return;
2036
- if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
2037
- return value;
2091
+ return unquoteSqlStringLiteral(value) ?? value;
2038
2092
  }
2039
2093
  //#endregion
2040
2094
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-sqlite",
3
- "version": "0.1.161",
3
+ "version": "0.1.163",
4
4
  "description": "SQLite adapter for @atscript/db with swappable driver support.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -36,21 +36,21 @@
36
36
  "access": "public"
37
37
  },
38
38
  "devDependencies": {
39
- "@atscript/core": "^0.1.108",
40
- "@atscript/typescript": "^0.1.108",
39
+ "@atscript/core": "^0.1.109",
40
+ "@atscript/typescript": "^0.1.109",
41
41
  "@types/better-sqlite3": "^7.6.13",
42
42
  "@uniqu/core": "^0.1.15",
43
43
  "better-sqlite3": "^12.6.2",
44
44
  "sqlite-vec": "^0.1.9",
45
- "unplugin-atscript": "^0.1.108"
45
+ "unplugin-atscript": "^0.1.109"
46
46
  },
47
47
  "peerDependencies": {
48
- "@atscript/core": "^0.1.108",
49
- "@atscript/typescript": "^0.1.108",
48
+ "@atscript/core": "^0.1.109",
49
+ "@atscript/typescript": "^0.1.109",
50
50
  "@uniqu/core": "^0.1.15",
51
51
  "better-sqlite3": ">=11.0.0",
52
- "@atscript/db-sql-tools": "^0.1.161",
53
- "@atscript/db": "^0.1.161"
52
+ "@atscript/db": "^0.1.163",
53
+ "@atscript/db-sql-tools": "^0.1.163"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "better-sqlite3": {