@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 +86 -23
- package/dist/index.d.cts +28 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +87 -24
- package/package.json +10 -10
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
|
-
|
|
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.
|
|
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.
|
|
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 } =
|
|
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
|
|
974
|
-
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 } =
|
|
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
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
40
|
-
"@atscript/typescript": "^0.1.
|
|
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.
|
|
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.
|
|
45
|
+
"unplugin-atscript": "^0.1.102"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@atscript/core": "^0.1.
|
|
49
|
-
"@atscript/typescript": "^0.1.
|
|
50
|
-
"@uniqu/core": "^0.1.
|
|
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.
|
|
53
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
52
|
+
"@atscript/db": "^0.1.152",
|
|
53
|
+
"@atscript/db-sql-tools": "^0.1.152"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"better-sqlite3": {
|