@wxn0brp/db-storage-sqlite 0.110.4 → 0.120.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,6 +59,9 @@ await db.users.update({ name: "John Doe" }, { email: "newemail@example.com" });
59
59
 
60
60
  // 8. Remove entries
61
61
  await db.users.remove({ name: "John Doe" });
62
+
63
+ // 9. Close the SQLite handle
64
+ await db.close();
62
65
  ```
63
66
 
64
67
  ## API Notes
@@ -0,0 +1,7 @@
1
+ import { AffinityMap } from "./types.js";
2
+ export interface WhereResult {
3
+ sql: string;
4
+ values: any[];
5
+ postFilter?: (row: any) => boolean;
6
+ }
7
+ export declare function buildWhere(search: any, affinities: AffinityMap): WhereResult;
@@ -0,0 +1,240 @@
1
+ import { hasFieldsAdvanced } from "@wxn0brp/db-core/utils/hasFieldsAdvanced";
2
+ import { PUSHABLE_OPS } from "./const.js";
3
+ import { qid, toSqlValue } from "./utils.js";
4
+ const FALSE_SQL = "1 = 0";
5
+ function isMatchAllCondition(cond) {
6
+ return (cond === undefined ||
7
+ cond === null ||
8
+ (typeof cond === "object" &&
9
+ !Array.isArray(cond) &&
10
+ Object.keys(cond).length === 0));
11
+ }
12
+ export function buildWhere(search, affinities) {
13
+ if (search === undefined || search === null) {
14
+ return {
15
+ sql: "",
16
+ values: [],
17
+ };
18
+ }
19
+ if (typeof search === "function") {
20
+ return {
21
+ sql: "",
22
+ values: [],
23
+ postFilter: search,
24
+ };
25
+ }
26
+ if (typeof search !== "object" || Array.isArray(search)) {
27
+ return {
28
+ sql: FALSE_SQL,
29
+ values: [],
30
+ };
31
+ }
32
+ if (Object.keys(search).length === 0) {
33
+ return {
34
+ sql: "",
35
+ values: [],
36
+ };
37
+ }
38
+ const $fields = {};
39
+ const flatFields = {};
40
+ for (const key of Object.keys(search)) {
41
+ if (key.startsWith("$")) {
42
+ $fields[key.toLowerCase()] = search[key];
43
+ }
44
+ else {
45
+ flatFields[key] = search[key];
46
+ }
47
+ }
48
+ const clauses = [];
49
+ const allValues = [];
50
+ const postFilters = [];
51
+ for (const [key, value] of Object.entries(flatFields)) {
52
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
53
+ postFilters.push(row => {
54
+ try {
55
+ return hasFieldsAdvanced(row, {
56
+ [key]: value,
57
+ });
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ });
63
+ }
64
+ else if (value === undefined) {
65
+ postFilters.push(() => false);
66
+ }
67
+ else {
68
+ clauses.push(`${qid(key)} = ?`);
69
+ allValues.push(toSqlValue(value, affinities[key]));
70
+ }
71
+ }
72
+ if ("$subset" in $fields) {
73
+ const subsetSearch = {
74
+ $subset: $fields["$subset"],
75
+ };
76
+ postFilters.push(row => {
77
+ try {
78
+ return hasFieldsAdvanced(row, subsetSearch);
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ });
84
+ delete $fields["$subset"];
85
+ }
86
+ if ("$not" in $fields) {
87
+ const notResult = buildWhere($fields["$not"], affinities);
88
+ if (notResult.sql && !notResult.postFilter) {
89
+ clauses.push(`NOT (${notResult.sql})`);
90
+ allValues.push(...notResult.values);
91
+ }
92
+ else {
93
+ const innerSearch = $fields["$not"];
94
+ postFilters.push(row => {
95
+ try {
96
+ return !hasFieldsAdvanced(row, innerSearch);
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ });
102
+ }
103
+ delete $fields["$not"];
104
+ }
105
+ if ("$and" in $fields) {
106
+ const andResult = buildAnd($fields["$and"], affinities);
107
+ if (andResult.sql) {
108
+ clauses.push(`(${andResult.sql})`);
109
+ allValues.push(...andResult.values);
110
+ }
111
+ if (andResult.postFilter) {
112
+ postFilters.push(andResult.postFilter);
113
+ }
114
+ delete $fields["$and"];
115
+ }
116
+ if ("$or" in $fields) {
117
+ const orResult = buildOr($fields["$or"], affinities);
118
+ if (orResult.sql) {
119
+ clauses.push(`(${orResult.sql})`);
120
+ allValues.push(...orResult.values);
121
+ }
122
+ if (orResult.postFilter) {
123
+ postFilters.push(orResult.postFilter);
124
+ }
125
+ delete $fields["$or"];
126
+ }
127
+ for (const [opKey, fieldMap] of Object.entries($fields)) {
128
+ if (typeof fieldMap !== "object" || fieldMap === null)
129
+ continue;
130
+ for (const [field, value] of Object.entries(fieldMap)) {
131
+ let pushed = false;
132
+ const pushable = PUSHABLE_OPS[opKey];
133
+ const isSimpleValue = !(value instanceof RegExp) &&
134
+ (typeof value !== "object" || value === null || Array.isArray(value));
135
+ if (pushable && !field.includes(".") && isSimpleValue) {
136
+ const result = pushable(field, value, affinities[field]);
137
+ if (result) {
138
+ clauses.push(result.sql);
139
+ allValues.push(...result.values);
140
+ pushed = true;
141
+ }
142
+ }
143
+ if (!pushed) {
144
+ postFilters.push(row => {
145
+ try {
146
+ return hasFieldsAdvanced(row, {
147
+ [opKey]: {
148
+ [field]: value,
149
+ },
150
+ });
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ });
156
+ }
157
+ }
158
+ }
159
+ let postFilter;
160
+ if (postFilters.length > 0) {
161
+ postFilter = row => postFilters.every(fn => fn(row));
162
+ }
163
+ return {
164
+ sql: clauses.join(" AND "),
165
+ values: allValues,
166
+ postFilter,
167
+ };
168
+ }
169
+ function buildAnd(conditions, affinities) {
170
+ if (!Array.isArray(conditions)) {
171
+ return {
172
+ sql: FALSE_SQL,
173
+ values: [],
174
+ };
175
+ }
176
+ const clauses = [];
177
+ const allValues = [];
178
+ const postFilters = [];
179
+ for (const condition of conditions) {
180
+ const result = buildWhere(condition, affinities);
181
+ if (result.sql) {
182
+ clauses.push(`(${result.sql})`);
183
+ allValues.push(...result.values);
184
+ }
185
+ if (result.postFilter) {
186
+ postFilters.push(result.postFilter);
187
+ }
188
+ }
189
+ let postFilter;
190
+ if (postFilters.length > 0) {
191
+ postFilter = row => postFilters.every(fn => fn(row));
192
+ }
193
+ return {
194
+ sql: clauses.join(" AND "),
195
+ values: allValues,
196
+ postFilter,
197
+ };
198
+ }
199
+ function buildOr(conditions, affinities) {
200
+ if (!Array.isArray(conditions)) {
201
+ return {
202
+ sql: FALSE_SQL,
203
+ values: [],
204
+ };
205
+ }
206
+ if (conditions.some(isMatchAllCondition)) {
207
+ return {
208
+ sql: "",
209
+ values: [],
210
+ };
211
+ }
212
+ const results = conditions.map(c => buildWhere(c, affinities));
213
+ const allFullyPushable = results.every(r => !r.postFilter);
214
+ if (allFullyPushable) {
215
+ const nonEmpty = results.filter(r => r.sql && r.sql !== FALSE_SQL);
216
+ if (nonEmpty.length === 0)
217
+ return {
218
+ sql: FALSE_SQL,
219
+ values: [],
220
+ };
221
+ return {
222
+ sql: nonEmpty.map(r => `(${r.sql})`).join(" OR "),
223
+ values: nonEmpty.flatMap(r => r.values),
224
+ };
225
+ }
226
+ return {
227
+ sql: "",
228
+ values: [],
229
+ postFilter: row => {
230
+ try {
231
+ return hasFieldsAdvanced(row, {
232
+ $or: conditions,
233
+ });
234
+ }
235
+ catch {
236
+ return false;
237
+ }
238
+ },
239
+ };
240
+ }
@@ -0,0 +1,12 @@
1
+ import { Affinity } from "./types.js";
2
+ export declare const MAX_STMT_CACHE = 100;
3
+ export declare const BATCH_SIZE: number;
4
+ interface PushResult {
5
+ sql: string;
6
+ values: any[];
7
+ }
8
+ type PushFn = (field: string, value: any, affinity?: Affinity) => PushResult | null;
9
+ export declare const PUSHABLE_OPS: Record<string, PushFn>;
10
+ export declare const NON_PUSHABLE_OPS: Set<string>;
11
+ export declare const COMPLEX_OPS: string[];
12
+ export {};
package/dist/const.js ADDED
@@ -0,0 +1,155 @@
1
+ import { globEscape, qid, toSqlValue } from "./utils.js";
2
+ export const MAX_STMT_CACHE = 100;
3
+ export const BATCH_SIZE = +process.env.VALTHERA_SQLITE_BATCH_SIZE || 500;
4
+ const NUMERIC_TYPEOF = "IN ('integer','real')";
5
+ function numGuard(field, op, value) {
6
+ const f = qid(field);
7
+ return {
8
+ sql: `(typeof(${f}) ${NUMERIC_TYPEOF} AND ${f} ${op} ?)`,
9
+ values: [
10
+ value,
11
+ ],
12
+ };
13
+ }
14
+ function textGuard(field, op, value) {
15
+ const f = qid(field);
16
+ return {
17
+ sql: `(typeof(${f}) = 'text' AND ${f} ${op} ?)`,
18
+ values: [
19
+ value,
20
+ ],
21
+ };
22
+ }
23
+ function rangeOp(field, value, op) {
24
+ if (typeof value === "number" && Number.isFinite(value))
25
+ return numGuard(field, op, value);
26
+ if (typeof value === "string")
27
+ return textGuard(field, op, value);
28
+ return null;
29
+ }
30
+ function buildInComposite(field, list) {
31
+ if (list.some(v => v === undefined || (typeof v === "number" && !Number.isFinite(v))))
32
+ return null;
33
+ const f = qid(field);
34
+ const numbers = list.filter(v => typeof v === "number");
35
+ const strings = list.filter(v => typeof v === "string");
36
+ const others = list.filter(v => typeof v === "boolean" || v === null);
37
+ const groups = [];
38
+ const values = [];
39
+ if (numbers.length > 0) {
40
+ groups.push(`(typeof(${f}) ${NUMERIC_TYPEOF} AND ${f} IN (${numbers.map(() => "?").join(",")}))`);
41
+ values.push(...numbers);
42
+ }
43
+ if (strings.length > 0) {
44
+ groups.push(`(typeof(${f}) = 'text' AND ${f} IN (${strings.map(() => "?").join(",")}))`);
45
+ values.push(...strings);
46
+ }
47
+ if (others.length > 0) {
48
+ groups.push(`${f} IN (${others.map(() => "?").join(",")})`);
49
+ values.push(...others.map(v => toSqlValue(v)));
50
+ }
51
+ if (groups.length === 0)
52
+ return null;
53
+ return {
54
+ sql: groups.join(" OR "),
55
+ values,
56
+ };
57
+ }
58
+ export const PUSHABLE_OPS = {
59
+ $gt: (field, value) => rangeOp(field, value, ">"),
60
+ $lt: (field, value) => rangeOp(field, value, "<"),
61
+ $gte: (field, value) => rangeOp(field, value, ">="),
62
+ $lte: (field, value) => rangeOp(field, value, "<="),
63
+ $in: (field, value) => {
64
+ if (!Array.isArray(value))
65
+ return null;
66
+ return buildInComposite(field, value);
67
+ },
68
+ $nin: (field, value) => {
69
+ if (!Array.isArray(value))
70
+ return null;
71
+ const composite = buildInComposite(field, value);
72
+ if (!composite)
73
+ return null;
74
+ return {
75
+ sql: `NOT (${composite.sql})`,
76
+ values: composite.values,
77
+ };
78
+ },
79
+ $between: (field, value) => {
80
+ if (!Array.isArray(value) || value.length !== 2)
81
+ return null;
82
+ const f = qid(field);
83
+ const [min, max] = value;
84
+ if (typeof min !== "number" ||
85
+ typeof max !== "number" ||
86
+ !Number.isFinite(min) ||
87
+ !Number.isFinite(max))
88
+ return null;
89
+ return {
90
+ sql: `(typeof(${f}) ${NUMERIC_TYPEOF} AND ${f} BETWEEN ? AND ?)`,
91
+ values: [
92
+ min,
93
+ max,
94
+ ],
95
+ };
96
+ },
97
+ $exists: (field, value) => {
98
+ const f = qid(field);
99
+ if (value === true)
100
+ return {
101
+ sql: `${f} IS NOT NULL`,
102
+ values: [],
103
+ };
104
+ if (value === false)
105
+ return {
106
+ sql: `${f} IS NULL`,
107
+ values: [],
108
+ };
109
+ return null;
110
+ },
111
+ $startswith: (field, value) => {
112
+ if (typeof value !== "string")
113
+ return null;
114
+ const f = qid(field);
115
+ return {
116
+ sql: `(typeof(${f}) = 'text' AND ${f} GLOB ?)`,
117
+ values: [
118
+ globEscape(value) + "*",
119
+ ],
120
+ };
121
+ },
122
+ $endswith: (field, value) => {
123
+ if (typeof value !== "string")
124
+ return null;
125
+ const f = qid(field);
126
+ return {
127
+ sql: `(typeof(${f}) = 'text' AND ${f} GLOB ?)`,
128
+ values: [
129
+ "*" + globEscape(value),
130
+ ],
131
+ };
132
+ },
133
+ };
134
+ export const NON_PUSHABLE_OPS = new Set([
135
+ "$type",
136
+ "$size",
137
+ "$arrinc",
138
+ "$arrincall",
139
+ "$idgt",
140
+ "$idlt",
141
+ "$idgte",
142
+ "$idlte",
143
+ "$regex",
144
+ ]);
145
+ export const COMPLEX_OPS = [
146
+ "$push",
147
+ "$pushall",
148
+ "$pushset",
149
+ "$pull",
150
+ "$pullall",
151
+ "$rename",
152
+ "$unset",
153
+ "$merge",
154
+ "$deepmerge",
155
+ ];
package/dist/find.js CHANGED
@@ -1,27 +1,46 @@
1
1
  import { findUtil } from "@wxn0brp/db-core/utils/action";
2
2
  import { findObj } from "@wxn0brp/db-core/utils/process";
3
- import { toSqlValue } from "./index.js";
3
+ import { buildWhere } from "./buildWhere.js";
4
+ import { decodeSqlValue, execStmt, qid } from "./utils.js";
5
+ function parseRow(row) {
6
+ const parsed = {};
7
+ for (const [key, value] of Object.entries(row)) {
8
+ if (value instanceof Uint8Array) {
9
+ parsed[key] = decodeSqlValue(value);
10
+ continue;
11
+ }
12
+ if (value === null)
13
+ continue;
14
+ if (typeof value === "string" &&
15
+ ((value.startsWith("[") && value.endsWith("]")) ||
16
+ (value.startsWith("{") && value.endsWith("}")))) {
17
+ try {
18
+ parsed[key] = JSON.parse(value);
19
+ continue;
20
+ }
21
+ catch { }
22
+ }
23
+ parsed[key] = value;
24
+ }
25
+ return parsed;
26
+ }
4
27
  export async function find(slv, config) {
5
28
  const { collection, search } = config;
29
+ const affinities = await slv._getColumnAffinities(collection);
30
+ const where = buildWhere(search, affinities);
6
31
  let sqlResult = [];
7
- const baseKeys = typeof search === "object" && search !== null && !Array.isArray(search)
8
- ? Object.keys(search)
9
- .filter(k => search[k] !== undefined)
10
- .filter(k => !k.startsWith("$"))
11
- .filter(k => typeof search[k] !== "object")
12
- : [];
13
- if (typeof search === "function" || baseKeys.length === 0) {
14
- const stmt = await slv._prepare(`SELECT * FROM ${collection}`);
15
- sqlResult = await Promise.resolve(stmt.all());
32
+ if (where.sql) {
33
+ const stmt = await slv._prepare(`SELECT * FROM ${qid(collection)} WHERE ${where.sql}`);
34
+ sqlResult = await execStmt(stmt, "all", ...where.values);
16
35
  }
17
36
  else {
18
- const baseSql = `SELECT * FROM ${collection} WHERE ${baseKeys.map(k => `${k} = ?`).join(" AND ")}`;
19
- const baseValues = baseKeys.map(k => toSqlValue(search[k]));
20
- const stmt = await slv._prepare(baseSql);
21
- sqlResult = await Promise.resolve(stmt.all(...baseValues));
37
+ const stmt = await slv._prepare(`SELECT * FROM ${qid(collection)}`);
38
+ sqlResult = await execStmt(stmt, "all");
39
+ }
40
+ sqlResult = sqlResult.map(parseRow);
41
+ if (where.postFilter) {
42
+ sqlResult = sqlResult.filter(where.postFilter);
22
43
  }
23
- const result = sqlResult
24
- .map(entry => findObj(config, entry))
25
- .filter(Boolean);
44
+ const result = sqlResult.map(entry => findObj(config, entry)).filter(Boolean);
26
45
  return findUtil(config, result, []);
27
46
  }
package/dist/index.d.ts CHANGED
@@ -2,14 +2,26 @@ import { ValtheraClass } from "@wxn0brp/db-core";
2
2
  import { ActionsBase } from "@wxn0brp/db-core/base/actions";
3
3
  import { Data } from "@wxn0brp/db-core/types/data";
4
4
  import { VQueryT } from "@wxn0brp/db-core/types/query";
5
- import { SupportedDB, VStatement } from "./types.js";
6
- export declare function toSqlValue(v: any): any;
5
+ import { AffinityMap, SupportedDB, VStatement } from "./types.js";
7
6
  export declare class SQLiteValthera extends ActionsBase {
8
7
  db: SupportedDB;
9
8
  primaryKey: Record<string, string>;
10
9
  _inited: boolean;
10
+ _stmtCache: Map<string, VStatement>;
11
+ _stmtCacheKeys: string[];
12
+ _pendingStmts: Map<string, Promise<VStatement>>;
13
+ _tableColumns: Map<string, Set<string>>;
14
+ _tableAffinities: Map<string, AffinityMap>;
11
15
  constructor(db: SupportedDB, primaryKey?: Record<string, string>);
16
+ _getTableInfo(collection: string): Promise<Record<string, string>>;
17
+ _invalidateTableCache(collection: string): Promise<void>;
18
+ _getTableColumns(collection: string): Promise<Set<string>>;
19
+ _getColumnAffinities(collection: string): Promise<AffinityMap>;
20
+ _ensureColumns(collection: string, keys: string[]): Promise<void>;
21
+ close(): Promise<void>;
12
22
  _prepare(sql: string): Promise<VStatement>;
23
+ private _prepareUncached;
24
+ _cacheStmt(sql: string, stmt: VStatement): void;
13
25
  getCollections(): Promise<string[]>;
14
26
  add(config: VQueryT.Add): Promise<Data>;
15
27
  find(config: VQueryT.Find): Promise<Data[]>;
package/dist/index.js CHANGED
@@ -1,56 +1,152 @@
1
1
  import { forgeTypedValthera, ValtheraClass } from "@wxn0brp/db-core";
2
2
  import { ActionsBase } from "@wxn0brp/db-core/base/actions";
3
3
  import { addId } from "@wxn0brp/db-core/helpers/addId";
4
+ import { MAX_STMT_CACHE } from "./const.js";
4
5
  import { find } from "./find.js";
5
6
  import { remove } from "./remove.js";
7
+ import { execStmt, computeAffinity, qid, toSqlValue } from "./utils.js";
6
8
  import { update } from "./update.js";
7
- export function toSqlValue(v) {
8
- if (typeof v === "boolean")
9
- return v ? 1 : 0;
10
- return v;
11
- }
12
9
  export class SQLiteValthera extends ActionsBase {
13
10
  db;
14
11
  primaryKey;
15
12
  _inited = true;
13
+ _stmtCache = new Map();
14
+ _stmtCacheKeys = [];
15
+ _pendingStmts = new Map();
16
+ _tableColumns = new Map();
17
+ _tableAffinities = new Map();
16
18
  constructor(db, primaryKey = {}) {
17
19
  super();
18
20
  this.db = db;
19
21
  this.primaryKey = primaryKey;
20
22
  }
23
+ async _getTableInfo(collection) {
24
+ const stmt = await this._prepare(`PRAGMA table_info(${qid(collection)})`);
25
+ const rows = await execStmt(stmt, "all");
26
+ const info = {};
27
+ for (const r of rows)
28
+ info[r.name] = r.type || "";
29
+ return info;
30
+ }
31
+ async _invalidateTableCache(collection) {
32
+ this._tableColumns.delete(collection);
33
+ this._tableAffinities.delete(collection);
34
+ }
35
+ async _getTableColumns(collection) {
36
+ const cached = this._tableColumns.get(collection);
37
+ if (cached)
38
+ return cached;
39
+ const info = await this._getTableInfo(collection);
40
+ const cols = new Set(Object.keys(info));
41
+ this._tableColumns.set(collection, cols);
42
+ return cols;
43
+ }
44
+ async _getColumnAffinities(collection) {
45
+ const cached = this._tableAffinities.get(collection);
46
+ if (cached)
47
+ return cached;
48
+ const info = await this._getTableInfo(collection);
49
+ const affinities = {};
50
+ for (const [name, type] of Object.entries(info))
51
+ affinities[name] = computeAffinity(type);
52
+ this._tableAffinities.set(collection, affinities);
53
+ return affinities;
54
+ }
55
+ async _ensureColumns(collection, keys) {
56
+ const existing = await this._getTableColumns(collection);
57
+ const missing = keys.filter(k => k !== "_id" && !existing.has(k));
58
+ if (missing.length === 0)
59
+ return;
60
+ for (const col of missing) {
61
+ const stmt = await this._prepare(`ALTER TABLE ${qid(collection)} ADD COLUMN ${qid(col)}`);
62
+ await execStmt(stmt, "run");
63
+ }
64
+ await this._invalidateTableCache(collection);
65
+ }
66
+ async close() {
67
+ this._stmtCache.clear();
68
+ this._stmtCacheKeys = [];
69
+ this._pendingStmts.clear();
70
+ this._tableColumns.clear();
71
+ this._tableAffinities.clear();
72
+ const close = this.db.close;
73
+ if (typeof close === "function")
74
+ await Promise.resolve(close.call(this.db));
75
+ }
21
76
  async _prepare(sql) {
77
+ const cached = this._stmtCache.get(sql);
78
+ if (cached)
79
+ return cached;
80
+ const pending = this._pendingStmts.get(sql);
81
+ if (pending)
82
+ return pending;
83
+ const promise = this._prepareUncached(sql).finally(() => {
84
+ this._pendingStmts.delete(sql);
85
+ });
86
+ this._pendingStmts.set(sql, promise);
87
+ return promise;
88
+ }
89
+ async _prepareUncached(sql) {
22
90
  const db = this.db;
23
- if (typeof db.prepare !== "undefined")
24
- return await db.prepare(sql);
25
- if (typeof db.prepareSync !== "undefined")
26
- return await db.prepareSync(sql);
27
- if (typeof db.query === "function") {
91
+ let stmt;
92
+ if (typeof db.prepare !== "undefined") {
93
+ stmt = await db.prepare(sql);
94
+ }
95
+ else if (typeof db.prepareSync !== "undefined") {
96
+ stmt = await db.prepareSync(sql);
97
+ }
98
+ else if (typeof db.query === "function") {
28
99
  const q = await db.query(sql);
29
- if (q && (q.all || q.get || q.run))
30
- return q;
100
+ if (q && (q.all || q.get || q.run)) {
101
+ stmt = q;
102
+ }
103
+ else {
104
+ throw new Error("Unsupported database");
105
+ }
106
+ }
107
+ else {
108
+ throw new Error("Unsupported database");
109
+ }
110
+ this._cacheStmt(sql, stmt);
111
+ return stmt;
112
+ }
113
+ _cacheStmt(sql, stmt) {
114
+ if (this._stmtCache.size >= MAX_STMT_CACHE) {
115
+ const oldest = this._stmtCacheKeys.shift();
116
+ if (oldest)
117
+ this._stmtCache.delete(oldest);
31
118
  }
32
- throw new Error("Unsupported database");
119
+ this._stmtCache.set(sql, stmt);
120
+ this._stmtCacheKeys.push(sql);
33
121
  }
34
122
  async getCollections() {
35
- const tables = (await this._prepare("SELECT name FROM sqlite_master WHERE type='table'")).all();
123
+ const stmt = await this._prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'");
124
+ const tables = await execStmt(stmt, "all");
36
125
  return tables.map((t) => t.name);
37
126
  }
38
127
  async add(config) {
39
128
  const { data, collection } = config;
40
129
  await addId(config, this, true);
41
- const keys = Object.keys(data);
130
+ const entries = Object.entries(data).filter(([, v]) => v !== undefined);
131
+ if (entries.length === 0)
132
+ throw new Error(`Cannot insert an empty document into "${collection}"`);
133
+ await this._ensureColumns(collection, entries.map(([k]) => k));
134
+ const affinities = await this._getColumnAffinities(collection);
135
+ const keys = entries.map(([k]) => k);
42
136
  const placeholders = keys.map(() => "?").join(", ");
43
- const values = keys.map(k => toSqlValue(data[k]));
44
- const sql = `INSERT INTO ${collection} (${keys.join(", ")}) VALUES (${placeholders})`;
137
+ const values = entries.map(([k, v]) => toSqlValue(v, affinities[k]));
138
+ const sql = `INSERT INTO ${qid(collection)} (${keys.map(k => qid(k)).join(", ")}) VALUES (${placeholders})`;
45
139
  const stmt = await this._prepare(sql);
46
- await Promise.resolve(stmt.run(...values));
140
+ await execStmt(stmt, "run", ...values);
47
141
  return data;
48
142
  }
49
143
  find(config) {
50
144
  return find(this, config);
51
145
  }
52
146
  async findOne(config) {
53
- config.dbFindOpts = { limit: 1 };
147
+ config.dbFindOpts = {
148
+ limit: 1,
149
+ };
54
150
  const result = await this.find(config);
55
151
  return result.length ? result[0] : null;
56
152
  }
@@ -69,15 +165,15 @@ export class SQLiteValthera extends ActionsBase {
69
165
  return res[0] || null;
70
166
  }
71
167
  async removeCollection(collection) {
72
- const sql = `DROP TABLE IF EXISTS ${collection}`;
168
+ const sql = `DROP TABLE IF EXISTS ${qid(collection)}`;
73
169
  const stmt = await this._prepare(sql);
74
- await Promise.resolve(stmt.run());
170
+ await execStmt(stmt, "run");
171
+ await this._invalidateTableCache(collection);
75
172
  return true;
76
173
  }
77
174
  async issetCollection(collection) {
78
- const sql = `SELECT name FROM sqlite_master WHERE type='table' AND name=?`;
79
- const stmt = await this._prepare(sql);
80
- const result = await Promise.resolve(stmt.all(collection));
175
+ const stmt = await this._prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?");
176
+ const result = await execStmt(stmt, "all", collection);
81
177
  return result.length > 0;
82
178
  }
83
179
  async ensureCollection(collection) {
@@ -89,8 +185,10 @@ export class SQLiteValthera extends ActionsBase {
89
185
  }
90
186
  }
91
187
  export function createSQLiteValthera(sqlDB) {
92
- const dbAction = new SQLiteValthera(sqlDB);
93
- const db = new ValtheraClass({ dbAction });
188
+ const adapter = new SQLiteValthera(sqlDB);
189
+ const db = new ValtheraClass({
190
+ adapter,
191
+ });
94
192
  return forgeTypedValthera(db);
95
193
  }
96
194
  export const DYNAMIC = {
@@ -116,5 +214,5 @@ export const DYNAMIC = {
116
214
  if (!opts)
117
215
  opts = {};
118
216
  return new SQLiteValthera(new def(file, opts), keys);
119
- }
217
+ },
120
218
  };
package/dist/remove.js CHANGED
@@ -1,20 +1,21 @@
1
+ import { BATCH_SIZE } from "./const.js";
1
2
  import { find } from "./find.js";
2
- const BATCH_SIZE = +process.env.VALTHERA_SQLITE_BATCH_SIZE || 500;
3
+ import { execStmt, qid } from "./utils.js";
3
4
  export async function remove(slv, query, one) {
4
5
  const { collection } = query;
5
6
  const toDelete = await find(slv, {
6
7
  ...query,
7
8
  dbFindOpts: {
8
- limit: one ? 1 : undefined
9
- }
9
+ limit: one ? 1 : undefined,
10
+ },
10
11
  });
11
12
  if (!toDelete.length)
12
13
  return [];
13
14
  const key = slv.primaryKey[collection] || "_id";
14
15
  for (let i = 0; i < toDelete.length; i += BATCH_SIZE) {
15
16
  const batch = toDelete.slice(i, i + BATCH_SIZE);
16
- const stmt = await slv._prepare(`DELETE FROM "${collection}" WHERE "${key}" IN (${batch.map(() => "?").join(", ")})`);
17
- await Promise.resolve(stmt.run(...batch.map(d => d[key])));
17
+ const stmt = await slv._prepare(`DELETE FROM ${qid(collection)} WHERE ${qid(key)} IN (${batch.map(() => "?").join(", ")})`);
18
+ await execStmt(stmt, "run", ...batch.map(d => d[key]));
18
19
  }
19
20
  return toDelete;
20
21
  }
package/dist/types.d.ts CHANGED
@@ -5,3 +5,5 @@ import type NodeSqlite from "node:sqlite";
5
5
  import type { DatabaseSync as NodeSqliteDB } from "node:sqlite";
6
6
  export type SupportedDB = BetterSqliteDB | NodeSqliteDB | BunSqliteDB;
7
7
  export type VStatement = BetterSqlite3.Statement | NodeSqlite.StatementSync;
8
+ export type Affinity = "INTEGER" | "TEXT" | "BLOB" | "REAL" | "NUMERIC" | "NONE";
9
+ export type AffinityMap = Record<string, Affinity>;
package/dist/update.js CHANGED
@@ -1,30 +1,190 @@
1
1
  import { updateObj } from "@wxn0brp/db-core/utils/process";
2
- import { toSqlValue } from "./index.js";
2
+ import { buildWhere } from "./buildWhere.js";
3
+ import { COMPLEX_OPS } from "./const.js";
3
4
  import { find } from "./find.js";
5
+ import { execStmt, qid, toSqlValue } from "./utils.js";
4
6
  export async function update(slv, query, one) {
5
- const { collection } = query;
7
+ const { collection, updater } = query;
6
8
  const matched = await find(slv, {
7
9
  ...query,
8
10
  dbFindOpts: {
9
- limit: one ? 1 : undefined
10
- }
11
+ limit: one ? 1 : undefined,
12
+ },
11
13
  });
12
14
  if (matched.length === 0)
13
15
  return [];
14
16
  const key = slv.primaryKey[collection] || "_id";
15
- const updateOne = async (target) => {
16
- const newData = updateObj(query, target);
17
- if (newData[key] !== target[key])
18
- newData[key] = target[key];
19
- const keys = Object.keys(newData).filter(k => k !== key);
20
- const values = keys.map(k => toSqlValue(newData[k]));
21
- const sql = `UPDATE "${collection}" SET ${keys.map(k => `"${k}" = ?`).join(", ")} WHERE "${key}" = ?`;
17
+ if (typeof updater === "function") {
18
+ const results = [];
19
+ for (const target of matched)
20
+ results.push(await updateOne(slv, query, target, key));
21
+ return results;
22
+ }
23
+ const affinities = await slv._getColumnAffinities(collection);
24
+ const where = buildWhere(query.search, affinities);
25
+ if (where.sql && !where.postFilter) {
26
+ const incFields = collectIncDecFields(updater);
27
+ const incCompatible = incFields.length === 0 ||
28
+ matched.every(row => incFields.every(field => {
29
+ const v = row[field];
30
+ return v === undefined || v === null || typeof v === "number";
31
+ }));
32
+ const simpleUpdateSql = incCompatible
33
+ ? tryBuildSimpleUpdate(updater, key, affinities)
34
+ : null;
35
+ if (simpleUpdateSql) {
36
+ await slv._ensureColumns(collection, simpleUpdateSql.columns);
37
+ const sql = `UPDATE ${qid(collection)} SET ${simpleUpdateSql.set} WHERE ${where.sql}`;
38
+ const values = [
39
+ ...simpleUpdateSql.values,
40
+ ...where.values,
41
+ ];
42
+ const stmt = await slv._prepare(sql);
43
+ await execStmt(stmt, "run", ...values);
44
+ return matched.map(row => {
45
+ const pk = row[key];
46
+ const newData = updateObj(query, row);
47
+ if (newData[key] !== pk)
48
+ newData[key] = pk;
49
+ return newData;
50
+ });
51
+ }
52
+ }
53
+ const stmtCache = new Map();
54
+ const getStmt = async (sql) => {
55
+ const cached = stmtCache.get(sql);
56
+ if (cached)
57
+ return cached;
22
58
  const stmt = await slv._prepare(sql);
23
- await Promise.resolve(stmt.run(...values, target[key]));
24
- return newData;
59
+ stmtCache.set(sql, stmt);
60
+ return stmt;
25
61
  };
26
62
  const results = [];
27
63
  for (const target of matched)
28
- results.push(await updateOne(target));
64
+ results.push(await updateOne(slv, query, target, key, getStmt));
29
65
  return results;
30
66
  }
67
+ async function updateOne(slv, query, target, key, getStmt) {
68
+ const beforeKeys = Object.keys(target);
69
+ const pkValue = target[key];
70
+ const newData = updateObj(query, target);
71
+ if (newData[key] !== pkValue)
72
+ newData[key] = pkValue;
73
+ const affinities = await slv._getColumnAffinities(query.collection);
74
+ const setClauses = [];
75
+ const values = [];
76
+ const columns = [];
77
+ for (const k of Object.keys(newData)) {
78
+ if (k === key)
79
+ continue;
80
+ columns.push(k);
81
+ if (newData[k] === undefined) {
82
+ setClauses.push(`${qid(k)} = NULL`);
83
+ continue;
84
+ }
85
+ setClauses.push(`${qid(k)} = ?`);
86
+ values.push(toSqlValue(newData[k], affinities[k]));
87
+ }
88
+ for (const k of beforeKeys) {
89
+ if (k === key || k in newData)
90
+ continue;
91
+ columns.push(k);
92
+ setClauses.push(`${qid(k)} = NULL`);
93
+ }
94
+ await slv._ensureColumns(query.collection, columns);
95
+ if (setClauses.length === 0)
96
+ return newData;
97
+ const sql = `UPDATE ${qid(query.collection)} SET ${setClauses.join(", ")} WHERE ${qid(key)} = ?`;
98
+ const stmt = getStmt ? await getStmt(sql) : await slv._prepare(sql);
99
+ await execStmt(stmt, "run", ...values, pkValue);
100
+ return newData;
101
+ }
102
+ function tryBuildSimpleUpdate(updater, key, affinities) {
103
+ const $fields = {};
104
+ const flatFields = {};
105
+ for (const k of Object.keys(updater)) {
106
+ if (k.startsWith("$")) {
107
+ $fields[k.toLowerCase()] = updater[k];
108
+ }
109
+ else {
110
+ flatFields[k] = updater[k];
111
+ }
112
+ }
113
+ const hasComplex = Object.keys($fields).some(k => COMPLEX_OPS.includes("$" + k));
114
+ if (hasComplex)
115
+ return null;
116
+ const setClauses = [];
117
+ const values = [];
118
+ const columns = [];
119
+ if ("$set" in $fields) {
120
+ for (const [field, value] of Object.entries($fields["$set"])) {
121
+ if (field === key)
122
+ continue;
123
+ columns.push(field);
124
+ if (value === undefined) {
125
+ setClauses.push(`${qid(field)} = NULL`);
126
+ continue;
127
+ }
128
+ setClauses.push(`${qid(field)} = ?`);
129
+ values.push(toSqlValue(value, affinities[field]));
130
+ }
131
+ }
132
+ if ("$inc" in $fields) {
133
+ for (const [field, value] of Object.entries($fields["$inc"])) {
134
+ if (field === key)
135
+ continue;
136
+ if (typeof value !== "number")
137
+ return null;
138
+ columns.push(field);
139
+ setClauses.push(incDecClause(field, "+"));
140
+ values.push(value, value);
141
+ }
142
+ }
143
+ if ("$dec" in $fields) {
144
+ for (const [field, value] of Object.entries($fields["$dec"])) {
145
+ if (field === key)
146
+ continue;
147
+ if (typeof value !== "number")
148
+ return null;
149
+ columns.push(field);
150
+ setClauses.push(incDecClause(field, "-"));
151
+ values.push(value, value);
152
+ }
153
+ }
154
+ for (const [field, value] of Object.entries(flatFields)) {
155
+ if (field === key)
156
+ continue;
157
+ if (typeof value === "object" && value !== null)
158
+ return null;
159
+ columns.push(field);
160
+ if (value === undefined) {
161
+ setClauses.push(`${qid(field)} = NULL`);
162
+ continue;
163
+ }
164
+ setClauses.push(`${qid(field)} = ?`);
165
+ values.push(toSqlValue(value, affinities[field]));
166
+ }
167
+ if (setClauses.length === 0)
168
+ return null;
169
+ return {
170
+ set: setClauses.join(", "),
171
+ values,
172
+ columns,
173
+ };
174
+ }
175
+ function incDecClause(field, op) {
176
+ const f = qid(field);
177
+ return `${f} = CASE WHEN ${f} IS NULL THEN ? WHEN typeof(${f}) IN ('integer','real') THEN ${f} ${op} ? ELSE ${f} END`;
178
+ }
179
+ function collectIncDecFields(updater) {
180
+ const fields = [];
181
+ for (const op of [
182
+ "$inc",
183
+ "$dec",
184
+ ]) {
185
+ const map = updater[op];
186
+ if (typeof map === "object" && map !== null)
187
+ fields.push(...Object.keys(map));
188
+ }
189
+ return fields;
190
+ }
@@ -0,0 +1,7 @@
1
+ import { Affinity, VStatement } from "./types.js";
2
+ export declare function qid(identifier: string): string;
3
+ export declare function computeAffinity(type: string): Affinity;
4
+ export declare function toSqlValue(v: any, affinity?: Affinity): any;
5
+ export declare function decodeSqlValue(value: any): any;
6
+ export declare function globEscape(value: string): string;
7
+ export declare function execStmt(stmt: VStatement, method: "all" | "run" | "get", ...args: any[]): Promise<any>;
package/dist/utils.js ADDED
@@ -0,0 +1,78 @@
1
+ const textEncoder = new TextEncoder();
2
+ const textDecoder = new TextDecoder();
3
+ export function qid(identifier) {
4
+ return `"${identifier.replace(/"/g, '""')}"`;
5
+ }
6
+ export function computeAffinity(type) {
7
+ const t = (type || "").toUpperCase();
8
+ if (!t)
9
+ return "NONE";
10
+ if (t.includes("INT"))
11
+ return "INTEGER";
12
+ if (t.includes("CHAR") || t.includes("CLOB") || t.includes("TEXT"))
13
+ return "TEXT";
14
+ if (t.includes("BLOB"))
15
+ return "NONE";
16
+ if (t.includes("REAL") || t.includes("FLOA") || t.includes("DOUB"))
17
+ return "REAL";
18
+ return "NUMERIC";
19
+ }
20
+ const BLOB_PREFIX = "VJ:";
21
+ const BLOB_PREFIX_BYTES = textEncoder.encode(BLOB_PREFIX);
22
+ function encodeJsonBlob(v) {
23
+ const json = textEncoder.encode(JSON.stringify(v));
24
+ const out = new Uint8Array(BLOB_PREFIX_BYTES.length + json.length);
25
+ out.set(BLOB_PREFIX_BYTES);
26
+ out.set(json, BLOB_PREFIX_BYTES.length);
27
+ return out;
28
+ }
29
+ function isNumericLike(s) {
30
+ return s.trim() !== "" && !Number.isNaN(Number(s));
31
+ }
32
+ export function toSqlValue(v, affinity) {
33
+ if (typeof v === "boolean")
34
+ return encodeJsonBlob(v);
35
+ if (v === null)
36
+ return encodeJsonBlob(null);
37
+ if (typeof v === "string") {
38
+ if ((affinity === "INTEGER" ||
39
+ affinity === "REAL" ||
40
+ affinity === "NUMERIC") &&
41
+ isNumericLike(v)) {
42
+ return encodeJsonBlob(v);
43
+ }
44
+ return v;
45
+ }
46
+ if (typeof v === "object" && v !== null)
47
+ return JSON.stringify(v);
48
+ return v;
49
+ }
50
+ export function decodeSqlValue(value) {
51
+ if (value instanceof Uint8Array) {
52
+ if (startsWithBytes(value, BLOB_PREFIX_BYTES)) {
53
+ const text = textDecoder.decode(value.subarray(BLOB_PREFIX_BYTES.length));
54
+ try {
55
+ return JSON.parse(text);
56
+ }
57
+ catch { }
58
+ }
59
+ return value;
60
+ }
61
+ return value;
62
+ }
63
+ function startsWithBytes(value, prefix) {
64
+ if (value.length < prefix.length)
65
+ return false;
66
+ for (let i = 0; i < prefix.length; i++)
67
+ if (value[i] !== prefix[i])
68
+ return false;
69
+ return true;
70
+ }
71
+ const GLOB_META = /[*?[\]]/g;
72
+ export function globEscape(value) {
73
+ return value.replace(GLOB_META, c => `[${c}]`);
74
+ }
75
+ export async function execStmt(stmt, method, ...args) {
76
+ const result = stmt[method](...args);
77
+ return result instanceof Promise ? await result : result;
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wxn0brp/db-storage-sqlite",
3
- "version": "0.110.4",
3
+ "version": "0.120.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "description": "A pure SQLite storage adapter for the ValtheraDB database library",
@@ -19,17 +19,18 @@
19
19
  "sqlite"
20
20
  ],
21
21
  "devDependencies": {
22
- "@types/better-sqlite3": "^7.6.13",
22
+ "@types/better-sqlite3": "^9.6.0",
23
23
  "@types/bun": "*",
24
24
  "@types/node": "*",
25
- "@wxn0brp/db-core": "^0.11.0",
26
- "better-sqlite3": "^12.10.0",
25
+ "@wxn0brp/biome": "*",
26
+ "@wxn0brp/db-core": "^0.12.0",
27
+ "better-sqlite3": "^13.0.3",
27
28
  "tsc-alias": "^1",
28
- "typescript": "^6"
29
+ "typescript": "^7"
29
30
  },
30
31
  "peerDependencies": {
31
- "@wxn0brp/db-core": "^0.11.0",
32
- "better-sqlite3": "^12.10.0"
32
+ "@wxn0brp/db-core": "^0.12.0",
33
+ "better-sqlite3": "^13.0.3"
33
34
  },
34
35
  "peerDependenciesMeta": {
35
36
  "@wxn0brp/db-core": {