@evolu/common 7.4.0 → 8.0.0-next.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 (251) hide show
  1. package/README.md +4 -61
  2. package/dist/src/Array.d.ts +343 -102
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +181 -85
  5. package/dist/src/Assert.d.ts +57 -11
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +47 -11
  8. package/dist/src/BigInt.d.ts +6 -1
  9. package/dist/src/BigInt.d.ts.map +1 -1
  10. package/dist/src/BigInt.js +5 -0
  11. package/dist/src/Brand.d.ts +40 -12
  12. package/dist/src/Brand.d.ts.map +1 -1
  13. package/dist/src/Brand.js +5 -0
  14. package/dist/src/Buffer.d.ts +6 -1
  15. package/dist/src/Buffer.d.ts.map +1 -1
  16. package/dist/src/Buffer.js +7 -2
  17. package/dist/src/Cache.d.ts +10 -2
  18. package/dist/src/Cache.d.ts.map +1 -1
  19. package/dist/src/Cache.js +6 -0
  20. package/dist/src/Callbacks.d.ts +10 -7
  21. package/dist/src/Callbacks.d.ts.map +1 -1
  22. package/dist/src/Callbacks.js +23 -13
  23. package/dist/src/Console.d.ts +361 -69
  24. package/dist/src/Console.d.ts.map +1 -1
  25. package/dist/src/Console.js +217 -117
  26. package/dist/src/Crypto.d.ts +70 -21
  27. package/dist/src/Crypto.d.ts.map +1 -1
  28. package/dist/src/Crypto.js +72 -27
  29. package/dist/src/Eq.d.ts +22 -2
  30. package/dist/src/Eq.d.ts.map +1 -1
  31. package/dist/src/Eq.js +21 -1
  32. package/dist/src/Error.d.ts +23 -12
  33. package/dist/src/Error.d.ts.map +1 -1
  34. package/dist/src/Error.js +27 -11
  35. package/dist/src/Function.d.ts +128 -56
  36. package/dist/src/Function.d.ts.map +1 -1
  37. package/dist/src/Function.js +118 -14
  38. package/dist/src/Identicon.d.ts +5 -0
  39. package/dist/src/Identicon.d.ts.map +1 -1
  40. package/dist/src/Identicon.js +6 -1
  41. package/dist/src/Lookup.d.ts +160 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +192 -0
  44. package/dist/src/Microtask.d.ts +21 -0
  45. package/dist/src/Microtask.d.ts.map +1 -0
  46. package/dist/src/Microtask.js +37 -0
  47. package/dist/src/Number.d.ts +18 -4
  48. package/dist/src/Number.d.ts.map +1 -1
  49. package/dist/src/Number.js +23 -1
  50. package/dist/src/Object.d.ts +110 -5
  51. package/dist/src/Object.d.ts.map +1 -1
  52. package/dist/src/Object.js +108 -5
  53. package/dist/src/Option.d.ts +58 -0
  54. package/dist/src/Option.d.ts.map +1 -0
  55. package/dist/src/Option.js +43 -0
  56. package/dist/src/Order.d.ts +5 -0
  57. package/dist/src/Order.d.ts.map +1 -1
  58. package/dist/src/Order.js +8 -3
  59. package/dist/src/Platform.d.ts +36 -2
  60. package/dist/src/Platform.d.ts.map +1 -1
  61. package/dist/src/Platform.js +10 -6
  62. package/dist/src/Polyfills.d.ts +27 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +299 -0
  65. package/dist/src/Random.d.ts +18 -7
  66. package/dist/src/Random.d.ts.map +1 -1
  67. package/dist/src/Random.js +9 -7
  68. package/dist/src/Redacted.d.ts +7 -4
  69. package/dist/src/Redacted.d.ts.map +1 -1
  70. package/dist/src/Redacted.js +5 -0
  71. package/dist/src/Ref.d.ts +31 -16
  72. package/dist/src/Ref.d.ts.map +1 -1
  73. package/dist/src/Ref.js +35 -7
  74. package/dist/src/RefCount.d.ts +62 -0
  75. package/dist/src/RefCount.d.ts.map +1 -0
  76. package/dist/src/RefCount.js +83 -0
  77. package/dist/src/Relation.d.ts +84 -29
  78. package/dist/src/Relation.d.ts.map +1 -1
  79. package/dist/src/Relation.js +83 -105
  80. package/dist/src/Resource.d.ts +263 -0
  81. package/dist/src/Resource.d.ts.map +1 -0
  82. package/dist/src/Resource.js +389 -0
  83. package/dist/src/Result.d.ts +390 -374
  84. package/dist/src/Result.d.ts.map +1 -1
  85. package/dist/src/Result.js +142 -70
  86. package/dist/src/Schedule.d.ts +953 -0
  87. package/dist/src/Schedule.d.ts.map +1 -0
  88. package/dist/src/Schedule.js +1199 -0
  89. package/dist/src/Set.d.ts +181 -0
  90. package/dist/src/Set.d.ts.map +1 -0
  91. package/dist/src/Set.js +137 -0
  92. package/dist/src/Skiplist.d.ts +6 -1
  93. package/dist/src/Skiplist.d.ts.map +1 -1
  94. package/dist/src/Skiplist.js +5 -0
  95. package/dist/src/Sqlite.d.ts +160 -60
  96. package/dist/src/Sqlite.d.ts.map +1 -1
  97. package/dist/src/Sqlite.js +290 -190
  98. package/dist/src/Store.d.ts +36 -28
  99. package/dist/src/Store.d.ts.map +1 -1
  100. package/dist/src/Store.js +55 -15
  101. package/dist/src/String.d.ts +5 -0
  102. package/dist/src/String.d.ts.map +1 -1
  103. package/dist/src/String.js +5 -0
  104. package/dist/src/Task.d.ts +2263 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1488 -348
  107. package/dist/src/Test.d.ts +122 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +66 -0
  110. package/dist/src/Time.d.ts +201 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +190 -86
  113. package/dist/src/Tracer.d.ts +48 -0
  114. package/dist/src/Tracer.d.ts.map +1 -0
  115. package/dist/src/Tracer.js +6 -0
  116. package/dist/src/Type.d.ts +582 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +362 -240
  119. package/dist/src/Types.d.ts +160 -6
  120. package/dist/src/Types.d.ts.map +1 -1
  121. package/dist/src/Types.js +22 -2
  122. package/dist/src/WebSocket.d.ts +113 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +210 -118
  125. package/dist/src/Worker.d.ts +245 -60
  126. package/dist/src/Worker.d.ts.map +1 -1
  127. package/dist/src/Worker.js +220 -64
  128. package/dist/src/index.d.ts +25 -3
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +19 -3
  131. package/dist/src/local-first/Db.d.ts +24 -213
  132. package/dist/src/local-first/Db.d.ts.map +1 -1
  133. package/dist/src/local-first/Db.js +622 -293
  134. package/dist/src/local-first/Error.d.ts +12 -0
  135. package/dist/src/local-first/Error.d.ts.map +1 -0
  136. package/dist/src/local-first/Error.js +6 -0
  137. package/dist/src/local-first/Evolu.d.ts +330 -285
  138. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  139. package/dist/src/local-first/Evolu.js +472 -402
  140. package/dist/src/local-first/LocalAuth.d.ts +10 -10
  141. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  142. package/dist/src/local-first/LocalAuth.js +6 -1
  143. package/dist/src/local-first/Owner.d.ts +70 -27
  144. package/dist/src/local-first/Owner.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.js +27 -14
  146. package/dist/src/local-first/Protocol.d.ts +58 -50
  147. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  148. package/dist/src/local-first/Protocol.js +253 -155
  149. package/dist/src/local-first/Query.d.ts +133 -43
  150. package/dist/src/local-first/Query.d.ts.map +1 -1
  151. package/dist/src/local-first/Query.js +139 -90
  152. package/dist/src/local-first/Relay.d.ts +18 -38
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -171
  155. package/dist/src/local-first/Schema.d.ts +139 -144
  156. package/dist/src/local-first/Schema.d.ts.map +1 -1
  157. package/dist/src/local-first/Schema.js +78 -163
  158. package/dist/src/local-first/Shared.d.ts +125 -0
  159. package/dist/src/local-first/Shared.d.ts.map +1 -0
  160. package/dist/src/local-first/Shared.js +569 -0
  161. package/dist/src/local-first/Storage.d.ts +50 -53
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +121 -173
  164. package/dist/src/local-first/Timestamp.d.ts +28 -35
  165. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  166. package/dist/src/local-first/Timestamp.js +24 -25
  167. package/dist/src/local-first/index.d.ts +2 -15
  168. package/dist/src/local-first/index.d.ts.map +1 -1
  169. package/dist/src/local-first/index.js +2 -15
  170. package/package.json +36 -26
  171. package/src/Array.ts +467 -106
  172. package/src/Assert.ts +87 -12
  173. package/src/BigInt.ts +7 -1
  174. package/src/Brand.ts +41 -12
  175. package/src/Buffer.ts +10 -4
  176. package/src/Cache.ts +11 -2
  177. package/src/Callbacks.ts +33 -23
  178. package/src/Console.ts +567 -178
  179. package/src/Crypto.ts +111 -62
  180. package/src/Eq.ts +30 -3
  181. package/src/Error.ts +50 -34
  182. package/src/Function.ts +140 -68
  183. package/src/Identicon.ts +7 -1
  184. package/src/Lookup.ts +415 -0
  185. package/src/Microtask.ts +58 -0
  186. package/src/Number.ts +40 -7
  187. package/src/Object.ts +155 -16
  188. package/src/Option.ts +74 -0
  189. package/src/Order.ts +11 -3
  190. package/src/Platform.ts +43 -7
  191. package/src/Polyfills.ts +465 -0
  192. package/src/Random.ts +25 -11
  193. package/src/Redacted.ts +8 -4
  194. package/src/Ref.ts +72 -21
  195. package/src/RefCount.ts +170 -0
  196. package/src/Relation.ts +188 -136
  197. package/src/Resource.ts +864 -0
  198. package/src/Result.ts +548 -381
  199. package/src/Schedule.ts +1471 -0
  200. package/src/Set.ts +247 -0
  201. package/src/Skiplist.ts +7 -1
  202. package/src/Sqlite.ts +409 -304
  203. package/src/Store.ts +94 -48
  204. package/src/String.ts +6 -0
  205. package/src/Task.ts +3685 -740
  206. package/src/Test.ts +162 -0
  207. package/src/Time.ts +348 -145
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +1064 -630
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +311 -178
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +871 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -823
  217. package/src/local-first/LocalAuth.ts +10 -10
  218. package/src/local-first/Owner.ts +94 -42
  219. package/src/local-first/Protocol.ts +440 -303
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +91 -252
  222. package/src/local-first/Schema.ts +284 -394
  223. package/src/local-first/Shared.ts +932 -0
  224. package/src/local-first/Storage.ts +209 -282
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/LICENSE +0 -21
  228. package/dist/src/Instances.d.ts +0 -34
  229. package/dist/src/Instances.d.ts.map +0 -1
  230. package/dist/src/Instances.js +0 -44
  231. package/dist/src/Resources.d.ts +0 -118
  232. package/dist/src/Resources.d.ts.map +0 -1
  233. package/dist/src/Resources.js +0 -197
  234. package/dist/src/local-first/Platform.d.ts +0 -25
  235. package/dist/src/local-first/Platform.d.ts.map +0 -1
  236. package/dist/src/local-first/Platform.js +0 -1
  237. package/dist/src/local-first/Public.d.ts +0 -22
  238. package/dist/src/local-first/Public.d.ts.map +0 -1
  239. package/dist/src/local-first/Public.js +0 -15
  240. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  241. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  242. package/dist/src/local-first/PublicKysely.js +0 -184
  243. package/dist/src/local-first/Sync.d.ts +0 -112
  244. package/dist/src/local-first/Sync.d.ts.map +0 -1
  245. package/dist/src/local-first/Sync.js +0 -529
  246. package/src/Instances.ts +0 -90
  247. package/src/Resources.ts +0 -367
  248. package/src/local-first/Platform.ts +0 -27
  249. package/src/local-first/Public.ts +0 -42
  250. package/src/local-first/PublicKysely.ts +0 -239
  251. package/src/local-first/Sync.ts +0 -960
