@peerbit/indexer-sqlite3 1.1.4 → 1.2.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/dist/peerbit/sqlite3-bundler-friendly.mjs +7 -7
- package/dist/peerbit/sqlite3-node.mjs +7 -7
- package/dist/peerbit/sqlite3.js +7 -7
- package/dist/peerbit/sqlite3.min.js +688 -168
- package/dist/peerbit/sqlite3.mjs +7 -7
- package/dist/peerbit/sqlite3.wasm +0 -0
- package/dist/peerbit/sqlite3.worker.min.js +19 -5
- package/dist/src/engine.d.ts +4 -1
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +125 -48
- package/dist/src/engine.js.map +1 -1
- package/dist/src/query-planner.d.ts +47 -0
- package/dist/src/query-planner.d.ts.map +1 -0
- package/dist/src/query-planner.js +290 -0
- package/dist/src/query-planner.js.map +1 -0
- package/dist/src/schema.d.ts +31 -7
- package/dist/src/schema.d.ts.map +1 -1
- package/dist/src/schema.js +370 -123
- package/dist/src/schema.js.map +1 -1
- package/dist/src/sqlite3-messages.worker.d.ts +4 -1
- package/dist/src/sqlite3-messages.worker.d.ts.map +1 -1
- package/dist/src/sqlite3-messages.worker.js.map +1 -1
- package/dist/src/sqlite3.browser.d.ts.map +1 -1
- package/dist/src/sqlite3.browser.js +7 -0
- package/dist/src/sqlite3.browser.js.map +1 -1
- package/dist/src/sqlite3.d.ts.map +1 -1
- package/dist/src/sqlite3.js +24 -14
- package/dist/src/sqlite3.js.map +1 -1
- package/dist/src/sqlite3.wasm.d.ts +1 -0
- package/dist/src/sqlite3.wasm.d.ts.map +1 -1
- package/dist/src/sqlite3.wasm.js +9 -1
- package/dist/src/sqlite3.wasm.js.map +1 -1
- package/dist/src/sqlite3.worker.js +7 -0
- package/dist/src/sqlite3.worker.js.map +1 -1
- package/dist/src/types.d.ts +1 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/engine.ts +143 -68
- package/src/query-planner.ts +334 -0
- package/src/schema.ts +519 -164
- package/src/sqlite3-messages.worker.ts +5 -0
- package/src/sqlite3.browser.ts +8 -0
- package/src/sqlite3.ts +24 -13
- package/src/sqlite3.wasm.ts +11 -1
- package/src/sqlite3.worker.ts +6 -1
- package/src/types.ts +1 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// track timing for optimal index selection
|
|
2
|
+
import { field, serialize, vec } from "@dao-xyz/borsh";
|
|
3
|
+
import { sha256Base64Sync } from "@peerbit/crypto";
|
|
4
|
+
import {
|
|
5
|
+
And,
|
|
6
|
+
BigUnsignedIntegerValue,
|
|
7
|
+
BoolQuery,
|
|
8
|
+
ByteMatchQuery,
|
|
9
|
+
Compare,
|
|
10
|
+
IntegerCompare,
|
|
11
|
+
IntegerValue,
|
|
12
|
+
IsNull,
|
|
13
|
+
Nested,
|
|
14
|
+
Not,
|
|
15
|
+
Or,
|
|
16
|
+
Query,
|
|
17
|
+
Sort,
|
|
18
|
+
StringMatch,
|
|
19
|
+
UnsignedIntegerValue,
|
|
20
|
+
} from "@peerbit/indexer-interface";
|
|
21
|
+
import { hrtime } from "@peerbit/time";
|
|
22
|
+
import { escapeColumnName } from "./schema.js";
|
|
23
|
+
|
|
24
|
+
export interface QueryIndexPlanner {
|
|
25
|
+
// assumes withing a query, each index can be picked independently. For example if we are to join two tables, we can pick the best index for each table
|
|
26
|
+
// sorted column names key to execution time for each index that was tried
|
|
27
|
+
columnsToIndexes: Map<
|
|
28
|
+
string,
|
|
29
|
+
{
|
|
30
|
+
results: {
|
|
31
|
+
used: number;
|
|
32
|
+
avg: number;
|
|
33
|
+
times: number[];
|
|
34
|
+
indexKey: string;
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
>; //
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type StmtStats = Map<string, QueryIndexPlanner>;
|
|
41
|
+
|
|
42
|
+
const getSortedNameKey = (tableName: string, names: string[]) =>
|
|
43
|
+
[tableName, ...names.sort()].join(",");
|
|
44
|
+
const createIndexKey = (tableName: string, fields: string[]) =>
|
|
45
|
+
`${tableName}_index_${fields.map((x) => x).join("_")}`;
|
|
46
|
+
|
|
47
|
+
const HALF_MAX_U32 = 2147483647; // rounded down
|
|
48
|
+
const HALF_MAX_U64 = 9223372036854775807n; // rounded down
|
|
49
|
+
|
|
50
|
+
export const flattenQuery = function* (props?: {
|
|
51
|
+
query: Query[];
|
|
52
|
+
sort?: Sort[] | Sort;
|
|
53
|
+
}): Generator<{ query: Query[]; sort?: Sort[] | Sort } | undefined> {
|
|
54
|
+
if (!props) {
|
|
55
|
+
return yield props;
|
|
56
|
+
}
|
|
57
|
+
// if query contains OR statements, split query into multiple queries so we can run each query with union and then sort
|
|
58
|
+
|
|
59
|
+
// TODO this only works atm for one OR statement in the query
|
|
60
|
+
let ors: Query[] = [];
|
|
61
|
+
let ands: Query[] = [];
|
|
62
|
+
let stack = [...props.query];
|
|
63
|
+
let foundOr = false;
|
|
64
|
+
for (const q of stack) {
|
|
65
|
+
if (q instanceof Or) {
|
|
66
|
+
if (foundOr) {
|
|
67
|
+
// multiple ORs are not supported
|
|
68
|
+
yield props;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ors = q.or;
|
|
73
|
+
foundOr = true;
|
|
74
|
+
} else if (q instanceof And) {
|
|
75
|
+
for (const a of q.and) {
|
|
76
|
+
stack.push(a);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
ands.push(q);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let maxFlatten = 4; // max 4 ORs else the query will be too big
|
|
84
|
+
if (ors.length === 0 || ors.length >= maxFlatten) {
|
|
85
|
+
yield {
|
|
86
|
+
query: ands,
|
|
87
|
+
sort: props.sort,
|
|
88
|
+
};
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const or of ors) {
|
|
92
|
+
yield {
|
|
93
|
+
query: [...ands, ...(Array.isArray(or) ? or : [or])],
|
|
94
|
+
sort: props.sort,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const reduceResolution = (value: IntegerValue): IntegerValue => {
|
|
100
|
+
if (value instanceof UnsignedIntegerValue) {
|
|
101
|
+
return value.number > HALF_MAX_U32
|
|
102
|
+
? new UnsignedIntegerValue(HALF_MAX_U32)
|
|
103
|
+
: new UnsignedIntegerValue(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (value instanceof BigUnsignedIntegerValue) {
|
|
107
|
+
return value.value > HALF_MAX_U64
|
|
108
|
+
? new BigUnsignedIntegerValue(HALF_MAX_U64)
|
|
109
|
+
: new BigUnsignedIntegerValue(0n);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw new Error("Unknown integer value type: " + value?.constructor.name);
|
|
113
|
+
};
|
|
114
|
+
const nullifyQuery = (query: Query): Query => {
|
|
115
|
+
if (query instanceof IntegerCompare) {
|
|
116
|
+
return new IntegerCompare({
|
|
117
|
+
compare: Compare.Equal,
|
|
118
|
+
value: reduceResolution(query.value),
|
|
119
|
+
key: query.key,
|
|
120
|
+
});
|
|
121
|
+
} else if (query instanceof StringMatch) {
|
|
122
|
+
return new StringMatch({
|
|
123
|
+
key: query.key,
|
|
124
|
+
value: "",
|
|
125
|
+
method: query.method,
|
|
126
|
+
});
|
|
127
|
+
} else if (query instanceof ByteMatchQuery) {
|
|
128
|
+
return new ByteMatchQuery({
|
|
129
|
+
key: query.key,
|
|
130
|
+
value: new Uint8Array(),
|
|
131
|
+
});
|
|
132
|
+
} else if (query instanceof BoolQuery) {
|
|
133
|
+
return new BoolQuery({
|
|
134
|
+
key: query.key,
|
|
135
|
+
value: false,
|
|
136
|
+
});
|
|
137
|
+
} else if (query instanceof And) {
|
|
138
|
+
let and: Query[] = [];
|
|
139
|
+
for (const condition of query.and) {
|
|
140
|
+
and.push(nullifyQuery(condition));
|
|
141
|
+
}
|
|
142
|
+
return new And(and);
|
|
143
|
+
} else if (query instanceof Or) {
|
|
144
|
+
let or: Query[] = [];
|
|
145
|
+
for (const condition of query.or) {
|
|
146
|
+
or.push(nullifyQuery(condition));
|
|
147
|
+
}
|
|
148
|
+
return new Or(or);
|
|
149
|
+
} else if (query instanceof Not) {
|
|
150
|
+
return new Not(nullifyQuery(query.not));
|
|
151
|
+
} else if (query instanceof IsNull) {
|
|
152
|
+
return query;
|
|
153
|
+
} else if (query instanceof Nested) {
|
|
154
|
+
// TODO remove
|
|
155
|
+
throw new Error("Unsupported query type, deprecated");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
throw new Error("Unknown query type: " + query?.constructor.name);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export class PlannableQuery {
|
|
162
|
+
@field({ type: vec(Query) })
|
|
163
|
+
query: Query[];
|
|
164
|
+
|
|
165
|
+
@field({ type: vec(Sort) })
|
|
166
|
+
sort: Sort[];
|
|
167
|
+
|
|
168
|
+
constructor(props: { query: Query[]; sort?: Sort[] | Sort }) {
|
|
169
|
+
this.query = props.query;
|
|
170
|
+
this.sort = Array.isArray(props.sort)
|
|
171
|
+
? props.sort
|
|
172
|
+
: props.sort
|
|
173
|
+
? [props.sort]
|
|
174
|
+
: [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get key(): string {
|
|
178
|
+
let query = this.query.map((x) => nullifyQuery(x));
|
|
179
|
+
let nullifiedPlannableQuery = new PlannableQuery({
|
|
180
|
+
query: query,
|
|
181
|
+
sort: this.sort,
|
|
182
|
+
});
|
|
183
|
+
return sha256Base64Sync(serialize(nullifiedPlannableQuery));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
export type PlanningSession = ReturnType<QueryPlanner["scope"]>;
|
|
187
|
+
|
|
188
|
+
export class QueryPlanner {
|
|
189
|
+
stats: StmtStats = new Map();
|
|
190
|
+
|
|
191
|
+
pendingIndexCreation: Map<string, Promise<void>> = new Map();
|
|
192
|
+
|
|
193
|
+
constructor(
|
|
194
|
+
readonly props: { exec: (query: string) => Promise<any> | any },
|
|
195
|
+
) {}
|
|
196
|
+
|
|
197
|
+
async stop() {
|
|
198
|
+
for (const promise of this.pendingIndexCreation.values()) {
|
|
199
|
+
await promise.catch(() => {});
|
|
200
|
+
}
|
|
201
|
+
this.stats.clear();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
scope(query: PlannableQuery) {
|
|
205
|
+
let obj = this.stats.get(query.key);
|
|
206
|
+
if (obj === undefined) {
|
|
207
|
+
obj = {
|
|
208
|
+
columnsToIndexes: new Map(),
|
|
209
|
+
};
|
|
210
|
+
this.stats.set(query.key, obj);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// returns a function that takes column names and return the index to use
|
|
214
|
+
let indexCreateCommands: { key: string; cmd: string }[] | undefined =
|
|
215
|
+
undefined;
|
|
216
|
+
let pickedIndexKeys: Map<string, string> = new Map(); // index key to column names key
|
|
217
|
+
return {
|
|
218
|
+
beforePrepare: async () => {
|
|
219
|
+
// create missing indices
|
|
220
|
+
if (indexCreateCommands != null) {
|
|
221
|
+
for (const { key, cmd } of indexCreateCommands) {
|
|
222
|
+
if (this.pendingIndexCreation.has(key)) {
|
|
223
|
+
await this.pendingIndexCreation.get(key);
|
|
224
|
+
}
|
|
225
|
+
const promise = this.props.exec(cmd);
|
|
226
|
+
this.pendingIndexCreation.set(key, promise);
|
|
227
|
+
await promise;
|
|
228
|
+
this.pendingIndexCreation.delete(key);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (this.pendingIndexCreation.size > 0) {
|
|
233
|
+
for (const picked of pickedIndexKeys.keys()) {
|
|
234
|
+
await this.pendingIndexCreation.get(picked);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
resolveIndex: (tableName: string, columns: string[]): string => {
|
|
239
|
+
// first we figure out whether we want to reuse the fastest index or try a new one
|
|
240
|
+
// only assume we either do forward or backward column order for now (not all n! permutations)
|
|
241
|
+
const sortedNameKey = getSortedNameKey(tableName, columns);
|
|
242
|
+
let indexStats = obj.columnsToIndexes.get(sortedNameKey);
|
|
243
|
+
if (indexStats === undefined) {
|
|
244
|
+
indexStats = {
|
|
245
|
+
results: [],
|
|
246
|
+
};
|
|
247
|
+
obj.columnsToIndexes.set(sortedNameKey, indexStats);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (indexStats.results.length === 0) {
|
|
251
|
+
// create both forward and backward permutations
|
|
252
|
+
const permutations = generatePermutations(columns);
|
|
253
|
+
for (const columns of permutations) {
|
|
254
|
+
const indexKey = createIndexKey(tableName, columns);
|
|
255
|
+
const command = `create index if not exists ${indexKey} on ${tableName} (${columns.map((n) => escapeColumnName(n)).join(", ")})`;
|
|
256
|
+
|
|
257
|
+
(indexCreateCommands || (indexCreateCommands = [])).push({
|
|
258
|
+
cmd: command,
|
|
259
|
+
key: indexKey,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
indexStats.results.push({
|
|
263
|
+
used: 0,
|
|
264
|
+
times: [],
|
|
265
|
+
avg: -1, // setting -1 will force the first time to be the fastest (i.e. new indices are always tested once)
|
|
266
|
+
indexKey,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// find the fastest index
|
|
272
|
+
let fastestIndex = indexStats.results[0];
|
|
273
|
+
fastestIndex.used++;
|
|
274
|
+
pickedIndexKeys.set(fastestIndex.indexKey, sortedNameKey);
|
|
275
|
+
|
|
276
|
+
return fastestIndex.indexKey!;
|
|
277
|
+
},
|
|
278
|
+
perform: async <T>(fn: () => Promise<T>): Promise<T> => {
|
|
279
|
+
// perform the query and meaasure time and updates stats for used indices
|
|
280
|
+
let t0 = hrtime.bigint();
|
|
281
|
+
const out = await fn();
|
|
282
|
+
let t1 = hrtime.bigint();
|
|
283
|
+
const time = Number(t1 - t0);
|
|
284
|
+
|
|
285
|
+
for (const [indexKey, columnsKey] of pickedIndexKeys) {
|
|
286
|
+
const indexStats = obj.columnsToIndexes.get(columnsKey);
|
|
287
|
+
if (indexStats === undefined) {
|
|
288
|
+
throw new Error("index stats not found");
|
|
289
|
+
}
|
|
290
|
+
const index = indexStats.results.find((x) => x.indexKey === indexKey);
|
|
291
|
+
if (index === undefined) {
|
|
292
|
+
throw new Error("index not found");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// recalculate the avg by updating the time array and calculating the average
|
|
296
|
+
index.times.push(time);
|
|
297
|
+
if (index.times.length > 20) {
|
|
298
|
+
index.times.shift();
|
|
299
|
+
}
|
|
300
|
+
index.avg =
|
|
301
|
+
index.times.reduce((a, b) => a + b, 0) / index.times.length;
|
|
302
|
+
|
|
303
|
+
indexStats.results.sort((a, b) => a.avg - b.avg); // make sure fastest is first
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return out;
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const generatePermutations = (list: string[]) => {
|
|
313
|
+
if (list.length === 1) return [list];
|
|
314
|
+
return [list, [...list].reverse()];
|
|
315
|
+
};
|
|
316
|
+
/* const generatePermutations = (list: string[]) => {
|
|
317
|
+
const results: string[][] = [];
|
|
318
|
+
|
|
319
|
+
function permute(arr: string[], start: number) {
|
|
320
|
+
if (start === arr.length - 1) {
|
|
321
|
+
results.push([...arr]); // Push a copy of the current permutation
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (let i = start; i < arr.length; i++) {
|
|
326
|
+
[arr[start], arr[i]] = [arr[i], arr[start]]; // Swap
|
|
327
|
+
permute(arr, start + 1); // Recurse
|
|
328
|
+
[arr[start], arr[i]] = [arr[i], arr[start]]; // Swap back (backtrack)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
permute(list, 0);
|
|
333
|
+
return results;
|
|
334
|
+
} */
|