@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/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/Model.ts CHANGED
@@ -1,13 +1,9 @@
1
1
  import * as S from "@effect/schema/Schema";
2
- import * as Brand from "effect/Brand";
3
2
  import { maybeJson } from "./Sqlite.js";
4
3
 
5
- /**
6
- * Branded Id Schema for any table Id. To create Id Schema for a specific table,
7
- * use {@link id}.
8
- */
4
+ /** Branded Id Schema. To create Id Schema for a specific table, use {@link id}. */
9
5
  export const Id = S.string.pipe(S.pattern(/^[\w-]{21}$/), S.brand("Id"));
10
- export type Id = S.Schema.To<typeof Id>;
6
+ export type Id = S.Schema.Type<typeof Id>;
11
7
 
12
8
  /**
13
9
  * A factory function to create {@link Id} Schema for a specific table.
@@ -17,11 +13,11 @@ export type Id = S.Schema.To<typeof Id>;
17
13
  * import { id } from "@evolu/react";
18
14
  *
19
15
  * const TodoId = id("Todo");
20
- * type TodoId = S.Schema.To<typeof TodoId>;
16
+ * type TodoId = S.Schema.Type<typeof TodoId>;
21
17
  */
22
18
  export const id = <T extends string>(
23
19
  table: T,
24
- ): S.BrandSchema<string & Brand.Brand<"Id"> & Brand.Brand<T>, string, never> =>
20
+ ): S.brand<S.brand<S.Schema<string, string, never>, "Id">, T> =>
25
21
  Id.pipe(S.brand(table));
26
22
 
27
23
  /**
@@ -33,7 +29,7 @@ export const SqliteDate = S.string.pipe(
33
29
  S.filter((s) => !isNaN(Date.parse(s))),
34
30
  S.brand("SqliteDate"),
35
31
  );
36
- export type SqliteDate = S.Schema.To<typeof SqliteDate>;
32
+ export type SqliteDate = S.Schema.Type<typeof SqliteDate>;
37
33
 
38
34
  /**
39
35
  * SQLite doesn't support the boolean type, so Evolu uses SqliteBoolean instead.
@@ -45,7 +41,7 @@ export const SqliteBoolean = S.number.pipe(
45
41
  S.filter((s) => s === 0 || s === 1),
46
42
  S.brand("SqliteBoolean"),
47
43
  );
48
- export type SqliteBoolean = S.Schema.To<typeof SqliteBoolean>;
44
+ export type SqliteBoolean = S.Schema.Type<typeof SqliteBoolean>;
49
45
 
50
46
  /**
51
47
  * A helper for casting types not supported by SQLite. SQLite doesn't support
@@ -96,7 +92,7 @@ export const String = S.string.pipe(
96
92
  ),
97
93
  S.brand("String"),
98
94
  );
99
- export type String = S.Schema.To<typeof String>;
95
+ export type String = S.Schema.Type<typeof String>;
100
96
 
101
97
  /**
102
98
  * A string with a maximum length of 1000 characters.
@@ -108,7 +104,7 @@ export type String = S.Schema.To<typeof String>;
108
104
  * S.decode(String1000)(value);
109
105
  */
110
106
  export const String1000 = String.pipe(S.maxLength(1000), S.brand("String1000"));
111
- export type String1000 = S.Schema.To<typeof String1000>;
107
+ export type String1000 = S.Schema.Type<typeof String1000>;
112
108
 
