@evolu/common 5.4.8 → 6.0.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 (242) hide show
  1. package/README.md +30 -34
  2. package/dist/src/Array.d.ts +17 -0
  3. package/dist/src/Array.d.ts.map +1 -0
  4. package/dist/src/Array.js +12 -0
  5. package/dist/src/Assert.d.ts +68 -0
  6. package/dist/src/Assert.d.ts.map +1 -0
  7. package/dist/src/Assert.js +77 -0
  8. package/dist/src/BigInt.d.ts +20 -0
  9. package/dist/src/BigInt.d.ts.map +1 -0
  10. package/dist/src/BigInt.js +18 -0
  11. package/dist/src/Buffer.d.ts +92 -0
  12. package/dist/src/Buffer.d.ts.map +1 -0
  13. package/dist/src/Buffer.js +62 -0
  14. package/dist/src/Callbacks.d.ts +20 -0
  15. package/dist/src/Callbacks.d.ts.map +1 -0
  16. package/dist/src/Callbacks.js +18 -0
  17. package/dist/src/Console.d.ts +78 -0
  18. package/dist/src/Console.d.ts.map +1 -0
  19. package/dist/src/Console.js +103 -0
  20. package/dist/src/Crypto.d.ts +72 -39
  21. package/dist/src/Crypto.d.ts.map +1 -1
  22. package/dist/src/Crypto.js +89 -54
  23. package/dist/src/Eq.d.ts +97 -0
  24. package/dist/src/Eq.d.ts.map +1 -0
  25. package/dist/src/Eq.js +167 -0
  26. package/dist/src/Error.d.ts +14 -10
  27. package/dist/src/Error.d.ts.map +1 -1
  28. package/dist/src/Error.js +43 -11
  29. package/dist/src/Evolu/Config.d.ts +69 -0
  30. package/dist/src/Evolu/Config.d.ts.map +1 -0
  31. package/dist/src/Evolu/Config.js +9 -0
  32. package/dist/src/Evolu/Db.d.ts +126 -0
  33. package/dist/src/Evolu/Db.d.ts.map +1 -0
  34. package/dist/src/Evolu/Db.js +774 -0
  35. package/dist/src/Evolu/Diff.d.ts +43 -0
  36. package/dist/src/Evolu/Diff.d.ts.map +1 -0
  37. package/dist/src/Evolu/Diff.js +95 -0
  38. package/dist/src/Evolu/Evolu.d.ts +334 -0
  39. package/dist/src/Evolu/Evolu.d.ts.map +1 -0
  40. package/dist/src/Evolu/Evolu.js +434 -0
  41. package/dist/src/Evolu/Internal.d.ts +26 -0
  42. package/dist/src/Evolu/Internal.d.ts.map +1 -0
  43. package/dist/src/Evolu/Internal.js +25 -0
  44. package/dist/src/Evolu/Kysely.d.ts +6 -0
  45. package/dist/src/Evolu/Kysely.d.ts.map +1 -0
  46. package/dist/src/Evolu/Kysely.js +21 -0
  47. package/dist/src/Evolu/Owner.d.ts +155 -0
  48. package/dist/src/Evolu/Owner.d.ts.map +1 -0
  49. package/dist/src/Evolu/Owner.js +126 -0
  50. package/dist/src/Evolu/Platform.d.ts +23 -0
  51. package/dist/src/Evolu/Platform.d.ts.map +1 -0
  52. package/dist/src/Evolu/Platform.js +1 -0
  53. package/dist/src/Evolu/Protocol.d.ts +401 -0
  54. package/dist/src/Evolu/Protocol.d.ts.map +1 -0
  55. package/dist/src/Evolu/Protocol.js +1151 -0
  56. package/dist/src/Evolu/Public.d.ts +18 -0
  57. package/dist/src/Evolu/Public.d.ts.map +1 -0
  58. package/dist/src/Evolu/Public.js +11 -0
  59. package/dist/src/Evolu/PublicKysely.d.ts +148 -0
  60. package/dist/src/Evolu/PublicKysely.d.ts.map +1 -0
  61. package/dist/src/Evolu/PublicKysely.js +185 -0
  62. package/dist/src/Evolu/Query.d.ts +63 -0
  63. package/dist/src/Evolu/Query.d.ts.map +1 -0
  64. package/dist/src/Evolu/Query.js +61 -0
  65. package/dist/src/Evolu/Relay.d.ts +13 -0
  66. package/dist/src/Evolu/Relay.d.ts.map +1 -0
  67. package/dist/src/Evolu/Relay.js +109 -0
  68. package/dist/src/Evolu/Schema.d.ts +201 -0
  69. package/dist/src/Evolu/Schema.d.ts.map +1 -0
  70. package/dist/src/Evolu/Schema.js +150 -0
  71. package/dist/src/Evolu/Storage.d.ts +49 -0
  72. package/dist/src/Evolu/Storage.d.ts.map +1 -0
  73. package/dist/src/Evolu/Storage.js +1111 -0
  74. package/dist/src/Evolu/Sync.d.ts +59 -0
  75. package/dist/src/Evolu/Sync.d.ts.map +1 -0
  76. package/dist/src/Evolu/Sync.js +29 -0
  77. package/dist/src/Evolu/Timestamp.d.ts +106 -0
  78. package/dist/src/Evolu/Timestamp.d.ts.map +1 -0
  79. package/dist/src/Evolu/Timestamp.js +179 -0
  80. package/dist/src/Function.d.ts +54 -0
  81. package/dist/src/Function.d.ts.map +1 -0
  82. package/dist/src/Function.js +38 -0
  83. package/dist/src/ManyToManyMap.d.ts +26 -0
  84. package/dist/src/ManyToManyMap.d.ts.map +1 -0
  85. package/dist/src/ManyToManyMap.js +92 -0
  86. package/dist/src/NanoId.d.ts +27 -0
  87. package/dist/src/NanoId.d.ts.map +1 -0
  88. package/dist/src/NanoId.js +6 -0
  89. package/dist/src/Number.d.ts +42 -0
  90. package/dist/src/Number.d.ts.map +1 -0
  91. package/dist/src/Number.js +55 -0
  92. package/dist/src/Object.d.ts +35 -0
  93. package/dist/src/Object.d.ts.map +1 -0
  94. package/dist/src/Object.js +36 -0
  95. package/dist/src/Order.d.ts +90 -0
  96. package/dist/src/Order.d.ts.map +1 -0
  97. package/dist/src/Order.js +85 -0
  98. package/dist/src/Promise.d.ts +180 -0
  99. package/dist/src/Promise.d.ts.map +1 -0
  100. package/dist/src/Promise.js +176 -0
  101. package/dist/src/Random.d.ts +52 -0
  102. package/dist/src/Random.d.ts.map +1 -0
  103. package/dist/src/Random.js +29 -0
  104. package/dist/src/Ref.d.ts +40 -0
  105. package/dist/src/Ref.d.ts.map +1 -0
  106. package/dist/src/Ref.js +13 -0
  107. package/dist/src/Result.d.ts +421 -0
  108. package/dist/src/Result.d.ts.map +1 -0
  109. package/dist/src/Result.js +357 -0
  110. package/dist/src/Skiplist.d.ts +23 -0
  111. package/dist/src/Skiplist.d.ts.map +1 -0
  112. package/dist/src/Skiplist.js +58 -0
  113. package/dist/src/Sqlite.d.ts +116 -52
  114. package/dist/src/Sqlite.d.ts.map +1 -1
  115. package/dist/src/Sqlite.js +183 -67
  116. package/dist/src/Store.d.ts +45 -8
  117. package/dist/src/Store.d.ts.map +1 -1
  118. package/dist/src/Store.js +33 -17
  119. package/dist/src/String.d.ts +2 -0
  120. package/dist/src/String.d.ts.map +1 -0
  121. package/dist/src/String.js +14 -0
  122. package/dist/src/Time.d.ts +20 -0
  123. package/dist/src/Time.d.ts.map +1 -0
  124. package/dist/src/Time.js +25 -0
  125. package/dist/src/Type.d.ts +1937 -0
  126. package/dist/src/Type.d.ts.map +1 -0
  127. package/dist/src/Type.js +2002 -0
  128. package/dist/src/Types.d.ts +188 -0
  129. package/dist/src/Types.d.ts.map +1 -0
  130. package/dist/src/Types.js +6 -0
  131. package/dist/src/WebSocket.d.ts +112 -0
  132. package/dist/src/WebSocket.d.ts.map +1 -0
  133. package/dist/src/WebSocket.js +139 -0
  134. package/dist/src/Worker.d.ts +44 -0
  135. package/dist/src/Worker.d.ts.map +1 -0
  136. package/dist/src/Worker.js +66 -0
  137. package/dist/src/index.d.ts +24 -11
  138. package/dist/src/index.d.ts.map +1 -1
  139. package/dist/src/index.js +24 -11
  140. package/package.json +29 -38
  141. package/src/Array.ts +39 -0
  142. package/src/Assert.ts +116 -0
  143. package/src/BigInt.ts +29 -0
  144. package/src/Buffer.ts +175 -0
  145. package/src/Callbacks.ts +43 -0
  146. package/src/Console.ts +159 -0
  147. package/src/Crypto.ts +169 -115
  148. package/src/Eq.ts +204 -0
  149. package/src/Error.ts +57 -20
  150. package/src/Evolu/Config.ts +83 -0
  151. package/src/Evolu/Db.ts +1275 -0
  152. package/src/Evolu/Diff.ts +142 -0
  153. package/src/Evolu/Evolu.ts +947 -0
  154. package/src/Evolu/Internal.ts +26 -0
  155. package/src/Evolu/Kysely.ts +38 -0
  156. package/src/Evolu/Owner.ts +296 -0
  157. package/src/Evolu/Platform.ts +27 -0
  158. package/src/Evolu/Protocol.ts +1857 -0
  159. package/src/Evolu/Public.ts +43 -0
  160. package/src/Evolu/PublicKysely.ts +240 -0
  161. package/src/Evolu/Query.ts +167 -0
  162. package/src/Evolu/Relay.ts +142 -0
  163. package/src/Evolu/Schema.ts +417 -0
  164. package/src/Evolu/Storage.ts +1281 -0
  165. package/src/Evolu/Sync.ts +105 -0
  166. package/src/Evolu/Timestamp.ts +311 -0
  167. package/src/Function.ts +58 -0
  168. package/src/ManyToManyMap.ts +140 -0
  169. package/src/NanoId.ts +39 -0
  170. package/src/Number.ts +90 -0
  171. package/src/Object.ts +64 -0
  172. package/src/Order.ts +113 -0
  173. package/src/Promise.ts +295 -0
  174. package/src/Random.ts +68 -0
  175. package/src/Ref.ts +63 -0
  176. package/src/Result.ts +453 -0
  177. package/src/Skiplist.ts +102 -0
  178. package/src/Sqlite.ts +366 -153
  179. package/src/Store.ts +79 -36
  180. package/src/String.ts +10 -0
  181. package/src/Time.ts +36 -0
  182. package/src/Type.ts +3978 -0
  183. package/src/Types.ts +209 -0
  184. package/src/WebSocket.ts +273 -0
  185. package/src/Worker.ts +129 -0
  186. package/src/index.ts +24 -11
  187. package/dist/src/Config.d.ts +0 -56
  188. package/dist/src/Config.d.ts.map +0 -1
  189. package/dist/src/Config.js +0 -39
  190. package/dist/src/Crdt.d.ts +0 -89
  191. package/dist/src/Crdt.d.ts.map +0 -1
  192. package/dist/src/Crdt.js +0 -181
  193. package/dist/src/Db.d.ts +0 -107
  194. package/dist/src/Db.d.ts.map +0 -1
  195. package/dist/src/Db.js +0 -443
  196. package/dist/src/Diff.d.ts +0 -27
  197. package/dist/src/Diff.d.ts.map +0 -1
  198. package/dist/src/Diff.js +0 -84
  199. package/dist/src/Evolu.d.ts +0 -426
  200. package/dist/src/Evolu.d.ts.map +0 -1
  201. package/dist/src/Evolu.js +0 -333
  202. package/dist/src/Model.d.ts +0 -141
  203. package/dist/src/Model.d.ts.map +0 -1
  204. package/dist/src/Model.js +0 -125
  205. package/dist/src/Murmurhash.d.ts +0 -2
  206. package/dist/src/Murmurhash.d.ts.map +0 -1
  207. package/dist/src/Murmurhash.js +0 -60
  208. package/dist/src/Owner.d.ts +0 -33
  209. package/dist/src/Owner.d.ts.map +0 -1
  210. package/dist/src/Owner.js +0 -26
  211. package/dist/src/Platform.d.ts +0 -37
  212. package/dist/src/Platform.d.ts.map +0 -1
  213. package/dist/src/Platform.js +0 -11
  214. package/dist/src/Protobuf.d.ts +0 -81
  215. package/dist/src/Protobuf.d.ts.map +0 -1
  216. package/dist/src/Protobuf.js +0 -92
  217. package/dist/src/Public.d.ts +0 -13
  218. package/dist/src/Public.d.ts.map +0 -1
  219. package/dist/src/Public.js +0 -6
  220. package/dist/src/Socket.d.ts +0 -8
  221. package/dist/src/Socket.d.ts.map +0 -1
  222. package/dist/src/Socket.js +0 -51
  223. package/dist/src/Sql.d.ts +0 -12
  224. package/dist/src/Sql.d.ts.map +0 -1
  225. package/dist/src/Sql.js +0 -30
  226. package/dist/src/Sync.d.ts +0 -70
  227. package/dist/src/Sync.d.ts.map +0 -1
  228. package/dist/src/Sync.js +0 -127
  229. package/src/Config.ts +0 -119
  230. package/src/Crdt.ts +0 -361
  231. package/src/Db.ts +0 -955
  232. package/src/Diff.ts +0 -114
  233. package/src/Evolu.ts +0 -1016
  234. package/src/Model.ts +0 -233
  235. package/src/Murmurhash.ts +0 -70
  236. package/src/Owner.ts +0 -69
  237. package/src/Platform.ts +0 -47
  238. package/src/Protobuf.ts +0 -155
  239. package/src/Public.ts +0 -12
  240. package/src/Socket.ts +0 -83
  241. package/src/Sql.ts +0 -41
  242. package/src/Sync.ts +0 -315
