@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 +3 -0
- package/dist/buildWhere.d.ts +7 -0
- package/dist/buildWhere.js +240 -0
- package/dist/const.d.ts +12 -0
- package/dist/const.js +155 -0
- package/dist/find.js +36 -17
- package/dist/index.d.ts +14 -2
- package/dist/index.js +125 -27
- package/dist/remove.js +6 -5
- package/dist/types.d.ts +2 -0
- package/dist/update.js +174 -14
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +78 -0
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -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
|
+
}
|
package/dist/const.d.ts
ADDED
|
@@ -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 {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (typeof db.
|
|
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
|
-
|
|
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
|
-
|
|
119
|
+
this._stmtCache.set(sql, stmt);
|
|
120
|
+
this._stmtCacheKeys.push(sql);
|
|
33
121
|
}
|
|
34
122
|
async getCollections() {
|
|
35
|
-
const
|
|
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
|
|
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 =
|
|
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
|
|
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 = {
|
|
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
|
|
170
|
+
await execStmt(stmt, "run");
|
|
171
|
+
await this._invalidateTableCache(collection);
|
|
75
172
|
return true;
|
|
76
173
|
}
|
|
77
174
|
async issetCollection(collection) {
|
|
78
|
-
const
|
|
79
|
-
const
|
|
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
|
|
93
|
-
const db = new ValtheraClass({
|
|
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
|
-
|
|
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
|
|
17
|
-
await
|
|
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 {
|
|
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
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
return
|
|
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
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -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.
|
|
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": "^
|
|
22
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
23
23
|
"@types/bun": "*",
|
|
24
24
|
"@types/node": "*",
|
|
25
|
-
"@wxn0brp/
|
|
26
|
-
"
|
|
25
|
+
"@wxn0brp/biome": "*",
|
|
26
|
+
"@wxn0brp/db-core": "^0.12.0",
|
|
27
|
+
"better-sqlite3": "^13.0.3",
|
|
27
28
|
"tsc-alias": "^1",
|
|
28
|
-
"typescript": "^
|
|
29
|
+
"typescript": "^7"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
31
|
-
"@wxn0brp/db-core": "^0.
|
|
32
|
-
"better-sqlite3": "^
|
|
32
|
+
"@wxn0brp/db-core": "^0.12.0",
|
|
33
|
+
"better-sqlite3": "^13.0.3"
|
|
33
34
|
},
|
|
34
35
|
"peerDependenciesMeta": {
|
|
35
36
|
"@wxn0brp/db-core": {
|