@evolu/common 4.0.5 → 4.1.1
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 +27 -7
- package/dist/src/Config.d.ts.map +1 -1
- package/dist/src/Config.js +7 -5
- package/dist/src/Db.d.ts +5 -9
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +108 -31
- 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/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 +30 -3
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +38 -5
- package/package.json +6 -6
- package/src/Config.ts +39 -17
- package/src/Db.ts +156 -78
- package/src/DbWorker.ts +57 -38
- package/src/Evolu.ts +36 -9
- package/src/Public.ts +2 -0
- package/src/Sql.ts +41 -123
- package/src/Sqlite.ts +82 -7
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";
|
|
@@ -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
|
|
|
@@ -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,152 @@ 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
|
+
`.trim(),
|
|
403
|
+
}),
|
|
404
|
+
Effect.map((result) =>
|
|
384
405
|
ReadonlyArray.map(
|
|
385
|
-
|
|
386
|
-
|
|
406
|
+
result.rows,
|
|
407
|
+
(row): Index => ({
|
|
408
|
+
name: row.name as string,
|
|
409
|
+
/**
|
|
410
|
+
* SQLite returns "CREATE INDEX" for "create index" for some reason.
|
|
411
|
+
* Other keywords remain unchanged. We have to normalize the casing
|
|
412
|
+
* for `indexEquivalence` manually.
|
|
413
|
+
*/
|
|
414
|
+
sql: (row.sql as string).replace("CREATE INDEX", "create index"),
|
|
415
|
+
}),
|
|
387
416
|
),
|
|
388
417
|
),
|
|
389
|
-
Effect.map(ReadonlyArray.join("")),
|
|
390
418
|
);
|
|
391
|
-
if (sql) yield* _(sqlite.exec(sql));
|
|
392
|
-
});
|
|
393
419
|
|
|
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
|
-
);
|
|
420
|
+
return { tables, indexes };
|
|
421
|
+
},
|
|
422
|
+
);
|
|
412
423
|
|
|
413
424
|
export const ensureSchema = (
|
|
414
|
-
|
|
425
|
+
schema: SqliteSchema,
|
|
415
426
|
): Effect.Effect<void, never, Sqlite> =>
|
|
416
|
-
Effect.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
427
|
+
Effect.gen(function* (_) {
|
|
428
|
+
const sqlite = yield* _(Sqlite);
|
|
429
|
+
const currentSchema = yield* _(getSchema);
|
|
430
|
+
|
|
431
|
+
const sql: string[] = [];
|
|
432
|
+
|
|
433
|
+
schema.tables.forEach((table) => {
|
|
434
|
+
const currentTable = currentSchema.tables.find(
|
|
435
|
+
(t) => t.name === table.name,
|
|
436
|
+
);
|
|
437
|
+
if (!currentTable) {
|
|
438
|
+
sql.push(
|
|
439
|
+
`
|
|
440
|
+
create table ${table.name} (
|
|
441
|
+
"id" text primary key,
|
|
442
|
+
${table.columns
|
|
443
|
+
.filter((c) => c !== "id")
|
|
444
|
+
// "A column with affinity BLOB does not prefer one storage class over another
|
|
445
|
+
// and no attempt is made to coerce data from one storage class into another."
|
|
446
|
+
// https://www.sqlite.org/datatype3.html
|
|
447
|
+
.map((name) => `"${name}" blob`)
|
|
448
|
+
.join(", ")}
|
|
449
|
+
);`.trim(),
|
|
450
|
+
);
|
|
451
|
+
} else {
|
|
452
|
+
ReadonlyArray.differenceWith(String.Equivalence)(
|
|
453
|
+
table.columns,
|
|
454
|
+
currentTable.columns,
|
|
455
|
+
).forEach((newColumn) => {
|
|
456
|
+
sql.push(
|
|
457
|
+
`alter table "${table.name}" add column "${newColumn}" blob;`,
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Remove old indexes.
|
|
464
|
+
ReadonlyArray.differenceWith(indexEquivalence)(
|
|
465
|
+
currentSchema.indexes,
|
|
466
|
+
ReadonlyArray.intersectionWith(indexEquivalence)(
|
|
467
|
+
currentSchema.indexes,
|
|
468
|
+
schema.indexes,
|
|
469
|
+
),
|
|
470
|
+
).forEach((indexToDrop) => {
|
|
471
|
+
sql.push(`drop index "${indexToDrop.name}";`);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Add new indexes.
|
|
475
|
+
ReadonlyArray.differenceWith(indexEquivalence)(
|
|
476
|
+
schema.indexes,
|
|
477
|
+
currentSchema.indexes,
|
|
478
|
+
).forEach((newIndex) => {
|
|
479
|
+
sql.push(`${newIndex.sql};`);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
if (sql.length > 0)
|
|
483
|
+
yield* _(
|
|
484
|
+
sqlite.exec({
|
|
485
|
+
sql: sql.join("\n"),
|
|
486
|
+
}),
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
export const dropAllTables: Effect.Effect<void, never, Sqlite> = Effect.gen(
|
|
491
|
+
function* (_) {
|
|
492
|
+
const sqlite = yield* _(Sqlite);
|
|
493
|
+
const schema = yield* _(getSchema);
|
|
494
|
+
const sql = schema.tables
|
|
495
|
+
// The dropped table is completely removed from the database schema and
|
|
496
|
+
// the disk file. The table can not be recovered.
|
|
497
|
+
// All indices and triggers associated with the table are also deleted.
|
|
498
|
+
// https://sqlite.org/lang_droptable.html
|
|
499
|
+
.map((table) => `drop table "${table.name}";`)
|
|
500
|
+
.join("");
|
|
501
|
+
yield* _(sqlite.exec({ sql }));
|
|
502
|
+
},
|
|
503
|
+
);
|
|
426
504
|
|
|
427
505
|
export type RowsStore = Store<RowsStoreValue>;
|
|
428
506
|
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),
|
package/src/Evolu.ts
CHANGED
|
@@ -32,7 +32,12 @@ import { SqliteBoolean, SqliteDate } from "./Model.js";
|
|
|
32
32
|
import { OnCompletes, OnCompletesLive } from "./OnCompletes.js";
|
|
33
33
|
import { Owner } from "./Owner.js";
|
|
34
34
|
import { AppState, FlushSync } from "./Platform.js";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
Index,
|
|
37
|
+
SqliteQuery,
|
|
38
|
+
SqliteQueryOptions,
|
|
39
|
+
isSqlMutation,
|
|
40
|
+
} from "./Sqlite.js";
|
|
36
41
|
import { Store, Unsubscribe, makeStore } from "./Store.js";
|
|
37
42
|
import { SyncState } from "./SyncWorker.js";
|
|
38
43
|
|
|
@@ -297,6 +302,7 @@ export interface Evolu<S extends DatabaseSchema = DatabaseSchema> {
|
|
|
297
302
|
*/
|
|
298
303
|
readonly ensureSchema: <From, To extends S>(
|
|
299
304
|
schema: S.Schema<To, From>,
|
|
305
|
+
indexes?: ReadonlyArray<Index>,
|
|
300
306
|
) => void;
|
|
301
307
|
|
|
302
308
|
/**
|
|
@@ -313,6 +319,7 @@ export const Evolu = Context.GenericTag<Evolu>("@services/Evolu");
|
|
|
313
319
|
|
|
314
320
|
type CreateQuery<S extends DatabaseSchema> = <R extends Row>(
|
|
315
321
|
queryCallback: QueryCallback<S, R>,
|
|
322
|
+
options?: SqliteQueryOptions,
|
|
316
323
|
) => Query<R>;
|
|
317
324
|
|
|
318
325
|
type QueryCallback<S extends DatabaseSchema, R extends Row> = (
|
|
@@ -334,6 +341,7 @@ type NullableExceptForIdAndAutomaticColumns<T> = {
|
|
|
334
341
|
: T[K] | null;
|
|
335
342
|
};
|
|
336
343
|
|
|
344
|
+
// https://kysely.dev/docs/recipes/splitting-query-building-and-execution
|
|
337
345
|
const kysely = new Kysely.Kysely<QuerySchema<DatabaseSchema>>({
|
|
338
346
|
dialect: {
|
|
339
347
|
createAdapter: (): Kysely.DialectAdapter => new Kysely.SqliteAdapter(),
|
|
@@ -346,18 +354,29 @@ const kysely = new Kysely.Kysely<QuerySchema<DatabaseSchema>>({
|
|
|
346
354
|
},
|
|
347
355
|
});
|
|
348
356
|
|
|
357
|
+
export const createIndex = kysely.schema.createIndex.bind(kysely.schema);
|
|
358
|
+
|
|
349
359
|
export const makeCreateQuery =
|
|
350
360
|
<S extends DatabaseSchema = DatabaseSchema>(): CreateQuery<S> =>
|
|
351
|
-
<R extends Row>(
|
|
361
|
+
<R extends Row>(
|
|
362
|
+
queryCallback: QueryCallback<S, R>,
|
|
363
|
+
options?: SqliteQueryOptions,
|
|
364
|
+
) =>
|
|
352
365
|
pipe(
|
|
353
366
|
queryCallback(kysely as Kysely.Kysely<QuerySchema<S>>).compile(),
|
|
354
|
-
(
|
|
355
|
-
if (isSqlMutation(sql))
|
|
367
|
+
(compiledQuery): SqliteQuery => {
|
|
368
|
+
if (isSqlMutation(compiledQuery.sql))
|
|
356
369
|
throw new Error(
|
|
357
370
|
"SQL mutation (INSERT, UPDATE, DELETE, etc.) isn't allowed in the Evolu `createQuery` function. Kysely suggests it because there is no read-only Kysely yet, and removing such an API is not possible. For mutations, use Evolu mutation API.",
|
|
358
371
|
);
|
|
359
|
-
|
|
360
|
-
|
|
372
|
+
const parameters = compiledQuery.parameters as NonNullable<
|
|
373
|
+
SqliteQuery["parameters"]
|
|
374
|
+
>;
|
|
375
|
+
return {
|
|
376
|
+
sql: compiledQuery.sql,
|
|
377
|
+
parameters,
|
|
378
|
+
...(options && { options }),
|
|
379
|
+
};
|
|
361
380
|
},
|
|
362
381
|
(query) => serializeQuery<R>(query),
|
|
363
382
|
);
|
|
@@ -739,7 +758,6 @@ const EvoluCommon = Layer.effect(
|
|
|
739
758
|
};
|
|
740
759
|
|
|
741
760
|
appState.init({ onRequestSync: sync });
|
|
742
|
-
|
|
743
761
|
sync();
|
|
744
762
|
|
|
745
763
|
return Evolu.of({
|
|
@@ -770,10 +788,11 @@ const EvoluCommon = Layer.effect(
|
|
|
770
788
|
restoreOwner: (mnemonic) =>
|
|
771
789
|
dbWorker.postMessage({ _tag: "reset", mnemonic }),
|
|
772
790
|
|
|
773
|
-
ensureSchema: (schema) =>
|
|
791
|
+
ensureSchema: (schema, indexes = []) =>
|
|
774
792
|
dbWorker.postMessage({
|
|
775
793
|
_tag: "ensureSchema",
|
|
776
794
|
tables: schemaToTables(schema),
|
|
795
|
+
indexes,
|
|
777
796
|
}),
|
|
778
797
|
|
|
779
798
|
sync,
|
|
@@ -815,6 +834,14 @@ export const makeCreateEvolu =
|
|
|
815
834
|
Effect.runSync,
|
|
816
835
|
),
|
|
817
836
|
);
|
|
818
|
-
|
|
837
|
+
|
|
838
|
+
const indexes = config?.indexes?.map(
|
|
839
|
+
(index): Index => ({
|
|
840
|
+
name: index.toOperationNode().name.name,
|
|
841
|
+
sql: index.compile().sql,
|
|
842
|
+
}),
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
evolu.ensureSchema(schema, indexes);
|
|
819
846
|
return evolu as Evolu<To>;
|
|
820
847
|
};
|
package/src/Public.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { sql } from "kysely";
|
|
1
2
|
export type { NotNull } from "kysely";
|
|
2
3
|
export { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
|
|
3
4
|
export type { Timestamp, TimestampError } from "./Crdt.js";
|
|
@@ -5,6 +6,7 @@ export type { InvalidMnemonicError, Mnemonic } from "./Crypto.js";
|
|
|
5
6
|
export { database, table } from "./Db.js";
|
|
6
7
|
export type { ExtractRow, QueryResult } from "./Db.js";
|
|
7
8
|
export type { EvoluError, UnexpectedError } from "./ErrorStore.js";
|
|
9
|
+
export { createIndex } from "./Evolu.js";
|
|
8
10
|
export * from "./Model.js";
|
|
9
11
|
export type { Owner, OwnerId } from "./Owner.js";
|
|
10
12
|
export { canUseDom } from "./Platform.js";
|