113
109
  /**
114
110
  * A nonempty string with a maximum length of 1000 characters.
@@ -124,7 +120,7 @@ export const NonEmptyString1000 = String.pipe(
124
120
  S.maxLength(1000),
125
121
  S.brand("NonEmptyString1000"),
126
122
  );
127
- export type NonEmptyString1000 = S.Schema.To<typeof NonEmptyString1000>;
123
+ export type NonEmptyString1000 = S.Schema.Type<typeof NonEmptyString1000>;
128
124
 
129
125
  /**
130
126
  * A positive integer.
@@ -140,4 +136,4 @@ export const PositiveInt = S.number.pipe(
140
136
  S.positive(),
141
137
  S.brand("PositiveInt"),
142
138
  );
143
- export type PositiveInt = S.Schema.To<typeof PositiveInt>;
139
+ export type PositiveInt = S.Schema.Type<typeof PositiveInt>;
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";
package/src/Sql.ts CHANGED
@@ -1,123 +1,41 @@
1
- // TODO: This file should be generated from a script via Kysely.
2
- // The reason for not using Kysely directly is bundle size.
3
- // [Playground Link](https://kyse.link/?p=b&i=haFkpnNxbGl0ZaF2pjAuMjYuMKFz2gFoaW1wb3J0IHsgR2VuZXJhdGVkIH0gZnJvbSAna3lzZWx5JwoKZGVjbGFyZSBnbG9iYWwgewogIGludGVyZmFjZSBEQiB7CiAgICBldm9sdV9tZXNzYWdlOiB7CiAgICAgIHRpbWVzdGFtcDogc3RyaW5nLAogICAgICB0YWJsZTogc3RyaW5nLAogICAgICByb3c6IHN0cmluZywKICAgICAgY29sdW1uOiBzdHJpbmcsCiAgICAgIHZhbHVlOiB1bmtub3duCiAgICB9LAoKICAgIGV2b2x1X293bmVyOiB7CiAgICAgIGlkOiBzdHJpbmcKICAgICAgbW5lbW9uaWM6IHN0cmluZwogICAgICBlbmNyeXB0aW9uS2V5OiBVaW50OEFycmF5LAogICAgICB0aW1lc3RhbXA6IHN0cmluZywKICAgICAgbWVya2xlVHJlZTogc3RyaW5nCiAgICB9CiAgfQp9oXHaBnhhd2FpdCBreXNlbHkuc2VsZWN0RnJvbSgiZXZvbHVfb3duZXIiKQogIC5zZWxlY3QoWyJpZCIsICJtbmVtb25pYyIsICJlbmNyeXB0aW9uS2V5Il0pCiAgLmV4ZWN1dGUoKQoKYXdhaXQga3lzZWx5LnNjaGVtYQogIC5jcmVhdGVUYWJsZSgnZXZvbHVfbWVzc2FnZScpCiAgLmFkZENvbHVtbigndGltZXN0YW1wJywgJ2Jsb2InLCBjb2wgPT4gY29sLnByaW1hcnlLZXkoKSkKICAuYWRkQ29sdW1uKCd0YWJsZScsICdibG9iJykKICAuYWRkQ29sdW1uKCdyb3cnLCAnYmxvYicpCiAgLmFkZENvbHVtbignY29sdW1uJywgJ2Jsb2InKQogIC5hZGRDb2x1bW4oJ3ZhbHVlJywgJ2Jsb2InKQogIC5leGVjdXRlKCkKCmF3YWl0IGt5c2VseS5zY2hlbWEKICAuY3JlYXRlSW5kZXgoImluZGV4X2V2b2x1X21lc3NhZ2UiKQogIC5vbigiZXZvbHVfbWVzc2FnZSIpCiAgLmNvbHVtbnMoWyJ0YWJsZSIsICJyb3ciLCAiY29sdW1uIiwgInRpbWVzdGFtcCJdKQogIC5leGVjdXRlKCkKCmF3YWl0IGt5c2VseS5zY2hlbWEKICAuY3JlYXRlVGFibGUoJ2V2b2x1X19vd25lcicpCiAgLmFkZENvbHVtbignaWQnLCAnYmxvYicpCiAgLmFkZENvbHVtbignbW5lbW9uaWMnLCAnYmxvYicpCiAgLmFkZENvbHVtbignZW5jcnlwdGlvbktleScsICdibG9iJykKICAuYWRkQ29sdW1uKCd0aW1lc3RhbXAnLCAnYmxvYicpCiAgLmFkZENvbHVtbignbWVya2xlVHJlZScsICdibG9iJykKICAuZXhlY3V0ZSgpCgphd2FpdCBreXNlbHkuaW5zZXJ0SW50bygiZXZvbHVfb3duZXIiKQogIC52YWx1ZXMoewogICAgImlkIjogImIiLAogICAgIm1uZW1vbmljIjogImEiLAogICAgImVuY3J5cHRpb25LZXkiOiBuZXcgVWludDhBcnJheSgpLAogICAgInRpbWVzdGFtcCI6ICJhIiwKICAgICJtZXJrbGVUcmVlIjogImIiCiAgfSkKICAuZXhlY3V0ZSgpCgphd2FpdCBreXNlbHkuc2VsZWN0RnJvbSgiZXZvbHVfb3duZXIiKQogIC5zZWxlY3QoWyJ0aW1lc3RhbXAiLCAibWVya2xlVHJlZSJdKQogIC5leGVjdXRlKCkKCmF3YWl0IGt5c2VseS5zZWxlY3RGcm9tKCJldm9sdV9tZXNzYWdlIikKICAuc2VsZWN0KCJ0aW1lc3RhbXAiKQogIC53aGVyZSgndGFibGUnLCAnPScsICcxJykKICAud2hlcmUoJ3JvdycsICc9JywgJzInKQogIC53aGVyZSgnY29sdW1uJywgJz0nLCAnMycpCiAgLm9yZGVyQnkoInRpbWVzdGFtcCIsICJkZXNjIikKICAubGltaXQoMSkKICAuZXhlY3V0ZVRha2VGaXJzdCgpCgphd2FpdCBreXNlbHkuaW5zZXJ0SW50bygiZXZvbHVfbWVzc2FnZSIpCiAgLnZhbHVlcyh7CiAgICAidGltZXN0YW1wIjogJzEnLAogICAgInRhYmxlIjogJzInLAogICAgInJvdyI6ICczJywKICAgICJjb2x1bW4iOiAnNCcsCiAgICAidmFsdWUiOiAnNScsCiAgfSkKICAub25Db25mbGljdChvYyA9PiBvYy5kb05vdGhpbmcoKSkKICAuZXhlY3V0ZSgpCgphd2FpdCBreXNlbHkudXBkYXRlVGFibGUoImV2b2x1X293bmVyIikKICAuc2V0KHsKICAgICJtZXJrbGVUcmVlIjogJzEnLAogICAgInRpbWVzdGFtcCI6ICIyIgogIH0pCiAgLmV4ZWN1dGUoKQoKYXdhaXQga3lzZWx5LnNlbGVjdEZyb20oImV2b2x1X21lc3NhZ2UiKQogIC5zZWxlY3RBbGwoKQogIC53aGVyZSgidGltZXN0YW1wIiwgIj49IiwgJzEnKQogIC5vcmRlckJ5KCJ0aW1lc3RhbXAiKQogIC5leGVjdXRlKCmhY8M=)
4
-
5
- export const selectOwner = `
6
- select
7
- "id",
8
- "mnemonic",
9
- "encryptionKey"
10
- from
11
- "evolu_owner"
12
- `.trim();
13
-
14
- export const createMessageTable = `
15
- create table
16
- "evolu_message" (
17
- "timestamp" blob primary key,
18
- "table" blob,
19
- "row" blob,
20
- "column" blob,
21
- "value" blob
22
- );
23
- `.trim();
24
-
25
- export const createMessageTableIndex = `
26
- create index "index_evolu_message" on "evolu_message" (
27
- "table", "row", "column", "timestamp"
28
- );
29
- `.trim();
30
-
31
- export const createOwnerTable = `
32
- create table
33
- "evolu_owner" (
34
- "id" blob,
35
- "mnemonic" blob,
36
- "encryptionKey" blob,
37
- "timestamp" blob,
38
- "merkleTree" blob
39
- );
40
- `.trim();
41
-
42
- export const insertOwner = `
43
- insert into
44
- "evolu_owner" (
45
- "id",
46
- "mnemonic",
47
- "encryptionKey",
48
- "timestamp",
49
- "merkleTree"
50
- )
51
- values
52
- (?, ?, ?, ?, ?);
53
- `.trim();
54
-
55
- export const selectOwnerTimestampAndMerkleTree = `
56
- select
57
- "timestamp",
58
- "merkleTree"
59
- from
60
- "evolu_owner"
61
- `.trim();
62
-
63
- export const selectLastTimestampForTableRowColumn = `
64
- select
65
- "timestamp"
66
- from
67
- "evolu_message"
68
- where
69
- "table" = ?
70
- and "row" = ?
71
- and "column" = ?
72
- order by
73
- "timestamp" desc
74
- limit
75
- 1
76
- `.trim();
77
-
78
- export const upsertValueIntoTableRowColumn = (
79
- table: string,
80
- column: string,
81
- ): string =>
82
- `
83
- insert into
84
- "${table}" ("id", "${column}", "createdAt", "updatedAt")
85
- values
86
- (?, ?, ?, ?)
87
- on conflict do update set
88
- "${column}" = ?,
89
- "updatedAt" = ?
90
- `.trim();
91
-
92
- export const deleteTableRow = (table: string): string =>
93
- `
94
- delete from "${table}"
95
- where
96
- "id" = ?;
97
- `.trim();
98
-
99
- export const insertIntoMessagesIfNew = `
100
- insert into
101
- "evolu_message" ("timestamp", "table", "row", "column", "value")
102
- values
103
- (?, ?, ?, ?, ?)
104
- on conflict do nothing
105
- `.trim();
106
-
107
- export const updateOwnerTimestampAndMerkleTree = `
108
- update "evolu_owner"
109
- set
110
- "timestamp" = ?,
111
- "merkleTree" = ?
112
- `.trim();
113
-
114
- export const selectMessagesToSync = `
115
- select
116
- *
117
- from
118
- "evolu_message"
119
- where
120
- "timestamp" >= ?
121
- order by
122
- "timestamp"
123
- `.trim();
1
+ // this file is generated by generateSql script
2
+ import { SqliteQuery } from "./Sqlite.js";
3
+ export const selectOwner: SqliteQuery = {
4
+ sql: `select "id", "mnemonic", "encryptionKey" from "evolu_owner"`,
5
+ };
6
+
7
+ export const createMessageTable: SqliteQuery = {
8
+ sql: `create table "evolu_message" ("timestamp" blob primary key, "table" blob, "row" blob, "column" blob, "value" blob)`,
9
+ };
10
+
11
+ export const createMessageTableIndex: SqliteQuery = {
12
+ sql: `create index "index_evolu_message" on "evolu_message" ("table", "row", "column", "timestamp" desc)`,
13
+ };
14
+
15
+ export const createOwnerTable: SqliteQuery = {
16
+ sql: `create table "evolu_owner" ("id" blob, "mnemonic" blob, "encryptionKey" blob, "timestamp" blob, "merkleTree" blob)`,
17
+ };
18
+
19
+ export const insertOwner: SqliteQuery = {
20
+ sql: `insert into "evolu_owner" ("id", "mnemonic", "encryptionKey", "timestamp", "merkleTree") values (?, ?, ?, ?, ?)`,
21
+ };
22
+
23
+ export const selectOwnerTimestampAndMerkleTree: SqliteQuery = {
24
+ sql: `select "timestamp", "merkleTree" from "evolu_owner"`,
25
+ };
26
+
27
+ export const selectLastTimestampForTableRowColumn: SqliteQuery = {
28
+ sql: `select "timestamp" from "evolu_message" where "table" = ? and "row" = ? and "column" = ? order by "timestamp" desc limit ?`,
29
+ };
30
+
31
+ export const insertIntoMessagesIfNew: SqliteQuery = {
32
+ sql: `insert into "evolu_message" ("timestamp", "table", "row", "column", "value") values (?, ?, ?, ?, ?) on conflict do nothing`,
33
+ };
34
+
35
+ export const updateOwnerTimestampAndMerkleTree: SqliteQuery = {
36
+ sql: `update "evolu_owner" set "merkleTree" = ?, "timestamp" = ?`,
37
+ };
38
+
39
+ export const selectMessagesToSync: SqliteQuery = {
40
+ sql: `select * from "evolu_message" where "timestamp" >= ? order by "timestamp"`,
41
+ };
package/src/Sqlite.ts CHANGED
@@ -1,16 +1,24 @@
1
1
  import * as Context from "effect/Context";
2
2
  import * as Effect from "effect/Effect";
3
+ import { Equivalence } from "effect/Equivalence";
3
4
  import * as Predicate from "effect/Predicate";
4
5
 
5
6
  export interface Sqlite {
6
- readonly exec: (arg: string | SqliteQuery) => Effect.Effect<SqliteExecResult>;
7
+ readonly exec: (query: SqliteQuery) => Effect.Effect<SqliteExecResult>;
7
8
  }
8
9
 
9
10
  export const Sqlite = Context.GenericTag<Sqlite>("@services/Sqlite");
10
11
 
11
12
  export interface SqliteQuery {
12
13
  readonly sql: string;
13
- readonly parameters: Value[];
14
+ readonly parameters?: Value[];
15
+ readonly options?: SqliteQueryOptions;
16
+ }
17
+
18
+ export interface SqliteQueryOptions {
19
+ readonly logQueryExecutionTime?: boolean;
20
+ /** https://www.sqlite.org/eqp.html */
21
+ readonly logExplainQueryPlan?: boolean;
14
22
  }