package/src/Sqlite.ts CHANGED
@@ -1,102 +1,108 @@
1
- import { Brand } from "./Brand.js";
2
- import { createLruCache } from "./Cache.js";
3
- import { ConsoleDep } from "./Console.js";
4
- import { EncryptionKey } from "./Crypto.js";
5
- import { Eq, eqArrayNumber } from "./Eq.js";
6
- import { createTransferableError, TransferableError } from "./Error.js";
7
- import { err, ok, Result, tryAsync, trySync } from "./Result.js";
1
+ /**
2
+ * SQLite database abstraction and query execution.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { assertNotDisposed } from "./Assert.js";
8
+ import type { Brand } from "./Brand.js";
9
+ import { bytesToHex, hexToBytes } from "./Buffer.js";
10
+ import type { EncryptionKey } from "./Crypto.js";
11
+ import type { Eq } from "./Eq.js";
12
+ import { createEqObject, eqArrayNumber, eqString } from "./Eq.js";
13
+ import { createRecord, objectToEntries } from "./Object.js";
14
+ import type { Result } from "./Result.js";
15
+ import { ok } from "./Result.js";
16
+ import type { Run, Task } from "./Task.js";
17
+ import { testCreateRun, type TestDeps } from "./Test.js";
18
+ import type { InferType, Name, Typed } from "./Type.js";
8
19
  import {
20
+ array,
9
21
  Null,
10
22
  Number,
11
- PositiveInt,
12
- SimpleName,
23
+ object,
24
+ record,
25
+ set,
13
26
  String,
27
+ testName,
14
28
  Uint8Array,
15
29
  union,
16
30
  } from "./Type.js";
17
- import { IntentionalNever, Predicate } from "./Types.js";
18
-
19
- /**
20
- * SQLite driver interface. This is the minimal interface that platform-specific
21
- * drivers must implement.
22
- */
23
- export interface SqliteDriver extends Disposable {
24
- readonly exec: (query: SqliteQuery, isMutation: boolean) => SqliteExecResult;
25
- readonly export: () => Uint8Array;
26
- }
27
-
28
- export type CreateSqliteDriver = (
29
- name: SimpleName,
30
- options?: SqliteDriverOptions,
31
- ) => Promise<SqliteDriver>;
32
-
33
- export interface CreateSqliteDriverDep {
34
- readonly createSqliteDriver: CreateSqliteDriver;
35
- }
36
-
37
- export interface SqliteDriverOptions {
38
- readonly memory?: boolean;
39
- readonly encryptionKey?: EncryptionKey | undefined;
40
- }
41
31
 