package/src/Sqlite.ts CHANGED
@@ -1,166 +1,353 @@
1
- import * as Console from "effect/Console";
2
- import * as Context from "effect/Context";
3
- import * as Effect from "effect/Effect";
4
- import * as Exit from "effect/Exit";
5
- import * as Layer from "effect/Layer";
6
- import * as Predicate from "effect/Predicate";
7
- import { Config } from "./Config.js";
8
-
9
- export interface Sqlite {
10
- readonly exec: (query: SqliteQuery) => Effect.Effect<SqliteExecResult>;
11
-
12
- readonly transaction: (
13
- /**
14
- * Use `exclusive` for mutations and `shared` for read-only queries. This
15
- * shared/exclusive lock pattern allows multiple simultaneous readers but
16
- * only one writer. In Evolu, this pattern also ensures that every write can
17
- * be immediately read without waiting to complete. For example, we can add
18
- * data on one page and then immediately redirect to another, and the data
19
- * will be there.
20
- *
21
- * There is also a `last` mode that ensures no other transaction can run.
22
- * It's for Db reset to ensure no data are accidentally saved after database
23
- * wipe-out.
24
- */
25
- mode: SqliteTransactionMode,
26
- ) => <A, E, R>(
27
- effect: Effect.Effect<A, E, R>,
28
- ) => Effect.Effect<A, E, Sqlite | R>;
29
-
30
- readonly export: () => Effect.Effect<Uint8Array>;
1
+ import { ConsoleDep } from "./Console.js";
2
+ import { createTransferableError, TransferableError } from "./Error.js";
3
+ import { err, ok, Result, tryAsync, trySync } from "./Result.js";
4
+ import {
5
+ Boolean,
6
+ Null,
7
+ Number,
8
+ SimpleName,
9
+ String,
10
+ transform,
11
+ Uint8Array,
12
+ union,
13
+ } from "./Type.js";
14
+ import { Brand, Predicate, IntentionalNever } from "./Types.js";
15
+
16
+ /**
17
+ * SQLite driver interface. This is the minimal interface that platform-specific
18
+ * drivers must implement.
19
+ */
20
+ export interface SqliteDriver extends Disposable {
21
+ readonly exec: (query: SqliteQuery, isMutation: boolean) => SqliteExecResult;
22
+ readonly export: () => Uint8Array;
31
23
  }
32
24
 
33
- export const Sqlite = Context.GenericTag<Sqlite>("Sqlite");
25
+ export type CreateSqliteDriver = (
26
+ name: SimpleName,
27
+ options?: SqliteDriverOptions,
28
+ ) => Promise<SqliteDriver>;
34
29
 
35
- export type SqliteTransactionMode = "exclusive" | "shared" | "last";
30
+ export interface CreateSqliteDriverDep {
31
+ readonly createSqliteDriver: CreateSqliteDriver;
32
+ }
36
33
 
37
- export class SqliteFactory extends Context.Tag("SqliteFactory")<
38
- SqliteFactory,
39
- {
40
- readonly createSqlite: Effect.Effect<Sqlite, never, Config>;
41
- }
42
- >() {
43
- static Common = Layer.effect(
44
- SqliteFactory,
45
- Effect.map(SqliteFactory, (platformSqliteFactory) => ({
46
- createSqlite: Effect.logTrace("SqliteFactory createSqlite").pipe(
47
- Effect.zipRight(platformSqliteFactory.createSqlite),
48
- Effect.map(
49
- (platformSqlite): Sqlite => ({
50
- ...platformSqlite,
51
-
52
- exec: (query) =>
53
- platformSqlite.exec(query).pipe(
54
- Effect.tap((result) => {
55
- maybeParseJson(result.rows);
56
- }),
57
- Effect.tap((result) =>
58
- ["begin", "rollback", "commit"].includes(query.sql)
59
- ? Effect.logDebug(`SQLiteCommon ${query.sql} transaction`)
60
- : Effect.logDebug(["SQLiteCommon exec", query, result]),
61
- ),
62
- ),
63
-
64
- transaction: (mode) => (effect) => {
65
- // Shared is for readonly queries.
66
- if (mode === "shared")
67
- return platformSqlite.transaction(mode)(effect);
68
- return Effect.flatMap(Sqlite, (sqlite) =>
69
- Effect.acquireUseRelease(
70
- sqlite.exec({ sql: "begin" }),
71
- () => effect,
72
- (_, exit) =>
73
- Exit.isFailure(exit)
74
- ? sqlite.exec({ sql: "rollback" })
75
- : sqlite.exec({ sql: "commit" }),
76
- ),
77
- ).pipe(platformSqlite.transaction(mode));
78
- },
79
- }),
80
- ),
81
- ),
82
- })),
83
- );
34
+ export interface SqliteDriverOptions {
35
+ memory?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Cross-platform SQLite abstraction.
40
+ *
41
+ * This API is sync only because SQLite is an embedded, single-threaded engine.
42
+ * All operations are blocking and in-process, so async APIs add needless
43
+ * complexity without any real benefit and are also slower. Check better-sqlite3
44
+ * GitHub issues and docs for details.
45
+ */
46
+ export interface Sqlite extends Disposable {
47
+ readonly exec: <R extends SqliteRow = SqliteRow>(
48
+ query: SqliteQuery,
49
+ ) => Result<SqliteExecResult<R>, SqliteError>;
50
+
51
+ /**
52
+ * Executes a transaction, running the provided callback within a begin/commit
53
+ * block. If the callback returns an error (E or {@link SqliteError}), the
54
+ * transaction is rolled back. If the rollback fails, a SqliteError is
55
+ * returned with both the original error and rollbackError.
56
+ */
57
+ readonly transaction: <T, E>(
58
+ callback: () => Result<T, E | SqliteError>,
59
+ ) => Result<T, E | SqliteError>;
60
+
61
+ readonly export: () => Result<Uint8Array, SqliteError>;
62
+ }
63
+
64
+ export interface SqliteDep {
65
+ readonly sqlite: Sqlite;
84
66
  }
85
67
 
86
68
  export interface SqliteQuery {
87
- readonly sql: string;
88
- readonly parameters?: Value[];
69
+ readonly sql: SafeSql;
70
+ readonly parameters: Array<SqliteValue>;
89
71
  readonly options?: SqliteQueryOptions;
90
72
  }
91
73
 
74
+ /** A type representing a sanitized SQL string. */
75
+ export type SafeSql = string & Brand<"TimestampString">;
76
+
77
+ /**
78
+ * A value that can be stored in Sqlite.
79
+ *
80
+ * Note that Evolu can't support Int64 because expo-sqlite (and some others) do
81
+ * not support it.
82
+ */
83
+ export const SqliteValue = union(Null, String, Number, Uint8Array);
84
+ export type SqliteValue = typeof SqliteValue.Type;
85
+
92
86
  export interface SqliteQueryOptions {
87
+ /**
88
+ * If set to `true`, logs the time taken to execute the SQL query. Useful for
89
+ * performance monitoring and identifying slow queries.
90
+ */
93
91
  readonly logQueryExecutionTime?: boolean;
94
- /** https://www.sqlite.org/eqp.html */
92
+
93
+ /**
94
+ * If set to `true`, logs the SQLite Explain Query Plan (EQP) for the query.
95
+ * This can help analyze how SQLite plans to execute the query and identify
96
+ * potential optimizations.
97
+ *
98
+ * See: {@link https://www.sqlite.org/eqp.html}.
99
+ */
95
100
  readonly logExplainQueryPlan?: boolean;
96
- }
97
101
 
98
- export type Value = SqliteValue | JsonObjectOrArray;
99
- export type SqliteValue = null | string | number | Uint8Array;
100
- export type JsonObjectOrArray = JsonObject | JsonArray;
101
- type JsonObject = { [key: string]: Json };
102
- type JsonArray = ReadonlyArray<Json>;
103
- type JsonPrimitive = string | number | boolean | null;
104
- type Json = JsonPrimitive | JsonObject | JsonArray;
102
+ /**
103
+ * If set to `true`, explicitly prepares the query before execution. Prepared
104
+ * statements can improve performance for repeated queries by reusing the
105
+ * compiled query.
106
+ *
107
+ * See: {@link https://sqlite.org/wasm/doc/trunk/api-oo1.md#db-prepare}.
108
+ */
109
+ readonly prepare?: boolean;
110
+ }
105
111
 
106
- export interface SqliteExecResult {
107
- readonly rows: SqliteRow[];
112
+ /** Result of executing a SQLite query. */
113
+ export interface SqliteExecResult<R extends SqliteRow = SqliteRow> {
114
+ readonly rows: ReadonlyArray<R>;
108
115
  readonly changes: number;
109
116
  }
110
117
 
118
+ /** Represents an error that occurred during a SQLite operation. */
119
+ export interface SqliteError {
120
+ readonly type: "SqliteError";
121
+ readonly error: TransferableError;
122
+ readonly rollbackError?: TransferableError;
123
+ }
124
+
111
125
  export type SqliteRow = Record<string, SqliteValue>;
112
126
 
113
- export const isJsonObjectOrArray: Predicate.Refinement<
114
- Value,
115
- JsonObjectOrArray
116
- > = (value): value is JsonObjectOrArray =>
117
- value !== null && typeof value === "object" && !Predicate.isUint8Array(value);
118
-
119
- export const valuesToSqliteValues = (
120
- values: ReadonlyArray<Value>,
121
- ): SqliteValue[] =>
122
- values.map((value) =>
123
- isJsonObjectOrArray(value) ? JSON.stringify(value) : value,
124
- );
125
-
126
- /** This function mutates for better performance. */
127
- export const maybeParseJson = (rows: SqliteRow[]): void => {
128
- parseArray(rows);
129
- };
127
+ /**
128
+ * Creates a fully featured {@link Sqlite} instance from a {@link SqliteDriver}
129
+ * implementation.
130
+ */
131
+ export const createSqlite =
132
+ (deps: CreateSqliteDriverDep & Partial<ConsoleDep>) =>
133
+ async (
134
+ name: SimpleName,
135
+ options?: SqliteDriverOptions,
136
+ ): Promise<Result<Sqlite, SqliteError>> =>
137
+ tryAsync(async () => {
138
+ const driver = await deps.createSqliteDriver(name, options);
139
+ let isDisposed = false;
140
+
141
+ const doRollback = () =>
142
+ trySync(() => {
143
+ deps.console?.log("[sql] rollback");
144
+ driver.exec(sql`rollback;`, true);
145
+ }, createSqliteError);
146
+
147
+ const sqlite: Sqlite = {
148
+ exec: (query) =>
149
+ trySync(
150
+ () => {
151
+ deps.console?.log("[sql]", query);
152
+
153
+ const result = maybeLogSqliteQueryExecutionTime(query, () =>
154
+ driver.exec(query, isSqlMutation(query.sql)),
155
+ );
156
+
157
+ deps.console?.log("[sql]", result);
158
+
159
+ return result as IntentionalNever;
160
+ },
161
+ (error): SqliteError => ({
162
+ type: "SqliteError",
163
+ error: createTransferableError(error),
164
+ }),
165
+ ),
166
+
167
+ transaction: (callback) => {
168
+ const transactionResult = trySync(() => {
169
+ deps.console?.log("[sql] begin");
170
+ driver.exec(sql`begin;`, true);
171
+
172
+ const result = callback();
173
+ if (!result.ok) return result;
174
+
175
+ deps.console?.log("[sql] commit");
176
+ driver.exec(sql`commit;`, true);
177
+
178
+ return result;
179
+ }, createSqliteError);
180
+
181
+ // There was an SqliteError during begin, callback, or commit
182
+ if (!transactionResult.ok) {
183
+ const rollback = doRollback();
184
+ if (!rollback.ok) {
185
+ deps.console?.log("[sql] rollback failed", rollback.error);
186
+ return err({
187
+ type: "SqliteError",
188
+ error: transactionResult.error.error,
189
+ rollbackError: rollback.error.error,
190
+ });
191
+ }
192
+ return transactionResult;
193
+ }
194
+
195
+ // Callback returned an error
196
+ if (!transactionResult.value.ok) {
197
+ const rollback = doRollback();
198
+ if (!rollback.ok) {
199
+ deps.console?.log("[sql] rollback failed", rollback.error);
200
+ return err({
201
+ type: "SqliteError",
202
+ error: createTransferableError(transactionResult.value.error),
203
+ rollbackError: rollback.error.error,
204
+ });
205
+ }
206
+ return transactionResult.value;
207
+ }
208
+
209
+ return ok(transactionResult.value.value);
210
+ },
211
+
212
+ export: () =>
213
+ trySync(
214
+ () => {
215
+ return driver.export();
216
+ },
217
+ (error): SqliteError => ({
218
+ type: "SqliteError",
219
+ error: createTransferableError(error),
220
+ }),
221
+ ),
130
222
 
131
- const parseArray = <T>(a: T[]): T[] => {
132
- for (let i = 0; i < a.length; ++i) a[i] = parse(a[i]) as T;
133
- return a;
223
+ [Symbol.dispose]: () => {
224
+ if (isDisposed) return;
225
+ isDisposed = true;
226
+ driver[Symbol.dispose]();
227
+ },
228
+ };
229
+
230
+ return sqlite;
231
+ }, createSqliteError);
232
+
233
+ const createSqliteError = (error: unknown): SqliteError => ({
234
+ type: "SqliteError",
235
+ error: createTransferableError(error),
236
+ });
237
+
238
+ const maybeLogSqliteQueryExecutionTime = <T>(
239
+ query: SqliteQuery,
240
+ callback: () => T,
241
+ ): T => {
242
+ if (!query.options?.logQueryExecutionTime) {
243
+ return callback();
244
+ }
245
+
246
+ const start = performance.now();
247
+ const result = callback();
248
+ const elapsed = performance.now() - start;
249
+
250
+ // eslint-disable-next-line no-console
251
+ console.log(`SqliteQueryExecutionTime: ${elapsed.toString()}ms`, query);
252
+
253
+ return result;
134
254
  };
135
255
 
136
- const parse = (o: unknown): unknown => {
137
- if (Predicate.isString(o)) return parseString(o);
138
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
139
- if (Array.isArray(o)) return parseArray(o);
140
- if (typeof o === "object" && o !== null && !Predicate.isUint8Array(o))
141
- return parseObject(o as Record<string, unknown>);
142
- return o;
256
+ export interface PreparedStatements<P> extends Disposable {
257
+ readonly get: <T extends boolean>(
258
+ query: SqliteQuery,
259
+ alwaysPrepare?: T,
260
+ ) => T extends true ? P : P | null;
261
+ }
262
+
263
+ export const createPreparedStatementsCache = <P>(
264
+ factory: (sql: SafeSql) => P,
265
+ disposeFn: (statement: P) => void,
266
+ ): PreparedStatements<P> => {
267
+ let isDisposed = false;
268
+ const cache = new Map<SafeSql, P>();
269
+
270
+ return {
271
+ get: (query, alwaysPrepare) => {
272
+ if (alwaysPrepare !== true && !query.options?.prepare)
273
+ return null as IntentionalNever;
274
+ let statement = cache.get(query.sql);
275
+ if (!statement) {
276
+ statement = factory(query.sql);
277
+ cache.set(query.sql, statement);
278
+ }
279
+ return statement as IntentionalNever;
280
+ },
281
+
282
+ [Symbol.dispose]: () => {
283
+ if (isDisposed) return;
284
+ isDisposed = true;
285
+ cache.forEach(disposeFn);
286
+ cache.clear();
287
+ },
288
+ };
143
289
  };
144
290
 
145
- const parseString = (s: string): unknown => {
146
- if (maybeJson(s))
147
- try {
148
- return parse(JSON.parse(s));
149
- } catch (err) {
150
- // Nothing to do.
291
+ export interface SqlIdentifier {
292
+ type: "SqlIdentifier";
293
+ sql: SafeSql;
294
+ }
295
+
296
+ export interface RawSql {
297
+ type: "RawSql";
298
+ sql: string;
299
+ }
300
+
301
+ export type SqlTemplateParam = SqliteValue | SqlIdentifier | RawSql;
302
+
303
+ /** TODO: Docs. */
304
+ export const sql = (
305
+ strings: TemplateStringsArray,
306
+ ...parameters: Array<SqlTemplateParam>
307
+ ): SqliteQuery => {
308
+ let sql = "";
309
+ const values: Array<SqliteValue> = [];
310
+
311
+ for (let i = 0; i < strings.length; i++) {
312
+ sql += strings[i];
313
+ if (i < parameters.length) {
314
+ const param = parameters[i];
315
+ if (typeof param === "object" && param != null && "type" in param) {
316
+ sql += param.sql;
317
+ } else {
318
+ sql += "?";
319
+ values.push(param);
320
+ }
151
321
  }
152
- return s;
322
+ }
323
+
324
+ return { sql: sql as SafeSql, parameters: values };
153
325
  };
154
326
 
155
- export const maybeJson: Predicate.Predicate<string> = (value) =>
156
- value.match(/^[[{]/) != null;
327
+ sql.identifier = (identifier: string): SqlIdentifier => ({
328
+ type: "SqlIdentifier",
329
+ // From Kysely
330
+ sql: `"${identifier.replace(/"/g, '""')}"` as SafeSql,
331
+ });
332
+
333
+ /**
334
+ * Insert any string.
335
+ *
336
+ * Sql.raw sometimes breaks auto-formatting because the parser does not consider
337
+ * it valid SQL. A workaround is to remove it, format it, and put it back.
338
+ *
339
+ * **Warning**: This is not escaped.
340
+ */
341
+ sql.raw = (raw: string): RawSql => ({ type: "RawSql", sql: raw });
157
342
 
158
- const parseObject = (o: Record<string, unknown>): Record<string, unknown> => {
159
- for (const key in o) o[key] = parse(o[key]);
160
- return o;
343
+ sql.prepared = (
344
+ strings: TemplateStringsArray,
345
+ ...parameters: Array<SqlTemplateParam>
346
+ ): SqliteQuery => {
347
+ const query = sql(strings, ...parameters);
348
+ return { ...query, options: { prepare: true } };
161
349
  };
162
350
 
163
- /** This is good enough detection because SQL strings in Evolu are predictable. */
164
351
  const isSqlMutationRegEx = new RegExp(
165
352
  `\\b(${[
166
353
  "alter",
@@ -170,34 +357,45 @@ const isSqlMutationRegEx = new RegExp(
170
357
  "insert",
171
358
  "replace",
172
359
  "update",
360
+ "begin",
361
+ "commit",
362
+ "rollback",
363
+ "pragma",
364
+ "vacuum",
173
365
  ].join("|")})\\b`,
366
+ "i",
174
367
  );
175
368
 
176
- export const isSqlMutation = (sql: string): boolean =>
177
- isSqlMutationRegEx.test(sql);
178
-
179
- export const maybeLogSqliteQueryExecutionTime =
180
- (query: SqliteQuery) =>
181
- <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => {
182
- if (!query.options?.logQueryExecutionTime) return effect;
183
- return Effect.Do.pipe(
184
- Effect.let("start", () => performance.now()),
185
- Effect.bind("result", () => effect),
186
- Effect.let("elapsed", ({ start }) => performance.now() - start),
187
- Effect.tap(({ elapsed }) =>
188
- Console.log(`QueryExecutionTime: ${elapsed}ms`, query),
189
- ),
190
- Effect.map(({ result }) => result),
191
- );
192
- };
369
+ export const isSqlMutation: Predicate<string> = (sql) =>
370
+ isSqlMutationRegEx.test(sql.replace(/--.*$/gm, ""));
193
371
 
194
- export type SqliteQueryPlanRow = {
372
+ export interface SqliteQueryPlanRow {
195
373
  id: number;
196
374
  parent: number;
197
375
  detail: string;
198
- };
376
+ }
377
+
378
+ export const explainSqliteQueryPlan =
379
+ (deps: SqliteDep) =>
380
+ (query: SqliteQuery): Result<void, SqliteError> => {
381
+ const result = deps.sqlite.exec({
382
+ ...query,
383
+ sql: `EXPLAIN QUERY PLAN ${query.sql}` as SafeSql,
384
+ });
385
+ if (!result.ok) return result;
386
+
387
+ // eslint-disable-next-line no-console
388
+ console.log("ExplainQueryPlan", query);
389
+ // eslint-disable-next-line no-console
390
+ console.log(
391
+ drawSqliteQueryPlan(
392
+ result.value.rows as unknown as Array<SqliteQueryPlanRow>,
393
+ ),
394
+ );
395
+ return ok();
396
+ };
199
397
 
200
- export const drawSqliteQueryPlan = (rows: SqliteQueryPlanRow[]): string =>
398
+ const drawSqliteQueryPlan = (rows: Array<SqliteQueryPlanRow>): string =>
201
399
  rows
202
400
  .map((row) => {
203
401
  let parentId = row.parent;
@@ -208,9 +406,24 @@ export const drawSqliteQueryPlan = (rows: SqliteQueryPlanRow[]): string =>
208
406
  if (!parent) break;
209
407
  parentId = parent.parent;
210
408
  indent++;
211
- // eslint-disable-next-line no-constant-condition
409
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
212
410
  } while (true);
213
411
 
214
412
  return `${" ".repeat(indent)}${row.detail}`;
215
413
  })
216
414
  .join("\n");
415
+
416
+ /**
417
+ * SQLite represents boolean values using `0` (false) and `1` (true) instead of
418
+ * a dedicated boolean type. This transform Type ensures that values conform to
419
+ * SQLite's boolean representation.
420
+ *
421
+ * See: https://www.sqlite.org/quirks.html#no_separate_boolean_datatype
422
+ */
423
+ export const SqliteBoolean = transform(
424
+ Boolean,
425
+ union(0, 1),
426
+ (value) => ok(value ? (1 as const) : (0 as const)),
427
+ (value) => value === 1,
428
+ );
429
+ export type SqliteBoolean = typeof SqliteBoolean.Type;