@evolu/common 4.0.5 → 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/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
- return Predicate.isUint8Array(p)
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
- export type Tables = ReadonlyArray<Table>;
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>): Tables =>
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) ? sqlite.exec("rollback") : sqlite.exec("end"),
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
- sql: insertOwner,
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 getTables: Effect.Effect<
355
- ReadonlyArray<string>,
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
- const sql = yield* _(
375
- sqlite.exec(`pragma table_info (${name})`),
376
- Effect.map((result) => result.rows),
377
- Effect.map(ReadonlyArray.map((row) => row.name as string)),
378
- Effect.map((existingColumns) =>
379
- ReadonlyArray.differenceWith(String.Equivalence)(existingColumns)(
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
- Effect.map(
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
- (newColumn) =>
386
- `alter table "${name}" add column "${newColumn}" blob;`,
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
- const createTable = ({
395
- name,
396
- columns,
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
- tables: Tables,
424
+ schema: SqliteSchema,
415
425
  ): Effect.Effect<void, never, Sqlite> =>
416
- Effect.flatMap(getTables, (existingTables) =>
417
- Effect.forEach(
418
- tables,
419
- (tableDefinition) =>
420
- existingTables.includes(tableDefinition.name)
421
- ? updateTable(tableDefinition)
422
- : createTable(tableDefinition),
423
- { discard: true },
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 { Sqlite, Value } from "./Sqlite.js";
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
- sqlite
194
- .exec(deserializeQuery(query))
195
- .pipe(Effect.map((result) => [query, result.rows] as const)),
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
- yield* _(ensureSchema(Array.from(tablesMap.values())));
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: Sql.upsertValueIntoTableRowColumn(message.table, message.column),
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
- sql: Sql.selectLastTimestampForTableRowColumn,
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
- sql: Sql.insertIntoMessagesIfNew,
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
- sql: Sql.updateOwnerTimestampAndMerkleTree,
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({ sql: Sql.deleteTableRow(table), parameters: [row] }),
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
- sql: Sql.selectMessagesToSync,
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
- const sqlite = yield* _(Sqlite);
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: ({ tables }) => ensureSchema(tables),
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 { SqliteQuery, isSqlMutation } from "./Sqlite.js";
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>(queryCallback: QueryCallback<S, R>) =>
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
- ({ sql, parameters }): SqliteQuery => {
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
- return { sql, parameters: parameters as SqliteQuery["parameters"] };
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
- evolu.ensureSchema(schema);
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";