42
32
  /**
43
- * Cross-platform SQLite abstraction.
33
+ * Platform-agnostic SQLite wrapping a {@link SqliteDriver}.
44
34
  *
45
- * This API is sync only because SQLite is an embedded, single-threaded engine.
46
- * All operations are blocking and in-process, so async APIs add needless
47
- * complexity without any real benefit and are also slower. Check better-sqlite3
48
- * GitHub issues and docs for details.
35
+ * API is synchronous because it provides
36
+ * {@link https://github.com/WiseLibs/better-sqlite3/issues/262 | better concurrency}
37
+ * for SQLite.
49
38
  */
50
- export interface Sqlite extends Disposable {
39
+ export interface Sqlite extends AsyncDisposable {
51
40
  readonly exec: <R extends SqliteRow = SqliteRow>(
52
41
  query: SqliteQuery,
53
- ) => Result<SqliteExecResult<R>, SqliteError>;
42
+ ) => SqliteExecResult<R>;
54
43
 
55
44
  /**
56
45
  * Executes a transaction, running the provided callback within a begin/commit
57
- * block. If the callback returns an error (E or {@link SqliteError}), the
58
- * transaction is rolled back. If the rollback fails, a SqliteError is
59
- * returned with both the original error and rollbackError.
46
+ * block.
47
+ *
48
+ * If the callback returns a {@link Result} error, the transaction is rolled
49
+ * back and the error result is returned.
50
+ *
51
+ * If the callback returns `void`, the transaction is committed unless the
52
+ * callback throws.
60
53
  */
61
- readonly transaction: <T, E>(
62
- callback: () => Result<T, E | SqliteError>,
63
- ) => Result<T, E | SqliteError>;
54
+ readonly transaction: SqliteTransaction;
64
55
 
65
- readonly export: () => Result<Uint8Array, SqliteError>;
56
+ /**
57
+ * Returns bytes backed by {@link ArrayBuffer}.
58
+ *
59
+ * Exported databases are forwarded through worker `postMessage` transfer
60
+ * lists, which require transferable `ArrayBuffer` backing.
61
+ */
62
+ readonly export: () => Uint8Array<ArrayBuffer>;
66
63
  }
67
64
 
68
65
  export interface SqliteDep {
69
66
  readonly sqlite: Sqlite;
70
67
  }
