@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.
Files changed (46) hide show
  1. package/dist/peerbit/sqlite3-bundler-friendly.mjs +7 -7
  2. package/dist/peerbit/sqlite3-node.mjs +7 -7
  3. package/dist/peerbit/sqlite3.js +7 -7
  4. package/dist/peerbit/sqlite3.min.js +688 -168
  5. package/dist/peerbit/sqlite3.mjs +7 -7
  6. package/dist/peerbit/sqlite3.wasm +0 -0
  7. package/dist/peerbit/sqlite3.worker.min.js +19 -5
  8. package/dist/src/engine.d.ts +4 -1
  9. package/dist/src/engine.d.ts.map +1 -1
  10. package/dist/src/engine.js +125 -48
  11. package/dist/src/engine.js.map +1 -1
  12. package/dist/src/query-planner.d.ts +47 -0
  13. package/dist/src/query-planner.d.ts.map +1 -0
  14. package/dist/src/query-planner.js +290 -0
  15. package/dist/src/query-planner.js.map +1 -0
  16. package/dist/src/schema.d.ts +31 -7
  17. package/dist/src/schema.d.ts.map +1 -1
  18. package/dist/src/schema.js +370 -123
  19. package/dist/src/schema.js.map +1 -1
  20. package/dist/src/sqlite3-messages.worker.d.ts +4 -1
  21. package/dist/src/sqlite3-messages.worker.d.ts.map +1 -1
  22. package/dist/src/sqlite3-messages.worker.js.map +1 -1
  23. package/dist/src/sqlite3.browser.d.ts.map +1 -1
  24. package/dist/src/sqlite3.browser.js +7 -0
  25. package/dist/src/sqlite3.browser.js.map +1 -1
  26. package/dist/src/sqlite3.d.ts.map +1 -1
  27. package/dist/src/sqlite3.js +24 -14
  28. package/dist/src/sqlite3.js.map +1 -1
  29. package/dist/src/sqlite3.wasm.d.ts +1 -0
  30. package/dist/src/sqlite3.wasm.d.ts.map +1 -1
  31. package/dist/src/sqlite3.wasm.js +9 -1
  32. package/dist/src/sqlite3.wasm.js.map +1 -1
  33. package/dist/src/sqlite3.worker.js +7 -0
  34. package/dist/src/sqlite3.worker.js.map +1 -1
  35. package/dist/src/types.d.ts +1 -0
  36. package/dist/src/types.d.ts.map +1 -1
  37. package/package.json +4 -4
  38. package/src/engine.ts +143 -68
  39. package/src/query-planner.ts +334 -0
  40. package/src/schema.ts +519 -164
  41. package/src/sqlite3-messages.worker.ts +5 -0
  42. package/src/sqlite3.browser.ts +8 -0
  43. package/src/sqlite3.ts +24 -13
  44. package/src/sqlite3.wasm.ts +11 -1
  45. package/src/sqlite3.worker.ts +6 -1
  46. 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
+ } */