@atscript/db-sqlite 0.1.150 → 0.1.152

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
@@ -6,6 +6,18 @@ let _atscript_db_sql_tools = require("@atscript/db-sql-tools");
6
6
  let _uniqu_core = require("@uniqu/core");
7
7
  let node_async_hooks = require("node:async_hooks");
8
8
  //#region src/better-sqlite3-driver.ts
9
+ const DEFAULT_STATEMENT_CACHE_SIZE = 256;
10
+ /** SQL longer than this is prepared per call (long `$in` lists, multi-row inserts). */
11
+ const MAX_CACHED_SQL_LENGTH = 8192;
12
+ /** Schema introspection (`PRAGMA …`) is never cached: it is cold, and a pragma may bake schema state in at prepare time. */
13
+ const PRAGMA_RE = /^\s*pragma\b/i;
14
+ /** Transaction-control statements `exec` runs through a statement prepared once. */
15
+ const TX_CONTROL_SQL = new Set([
16
+ "BEGIN",
17
+ "BEGIN IMMEDIATE",
18
+ "COMMIT",
19
+ "ROLLBACK"
20
+ ]);
9
21
  /**
10
22
  * {@link TSqliteDriver} implementation backed by `better-sqlite3`.
11
23
  *
@@ -43,8 +55,12 @@ let node_async_hooks = require("node:async_hooks");
43
55
  var BetterSqlite3Driver = class {
44
56
  db;
45
57
  hasVectorExt = false;
58
+ /** LRU of prepared statements by SQL text (Map insertion order = recency). */
59
+ _stmts = /* @__PURE__ */ new Map();
60
+ _maxStmts;
46
61
  constructor(pathOrDb, options) {
47
- const { vector, loadExtensions, ...nativeOptions } = options ?? {};
62
+ const { vector, loadExtensions, statementCacheSize, ...nativeOptions } = options ?? {};
63
+ this._maxStmts = Math.max(0, statementCacheSize ?? DEFAULT_STATEMENT_CACHE_SIZE);
48
64
  const req = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
49
65
  if (typeof pathOrDb === "string") {
50
66
  const Database = req("better-sqlite3");
@@ -56,8 +72,28 @@ var BetterSqlite3Driver = class {
56
72
  this.hasVectorExt = true;
57
73
  }
58
74
  }
59
- run(sql, params) {
75
+ /**
76
+ * The prepared statement for `sql` — reused from the LRU cache when present.
77
+ * A cached statement stays valid across schema changes: SQLite re-prepares
78
+ * it transparently on its next step (and `exec` — the DDL path — clears the
79
+ * cache anyway).
80
+ */
81
+ _prepare(sql) {
82
+ const cached = this._stmts.get(sql);
83
+ if (cached !== void 0) {
84
+ this._stmts.delete(sql);
85
+ this._stmts.set(sql, cached);
86
+ return cached;
87
+ }
60
88
  const stmt = this.db.prepare(sql);
89
+ if (this._maxStmts > 0 && sql.length <= MAX_CACHED_SQL_LENGTH && !PRAGMA_RE.test(sql)) {
90
+ this._stmts.set(sql, stmt);
91
+ if (this._stmts.size > this._maxStmts) this._stmts.delete(this._stmts.keys().next().value);
92
+ }
93
+ return stmt;
94
+ }
95
+ run(sql, params) {
96
+ const stmt = this._prepare(sql);
61
97
  const result = params ? stmt.run(...params) : stmt.run();
62
98
  return {
63
99
  changes: result.changes,
@@ -65,21 +101,27 @@ var BetterSqlite3Driver = class {
65
101
  };
66
102
  }
67
103
  all(sql, params) {
68
- const stmt = this.db.prepare(sql);
104
+ const stmt = this._prepare(sql);
69
105
  return params ? stmt.all(...params) : stmt.all();
70
106
  }
71
107
  get(sql, params) {
72
- const stmt = this.db.prepare(sql);
108
+ const stmt = this._prepare(sql);
73
109
  return (params ? stmt.get(...params) : stmt.get()) ?? null;
74
110
  }
75
111
  exec(sql) {
112
+ if (TX_CONTROL_SQL.has(sql)) {
113
+ this._prepare(sql).run();
114
+ return;
115
+ }
76
116
  this.db.exec(sql);
117
+ this._stmts.clear();
77
118
  }
78
119
  registerFunction(name, fn, opts) {
79
120
  this.db.function(name, { deterministic: opts?.deterministic ?? false }, fn);
80
121
  }
81
122
  /** Idempotent: closing an already closed database is a no-op. */
82
123
  close() {
124
+ this._stmts.clear();
83
125
  if (!this.db.open) return;
84
126
  this.db.close();
85
127
  }
@@ -330,16 +372,6 @@ function haversineDistanceExpr(quotedCol, point) {
330
372
  };
331
373
  }
332
374
  /**
333
- * Builds an INSERT statement.
334
- *
335
- * @param table - Table name.
336
- * @param data - Column→value map.
337
- * @returns `{ sql, params }` ready for `driver.run()`.
338
- */
339
- function buildInsert(table, data) {
340
- return (0, _atscript_db_sql_tools.buildInsert)(sqliteDialect, table, data);
341
- }
342
- /**
343
375
  * Builds a SELECT statement with optional sort, limit, offset, projection.
344
376
  */
345
377
  function buildSelect(table, where, controls) {
@@ -765,6 +797,8 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
765
797
  viewCapabilities() {
766
798
  return _atscript_db.ALL_VIEW_CAPABILITIES;
767
799
  }
800
+ /** INSERT text per column signature (rows of one table share a handful of shapes). */
801
+ _insertSql = new _atscript_db_sql_tools.InsertSqlCache(sqliteDialect);
768
802
  /** Whether the SQLite connection has the sqlite-vec extension loaded. */
769
803
  _supportsVector;
770
804
  /** Whether SQLite math functions (radians/asin/...) are available — geo search. */
@@ -963,20 +997,31 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
963
997
  }
964
998
  }
965
999
  async insertOne(data) {
966
- const { sql, params } = buildInsert(this.resolveTableName(), data);
1000
+ const { sql, params } = this._insertSql.build(this.resolveTableName(), data);
967
1001
  this._log(sql, params);
968
1002
  const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
969
1003
  return { insertedId: this._resolveInsertedId(data, result.lastInsertRowid) };
970
1004
  }
971
1005
  async insertMany(data) {
1006
+ if (data.length === 1) {
1007
+ const { insertedId } = await this.insertOne(data[0]);
1008
+ return {
1009
+ insertedCount: 1,
1010
+ insertedIds: [insertedId]
1011
+ };
1012
+ }
972
1013
  return this.withTransaction(async () => {
973
- const ids = [];
974
- for (const row of data) {
975
- const { sql, params } = buildInsert(this.resolveTableName(), row);
976
- this._log(sql, params);
977
- const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
978
- ids.push(this._resolveInsertedId(row, result.lastInsertRowid));
979
- }
1014
+ const tableName = this.resolveTableName();
1015
+ const ids = await this._stmt(() => {
1016
+ const out = [];
1017
+ for (const row of data) {
1018
+ const { sql, params } = this._insertSql.build(tableName, row);
1019
+ this._log(sql, params);
1020
+ const result = this._wrapConstraintError(() => this.driver.run(sql, params));
1021
+ out.push(this._resolveInsertedId(row, result.lastInsertRowid));
1022
+ }
1023
+ return out;
1024
+ });
980
1025
  return {
981
1026
  insertedCount: ids.length,
982
1027
  insertedIds: ids
@@ -996,7 +1041,7 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
996
1041
  const slots = [];
997
1042
  const tableName = this.resolveTableName();
998
1043
  for (const row of data) {
999
- const built = buildInsert(tableName, row);
1044
+ const built = this._insertSql.build(tableName, row);
1000
1045
  const sql = `${built.sql} ON CONFLICT DO NOTHING`;
1001
1046
  const params = built.params;
1002
1047
  this._log(sql, params);
@@ -1032,6 +1077,24 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
1032
1077
  this._log(sql, params);
1033
1078
  return (0, _atscript_db_sql_tools.stripPartitionRowNumber)(await this._stmt(() => this.driver.all(sql, params)));
1034
1079
  }
1080
+ /**
1081
+ * Page + total in one synchronous segment: the filter's WHERE is built once
1082
+ * for both statements, and both read the same snapshot. A subclass that
1083
+ * overrides `findMany` / `count` keeps the base behaviour (both through them).
1084
+ */
1085
+ async findManyWithCount(query) {
1086
+ if (this.findMany !== SqliteAdapter.prototype.findMany || this.count !== SqliteAdapter.prototype.count) return super.findManyWithCount(query);
1087
+ const where = buildWhere(query.filter);
1088
+ const tableName = this.resolveTableName();
1089
+ const { sql, params } = buildSelect(tableName, where, query.controls);
1090
+ const countSql = `SELECT COUNT(*) as cnt FROM "${esc(tableName)}" WHERE ${where.sql}`;
1091
+ this._log(sql, params);
1092
+ this._log(countSql, where.params);
1093
+ return this._stmt(() => ({
1094
+ data: this.driver.all(sql, params),
1095
+ count: this.driver.get(countSql, where.params)?.cnt ?? 0
1096
+ }));
1097
+ }
1035
1098
  async count(query) {
1036
1099
  const where = buildWhere(query.filter);
1037
1100
  const sql = `SELECT COUNT(*) as cnt FROM "${esc(this.resolveTableName())}" WHERE ${where.sql}`;
package/dist/index.d.cts CHANGED
@@ -75,6 +75,13 @@ interface TBetterSqlite3DriverOptions extends Record<string, unknown> {
75
75
  vector?: boolean;
76
76
  /** Absolute paths to SQLite loadable extensions, passed to `Database.loadExtension`. */
77
77
  loadExtensions?: string[];
78
+ /**
79
+ * Max prepared statements kept per connection (least-recently-used evicted).
80
+ * `run` / `all` / `get` reuse the statement prepared for the same SQL text
81
+ * instead of preparing it on every call. Default: 256; `0` disables the cache.
82
+ * @since 0.1.151
83
+ */
84
+ statementCacheSize?: number;
78
85
  }
79
86
  /**
80
87
  * {@link TSqliteDriver} implementation backed by `better-sqlite3`.
@@ -113,7 +120,17 @@ interface TBetterSqlite3DriverOptions extends Record<string, unknown> {
113
120
  declare class BetterSqlite3Driver implements TSqliteDriver {
114
121
  private db;
115
122
  readonly hasVectorExt: boolean;
123
+ /** LRU of prepared statements by SQL text (Map insertion order = recency). */
124
+ private readonly _stmts;
125
+ private readonly _maxStmts;
116
126
  constructor(pathOrDb: string | import("better-sqlite3").Database, options?: TBetterSqlite3DriverOptions);
127
+ /**
128
+ * The prepared statement for `sql` — reused from the LRU cache when present.
129
+ * A cached statement stays valid across schema changes: SQLite re-prepares
130
+ * it transparently on its next step (and `exec` — the DDL path — clears the
131
+ * cache anyway).
132
+ */
133
+ private _prepare;
117
134
  run(sql: string, params?: unknown[]): TSqliteRunResult;
118
135
  all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
119
136
  get<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
@@ -274,6 +291,8 @@ declare class SqliteAdapter extends BaseDbAdapter {
274
291
  supportsAggregateExpressions(): boolean;
275
292
  /** Computed view columns and first-row joins. */
276
293
  viewCapabilities(): ReadonlySet<TViewCapability>;
294
+ /** INSERT text per column signature (rows of one table share a handful of shapes). */
295
+ private readonly _insertSql;
277
296
  /** Whether the SQLite connection has the sqlite-vec extension loaded. */
278
297
  private _supportsVector;
279
298
  /** Whether SQLite math functions (radians/asin/...) are available — geo search. */
@@ -355,6 +374,15 @@ declare class SqliteAdapter extends BaseDbAdapter {
355
374
  * window over `partitionBy` (the generic `$with` loader's per-parent page).
356
375
  */
357
376
  findManyPerPartition(query: DbQuery, partitionBy: readonly string[]): Promise<Array<Record<string, unknown>>>;
377
+ /**
378
+ * Page + total in one synchronous segment: the filter's WHERE is built once
379
+ * for both statements, and both read the same snapshot. A subclass that
380
+ * overrides `findMany` / `count` keeps the base behaviour (both through them).
381
+ */
382
+ findManyWithCount(query: DbQuery): Promise<{
383
+ data: Array<Record<string, unknown>>;
384
+ count: number;
385
+ }>;
358
386
  count(query: DbQuery): Promise<number>;
359
387
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
360
388
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
package/dist/index.d.mts CHANGED
@@ -75,6 +75,13 @@ interface TBetterSqlite3DriverOptions extends Record<string, unknown> {
75
75
  vector?: boolean;
76
76
  /** Absolute paths to SQLite loadable extensions, passed to `Database.loadExtension`. */
77
77
  loadExtensions?: string[];
78
+ /**
79
+ * Max prepared statements kept per connection (least-recently-used evicted).
80
+ * `run` / `all` / `get` reuse the statement prepared for the same SQL text
81
+ * instead of preparing it on every call. Default: 256; `0` disables the cache.
82
+ * @since 0.1.151
83
+ */
84
+ statementCacheSize?: number;
78
85
  }
79
86
  /**
80
87
  * {@link TSqliteDriver} implementation backed by `better-sqlite3`.
@@ -113,7 +120,17 @@ interface TBetterSqlite3DriverOptions extends Record<string, unknown> {
113
120
  declare class BetterSqlite3Driver implements TSqliteDriver {
114
121
  private db;
115
122
  readonly hasVectorExt: boolean;
123
+ /** LRU of prepared statements by SQL text (Map insertion order = recency). */
124
+ private readonly _stmts;
125
+ private readonly _maxStmts;
116
126
  constructor(pathOrDb: string | import("better-sqlite3").Database, options?: TBetterSqlite3DriverOptions);
127
+ /**
128
+ * The prepared statement for `sql` — reused from the LRU cache when present.
129
+ * A cached statement stays valid across schema changes: SQLite re-prepares
130
+ * it transparently on its next step (and `exec` — the DDL path — clears the
131
+ * cache anyway).
132
+ */
133
+ private _prepare;
117
134
  run(sql: string, params?: unknown[]): TSqliteRunResult;
118
135
  all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
119
136
  get<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
@@ -274,6 +291,8 @@ declare class SqliteAdapter extends BaseDbAdapter {
274
291
  supportsAggregateExpressions(): boolean;
275
292
  /** Computed view columns and first-row joins. */
276
293
  viewCapabilities(): ReadonlySet<TViewCapability>;
294
+ /** INSERT text per column signature (rows of one table share a handful of shapes). */
295
+ private readonly _insertSql;
277
296
  /** Whether the SQLite connection has the sqlite-vec extension loaded. */
278
297
  private _supportsVector;
279
298
  /** Whether SQLite math functions (radians/asin/...) are available — geo search. */
@@ -355,6 +374,15 @@ declare class SqliteAdapter extends BaseDbAdapter {
355
374
  * window over `partitionBy` (the generic `$with` loader's per-parent page).
356
375
  */
357
376
  findManyPerPartition(query: DbQuery, partitionBy: readonly string[]): Promise<Array<Record<string, unknown>>>;
377
+ /**
378
+ * Page + total in one synchronous segment: the filter's WHERE is built once
379
+ * for both statements, and both read the same snapshot. A subclass that
380
+ * overrides `findMany` / `count` keeps the base behaviour (both through them).
381
+ */
382
+ findManyWithCount(query: DbQuery): Promise<{
383
+ data: Array<Record<string, unknown>>;
384
+ count: number;
385
+ }>;
358
386
  count(query: DbQuery): Promise<number>;
359
387
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
360
388
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
package/dist/index.mjs CHANGED
@@ -1,10 +1,22 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, defaultFulltextIndex, describeFulltext, fkColumns, searchIndexNotFoundMessage, searchTermInteger, splitFulltextFields, vectorIndexNotFoundMessage } from "@atscript/db";
3
3
  import { resolveAggregateSearch } from "@atscript/db/agg";
4
- import { EMPTY_AND, SEARCH_SOURCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, geoWindowFromControls, jsonDollarPath, orFragment, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue } from "@atscript/db-sql-tools";
4
+ import { EMPTY_AND, InsertSqlCache, SEARCH_SOURCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, geoWindowFromControls, jsonDollarPath, orFragment, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue } 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
8
+ const DEFAULT_STATEMENT_CACHE_SIZE = 256;
9
+ /** SQL longer than this is prepared per call (long `$in` lists, multi-row inserts). */
10
+ const MAX_CACHED_SQL_LENGTH = 8192;
11
+ /** Schema introspection (`PRAGMA …`) is never cached: it is cold, and a pragma may bake schema state in at prepare time. */
12
+ const PRAGMA_RE = /^\s*pragma\b/i;
13
+ /** Transaction-control statements `exec` runs through a statement prepared once. */
14
+ const TX_CONTROL_SQL = new Set([
15
+ "BEGIN",
16
+ "BEGIN IMMEDIATE",
17
+ "COMMIT",
18
+ "ROLLBACK"
19
+ ]);
8
20
  /**
9
21
  * {@link TSqliteDriver} implementation backed by `better-sqlite3`.
10
22
  *
@@ -42,8 +54,12 @@ import { AsyncLocalStorage } from "node:async_hooks";
42
54
  var BetterSqlite3Driver = class {
43
55
  db;
44
56
  hasVectorExt = false;
57
+ /** LRU of prepared statements by SQL text (Map insertion order = recency). */
58
+ _stmts = /* @__PURE__ */ new Map();
59
+ _maxStmts;
45
60
  constructor(pathOrDb, options) {
46
- const { vector, loadExtensions, ...nativeOptions } = options ?? {};
61
+ const { vector, loadExtensions, statementCacheSize, ...nativeOptions } = options ?? {};
62
+ this._maxStmts = Math.max(0, statementCacheSize ?? DEFAULT_STATEMENT_CACHE_SIZE);
47
63
  const req = createRequire(import.meta.url);
48
64
  if (typeof pathOrDb === "string") {
49
65
  const Database = req("better-sqlite3");
@@ -55,8 +71,28 @@ var BetterSqlite3Driver = class {
55
71
  this.hasVectorExt = true;
56
72
  }
57
73
  }
58
- run(sql, params) {
74
+ /**
75
+ * The prepared statement for `sql` — reused from the LRU cache when present.
76
+ * A cached statement stays valid across schema changes: SQLite re-prepares
77
+ * it transparently on its next step (and `exec` — the DDL path — clears the
78
+ * cache anyway).
79
+ */
80
+ _prepare(sql) {
81
+ const cached = this._stmts.get(sql);
82
+ if (cached !== void 0) {
83
+ this._stmts.delete(sql);
84
+ this._stmts.set(sql, cached);
85
+ return cached;
86
+ }
59
87
  const stmt = this.db.prepare(sql);
88
+ if (this._maxStmts > 0 && sql.length <= MAX_CACHED_SQL_LENGTH && !PRAGMA_RE.test(sql)) {
89
+ this._stmts.set(sql, stmt);
90
+ if (this._stmts.size > this._maxStmts) this._stmts.delete(this._stmts.keys().next().value);
91
+ }
92
+ return stmt;
93
+ }
94
+ run(sql, params) {
95
+ const stmt = this._prepare(sql);
60
96
  const result = params ? stmt.run(...params) : stmt.run();
61
97
  return {
62
98
  changes: result.changes,
@@ -64,21 +100,27 @@ var BetterSqlite3Driver = class {
64
100
  };
65
101
  }
66
102
  all(sql, params) {
67
- const stmt = this.db.prepare(sql);
103
+ const stmt = this._prepare(sql);
68
104
  return params ? stmt.all(...params) : stmt.all();
69
105
  }
70
106
  get(sql, params) {
71
- const stmt = this.db.prepare(sql);
107
+ const stmt = this._prepare(sql);
72
108
  return (params ? stmt.get(...params) : stmt.get()) ?? null;
73
109
  }
74
110
  exec(sql) {
111
+ if (TX_CONTROL_SQL.has(sql)) {
112
+ this._prepare(sql).run();
113
+ return;
114
+ }
75
115
  this.db.exec(sql);
116
+ this._stmts.clear();
76
117
  }
77
118
  registerFunction(name, fn, opts) {
78
119
  this.db.function(name, { deterministic: opts?.deterministic ?? false }, fn);
79
120
  }
80
121
  /** Idempotent: closing an already closed database is a no-op. */
81
122
  close() {
123
+ this._stmts.clear();
82
124
  if (!this.db.open) return;
83
125
  this.db.close();
84
126
  }
@@ -329,16 +371,6 @@ function haversineDistanceExpr(quotedCol, point) {
329
371
  };
330
372
  }
331
373
  /**
332
- * Builds an INSERT statement.
333
- *
334
- * @param table - Table name.
335
- * @param data - Column→value map.
336
- * @returns `{ sql, params }` ready for `driver.run()`.
337
- */
338
- function buildInsert$1(table, data) {
339
- return buildInsert(sqliteDialect, table, data);
340
- }
341
- /**
342
374
  * Builds a SELECT statement with optional sort, limit, offset, projection.
343
375
  */
344
376
  function buildSelect$1(table, where, controls) {
@@ -764,6 +796,8 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
764
796
  viewCapabilities() {
765
797
  return ALL_VIEW_CAPABILITIES;
766
798
  }
799
+ /** INSERT text per column signature (rows of one table share a handful of shapes). */
800
+ _insertSql = new InsertSqlCache(sqliteDialect);
767
801
  /** Whether the SQLite connection has the sqlite-vec extension loaded. */
768
802
  _supportsVector;
769
803
  /** Whether SQLite math functions (radians/asin/...) are available — geo search. */
@@ -962,20 +996,31 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
962
996
  }
963
997
  }
964
998
  async insertOne(data) {
965
- const { sql, params } = buildInsert$1(this.resolveTableName(), data);
999
+ const { sql, params } = this._insertSql.build(this.resolveTableName(), data);
966
1000
  this._log(sql, params);
967
1001
  const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
968
1002
  return { insertedId: this._resolveInsertedId(data, result.lastInsertRowid) };
969
1003
  }
970
1004
  async insertMany(data) {
1005
+ if (data.length === 1) {
1006
+ const { insertedId } = await this.insertOne(data[0]);
1007
+ return {
1008
+ insertedCount: 1,
1009
+ insertedIds: [insertedId]
1010
+ };
1011
+ }
971
1012
  return this.withTransaction(async () => {
972
- const ids = [];
973
- for (const row of data) {
974
- const { sql, params } = buildInsert$1(this.resolveTableName(), row);
975
- this._log(sql, params);
976
- const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
977
- ids.push(this._resolveInsertedId(row, result.lastInsertRowid));
978
- }
1013
+ const tableName = this.resolveTableName();
1014
+ const ids = await this._stmt(() => {
1015
+ const out = [];
1016
+ for (const row of data) {
1017
+ const { sql, params } = this._insertSql.build(tableName, row);
1018
+ this._log(sql, params);
1019
+ const result = this._wrapConstraintError(() => this.driver.run(sql, params));
1020
+ out.push(this._resolveInsertedId(row, result.lastInsertRowid));
1021
+ }
1022
+ return out;
1023
+ });
979
1024
  return {
980
1025
  insertedCount: ids.length,
981
1026
  insertedIds: ids
@@ -995,7 +1040,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
995
1040
  const slots = [];
996
1041
  const tableName = this.resolveTableName();
997
1042
  for (const row of data) {
998
- const built = buildInsert$1(tableName, row);
1043
+ const built = this._insertSql.build(tableName, row);
999
1044
  const sql = `${built.sql} ON CONFLICT DO NOTHING`;
1000
1045
  const params = built.params;
1001
1046
  this._log(sql, params);
@@ -1031,6 +1076,24 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1031
1076
  this._log(sql, params);
1032
1077
  return stripPartitionRowNumber(await this._stmt(() => this.driver.all(sql, params)));
1033
1078
  }
1079
+ /**
1080
+ * Page + total in one synchronous segment: the filter's WHERE is built once
1081
+ * for both statements, and both read the same snapshot. A subclass that
1082
+ * overrides `findMany` / `count` keeps the base behaviour (both through them).
1083
+ */
1084
+ async findManyWithCount(query) {
1085
+ if (this.findMany !== SqliteAdapter.prototype.findMany || this.count !== SqliteAdapter.prototype.count) return super.findManyWithCount(query);
1086
+ const where = buildWhere(query.filter);
1087
+ const tableName = this.resolveTableName();
1088
+ const { sql, params } = buildSelect$1(tableName, where, query.controls);
1089
+ const countSql = `SELECT COUNT(*) as cnt FROM "${esc(tableName)}" WHERE ${where.sql}`;
1090
+ this._log(sql, params);
1091
+ this._log(countSql, where.params);
1092
+ return this._stmt(() => ({
1093
+ data: this.driver.all(sql, params),
1094
+ count: this.driver.get(countSql, where.params)?.cnt ?? 0
1095
+ }));
1096
+ }
1034
1097
  async count(query) {
1035
1098
  const where = buildWhere(query.filter);
1036
1099
  const sql = `SELECT COUNT(*) as cnt FROM "${esc(this.resolveTableName())}" WHERE ${where.sql}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-sqlite",
3
- "version": "0.1.150",
3
+ "version": "0.1.152",
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.101",
40
- "@atscript/typescript": "^0.1.101",
39
+ "@atscript/core": "^0.1.102",
40
+ "@atscript/typescript": "^0.1.102",
41
41
  "@types/better-sqlite3": "^7.6.13",
42
- "@uniqu/core": "^0.1.13",
42
+ "@uniqu/core": "^0.1.14",
43
43
  "better-sqlite3": "^12.6.2",
44
44
  "sqlite-vec": "^0.1.9",
45
- "unplugin-atscript": "^0.1.101"
45
+ "unplugin-atscript": "^0.1.102"
46
46
  },
47
47
  "peerDependencies": {
48
- "@atscript/core": "^0.1.101",
49
- "@atscript/typescript": "^0.1.101",
50
- "@uniqu/core": "^0.1.13",
48
+ "@atscript/core": "^0.1.102",
49
+ "@atscript/typescript": "^0.1.102",
50
+ "@uniqu/core": "^0.1.14",
51
51
  "better-sqlite3": ">=11.0.0",
52
- "@atscript/db": "^0.1.150",
53
- "@atscript/db-sql-tools": "^0.1.150"
52
+ "@atscript/db": "^0.1.152",
53
+ "@atscript/db-sql-tools": "^0.1.152"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "better-sqlite3": {