71
68
 
69
+ export interface SqliteTransaction {
70
+ <T, E>(callback: () => Result<T, E>): Result<T, E>;
71
+ (callback: () => void): void;
72
+ }
73
+
74
+ /** Represents a SQL query to be executed on a {@link Sqlite} database. */
72
75
  export interface SqliteQuery {
73
76
  readonly sql: SafeSql;
74
77
  readonly parameters: Array<SqliteValue>;
75
78
  readonly options?: SqliteQueryOptions;
76
79
  }
77
80
 
78
- /** A type representing a sanitized SQL string. */
81
+ /** Serialized {@link SqliteQuery} used as a stable string key. */
82
+ export type SqliteQueryString = string & Brand<"SqliteQueryString">;
83
+
84
+ /** A sanitized SQL string for {@link SqliteQuery}. */
79
85
  export type SafeSql = string & Brand<"SafeSql">;
80
86
 
81
87
  /**
82
- * A value that can be stored in Sqlite.
88
+ * A value that can be stored in {@link Sqlite}.
83
89
  *
84
90
  * Note that Evolu can't support Int64 because expo-sqlite (and some others) do
85
91
  * not support it.
86
92
  */
87
- export const SqliteValue = union(Null, String, Number, Uint8Array);
93
+ export const SqliteValue = /*#__PURE__*/ union(
94
+ Null,
95
+ String,
96
+ Number,
97
+ Uint8Array,
98
+ );
88
99
  export type SqliteValue = typeof SqliteValue.Type;
89
100
 
90
- export const eqSqliteValue: Eq<SqliteValue> = (x, y) => {
91
- if (
92
- x instanceof globalThis.Uint8Array &&
93
- y instanceof globalThis.Uint8Array
94
- ) {
95
- return eqArrayNumber(x, y);
96
- }
97
- return x === y;
98
- };
101
+ /** Equality comparison for {@link SqliteValue}. */
102
+ export const eqSqliteValue: Eq<SqliteValue> = (x, y) =>
103
+ Uint8Array.is(x) && Uint8Array.is(y) ? eqArrayNumber(x, y) : x === y;
99
104
 
