@evolu/common 4.0.4 → 4.1.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 +6 -0
- package/dist/src/Config.d.ts +18 -0
- package/dist/src/Config.d.ts.map +1 -1
- package/dist/src/Config.js +1 -0
- package/dist/src/Crdt.d.ts +4 -4
- package/dist/src/Crdt.d.ts.map +1 -1
- package/dist/src/Crypto.d.ts +2 -2
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Db.d.ts +19 -29
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +111 -34
- package/dist/src/DbWorker.d.ts +3 -4
- package/dist/src/DbWorker.d.ts.map +1 -1
- package/dist/src/DbWorker.js +42 -22
- package/dist/src/Evolu.d.ts +4 -2
- package/dist/src/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu.js +18 -6
- package/dist/src/Model.d.ts +17 -21
- package/dist/src/Model.d.ts.map +1 -1
- package/dist/src/Model.js +2 -5
- package/dist/src/Public.d.ts +2 -0
- package/dist/src/Public.d.ts.map +1 -1
- package/dist/src/Public.js +2 -0
- package/dist/src/Sql.d.ts +11 -12
- package/dist/src/Sql.d.ts.map +1 -1
- package/dist/src/Sql.js +30 -106
- package/dist/src/Sqlite.d.ts +29 -3
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +25 -5
- package/package.json +13 -13
- package/src/Config.ts +20 -0
- package/src/Crdt.ts +2 -2
- package/src/Crypto.ts +1 -1
- package/src/Db.ts +160 -84
- package/src/DbWorker.ts +57 -38
- package/src/Evolu.ts +36 -9
- package/src/Model.ts +10 -14
- package/src/Public.ts +2 -0
- package/src/Sql.ts +41 -123
- package/src/Sqlite.ts +65 -7
package/src/Config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as Context from "effect/Context";
|
|
2
2
|
import * as Layer from "effect/Layer";
|
|
3
|
+
import { CreateIndexBuilder } from "kysely";
|
|
3
4
|
|
|
4
5
|
export interface Config {
|
|
5
6
|
/** Alternate URL to Evolu sync and backup server. */
|
|
@@ -23,6 +24,24 @@ export interface Config {
|
|
|
23
24
|
* default value is: "Evolu".
|
|
24
25
|
*/
|
|
25
26
|
readonly name: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Use the `indexes` property to define SQLite indexes.
|
|
30
|
+
*
|
|
31
|
+
* Table and column names are not typed because Kysely doesn't support it.
|
|
32
|
+
*
|
|
33
|
+
* https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const indexes = [
|
|
37
|
+
* createIndex("indexTodoCreatedAt").on("todo").column("createdAt"),
|
|
38
|
+
*
|
|
39
|
+
* createIndex("indexTodoCategoryCreatedAt")
|
|
40
|
+
* .on("todoCategory")
|
|
41
|
+
* .column("createdAt"),
|
|
42
|
+
* ];
|
|
43
|
+
*/
|
|
44
|
+
readonly indexes: ReadonlyArray<CreateIndexBuilder<any>>;
|
|
26
45
|
}
|
|
27
46
|
|
|
28
47
|
export const Config = Context.GenericTag<Config>("@services/Config");
|
|
@@ -35,6 +54,7 @@ export const ConfigLive = (config?: Partial<Config>): Layer.Layer<Config> =>
|
|
|
35
54
|
maxDrift: 5 * 60 * 1000,
|
|
36
55
|
reloadUrl: "/",
|
|
37
56
|
name: "Evolu",
|
|
57
|
+
indexes: [],
|
|
38
58
|
...config,
|
|
39
59
|
}),
|
|
40
60
|
);
|
package/src/Crdt.ts
CHANGED
|
@@ -42,14 +42,14 @@ export const Millis = S.number.pipe(
|
|
|
42
42
|
S.brand("Millis"),
|
|
43
43
|
);
|
|
44
44
|
|
|
45
|
-
export type Millis = S.Schema.
|
|
45
|
+
export type Millis = S.Schema.Type<typeof Millis>;
|
|
46
46
|
|
|
47
47
|
export const initialMillis = S.decodeSync(Millis)(
|
|
48
48
|
AllowedTimeRange.greaterThan + 1,
|
|
49
49
|
);
|
|
50
50
|
|
|
51
51
|
export const Counter = S.number.pipe(S.between(0, 65535), S.brand("Counter"));
|
|
52
|
-
export type Counter = S.Schema.
|
|
52
|
+
export type Counter = S.Schema.Type<typeof Counter>;
|
|
53
53
|
|
|
54
54
|
const initialCounter = S.decodeSync(Counter)(0);
|
|
55
55
|
|
package/src/Crypto.ts
CHANGED
|
@@ -51,7 +51,7 @@ export const NodeId = S.string.pipe(
|
|
|
51
51
|
S.pattern(/^[\w-]{16}$/),
|
|
52
52
|
S.brand("NodeId"),
|
|
53
53
|
);
|
|
54
|
-
export type NodeId = S.Schema.
|
|
54
|
+
export type NodeId = S.Schema.Type<typeof NodeId>;
|
|
55
55
|
|
|
56
56
|
const nanoidForNodeId = customAlphabet("0123456789abcdef", 16);
|
|
57
57
|
|
package/src/Db.ts
CHANGED
|
@@ -31,9 +31,14 @@ import {
|
|
|
31
31
|
insertOwner,
|
|
32
32
|
} from "./Sql.js";
|
|
33
33
|
import {
|
|
34
|
+
Index,
|
|
35
|
+
indexEquivalence,
|
|
34
36
|
JsonObjectOrArray,
|
|
35
37
|
Sqlite,
|
|
36
38
|
SqliteQuery,
|
|
39
|
+
SqliteQueryOptions,
|
|
40
|
+
SqliteSchema,
|
|
41
|
+
Table,
|
|
37
42
|
Value,
|
|
38
43
|
isJsonObjectOrArray,
|
|
39
44
|
} from "./Sqlite.js";
|
|
@@ -56,14 +61,14 @@ type TableSchema = ReadonlyRecord.ReadonlyRecord<string, Value> & {
|
|
|
56
61
|
*
|
|
57
62
|
* @example
|
|
58
63
|
* const TodoId = id("Todo");
|
|
59
|
-
* type TodoId = S.Schema.
|
|
64
|
+
* type TodoId = S.Schema.Type<typeof TodoId>;
|
|
60
65
|
*
|
|
61
66
|
* const TodoTable = table({
|
|
62
67
|
* id: TodoId,
|
|
63
68
|
* title: NonEmptyString1000,
|
|
64
69
|
* isCompleted: S.nullable(SqliteBoolean),
|
|
65
70
|
* });
|
|
66
|
-
* type TodoTable = S.Schema.
|
|
71
|
+
* type TodoTable = S.Schema.Type<typeof TodoTable>;
|
|
67
72
|
*/
|
|
68
73
|
export const table = <Fields extends TableFields>(
|
|
69
74
|
fields: Fields,
|
|
@@ -73,10 +78,10 @@ export const table = <Fields extends TableFields>(
|
|
|
73
78
|
? ValidateFieldsHasId<Fields> extends true
|
|
74
79
|
? S.Schema<
|
|
75
80
|
Types.Simplify<
|
|
76
|
-
S.
|
|
81
|
+
S.Struct.Type<Fields> & S.Schema.Type<typeof ReservedColumns>
|
|
77
82
|
>,
|
|
78
83
|
Types.Simplify<
|
|
79
|
-
S.
|
|
84
|
+
S.Struct.Encoded<Fields> & S.Schema.Encoded<typeof ReservedColumns>
|
|
80
85
|
>
|
|
81
86
|
>
|
|
82
87
|
: EvoluTypeError<"table() called without id column.">
|
|
@@ -132,7 +137,7 @@ type ValidateFieldsHasId<Fields extends TableFields> = "id" extends keyof Fields
|
|
|
132
137
|
* todo: TodoTable,
|
|
133
138
|
* todoCategory: TodoCategoryTable,
|
|
134
139
|
* });
|
|
135
|
-
* type Database = S.Schema.
|
|
140
|
+
* type Database = S.Schema.Type<typeof Database>;
|
|
136
141
|
*/
|
|
137
142
|
export const database = S.struct;
|
|
138
143
|
|
|
@@ -157,22 +162,25 @@ interface SerializedSqliteQuery {
|
|
|
157
162
|
| Array<number>
|
|
158
163
|
| { json: JsonObjectOrArray }
|
|
159
164
|
)[];
|
|
165
|
+
readonly options?: SqliteQueryOptions;
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
// We use queries as keys, hence JSON.stringify.
|
|
163
169
|
export const serializeQuery = <R extends Row>({
|
|
164
170
|
sql,
|
|
165
|
-
parameters,
|
|
171
|
+
parameters = [],
|
|
172
|
+
options,
|
|
166
173
|
}: SqliteQuery): Query<R> => {
|
|
167
174
|
const query: SerializedSqliteQuery = {
|
|
168
175
|
sql,
|
|
169
|
-
parameters: parameters.map((p) =>
|
|
170
|
-
|
|
176
|
+
parameters: parameters.map((p) =>
|
|
177
|
+
Predicate.isUint8Array(p)
|
|
171
178
|
? Array.from(p)
|
|
172
179
|
: isJsonObjectOrArray(p)
|
|
173
180
|
? { json: p }
|
|
174
|
-
: p
|
|
175
|
-
|
|
181
|
+
: p,
|
|
182
|
+
),
|
|
183
|
+
...(options && { options }),
|
|
176
184
|
};
|
|
177
185
|
return JSON.stringify(query) as Query<R>;
|
|
178
186
|
};
|
|
@@ -249,13 +257,7 @@ export const queryResultFromRows = <R extends Row>(
|
|
|
249
257
|
return queryResult as QueryResult<R>;
|
|
250
258
|
};
|
|
251
259
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
export interface Table {
|
|
255
|
-
readonly name: string;
|
|
256
|
-
readonly columns: ReadonlyArray<string>;
|
|
257
|
-
}
|
|
258
|
-
|
|
260
|
+
// TODO: https://discord.com/channels/795981131316985866/1218626687546294386/1218796529725476935
|
|
259
261
|
// https://github.com/Effect-TS/schema/releases/tag/v0.18.0
|
|
260
262
|
const getPropertySignatures = <I extends { [K in keyof A]: any }, A>(
|
|
261
263
|
schema: S.Schema<A, I>,
|
|
@@ -270,7 +272,7 @@ const getPropertySignatures = <I extends { [K in keyof A]: any }, A>(
|
|
|
270
272
|
return out as any;
|
|
271
273
|
};
|
|
272
274
|
|
|
273
|
-
export const schemaToTables = (schema: S.Schema<any>):
|
|
275
|
+
export const schemaToTables = (schema: S.Schema<any>): ReadonlyArray<Table> =>
|
|
274
276
|
pipe(
|
|
275
277
|
getPropertySignatures(schema),
|
|
276
278
|
ReadonlyRecord.toEntries,
|
|
@@ -287,10 +289,12 @@ export const transaction = <R, E, A>(
|
|
|
287
289
|
): Effect.Effect<A, E, Sqlite | R> =>
|
|
288
290
|
Effect.flatMap(Sqlite, (sqlite) =>
|
|
289
291
|
Effect.acquireUseRelease(
|
|
290
|
-
sqlite.exec("begin"),
|
|
292
|
+
sqlite.exec({ sql: "begin" }),
|
|
291
293
|
() => effect,
|
|
292
294
|
(_, exit) =>
|
|
293
|
-
Exit.isFailure(exit)
|
|
295
|
+
Exit.isFailure(exit)
|
|
296
|
+
? sqlite.exec({ sql: "rollback" })
|
|
297
|
+
: sqlite.exec({ sql: "end" }),
|
|
294
298
|
),
|
|
295
299
|
);
|
|
296
300
|
|
|
@@ -305,7 +309,7 @@ export const SqliteNoSuchTableOrColumnError = S.struct({
|
|
|
305
309
|
S.string.pipe(S.includes("has no column")),
|
|
306
310
|
),
|
|
307
311
|
});
|
|
308
|
-
export type SqliteNoSuchTableOrColumnError = S.Schema.
|
|
312
|
+
export type SqliteNoSuchTableOrColumnError = S.Schema.Type<
|
|
309
313
|
typeof SqliteNoSuchTableOrColumnError
|
|
310
314
|
>;
|
|
311
315
|
|
|
@@ -336,7 +340,7 @@ export const lazyInit = (
|
|
|
336
340
|
sqlite.exec(createMessageTableIndex),
|
|
337
341
|
sqlite.exec(createOwnerTable),
|
|
338
342
|
sqlite.exec({
|
|
339
|
-
|
|
343
|
+
...insertOwner,
|
|
340
344
|
parameters: [
|
|
341
345
|
owner.id,
|
|
342
346
|
owner.mnemonic,
|
|
@@ -351,78 +355,150 @@ export const lazyInit = (
|
|
|
351
355
|
return owner;
|
|
352
356
|
});
|
|
353
357
|
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
never,
|
|
357
|
-
Sqlite
|
|
358
|
-
> = Sqlite.pipe(
|
|
359
|
-
Effect.flatMap((sqlite) =>
|
|
360
|
-
sqlite.exec(`select "name" from "sqlite_schema" where "type" = 'table'`),
|
|
361
|
-
),
|
|
362
|
-
Effect.map((result) => result.rows),
|
|
363
|
-
Effect.map(ReadonlyArray.map((row) => (row.name as string) + "")),
|
|
364
|
-
Effect.map(ReadonlyArray.filter(Predicate.not(String.startsWith("__")))),
|
|
365
|
-
Effect.map(ReadonlyArray.dedupeWith(String.Equivalence)),
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
const updateTable = ({
|
|
369
|
-
name,
|
|
370
|
-
columns,
|
|
371
|
-
}: Table): Effect.Effect<void, never, Sqlite> =>
|
|
372
|
-
Effect.gen(function* (_) {
|
|
358
|
+
const getSchema: Effect.Effect<SqliteSchema, never, Sqlite> = Effect.gen(
|
|
359
|
+
function* (_) {
|
|
373
360
|
const sqlite = yield* _(Sqlite);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
361
|
+
|
|
362
|
+
const tables = yield* _(
|
|
363
|
+
sqlite.exec({
|
|
364
|
+
// https://til.simonwillison.net/sqlite/list-all-columns-in-a-database
|
|
365
|
+
sql: `
|
|
366
|
+
select
|
|
367
|
+
sqlite_master.name as tableName,
|
|
368
|
+
table_info.name as columnName
|
|
369
|
+
from
|
|
370
|
+
sqlite_master
|
|
371
|
+
join pragma_table_info(sqlite_master.name) as table_info
|
|
372
|
+
`.trim(),
|
|
373
|
+
}),
|
|
374
|
+
Effect.map(({ rows }) => {
|
|
375
|
+
const map = new Map<string, string[]>();
|
|
376
|
+
rows.forEach((row) => {
|
|
377
|
+
const { tableName, columnName } = row as {
|
|
378
|
+
tableName: string;
|
|
379
|
+
columnName: string;
|
|
380
|
+
};
|
|
381
|
+
if (!map.has(tableName)) map.set(tableName, []);
|
|
382
|
+
map.get(tableName)?.push(columnName);
|
|
383
|
+
});
|
|
384
|
+
return Array.from(map, ([name, columns]) => ({
|
|
385
|
+
name,
|
|
380
386
|
columns,
|
|
381
|
-
)
|
|
382
|
-
),
|
|
383
|
-
|
|
387
|
+
}));
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const indexes = yield* _(
|
|
392
|
+
sqlite.exec({
|
|
393
|
+
sql: `
|
|
394
|
+
select
|
|
395
|
+
name, sql
|
|
396
|
+
from
|
|
397
|
+
sqlite_master
|
|
398
|
+
where
|
|
399
|
+
type='index' and
|
|
400
|
+
name not like 'sqlite_%' and
|
|
401
|
+
name not like 'index_evolu_%'`,
|
|
402
|
+
}),
|
|
403
|
+
Effect.map((result) =>
|
|
384
404
|
ReadonlyArray.map(
|
|
385
|
-
|
|
386
|
-
|
|
405
|
+
result.rows,
|
|
406
|
+
(row): Index => ({
|
|
407
|
+
name: row.name as string,
|
|
408
|
+
/**
|
|
409
|
+
* SQLite returns "CREATE INDEX" for "create index" for some reason.
|
|
410
|
+
* Other keywords remain unchanged. We have to normalize the casing
|
|
411
|
+
* for `indexEquivalence` manually.
|
|
412
|
+
*/
|
|
413
|
+
sql: (row.sql as string).replace("CREATE INDEX", "create index"),
|
|
414
|
+
}),
|
|
387
415
|
),
|
|
388
416
|
),
|
|
389
|
-
Effect.map(ReadonlyArray.join("")),
|
|
390
417
|
);
|
|
391
|
-
if (sql) yield* _(sqlite.exec(sql));
|
|
392
|
-
});
|
|
393
418
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}: Table): Effect.Effect<void, never, Sqlite> =>
|
|
398
|
-
Effect.flatMap(Sqlite, (sqlite) =>
|
|
399
|
-
sqlite.exec(`
|
|
400
|
-
create table ${name} (
|
|
401
|
-
"id" text primary key,
|
|
402
|
-
${columns
|
|
403
|
-
.filter((c) => c !== "id")
|
|
404
|
-
// "A column with affinity BLOB does not prefer one storage class over another
|
|
405
|
-
// and no attempt is made to coerce data from one storage class into another."
|
|
406
|
-
// https://www.sqlite.org/datatype3.html
|
|
407
|
-
.map((name) => `"${name}" blob`)
|
|
408
|
-
.join(", ")}
|
|
409
|
-
);
|
|
410
|
-
`),
|
|
411
|
-
);
|
|
419
|
+
return { tables, indexes };
|
|
420
|
+
},
|
|
421
|
+
);
|
|
412
422
|
|
|
413
423
|
export const ensureSchema = (
|
|
414
|
-
|
|
424
|
+
schema: SqliteSchema,
|
|
415
425
|
): Effect.Effect<void, never, Sqlite> =>
|
|
416
|
-
Effect.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
+
Effect.gen(function* (_) {
|
|
427
|
+
const sqlite = yield* _(Sqlite);
|
|
428
|
+
const currentSchema = yield* _(getSchema);
|
|
429
|
+
|
|
430
|
+
const sql: string[] = [];
|
|
431
|
+
|
|
432
|
+
schema.tables.forEach((table) => {
|
|
433
|
+
const currentTable = currentSchema.tables.find(
|
|
434
|
+
(t) => t.name === table.name,
|
|
435
|
+
);
|
|
436
|
+
if (!currentTable) {
|
|
437
|
+
sql.push(`
|
|
438
|
+
create table ${table.name} (
|
|
439
|
+
"id" text primary key,
|
|
440
|
+
${table.columns
|
|
441
|
+
.filter((c) => c !== "id")
|
|
442
|
+
// "A column with affinity BLOB does not prefer one storage class over another
|
|
443
|
+
// and no attempt is made to coerce data from one storage class into another."
|
|
444
|
+
// https://www.sqlite.org/datatype3.html
|
|
445
|
+
.map((name) => `"${name}" blob`)
|
|
446
|
+
.join(", ")}
|
|
447
|
+
);
|
|
448
|
+
`);
|
|
449
|
+
} else {
|
|
450
|
+
ReadonlyArray.differenceWith(String.Equivalence)(
|
|
451
|
+
table.columns,
|
|
452
|
+
currentTable.columns,
|
|
453
|
+
).forEach((newColumn) => {
|
|
454
|
+
sql.push(
|
|
455
|
+
`alter table "${table.name}" add column "${newColumn}" blob;`,
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Remove old indexes.
|
|
462
|
+
ReadonlyArray.differenceWith(indexEquivalence)(
|
|
463
|
+
currentSchema.indexes,
|
|
464
|
+
ReadonlyArray.intersectionWith(indexEquivalence)(
|
|
465
|
+
currentSchema.indexes,
|
|
466
|
+
schema.indexes,
|
|
467
|
+
),
|
|
468
|
+
).forEach((indexToDrop) => {
|
|
469
|
+
sql.push(`drop index "${indexToDrop.name}";`);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Add new indexes.
|
|
473
|
+
ReadonlyArray.differenceWith(indexEquivalence)(
|
|
474
|
+
schema.indexes,
|
|
475
|
+
currentSchema.indexes,
|
|
476
|
+
).forEach((newIndex) => {
|
|
477
|
+
sql.push(`${newIndex.sql};`);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
if (sql.length > 0)
|
|
481
|
+
yield* _(
|
|
482
|
+
sqlite.exec({
|
|
483
|
+
sql: sql.join(""),
|
|
484
|
+
}),
|
|
485
|
+
);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
export const dropAllTables: Effect.Effect<void, never, Sqlite> = Effect.gen(
|
|
489
|
+
function* (_) {
|
|
490
|
+
const sqlite = yield* _(Sqlite);
|
|
491
|
+
const schema = yield* _(getSchema);
|
|
492
|
+
const sql = schema.tables
|
|
493
|
+
// The dropped table is completely removed from the database schema and
|
|
494
|
+
// the disk file. The table can not be recovered.
|
|
495
|
+
// All indices and triggers associated with the table are also deleted.
|
|
496
|
+
// https://sqlite.org/lang_droptable.html
|
|
497
|
+
.map((table) => `drop table "${table.name}";`)
|
|
498
|
+
.join("");
|
|
499
|
+
yield* _(sqlite.exec({ sql }));
|
|
500
|
+
},
|
|
501
|
+
);
|
|
426
502
|
|
|
427
503
|
export type RowsStore = Store<RowsStoreValue>;
|
|
428
504
|
export const RowsStore = Context.GenericTag<RowsStore>("@services/RowsStore");
|
package/src/DbWorker.ts
CHANGED
|
@@ -34,9 +34,8 @@ import {
|
|
|
34
34
|
Query,
|
|
35
35
|
RowsStore,
|
|
36
36
|
RowsStoreLive,
|
|
37
|
-
Table,
|
|
38
|
-
Tables,
|
|
39
37
|
deserializeQuery,
|
|
38
|
+
dropAllTables,
|
|
40
39
|
ensureSchema,
|
|
41
40
|
lazyInit,
|
|
42
41
|
someDefectToNoSuchTableOrColumnError,
|
|
@@ -49,7 +48,14 @@ import { OnCompleteId } from "./OnCompletes.js";
|
|
|
49
48
|
import { Owner, OwnerId } from "./Owner.js";
|
|
50
49
|
import { DbWorkerLock } from "./Platform.js";
|
|
51
50
|
import * as Sql from "./Sql.js";
|
|
52
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
Sqlite,
|
|
53
|
+
SqliteQueryPlanRow,
|
|
54
|
+
SqliteSchema,
|
|
55
|
+
Table,
|
|
56
|
+
Value,
|
|
57
|
+
drawSqliteQueryPlan as drawExplainQueryPlan,
|
|
58
|
+
} from "./Sqlite.js";
|
|
53
59
|
import {
|
|
54
60
|
Message,
|
|
55
61
|
NewMessage,
|
|
@@ -99,9 +105,8 @@ interface DbWorkerInputReset {
|
|
|
99
105
|
readonly mnemonic?: Mnemonic;
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
interface DbWorkerInputEnsureSchema {
|
|
108
|
+
interface DbWorkerInputEnsureSchema extends SqliteSchema {
|
|
103
109
|
readonly _tag: "ensureSchema";
|
|
104
|
-
readonly tables: Tables;
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
type DbWorkerOnMessage = DbWorker["onMessage"];
|
|
@@ -189,11 +194,30 @@ const query = ({
|
|
|
189
194
|
|
|
190
195
|
const queriesRows = yield* _(
|
|
191
196
|
ReadonlyArray.dedupe(queries),
|
|
192
|
-
Effect.forEach((query) =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
Effect.forEach((query) => {
|
|
198
|
+
const sqliteQuery = deserializeQuery(query);
|
|
199
|
+
return sqlite.exec(sqliteQuery).pipe(
|
|
200
|
+
Effect.map((result) => [query, result.rows] as const),
|
|
201
|
+
Effect.tap(() => {
|
|
202
|
+
if (!sqliteQuery.options?.logExplainQueryPlan) return;
|
|
203
|
+
return sqlite
|
|
204
|
+
.exec({
|
|
205
|
+
...sqliteQuery,
|
|
206
|
+
sql: `EXPLAIN QUERY PLAN ${sqliteQuery.sql}`,
|
|
207
|
+
})
|
|
208
|
+
.pipe(
|
|
209
|
+
Effect.tap(() => Effect.log("ExplainQueryPlan")),
|
|
210
|
+
Effect.tap(({ rows }) => {
|
|
211
|
+
// Not using Effect.log because of formating
|
|
212
|
+
// eslint-disable-next-line no-console
|
|
213
|
+
console.log(
|
|
214
|
+
drawExplainQueryPlan(rows as SqliteQueryPlanRow[]),
|
|
215
|
+
);
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
}),
|
|
197
221
|
);
|
|
198
222
|
|
|
199
223
|
const previous = rowsStore.getState();
|
|
@@ -288,7 +312,8 @@ const ensureSchemaByNewMessages = (
|
|
|
288
312
|
columns: table.columns.concat(message.column),
|
|
289
313
|
});
|
|
290
314
|
});
|
|
291
|
-
|
|
315
|
+
const tables = Array.from(tablesMap.values());
|
|
316
|
+
yield* _(ensureSchema({ tables, indexes: [] }));
|
|
292
317
|
});
|
|
293
318
|
|
|
294
319
|
export const upsertValueIntoTableRowColumn = (
|
|
@@ -300,7 +325,15 @@ export const upsertValueIntoTableRowColumn = (
|
|
|
300
325
|
const sqlite = yield* _(Sqlite);
|
|
301
326
|
const createdAtOrUpdatedAt = cast(new Date(millis));
|
|
302
327
|
const insert = sqlite.exec({
|
|
303
|
-
sql:
|
|
328
|
+
sql: `
|
|
329
|
+
insert into
|
|
330
|
+
"${message.table}" ("id", "${message.column}", "createdAt", "updatedAt")
|
|
331
|
+
values
|
|
332
|
+
(?, ?, ?, ?)
|
|
333
|
+
on conflict do update set
|
|
334
|
+
"${message.column}" = ?,
|
|
335
|
+
"updatedAt" = ?
|
|
336
|
+
`.trim(),
|
|
304
337
|
parameters: [
|
|
305
338
|
message.row,
|
|
306
339
|
message.value,
|
|
@@ -334,8 +367,8 @@ const applyMessages = ({
|
|
|
334
367
|
for (const message of messages) {
|
|
335
368
|
const timestamp: TimestampString | null = yield* _(
|
|
336
369
|
sqlite.exec({
|
|
337
|
-
|
|
338
|
-
parameters: [message.table, message.row, message.column],
|
|
370
|
+
...Sql.selectLastTimestampForTableRowColumn,
|
|
371
|
+
parameters: [message.table, message.row, message.column, 1],
|
|
339
372
|
}),
|
|
340
373
|
Effect.map((result) => result.rows),
|
|
341
374
|
Effect.flatMap(ReadonlyArray.head),
|
|
@@ -351,7 +384,7 @@ const applyMessages = ({
|
|
|
351
384
|
if (timestamp == null || timestamp !== message.timestamp) {
|
|
352
385
|
const { changes } = yield* _(
|
|
353
386
|
sqlite.exec({
|
|
354
|
-
|
|
387
|
+
...Sql.insertIntoMessagesIfNew,
|
|
355
388
|
parameters: [
|
|
356
389
|
message.timestamp,
|
|
357
390
|
message.table,
|
|
@@ -377,10 +410,10 @@ const writeTimestampAndMerkleTree = ({
|
|
|
377
410
|
}: TimestampAndMerkleTree): Effect.Effect<void, never, Sqlite> =>
|
|
378
411
|
Effect.flatMap(Sqlite, (sqlite) =>
|
|
379
412
|
sqlite.exec({
|
|
380
|
-
|
|
413
|
+
...Sql.updateOwnerTimestampAndMerkleTree,
|
|
381
414
|
parameters: [
|
|
382
|
-
timestampToString(timestamp),
|
|
383
415
|
merkleTreeToString(merkleTree),
|
|
416
|
+
timestampToString(timestamp),
|
|
384
417
|
],
|
|
385
418
|
}),
|
|
386
419
|
);
|
|
@@ -420,7 +453,10 @@ const mutate = ({
|
|
|
420
453
|
const { exec } = yield* _(Sqlite);
|
|
421
454
|
yield* _(
|
|
422
455
|
Effect.forEach(toDelete, ({ table, row }) =>
|
|
423
|
-
exec({
|
|
456
|
+
exec({
|
|
457
|
+
sql: `delete from "${table}" where "id" = ?;`,
|
|
458
|
+
parameters: [row],
|
|
459
|
+
}),
|
|
424
460
|
),
|
|
425
461
|
);
|
|
426
462
|
|
|
@@ -504,7 +540,7 @@ const handleSyncResponse = ({
|
|
|
504
540
|
|
|
505
541
|
const messagesToSync = yield* _(
|
|
506
542
|
sqlite.exec({
|
|
507
|
-
|
|
543
|
+
...Sql.selectMessagesToSync,
|
|
508
544
|
parameters: [timestampToString(makeSyncTimestamp(diff.value))],
|
|
509
545
|
}),
|
|
510
546
|
Effect.map(({ rows }) => rows as unknown as ReadonlyArray<Message>),
|
|
@@ -561,25 +597,8 @@ const reset = (
|
|
|
561
597
|
Sqlite | Bip39 | NanoIdGenerator | DbWorkerOnMessage
|
|
562
598
|
> =>
|
|
563
599
|
Effect.gen(function* (_) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
yield* _(
|
|
567
|
-
sqlite.exec(`SELECT "name" FROM "sqlite_master" WHERE "type" = 'table'`),
|
|
568
|
-
Effect.map((result) => result.rows),
|
|
569
|
-
Effect.flatMap(
|
|
570
|
-
// The dropped table is completely removed from the database schema and
|
|
571
|
-
// the disk file. The table can not be recovered.
|
|
572
|
-
// All indices and triggers associated with the table are also deleted.
|
|
573
|
-
// https://sqlite.org/lang_droptable.html
|
|
574
|
-
Effect.forEach(
|
|
575
|
-
({ name }) => sqlite.exec(`DROP TABLE "${name as string}"`),
|
|
576
|
-
{ discard: true },
|
|
577
|
-
),
|
|
578
|
-
),
|
|
579
|
-
);
|
|
580
|
-
|
|
600
|
+
yield* _(dropAllTables);
|
|
581
601
|
if (input.mnemonic) yield* _(lazyInit(input.mnemonic));
|
|
582
|
-
|
|
583
602
|
const onMessage = yield* _(DbWorkerOnMessage);
|
|
584
603
|
onMessage({ _tag: "onResetOrRestore" });
|
|
585
604
|
});
|
|
@@ -666,7 +685,7 @@ export const DbWorkerCommonLive = Layer.effect(
|
|
|
666
685
|
skipAllBecauseOfReset = true;
|
|
667
686
|
return reset(input);
|
|
668
687
|
},
|
|
669
|
-
ensureSchema
|
|
688
|
+
ensureSchema,
|
|
670
689
|
SyncWorkerOutputSyncResponse: handleSyncResponse,
|
|
671
690
|
}),
|
|
672
691
|
Effect.provide(layer),
|