15
23
 
16
24
  export type Value = SqliteValue | JsonObjectOrArray;
@@ -41,11 +49,6 @@ export const valuesToSqliteValues = (
41
49
  isJsonObjectOrArray(value) ? JSON.stringify(value) : value,
42
50
  );
43
51
 
44
- export const ensureSqliteQuery = (arg: string | SqliteQuery): SqliteQuery => {
45
- if (typeof arg !== "string") return arg;
46
- return { sql: arg, parameters: [] };
47
- };
48
-
49
52
  export const maybeParseJson = (rows: SqliteRow[]): SqliteRow[] =>
50
53
  parseArray(rows);
51
54
 
@@ -96,3 +99,58 @@ const isSqlMutationRegEx = new RegExp(
96
99
 
97
100
  export const isSqlMutation = (sql: string): boolean =>
98
101
  isSqlMutationRegEx.test(sql);
102
+
103
+ export const maybeLogSqliteQueryExecutionTime =
104
+ (query: SqliteQuery) =>
105
+ <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => {
106
+ if (!query.options?.logQueryExecutionTime) return effect;
107
+ return effect.pipe(
108
+ Effect.tap(() => Effect.log("QueryExecutionTime")),
109
+ // Not using Effect.log because of formating
110
+ // eslint-disable-next-line no-console
111
+ Effect.tap(() => console.log(query.sql)),
112
+ Effect.withLogSpan("duration"),
113
+ );
114
+ };
115
+
116
+ export type SqliteQueryPlanRow = {
117
+ id: number;
118
+ parent: number;
119
+ detail: string;
120
+ };
121
+
122
+ export const drawSqliteQueryPlan = (rows: SqliteQueryPlanRow[]): string =>
123
+ rows
124
+ .map((row) => {
125
+ let parentId = row.parent;
126
+ let indent = 0;
127
+
128
+ do {
129
+ const parent = rows.find((r) => r.id === parentId);
130
+ if (!parent) break;
131
+ parentId = parent.parent;
132
+ indent++;
133
+ // eslint-disable-next-line no-constant-condition
134
+ } while (true);
135
+
136
+ return `${" ".repeat(indent)}${row.detail}`;
137
+ })
138
+ .join("\n");
139
+
140
+ export interface SqliteSchema {
141
+ readonly tables: ReadonlyArray<Table>;
142
+ readonly indexes: ReadonlyArray<Index>;
143
+ }
144
+
145
+ export interface Table {
146
+ readonly name: string;
147
+ readonly columns: ReadonlyArray<string>;
148
+ }
149
+
150
+ export interface Index {
151
+ readonly name: string;
152
+ readonly sql: string;
153
+ }
154
+
155
+ export const indexEquivalence: Equivalence<Index> = (self, that) =>
156
+ self.name === that.name && self.sql === that.sql;