105
+ /** Options for configuring {@link SqliteQuery} execution behavior. */
100
106
  export interface SqliteQueryOptions {
101
107
  /**
102
108
  * If set to `true`, logs the time taken to execute the SQL query. Useful for
@@ -123,150 +129,239 @@ export interface SqliteQueryOptions {
123
129
  readonly prepare?: boolean;
124
130
  }
125
131
 
132
+ /** Converts a {@link SqliteQuery} into a stable {@link SqliteQueryString}. */
133
+ export const sqliteQueryToSqliteQueryString = (
134
+ query: SqliteQuery,
135
+ ): SqliteQueryString => {
136
+ const params = query.parameters.map((value) =>
137
+ value instanceof globalThis.Uint8Array
138
+ ? (["b", bytesToHex(value)] as const)
139
+ : (["j", value] as const),
140
+ );
141
+
142
+ const options = query.options
143
+ ? objectToEntries(query.options).toSorted(([a], [b]) => a.localeCompare(b))
144
+ : [];
145
+
146
+ return JSON.stringify([query.sql, params, options]) as SqliteQueryString;
147
+ };
148
+
149
+ /** Converts a {@link SqliteQueryString} back into a {@link SqliteQuery}. */
150
+ export const sqliteQueryStringToSqliteQuery = (
151
+ query: SqliteQueryString,
152
+ ): SqliteQuery => {
153
+ const [sql, paramsArr, optionsArr] = JSON.parse(query) as [
154
+ SafeSql,
155
+ Array<readonly ["b", string] | readonly ["j", string | number | null]>,
156
+ Array<Array<string | number | null>>,
157
+ ];
158
+
159
+ const parameters = paramsArr.map(([type, value]) =>
160
+ type === "b" ? hexToBytes(value) : value,
161
+ );
162
+
163
+ const options = optionsArr.length
164
+ ? (Object.fromEntries(optionsArr) as SqliteQueryOptions)
165
+ : undefined;
166
+
167
+ return {
168
+ sql,
169
+ parameters,
170
+ ...(options !== undefined && { options }),
171
+ };
172
+ };
173
+
126
174
  /** Result of executing a SQLite query. */
127
175
  export interface SqliteExecResult<R extends SqliteRow = SqliteRow> {
128
176
  readonly rows: ReadonlyArray<R>;
129
177
  readonly changes: number;
130
178
  }
131
179
 
132
- /** Represents an error that occurred during a SQLite operation. */
133
- export interface SqliteError {
134
- readonly type: "SqliteError";
135
- readonly error: TransferableError;
136
- readonly rollbackError?: TransferableError;
180
+ /**
181
+ * A row returned from a {@link Sqlite} query, mapping column names to
182
+ * {@link SqliteValue}.
183
+ */
184
+ export type SqliteRow = Record<string, SqliteValue>;
185
+
186
+ /**
187
+ * SQLite driver interface.
188
+ *
189
+ * Platform-specific drivers must implement this interface.
190
+ */
191
+ export interface SqliteDriver extends Disposable {
192
+ readonly exec: (query: SqliteQuery) => SqliteExecResult;
193
+
194
+ /**
195
+ * Returns bytes backed by {@link ArrayBuffer}.
196
+ *
197
+ * Exported databases are forwarded through worker `postMessage` transfer
198
+ * lists, which require transferable `ArrayBuffer` backing.
199
+ */
200
+ readonly export: () => Uint8Array<ArrayBuffer>;
137
201
  }
138
202
 
139
- export type SqliteRow = Record<string, SqliteValue>;
203
+ /** Creates a {@link SqliteDriver}. */
204
+ export type CreateSqliteDriver = (
205
+ name: Name,
206
+ options?: SqliteDriverOptions,
207
+ ) => Task<SqliteDriver>;
208
+
209
+ export interface CreateSqliteDriverDep {
210
+ createSqliteDriver: CreateSqliteDriver;
211
+ }
212
+
213
+ /**
214
+ * Options for creating a {@link CreateSqliteDriver}.
215
+ *
216
+ * Three mutually exclusive modes: in-memory (for testing), encrypted persistent
217
+ * (OPFS/file with encryption key), or persistent (default when omitted).
218
+ */
219
+ export type SqliteDriverOptions =
220
+ | { readonly mode: "memory" }
221
+ | { readonly mode: "encrypted"; readonly encryptionKey: EncryptionKey };
140
222
 
141
223
  /**
142
- * Creates a fully featured {@link Sqlite} instance from a {@link SqliteDriver}
143
- * implementation.
224
+ * Creates a {@link Sqlite} instance backed by a platform-specific driver.
225
+ *
226
+ * The driver is created via {@link CreateSqliteDriver} and wrapped with logging,
227
+ * error handling, and transaction helpers.
144
228
  */
145
229
  export const createSqlite =
146
- (deps: CreateSqliteDriverDep & Partial<ConsoleDep>) =>
147
- async (
148
- name: SimpleName,
230
+ (
231
+ name: Name,
149
232
  options?: SqliteDriverOptions,
150
- ): Promise<Result<Sqlite, SqliteError>> =>
151
- tryAsync(async () => {
152
- const driver = await deps.createSqliteDriver(name, options);
153
- let isDisposed = false;
154
-
155
- const doRollback = () =>
156
- trySync(() => {
157
- deps.console?.log("[sql] rollback");
158
- driver.exec(sql`rollback;`, true);
159
- }, createSqliteError);
160
-
161
- const sqlite: Sqlite = {
162
- exec: (query) =>
163
- trySync(
164
- () => {
165
- deps.console?.log("[sql]", { query });
166
-
167
- const result = maybeLogSqliteQueryExecutionTime(query, () =>
168
- driver.exec(query, isSqlMutation(query.sql)),
169
- );
170
-
171
- deps.console?.log("[sql]", { result });
172
-
173
- return result as IntentionalNever;
174
- },
175
- (error): SqliteError => ({
176
- type: "SqliteError",
177
- error: createTransferableError(error),
178
- }),
179
- ),
180
-
181
- transaction: (callback) => {
182
- const transactionResult = trySync(() => {
183
- deps.console?.log("[sql] begin");
184
- driver.exec(sql`begin;`, true);
185
-
186
- const result = callback();
187
- if (!result.ok) return result;
188
-
189
- deps.console?.log("[sql] commit");
190
- driver.exec(sql`commit;`, true);
191
-
192
- return result;
193
- }, createSqliteError);
194
-
195
- // There was an SqliteError during begin, callback, or commit
196
- if (!transactionResult.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: transactionResult.error.error,
203
- rollbackError: rollback.error.error,
204
- });
205
- }
206
- return transactionResult;
207
- }
208
-
209
- // Callback returned an error
210
- if (!transactionResult.value.ok) {
211
- const rollback = doRollback();
212
- if (!rollback.ok) {
213
- deps.console?.log("[sql] rollback failed", rollback.error);
214
- return err({
215
- type: "SqliteError",
216
- error: createTransferableError(transactionResult.value.error),
217
- rollbackError: rollback.error.error,
218
- });
219
- }
220
- return transactionResult.value;
221
- }
222
-
223
- return ok(transactionResult.value.value);
224
- },
225
-
226
- export: () =>
227
- trySync(
228
- () => {
229
- return driver.export();
230
- },
231
- (error): SqliteError => ({
232
- type: "SqliteError",
233
- error: createTransferableError(error),
234
- }),
235
- ),
236
-
237
- [Symbol.dispose]: () => {
238
- if (isDisposed) return;
239
- isDisposed = true;
240
- driver[Symbol.dispose]();
241
- },
242
- };
243
-
244
- return sqlite;
245
- }, createSqliteError);
246
-
247
- const createSqliteError = (error: unknown): SqliteError => ({
248
- type: "SqliteError",
249
- error: createTransferableError(error),
250
- });
233
+ ): Task<Sqlite, never, CreateSqliteDriverDep> =>
234
+ async (run) => {
235
+ const { createSqliteDriver } = run.deps;
236
+ const console = run.deps.console.child("sql");
237
+ await using stack = new AsyncDisposableStack();
238
+
239
+ const driverResult = await run(createSqliteDriver(name, options));
240
+ if (!driverResult.ok) return driverResult;
241
+ const driver = stack.use(driverResult.value);
242
+ console.debug("SQLite driver created");
243
+
244
+ const moved = stack.move();
245
+
246
+ const sqlite: Sqlite = {
247
+ exec: <R extends SqliteRow = SqliteRow>(query: SqliteQuery) => {
248
+ assertNotDisposed(moved);
249
+ console.debug({ query });
250
+
251
+ const label =
252
+ query.options?.logQueryExecutionTime &&
253
+ `SqliteQueryExecutionTime ${query.sql}`;
254
+
255
+ if (label) console.time(label);
256
+ const result = driver.exec(query);
257
+ if (label) console.timeEnd(label);
258
+
259
+ if (query.options?.logExplainQueryPlan) {
260
+ const result = driver.exec({
261
+ ...query,
262
+ sql: `EXPLAIN QUERY PLAN ${query.sql}` as SafeSql,
263
+ });
264
+ console.log("[logExplainQueryPlan]", query);
265
+ console.log(
266
+ drawSqliteQueryPlan(
267
+ result.rows as unknown as Array<SqliteQueryPlanRow>,
268
+ ),
269
+ );
270
+ }
271
+
272
+ console.debug({ result });
273
+ return result as SqliteExecResult<R>;
274
+ },
275
+
276
+ transaction: ((callback: () => Result<unknown, unknown> | void) => {
277
+ assertNotDisposed(moved);
278
+ console.debug("begin");
279
+ driver.exec(sql`begin;`);
280
+
281
+ let shouldRollback = true;
282
+ using _rollback = {
283
+ [Symbol.dispose]: () => {
284
+ if (!shouldRollback) return;
285
+ console.debug("rollback");
286
+ driver.exec(sql`rollback;`);
287
+ },
288
+ };
289
+
290
+ const result = callback();
291
+ if (result != null && !result.ok) return result;
292
+
293
+ console.debug("commit");
294
+ driver.exec(sql`commit;`);
295
+ shouldRollback = false;
296
+
297
+ return result;
298
+ }) as SqliteTransaction,
299
+
300
+ export: () => {
301
+ assertNotDisposed(moved);
302
+ return driver.export();
303
+ },
304
+
305
+ [Symbol.asyncDispose]: () => moved.disposeAsync(),
306
+ };
307
+
308
+ return ok(sqlite);
309
+ };
251
310
 
252
- const maybeLogSqliteQueryExecutionTime = <T>(
253
- query: SqliteQuery,
254
- callback: () => T,
255
- ): T => {
256
- if (!query.options?.logQueryExecutionTime) {
257
- return callback();
258
- }
311
+ /** Creates a test setup with a in-memory {@link Sqlite}. */
312
+ export const testSetupSqlite = async (
313
+ deps: CreateSqliteDriverDep,
314
+ ): Promise<
315
+ AsyncDisposable &
316
+ SqliteDep & {
317
+ readonly run: Run<TestDeps & CreateSqliteDriverDep & SqliteDep>;
318
+ readonly sqlite: Sqlite;
319
+ }
320
+ > => {
321
+ await using stack = new AsyncDisposableStack();
322
+ const run = stack.use(testCreateRun(deps));
323
+ const sqlite = stack.use(
324
+ await run.orThrow(createSqlite(testName, { mode: "memory" })),
325
+ );
326
+ const moved = stack.move();
327
+
328
+ return {
329
+ run: run.addDeps({ sqlite }),
330
+ sqlite,
331
+ [Symbol.asyncDispose]: () => moved.disposeAsync(),
332
+ };
333
+ };
334
+
335
+ interface SqliteQueryPlanRow {
336
+ id: number;
337
+ parent: number;
338
+ detail: string;
339
+ }
259
340
 
260
- const start = performance.now();
261
- const result = callback();
262
- const elapsed = performance.now() - start;
341
+ const drawSqliteQueryPlan = (rows: Array<SqliteQueryPlanRow>): string =>
342
+ rows
343
+ .map((row) => {
344
+ let parentId = row.parent;
345
+ let indent = 0;
263
346
 
264
- // eslint-disable-next-line no-console
265
- console.log(`SqliteQueryExecutionTime: ${elapsed.toString()}ms`, query);
347
+ do {
348
+ const parent = rows.find((r) => r.id === parentId);
349
+ if (!parent) break;
350
+ parentId = parent.parent;
351
+ indent++;
352
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
353
+ } while (true);
266
354
 
267
- return result;
268
- };
355
+ return `${" ".repeat(indent)}${row.detail}`;
356
+ })
357
+ .join("\n");
269
358
 
359
+ /**
360
+ * Cache for compiled prepared statements.
361
+ *
362
+ * Statements are created on first access and reused for subsequent calls with
363
+ * the same SQL. Disposing the cache finalizes all cached statements.
364
+ */
270
365
  export interface PreparedStatements<P> extends Disposable {
271
366
  readonly get: <T extends boolean>(
272
367
  query: SqliteQuery,
@@ -274,6 +369,10 @@ export interface PreparedStatements<P> extends Disposable {
274
369
  ) => T extends true ? P : P | null;
275
370
  }
276
371
 
372
+ /**
373
+ * Creates a {@link PreparedStatements} cache backed by the given factory and
374
+ * dispose function.
375
+ */
277
376
  export const createPreparedStatementsCache = <P>(
278
377
  factory: (sql: SafeSql) => P,
279
378
  disposeFn: (statement: P) => void,
@@ -284,13 +383,13 @@ export const createPreparedStatementsCache = <P>(
284
383
  return {
285
384
  get: (query, alwaysPrepare) => {
286
385
  if (alwaysPrepare !== true && !query.options?.prepare)
287
- return null as IntentionalNever;
386
+ return null as never;
288
387
  let statement = cache.get(query.sql);
289
388
  if (!statement) {
290
389
  statement = factory(query.sql);
291
390
  cache.set(query.sql, statement);
292
391
  }
293
- return statement as IntentionalNever;
392
+ return statement as never;
294
393
  },
295
394
 
296
395
  [Symbol.dispose]: () => {
@@ -302,16 +401,21 @@ export const createPreparedStatementsCache = <P>(
302
401
  };
303
402
  };
304
403
 
305
- export interface SqlIdentifier {
306
- readonly type: "SqlIdentifier";
404
+ /** A double-quoted SQL identifier for safe column or table name interpolation. */
405
+ export interface SqlIdentifier extends Typed<"SqlIdentifier"> {
307
406
  readonly sql: SafeSql;
308
407
  }
309
408
 
310
- export interface RawSql {
311
- readonly type: "RawSql";
409
+ /**
410
+ * An unescaped SQL fragment inserted verbatim into a query.
411
+ *
412
+ * **Warning**: Use only with trusted, constant strings to avoid SQL injection.
413
+ */
414
+ export interface RawSql extends Typed<"RawSql"> {
312
415
  readonly sql: string;
313
416
  }
314
417
 
418
+ /** A parameter accepted by the {@link sql} tagged template. */
315
419
  export type SqlTemplateParam = SqliteValue | SqlIdentifier | RawSql;
316
420
 
317
421
  /**
@@ -346,7 +450,7 @@ export type SqlTemplateParam = SqliteValue | SqlIdentifier | RawSql;
346
450
  * sqlite.exec(sql`select * from users order by ${sql.raw(orderBy)};`);
347
451
  * ```
348
452
  *
349
- * ### TIP
453
+ * ## TIP
350
454
  *
351
455
  * Use `prettier-plugin-sql-cst` for SQL formatting. Like Prettier for
352
456
  * JavaScript, this plugin formats SQL expressions differently depending on
@@ -372,7 +476,7 @@ export const sql = (
372
476
  }
373
477
  }
374
478
 
375
- return { sql: sql as SafeSql, parameters: values };
479
+ return { sql: sql.trim() as SafeSql, parameters: values };
376
480
  };
377
481
 
378
482
  sql.identifier = (identifier: string): SqlIdentifier => ({
@@ -382,15 +486,14 @@ sql.identifier = (identifier: string): SqlIdentifier => ({
382
486
  });
383
487
 
384
488
  /**
385
- * Insert any string.
386
- *
387
- * Sql.raw sometimes breaks auto-formatting because the parser does not consider
388
- * it valid SQL. A workaround is to remove it, format it, and put it back.
489
+ * Insert any string verbatim.
389
490
  *
390
- * **Warning**: This is not escaped.
491
+ * **Warning**: This is not escaped. Use only with trusted, constant SQL
492
+ * fragments to avoid SQL injection.
391
493
  */
392
494
  sql.raw = (raw: string): RawSql => ({ type: "RawSql", sql: raw });
393
495
 
496
+ /** Tagged template that creates a {@link SqliteQuery} with `prepare: true`. */
394
497
  sql.prepared = (
395
498
  strings: TemplateStringsArray,
396
499
  ...parameters: Array<SqlTemplateParam>
@@ -399,124 +502,126 @@ sql.prepared = (
399
502
  return { ...query, options: { prepare: true } };
400
503
  };
401
504
 
505
+ /** Index metadata stored in `sqlite_master` for a {@link Sqlite} database. */
506
+ export const SqliteIndex = /*#__PURE__*/ object({ name: String, sql: String });
507
+ export interface SqliteIndex extends InferType<typeof SqliteIndex> {}
508
+
509
+ /** {@link Eq} instance for {@link SqliteIndex}. */
510
+ export const eqSqliteIndex: Eq<SqliteIndex> = /*#__PURE__*/ createEqObject({
511
+ name: eqString,
512
+ sql: eqString,
513
+ });
514
+
402
515
  /**
403
- * Checks if a SQL string contains mutation keywords (insert, update, delete,
404
- * etc.). Results are cached for performance.
516
+ * Full schema metadata for a {@link Sqlite} database.
517
+ *
518
+ * Includes table-column mappings and user-visible indexes.
405
519
  */
406
- export const isSqlMutation: Predicate<string> = (sql) => {
407
- /**
408
- * Without cache, "insert 1_000_000" Storage test dropped from 57742
409
- * inserts/sec to 34k. Regex we used was fast, but CodeQL flagged it as a
410
- * potential ReDoS vulnerability, so manual comment removal was the only
411
- * option. LRU cache restores performance.
412
- */
413
- const cached = isSqlMutationCache.get(sql);
414
- if (cached !== undefined) return cached;
520
+ export const SqliteSchema = /*#__PURE__*/ object({
521
+ tables: /*#__PURE__*/ record(String, /*#__PURE__*/ set(String)),
522
+ indexes: /*#__PURE__*/ array(SqliteIndex),
523
+ });
524
+ export interface SqliteSchema extends InferType<typeof SqliteSchema> {}
415
525
 
416
- const result = isSqlMutationRegEx.test(removeSqlComments(sql));
417
- isSqlMutationCache.set(sql, result);
418
- return result;
419
- };
526
+ /** Get the current SQLite schema by reading SQLite metadata. */
527
+ export const getSqliteSchema =
528
+ (deps: SqliteDep) =>
529
+ ({
530
+ excludeIndexNamePrefix,
531
+ }: {
532
+ /**
533
+ * If provided, indexes with names starting with this prefix are excluded in
534
+ * SQLite query.
535
+ */
536
+ excludeIndexNamePrefix?: string;
537
+ } = {}): SqliteSchema => {
538
+ const tables = createRecord<string, Set<string>>();
539
+
540
+ const tableAndColumnInfoRows = deps.sqlite.exec<{
541
+ tableName: string;
542
+ columnName: string;
543
+ }>(sql`
544
+ select
545
+ sqlite_master.name as tableName,
546
+ table_info.name as columnName
547
+ from
548
+ sqlite_master
549
+ join pragma_table_info(sqlite_master.name) as table_info;
550
+ `);
551
+
552
+ tableAndColumnInfoRows.rows.forEach(({ tableName, columnName }) => {
553
+ (tables[tableName] ??= new Set()).add(columnName);
554
+ });
420
555
 
421
- const isSqlMutationCache = createLruCache<string, boolean>(
422
- PositiveInt.orThrow(10_000),
423
- );
556
+ const indexNamePrefixFilter =
557
+ excludeIndexNamePrefix != null
558
+ ? ` and name not like '${excludeIndexNamePrefix.replaceAll("'", "''")}%'`
559
+ : "";
560
+
561
+ const indexesRows = deps.sqlite.exec<{ name: string; sql: string }>(
562
+ sql`
563
+ select name, sql
564
+ from sqlite_master
565
+ where
566
+ type = 'index'
567
+ and name not like 'sqlite_%'
568
+ ${sql.raw(indexNamePrefixFilter)};
569
+ `,
570
+ );
424
571
 
425
- const isSqlMutationRegEx = new RegExp(
426
- `\\b(${[
427
- "alter",
428
- "create",
429
- "delete",
430
- "drop",
431
- "insert",
432
- "replace",
433
- "update",
434
- "begin",
435
- "commit",
436
- "rollback",
437
- "pragma",
438
- "vacuum",
439
- ].join("|")})\\b`,
440
- "i",
441
- );
572
+ const indexes = indexesRows.rows.map(
573
+ (row): SqliteIndex => ({
574
+ name: row.name,
575
+ /**
576
+ * SQLite returns "CREATE INDEX" for "create index" for some reason.
577
+ * Other keywords remain unchanged. We have to normalize the casing for
578
+ * schema comparison manually.
579
+ */
580
+ sql: row.sql
581
+ .replace("CREATE INDEX", "create index")
582
+ .replace("CREATE UNIQUE INDEX", "create unique index"),
583
+ }),
584
+ );
585
+
586
+ return { tables, indexes };
587
+ };
442
588
 
443
589
  /**
444
- * Removes SQL line comments (--) from a SQL string without using regex to avoid
445
- * ReDoS vulnerabilities.
590
+ * Returns {@link SqliteSchema} and full {@link SqliteRow} table contents for
591
+ * inspection and testing.
446
592
  */
447
- const removeSqlComments = (sql: string): string => {
448
- // Fast path: if there are no comments, return the original string
449
- if (!sql.includes("--")) return sql;
450
-
451
- let result = "";
452
- let i = 0;
453
-
454
- while (i < sql.length) {
455
- // Check for comment start
456
- if (i < sql.length - 1 && sql[i] === "-" && sql[i + 1] === "-") {
457
- // Skip until end of line or end of string
458
- i += 2;
459
- while (i < sql.length && sql[i] !== "\n") {
460
- i++;
461
- }
462
- // Keep the newline if present
463
- if (i < sql.length && sql[i] === "\n") {
464
- result += "\n";
465
- i++;
466
- }
467
- } else {
468
- result += sql[i];
469
- i++;
470
- }
471
- }
472
-
473
- return result;
474
- };
475
-
476
- export interface SqliteQueryPlanRow {
477
- id: number;
478
- parent: number;
479
- detail: string;
593
+ export interface SqliteSnapshot {
594
+ readonly schema: SqliteSchema;
595
+ readonly tables: Array<{
596
+ readonly name: string;
597
+ readonly rows: ReadonlyArray<SqliteRow>;
598
+ }>;
480
599
  }
481
600
 
482
- export const explainSqliteQueryPlan =
483
- (deps: SqliteDep) =>
484
- (query: SqliteQuery): Result<void, SqliteError> => {
485
- const result = deps.sqlite.exec({
486
- ...query,
487
- sql: `EXPLAIN QUERY PLAN ${query.sql}` as SafeSql,
488
- });
489
- if (!result.ok) return result;
490
-
491
- // eslint-disable-next-line no-console
492
- console.log("[explainSqliteQueryPlan]", query);
493
- // eslint-disable-next-line no-console
494
- console.log(
495
- drawSqliteQueryPlan(
496
- result.value.rows as unknown as Array<SqliteQueryPlanRow>,
497
- ),
498
- );
601
+ /**
602
+ * Captures a full {@link SqliteSnapshot} for testing and diagnostics.
603
+ *
604
+ * The snapshot includes current {@link SqliteSchema} and all rows from every
605
+ * discovered table. Table order follows `schema.tables` iteration order.
606
+ */
607
+ export const getSqliteSnapshot = (deps: SqliteDep): SqliteSnapshot => {
608
+ const schema = getSqliteSchema(deps)();
499
609
 
500
- return ok();
501
- };
610
+ const tables: SqliteSnapshot["tables"] = [];
502
611
 
503
- const drawSqliteQueryPlan = (rows: Array<SqliteQueryPlanRow>): string =>
504
- rows
505
- .map((row) => {
506
- let parentId = row.parent;
507
- let indent = 0;
612
+ for (const tableName in schema.tables) {
613
+ const result = deps.sqlite.exec(sql`
614
+ select * from ${sql.identifier(tableName)};
615
+ `);
508
616
 
509
- do {
510
- const parent = rows.find((r) => r.id === parentId);
511
- if (!parent) break;
512
- parentId = parent.parent;
513
- indent++;
514
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-constant-condition
515
- } while (true);
617
+ tables.push({
618
+ name: tableName,
619
+ rows: result.rows,
620
+ });
621
+ }
516
622
 
517
- return `${" ".repeat(indent)}${row.detail}`;
518
- })
519
- .join("\n");
623
+ return { schema, tables };
624
+ };
520
625
 
521
626
  /**
522
627
  * SQLite represents boolean values using `0` (false) and `1` (true) instead of
@@ -524,14 +629,14 @@ const drawSqliteQueryPlan = (rows: Array<SqliteQueryPlanRow>): string =>
524
629
  *
525
630
  * See: https://www.sqlite.org/quirks.html#no_separate_boolean_datatype
526
631
  *
527
- * ### Tips
632
+ * ## Tips
528
633
  *
529
634
  * - Use {@link sqliteTrue} and {@link sqliteFalse} constants for better
530
635
  * readability.
531
636
  * - Use {@link booleanToSqliteBoolean} and {@link sqliteBooleanToBoolean} for
532
637
  * converting between JavaScript booleans and SQLite boolean values.
533
638
  */
534
- export const SqliteBoolean = union(0, 1);
639
+ export const SqliteBoolean = /*#__PURE__*/ union(0, 1);
535
640
  export type SqliteBoolean = typeof SqliteBoolean.Type;
536
641
 
537
642
  /**