@evolu/common 8.0.0-next.4 → 8.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 (237) hide show
  1. package/dist/src/Array.d.ts +279 -173
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +105 -101
  4. package/dist/src/Assert.d.ts +44 -44
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +44 -32
  7. package/dist/src/BigInt.d.ts +6 -3
  8. package/dist/src/BigInt.d.ts.map +1 -1
  9. package/dist/src/BigInt.js +5 -2
  10. package/dist/src/Brand.d.ts +27 -40
  11. package/dist/src/Brand.d.ts.map +1 -1
  12. package/dist/src/Buffer.d.ts +27 -11
  13. package/dist/src/Buffer.d.ts.map +1 -1
  14. package/dist/src/Cache.d.ts +9 -4
  15. package/dist/src/Cache.d.ts.map +1 -1
  16. package/dist/src/Cache.js +8 -3
  17. package/dist/src/Callbacks.d.ts +30 -19
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Console.d.ts +145 -66
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +91 -43
  22. package/dist/src/Crypto.d.ts +85 -42
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +44 -20
  25. package/dist/src/Eq.d.ts +87 -21
  26. package/dist/src/Eq.d.ts.map +1 -1
  27. package/dist/src/Eq.js +67 -19
  28. package/dist/src/Error.d.ts +5 -3
  29. package/dist/src/Error.d.ts.map +1 -1
  30. package/dist/src/Error.js +16 -0
  31. package/dist/src/Function.d.ts +119 -58
  32. package/dist/src/Function.d.ts.map +1 -1
  33. package/dist/src/Function.js +86 -43
  34. package/dist/src/Http.d.ts +394 -0
  35. package/dist/src/Http.d.ts.map +1 -0
  36. package/dist/src/Http.js +102 -0
  37. package/dist/src/Identicon.d.ts +16 -7
  38. package/dist/src/Identicon.d.ts.map +1 -1
  39. package/dist/src/Identicon.js +14 -5
  40. package/dist/src/LeakDetector.d.ts +88 -0
  41. package/dist/src/LeakDetector.d.ts.map +1 -0
  42. package/dist/src/LeakDetector.js +86 -0
  43. package/dist/src/LockManager.d.ts +18 -9
  44. package/dist/src/LockManager.d.ts.map +1 -1
  45. package/dist/src/LockManager.js +7 -8
  46. package/dist/src/Lookup.d.ts +43 -13
  47. package/dist/src/Lookup.d.ts.map +1 -1
  48. package/dist/src/Lookup.js +28 -8
  49. package/dist/src/Microtask.d.ts +1 -1
  50. package/dist/src/Microtask.d.ts.map +1 -1
  51. package/dist/src/Microtask.js +3 -7
  52. package/dist/src/Number.d.ts +49 -13
  53. package/dist/src/Number.d.ts.map +1 -1
  54. package/dist/src/Number.js +34 -5
  55. package/dist/src/Object.d.ts +160 -40
  56. package/dist/src/Object.d.ts.map +1 -1
  57. package/dist/src/Object.js +92 -46
  58. package/dist/src/Option.d.ts +10 -7
  59. package/dist/src/Option.d.ts.map +1 -1
  60. package/dist/src/Option.js +9 -6
  61. package/dist/src/Order.d.ts +21 -21
  62. package/dist/src/Order.d.ts.map +1 -1
  63. package/dist/src/Order.js +18 -21
  64. package/dist/src/Platform.d.ts +31 -0
  65. package/dist/src/Platform.d.ts.map +1 -1
  66. package/dist/src/Platform.js +98 -0
  67. package/dist/src/Random.d.ts +32 -17
  68. package/dist/src/Random.d.ts.map +1 -1
  69. package/dist/src/Random.js +12 -8
  70. package/dist/src/Redacted.d.ts +43 -29
  71. package/dist/src/Redacted.d.ts.map +1 -1
  72. package/dist/src/Redacted.js +14 -3
  73. package/dist/src/Ref.d.ts +11 -3
  74. package/dist/src/Ref.d.ts.map +1 -1
  75. package/dist/src/Ref.js +2 -2
  76. package/dist/src/RefCount.d.ts +2 -6
  77. package/dist/src/RefCount.d.ts.map +1 -1
  78. package/dist/src/RefCount.js +33 -90
  79. package/dist/src/Relation.d.ts +70 -5
  80. package/dist/src/Relation.d.ts.map +1 -1
  81. package/dist/src/Relation.js +77 -2
  82. package/dist/src/Resource.d.ts +590 -155
  83. package/dist/src/Resource.d.ts.map +1 -1
  84. package/dist/src/Resource.js +592 -343
  85. package/dist/src/Result.d.ts +838 -191
  86. package/dist/src/Result.d.ts.map +1 -1
  87. package/dist/src/Result.js +163 -102
  88. package/dist/src/Schedule.d.ts +684 -231
  89. package/dist/src/Schedule.d.ts.map +1 -1
  90. package/dist/src/Schedule.js +766 -295
  91. package/dist/src/Set.d.ts +89 -40
  92. package/dist/src/Set.d.ts.map +1 -1
  93. package/dist/src/Set.js +50 -29
  94. package/dist/src/Sqlite.d.ts +29 -72
  95. package/dist/src/Sqlite.d.ts.map +1 -1
  96. package/dist/src/Sqlite.js +41 -46
  97. package/dist/src/StackTrace.d.ts +40 -0
  98. package/dist/src/StackTrace.d.ts.map +1 -0
  99. package/dist/src/StackTrace.js +140 -0
  100. package/dist/src/Store.d.ts +5 -4
  101. package/dist/src/Store.d.ts.map +1 -1
  102. package/dist/src/Store.js +5 -4
  103. package/dist/src/String.d.ts +7 -1
  104. package/dist/src/String.d.ts.map +1 -1
  105. package/dist/src/String.js +20 -7
  106. package/dist/src/Task.d.ts +3572 -1770
  107. package/dist/src/Task.d.ts.map +1 -1
  108. package/dist/src/Task.js +1879 -1274
  109. package/dist/src/Test.d.ts +17 -125
  110. package/dist/src/Test.d.ts.map +1 -1
  111. package/dist/src/Test.js +17 -68
  112. package/dist/src/Time.d.ts +108 -49
  113. package/dist/src/Time.d.ts.map +1 -1
  114. package/dist/src/Time.js +172 -73
  115. package/dist/src/Type.d.ts +3542 -1926
  116. package/dist/src/Type.d.ts.map +1 -1
  117. package/dist/src/Type.js +3070 -1784
  118. package/dist/src/Types.d.ts +200 -108
  119. package/dist/src/Types.d.ts.map +1 -1
  120. package/dist/src/Types.js +39 -8
  121. package/dist/src/WebSocket.d.ts +48 -23
  122. package/dist/src/WebSocket.d.ts.map +1 -1
  123. package/dist/src/WebSocket.js +15 -6
  124. package/dist/src/Worker.d.ts +94 -36
  125. package/dist/src/Worker.d.ts.map +1 -1
  126. package/dist/src/Worker.js +58 -24
  127. package/dist/src/index.d.ts +55 -52
  128. package/dist/src/index.d.ts.map +1 -1
  129. package/dist/src/index.js +3 -1
  130. package/dist/src/intl/cs.d.ts +94 -0
  131. package/dist/src/intl/cs.d.ts.map +1 -0
  132. package/dist/src/intl/cs.js +220 -0
  133. package/dist/src/intl/index.d.ts +7 -0
  134. package/dist/src/intl/index.d.ts.map +1 -0
  135. package/dist/src/intl/index.js +6 -0
  136. package/dist/src/local-first/Db.d.ts +12 -8
  137. package/dist/src/local-first/Db.d.ts.map +1 -1
  138. package/dist/src/local-first/Db.js +109 -132
  139. package/dist/src/local-first/Error.d.ts +4 -4
  140. package/dist/src/local-first/Evolu.d.ts +338 -92
  141. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  142. package/dist/src/local-first/Evolu.js +47 -55
  143. package/dist/src/local-first/LocalAuth.d.ts +4 -4
  144. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.d.ts +61 -24
  146. package/dist/src/local-first/Owner.d.ts.map +1 -1
  147. package/dist/src/local-first/Owner.js +45 -8
  148. package/dist/src/local-first/Protocol.d.ts +44 -31
  149. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  150. package/dist/src/local-first/Protocol.js +36 -13
  151. package/dist/src/local-first/Query.d.ts +147 -62
  152. package/dist/src/local-first/Query.d.ts.map +1 -1
  153. package/dist/src/local-first/Query.js +112 -58
  154. package/dist/src/local-first/Relay.d.ts +44 -14
  155. package/dist/src/local-first/Relay.d.ts.map +1 -1
  156. package/dist/src/local-first/Schema.d.ts +92 -41
  157. package/dist/src/local-first/Schema.d.ts.map +1 -1
  158. package/dist/src/local-first/Schema.js +25 -8
  159. package/dist/src/local-first/Shared.d.ts +23 -21
  160. package/dist/src/local-first/Shared.d.ts.map +1 -1
  161. package/dist/src/local-first/Shared.js +100 -57
  162. package/dist/src/local-first/Storage.d.ts +53 -32
  163. package/dist/src/local-first/Storage.d.ts.map +1 -1
  164. package/dist/src/local-first/Storage.js +194 -234
  165. package/dist/src/local-first/Timestamp.d.ts +21 -21
  166. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  167. package/dist/src/local-first/Timestamp.js +2 -2
  168. package/dist/src/local-first/index.d.ts +11 -11
  169. package/package.json +14 -13
  170. package/src/Array.ts +306 -205
  171. package/src/Assert.ts +54 -69
  172. package/src/BigInt.ts +6 -3
  173. package/src/Brand.ts +27 -40
  174. package/src/Buffer.ts +28 -12
  175. package/src/Cache.ts +9 -4
  176. package/src/Callbacks.ts +30 -19
  177. package/src/Console.ts +156 -84
  178. package/src/Crypto.ts +83 -41
  179. package/src/Eq.ts +95 -22
  180. package/src/Error.ts +8 -4
  181. package/src/Function.ts +167 -59
  182. package/src/Http.ts +549 -0
  183. package/src/Identicon.ts +16 -7
  184. package/src/LeakDetector.ts +182 -0
  185. package/src/LockManager.ts +43 -39
  186. package/src/Lookup.ts +62 -28
  187. package/src/Microtask.ts +19 -23
  188. package/src/Number.ts +64 -12
  189. package/src/Object.ts +173 -49
  190. package/src/Option.ts +10 -7
  191. package/src/Order.ts +23 -21
  192. package/src/Platform.ts +136 -0
  193. package/src/Polyfills.ts +6 -12
  194. package/src/Random.ts +32 -19
  195. package/src/Redacted.ts +44 -30
  196. package/src/Ref.ts +14 -6
  197. package/src/RefCount.ts +38 -80
  198. package/src/Relation.ts +202 -10
  199. package/src/Resource.ts +1199 -591
  200. package/src/Result.ts +930 -271
  201. package/src/Schedule.ts +859 -343
  202. package/src/Set.ts +89 -40
  203. package/src/Sqlite.ts +144 -138
  204. package/src/StackTrace.ts +230 -0
  205. package/src/Store.ts +8 -7
  206. package/src/String.ts +22 -5
  207. package/src/Task.ts +5101 -2832
  208. package/src/Test.ts +20 -168
  209. package/src/Time.ts +293 -90
  210. package/src/Type.ts +10034 -4117
  211. package/src/Types.ts +227 -110
  212. package/src/WebSocket.ts +64 -36
  213. package/src/Worker.ts +180 -72
  214. package/src/index.ts +55 -52
  215. package/src/intl/cs.ts +388 -0
  216. package/src/intl/index.ts +7 -0
  217. package/src/local-first/Db.ts +152 -179
  218. package/src/local-first/Error.ts +4 -4
  219. package/src/local-first/Evolu.ts +495 -250
  220. package/src/local-first/LocalAuth.ts +6 -12
  221. package/src/local-first/Owner.ts +58 -21
  222. package/src/local-first/Protocol.ts +49 -31
  223. package/src/local-first/Query.ts +146 -61
  224. package/src/local-first/Relay.ts +51 -21
  225. package/src/local-first/Schema.ts +109 -51
  226. package/src/local-first/Shared.ts +266 -226
  227. package/src/local-first/Storage.ts +266 -264
  228. package/src/local-first/Timestamp.ts +25 -18
  229. package/src/local-first/index.ts +11 -11
  230. package/dist/src/Skiplist.d.ts +0 -28
  231. package/dist/src/Skiplist.d.ts.map +0 -1
  232. package/dist/src/Skiplist.js +0 -64
  233. package/dist/src/Tracer.d.ts +0 -48
  234. package/dist/src/Tracer.d.ts.map +0 -1
  235. package/dist/src/Tracer.js +0 -6
  236. package/src/Skiplist.ts +0 -108
  237. package/src/Tracer.ts +0 -54
@@ -11,7 +11,7 @@ import { decrement } from "../Number.js";
11
11
  import { err, ok } from "../Result.js";
12
12
  import { sql, SqliteValue } from "../Sqlite.js";
13
13
  import { Millis } from "../Time.js";
14
- import { Boolean, brand, Id, NonNegativeInt, nullOr, object, PositiveInt, record, String, } from "../Type.js";
14
+ import { Boolean, brand, Id, NonNegativeInt, Null, nullOr, object, PositiveInt, record, String, } from "../Type.js";
15
15
  import { OwnerId, OwnerWriteKey } from "./Owner.js";
16
16
  import { systemColumnsWithId } from "./Schema.js";
17
17
  import { createTimestamp, orderTimestampBytes, Timestamp, TimestampBytes, } from "./Timestamp.js";
@@ -47,8 +47,8 @@ export const ValidDbChangeValues = /*#__PURE__*/ brand("ValidDbChangeValues", Db
47
47
  value,
48
48
  invalidColumns,
49
49
  });
50
- return ok(value);
51
- });
50
+ return ok();
51
+ }, (error) => `DbChange values contain reserved system columns: ${error.invalidColumns.join(", ")}.`);
52
52
  /**
53
53
  * A DbChange is a change to a table row. Together with a unique
54
54
  * {@link Timestamp}, it forms a {@link CrdtMessage}.
@@ -229,18 +229,31 @@ export const getTimestampInsertStrategy = (timestamp, firstTimestamp, lastTimest
229
229
  return ["insert", firstTimestamp, lastTimestamp];
230
230
  };
231
231
  /**
232
- * AFAIK, we can't do both insert and update in one query, and that's probably
233
- * why append is 2x faster than insert. Prepend also has to update parents, but
234
- * it's constantly fast. Insert degrades for reversed (yet LIMIT X magically
235
- * fixes that) but it's OK for append.
232
+ * Append derives the new node's metadata in its insertion statement. Prepend
233
+ * and insert require a second statement to update existing Skiplist ancestors.
234
+ *
235
+ * A non-level-1 append summarizes the suffix after the nearest preceding node
236
+ * at the same or a higher level. A reverse primary-key scan finds that boundary
237
+ * directly, so metadata traversal starts one level below the new node instead
238
+ * of descending from the maximum level.
239
+ *
240
+ * Append is the fastest strategy because level 1, which occurs 75% of the time,
241
+ * needs only the insertion, and promoted nodes derive their metadata without
242
+ * updating existing nodes. Prepend inserts first, then updates one successor at
243
+ * each relevant higher level through predictable edge searches. Insert is the
244
+ * slowest because an arbitrary interior position requires predecessor metadata
245
+ * for the new node and successor discovery to update existing ancestors. All
246
+ * traversals use bounded index ranges, but insert necessarily performs the most
247
+ * Skiplist work.
236
248
  *
237
- * Note: SQL operations are idempotent (using `on conflict do nothing` and
238
- * `changes() > 0`), but this is no longer required here since we use
239
- * {@link BaseSqliteStorage.getExistingTimestamps} to filter out duplicates
240
- * before insertion, which we need for quota checks anyway.
249
+ * SQLite does not support `LATERAL` subqueries. Recursive traversals therefore
250
+ * alternate between finding the next timestamp and loading that row by primary
251
+ * key instead of repeating the same correlated range lookup for every column.
241
252
  *
242
- * TODO: Remove idempotency (`on conflict do nothing` and `changes() > 0`) since
243
- * duplicates are now filtered before insertion.
253
+ * Inserts are idempotent to support direct calls and message replay. `on
254
+ * conflict do nothing` makes a duplicate insertion a no-op, and `changes() > 0`
255
+ * ensures ancestor metadata is updated only when the preceding insertion added
256
+ * a timestamp.
244
257
  */
245
258
  const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
246
259
  const [h1, h2] = fingerprintToSqliteFingerprint(timestampBytesToFingerprint(timestamp));
@@ -261,12 +274,20 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
261
274
  fc(b, cl, pt, nt, ih1, ih2, ic) as (
262
275
  select
263
276
  0,
264
- (
265
- select max(l)
266
- from evolu_timestamp
267
- where ownerId = ${ownerId}
277
+ ${level} - 1,
278
+ ifnull(
279
+ (
280
+ select t
281
+ from evolu_timestamp
282
+ where
283
+ ownerId = ${ownerId}
284
+ and l >= ${level}
285
+ and t < ${timestamp}
286
+ order by t desc
287
+ limit 1
288
+ ),
289
+ zeroblob(0)
268
290
  ),
269
- zeroblob(0),
270
291
  null,
271
292
  0,
272
293
  0,
@@ -292,33 +313,20 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
292
313
  )
293
314
  ),
294
315
  iif(
295
- b and cl < ${level} and nt is not null,
296
- (
297
- select (ih1 | h1) - (ih1 & h1)
298
- from evolu_timestamp
299
- where ownerId = ${ownerId} and t = nt
300
- ),
316
+ node.t is not null,
317
+ (ih1 | node.h1) - (ih1 & node.h1),
301
318
  ih1
302
319
  ),
303
320
  iif(
304
- b and cl < ${level} and nt is not null,
305
- (
306
- select (ih2 | h2) - (ih2 & h2)
307
- from evolu_timestamp
308
- where ownerId = ${ownerId} and t = nt
309
- ),
321
+ node.t is not null,
322
+ (ih2 | node.h2) - (ih2 & node.h2),
310
323
  ih2
311
324
  ),
312
- iif(
313
- b and cl < ${level} and nt is not null,
314
- (
315
- select ic + c
316
- from evolu_timestamp
317
- where ownerId = ${ownerId} and t = nt
318
- ),
319
- ic
320
- )
321
- from fc
325
+ iif(node.t is not null, ic + node.c, ic)
326
+ from
327
+ fc
328
+ left join evolu_timestamp as node
329
+ on b and node.ownerId = ${ownerId} and node.t = nt
322
330
  where cl > 0
323
331
  )
324
332
  insert into evolu_timestamp (ownerId, t, l, h1, h2, c)
@@ -352,64 +360,60 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
352
360
  from evolu_timestamp
353
361
  where ownerId = ${ownerId}
354
362
  ),
355
- fp(b, cl, pt, nt, h1, h2, c) as (
363
+ fp(b, cl, pt, nt, h1, h2) as (
356
364
  select
357
365
  0,
358
366
  (select ml from ml),
359
367
  null,
360
368
  null,
361
369
  null,
362
- null,
363
370
  null
364
371
  union all
365
372
  select
366
- not b,
367
- iif(b, cl - 1, cl),
373
+ not fp.b,
374
+ iif(fp.b, fp.cl - 1, fp.cl),
368
375
  iif(
369
- b,
370
- iif(nt is not null and (pt is null or nt < pt), nt, pt),
371
- pt
376
+ fp.b,
377
+ iif(
378
+ fp.nt is not null and (fp.pt is null or fp.nt < fp.pt),
379
+ fp.nt,
380
+ fp.pt
381
+ ),
382
+ fp.pt
372
383
  ),
373
384
  iif(
374
- b,
385
+ fp.b,
375
386
  null,
376
387
  (
377
388
  select t
378
389
  from evolu_timestamp
379
- where ownerId = ${ownerId} and l = cl and t > ${timestamp}
390
+ where
391
+ ownerId = ${ownerId}
392
+ and l = fp.cl
393
+ and t > ${timestamp}
380
394
  order by t
381
395
  limit 1
382
396
  )
383
397
  ),
384
398
  iif(
385
- b and nt is not null and (pt is null or nt < pt),
386
- (
387
- select h1
388
- from evolu_timestamp
389
- where ownerId = ${ownerId} and t = nt
390
- ),
391
- null
392
- ),
393
- iif(
394
- b and nt is not null and (pt is null or nt < pt),
395
- (
396
- select h2
397
- from evolu_timestamp
398
- where ownerId = ${ownerId} and t = nt
399
- ),
399
+ fp.b
400
+ and fp.nt is not null
401
+ and (fp.pt is null or fp.nt < fp.pt),
402
+ node.h1,
400
403
  null
401
404
  ),
402
405
  iif(
403
- b and nt is not null and (pt is null or nt < pt),
404
- (
405
- select c
406
- from evolu_timestamp
407
- where ownerId = ${ownerId} and t = nt
408
- ),
406
+ fp.b
407
+ and fp.nt is not null
408
+ and (fp.pt is null or fp.nt < fp.pt),
409
+ node.h2,
409
410
  null
410
411
  )
411
- from fp
412
- where cl > ${level}
412
+ from
413
+ fp
414
+ left join evolu_timestamp as node
415
+ on fp.b and node.ownerId = ${ownerId} and node.t = fp.nt
416
+ where fp.cl > ${level}
413
417
  ),
414
418
  u(t, h1, h2) as (
415
419
  select
@@ -419,8 +423,7 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
419
423
  from fp
420
424
  where h1 is not null and pt is not null
421
425
  order by pt
422
- -- Check skiplistMaxLevel docs.
423
- limit 10
426
+ limit ${sql.raw(skiplistMaxLevel.toString())}
424
427
  )
425
428
  update evolu_timestamp
426
429
  set
@@ -446,19 +449,17 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
446
449
  (${ownerId}, 1, ${timestamp}, ${h1}, ${h2}, 1)
447
450
  on conflict do nothing;
448
451
  `,
449
- // DEV: Check whether a boolean flag is faster.
450
452
  sql.prepared `
451
453
  with
452
- p(l, t, h1, h2) as (
454
+ p(l, t, pt) as (
453
455
  select
454
456
  (
455
457
  select max(l) + 1
456
458
  from evolu_timestamp
457
459
  where ownerId = ${ownerId}
458
460
  ),
459
- null,
460
- null,
461
- null
461
+ X'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
462
+ X'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'
462
463
  union all
463
464
  select
464
465
  p.l - 1,
@@ -470,46 +471,27 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
470
471
  ownerId = ${ownerId}
471
472
  and l = p.l - 1
472
473
  and t > ${timestamp}
473
- and (p.t is null or p.t > t)
474
+ and t < p.t
474
475
  order by t
475
476
  limit 1
476
477
  ),
477
478
  p.t
478
479
  ),
479
- (
480
- select h1
481
- from evolu_timestamp
482
- where
483
- ownerId = ${ownerId}
484
- and l = p.l - 1
485
- and t > ${timestamp}
486
- and (p.t is null or p.t > t)
487
- order by t
488
- limit 1
489
- ),
490
- (
491
- select h2
492
- from evolu_timestamp
493
- where
494
- ownerId = ${ownerId}
495
- and l = p.l - 1
496
- and t > ${timestamp}
497
- and (p.t is null or p.t > t)
498
- order by t
499
- limit 1
500
- )
480
+ p.t
501
481
  from p
502
482
  where p.l > 2
503
- -- Check skiplistMaxLevel docs.
504
- limit 10
483
+ limit ${sql.raw(skiplistMaxLevel.toString())}
505
484
  ),
506
485
  u(t, h1, h2) as (
507
486
  select
508
- t,
509
- (${h1} | h1) - (${h1} & h1),
510
- (${h2} | h2) - (${h2} & h2)
511
- from p
512
- where h1 is not null
487
+ p.t,
488
+ (${h1} | node.h1) - (${h1} & node.h1),
489
+ (${h2} | node.h2) - (${h2} & node.h2)
490
+ from
491
+ p
492
+ join evolu_timestamp as node
493
+ on node.ownerId = ${ownerId} and node.t = p.t
494
+ where p.t is not p.pt
513
495
  )
514
496
  update evolu_timestamp
515
497
  set
@@ -564,34 +546,16 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
564
546
  limit 1
565
547
  )
566
548
  ),
567
- iif(
568
- b and cl < ${level} and nt is not null,
569
- (
570
- select h1
571
- from evolu_timestamp
572
- where ownerId = ${ownerId} and t = nt
573
- ),
574
- null
575
- ),
576
- iif(
577
- b and cl < ${level} and nt is not null,
578
- (
579
- select h2
580
- from evolu_timestamp
581
- where ownerId = ${ownerId} and t = nt
582
- ),
583
- null
584
- ),
585
- iif(
586
- b and cl < ${level} and nt is not null,
587
- (
588
- select c
589
- from evolu_timestamp
590
- where ownerId = ${ownerId} and t = nt
591
- ),
592
- null
593
- )
594
- from c0
549
+ node.h1,
550
+ node.h2,
551
+ node.c
552
+ from
553
+ c0
554
+ left join evolu_timestamp as node
555
+ on c0.b
556
+ and c0.cl < ${level}
557
+ and node.ownerId = ${ownerId}
558
+ and node.t = c0.nt
595
559
  where cl > 0
596
560
  ),
597
561
  c1(l, t, h1, h2, c) as (
@@ -629,22 +593,27 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
629
593
  from c3
630
594
  group by l
631
595
  ),
632
- -- DEV: Check whether a boolean flag is faster.
633
- n(l, t, h1, h2, c) as (
596
+ -- Alternate range discovery and primary-key loading.
597
+ n(b, l, t, nt, h1, h2, c) as (
634
598
  select
599
+ 1,
635
600
  (
636
601
  select max(l) + 1
637
602
  from evolu_timestamp
638
603
  where ownerId = ${ownerId}
639
604
  ),
605
+ X'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
640
606
  null,
641
607
  null,
642
608
  null,
643
609
  null
644
610
  union all
645
611
  select
646
- n.l - 1,
647
- ifnull(
612
+ not n.b,
613
+ iif(n.b, n.l - 1, n.l),
614
+ iif(n.b, n.t, ifnull(n.nt, n.t)),
615
+ iif(
616
+ n.b,
648
617
  (
649
618
  select t
650
619
  from evolu_timestamp
@@ -652,47 +621,22 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
652
621
  ownerId = ${ownerId}
653
622
  and l = n.l - 1
654
623
  and t > ${timestamp}
655
- and (n.t is null or t < n.t)
624
+ and t < n.t
656
625
  order by t
657
626
  limit 1
658
627
  ),
659
- n.t
660
- ),
661
- (
662
- select h1
663
- from evolu_timestamp
664
- where
665
- ownerId = ${ownerId}
666
- and l = n.l - 1
667
- and t > ${timestamp}
668
- and (n.t is null or t < n.t)
669
- order by t
670
- limit 1
671
- ),
672
- (
673
- select h2
674
- from evolu_timestamp
675
- where
676
- ownerId = ${ownerId}
677
- and l = n.l - 1
678
- and t > ${timestamp}
679
- and (n.t is null or t < n.t)
680
- order by t
681
- limit 1
628
+ null
682
629
  ),
683
- (
684
- select c
685
- from evolu_timestamp
686
- where
687
- ownerId = ${ownerId}
688
- and l = n.l - 1
689
- and t > ${timestamp}
690
- and (n.t is null or t < n.t)
691
- order by t
692
- limit 1
693
- )
694
- from n
695
- where l - 1 > (select min(l) from c4)
630
+ iif(not n.b and n.nt is not null, node.h1, null),
631
+ iif(not n.b and n.nt is not null, node.h2, null),
632
+ iif(not n.b and n.nt is not null, node.c, null)
633
+ from
634
+ n
635
+ left join evolu_timestamp as node
636
+ on not n.b
637
+ and node.ownerId = ${ownerId}
638
+ and node.t = n.nt
639
+ where iif(n.b, n.l - 1 > (select min(l) from c4), 1)
696
640
  ),
697
641
  u(ut, uh1, uh2, uc) as (
698
642
  select t, h1, h2, c from c4 where t = ${timestamp}
@@ -730,9 +674,9 @@ const insertTimestamp = (deps) => (ownerId, timestamp, level, strategy) => {
730
674
  )
731
675
  )
732
676
  from n
677
+ where b
733
678
  group by t
734
- -- Check skiplistMaxLevel docs.
735
- limit 10
679
+ limit ${sql.raw(skiplistMaxLevel.toString())}
736
680
  )
737
681
  update evolu_timestamp
738
682
  set
@@ -753,6 +697,13 @@ export const timestampBytesToFingerprint = (timestamp) => {
753
697
  const hash = sha256(timestamp).slice(0, fingerprintSize);
754
698
  return hash;
755
699
  };
700
+ /**
701
+ * Computes a brute-force {@link Fingerprint} from {@link TimestampBytes} values
702
+ * for tests and benchmarks.
703
+ */
704
+ export const testFingerprintTimestamps = (timestamps) => timestamps
705
+ .map(timestampBytesToFingerprint)
706
+ .reduce((left, right) => Uint8Array.from(left, (byte, index) => byte ^ right[index]), zeroFingerprint);
756
707
  /**
757
708
  * Generates a random skiplist level in the range [1, skiplistMaxLevel].
758
709
  * Probabilistic approach avoids the need for explicit tree balancing.
@@ -772,13 +723,32 @@ const randomSkiplistLevel = (deps) => {
772
723
  */
773
724
  const skiplistProbability = 0.25;
774
725
  /**
775
- * The SQLite has weird behaviour when we have to limit CTEs even when we don't
776
- * actually limit anything; otherwise, it would not work. But without it, SQLite
777
- * insert and prepend is slow. The 10 is the maximum allowed number, which
778
- * "fixes" SQLite.
726
+ * Maximum Skiplist level and exact upper bound for planner-sensitive CTEs.
727
+ *
728
+ * The three CTEs bounded by this value feed `update ... from` statements in the
729
+ * prepend and insert paths. Each emits at most one row per Skiplist level.
730
+ * Without a known integer `limit`, SQLite estimates a recursive CTE at about
731
+ * 2^32 rows and scans every timestamp for the owner instead of scanning the CTE
732
+ * and looking up each timestamp by primary key.
733
+ *
734
+ * Evolu does not run SQLite's `ANALYZE` command or `PRAGMA optimize`. `ANALYZE`
735
+ * samples database contents and stores statistics in tables such as
736
+ * `sqlite_stat1`; without those statistics, the query planner uses default
737
+ * estimates. With those default estimates, 10 is the planner crossover.
738
+ * Collected statistics or a planner change can move it because this is
739
+ * undocumented cost model behavior. Do not enable planner statistics without
740
+ * benchmarking every storage workload and verifying its query plans on a
741
+ * representative populated database.
779
742
  *
780
- * Because we are using {@link skiplistProbability} 0.25, it's OK even for
781
- * millions of rows.
743
+ * The queries inject this value with `sql.raw`. SQLite versions before 3.47
744
+ * cannot derive the estimate from a bound parameter. Newer versions require
745
+ * QPSG to be disabled and invalidate the query plan whenever that parameter is
746
+ * bound, which would recompile these hot cached statements on every execution.
747
+ *
748
+ * Because the limit can emit at most one row per Skiplist level, the Skiplist
749
+ * is capped at the same value to prevent updates from being truncated. With
750
+ * {@link skiplistProbability} 0.25, level 10 occurs once per 4^9 rows on
751
+ * average, keeping the highest-level scan small for millions of rows.
782
752
  */
783
753
  const skiplistMaxLevel = 10;
784
754
  const fingerprintToSqliteFingerprint = (fingerprint) => {
@@ -922,12 +892,15 @@ const fingerprint = (deps) => (ownerId, begin, end) => {
922
892
  return fingerprintRanges(deps)(ownerId, [begin, end])[1].fingerprint;
923
893
  };
924
894
  /**
925
- * First, check this: https://logperiodic.com/rbsr.html#tree-friendly-functions
895
+ * Computes all RBSR fingerprint buckets in one SQL statement.
896
+ *
897
+ * Continuous ranges reduce the required traversals from 32 to 16. Each
898
+ * recursive step first finds the next timestamp, then loads its count and
899
+ * fingerprint by primary key in the following step. This two-phase traversal
900
+ * avoids repeated correlated range lookups because SQLite does not support
901
+ * `LATERAL` subqueries.
926
902
  *
927
- * We are a little smarter. We leverage continuous ranges to have half of
928
- * traversals. 16 instead of 32. And we compute all of them in a single SQL
929
- * select. If only we could get rid of those subqueries. Then, it would be
930
- * perfect. Btw, reading h1, h2, c in the second step would be slighly faster.
903
+ * See https://logperiodic.com/rbsr.html#tree-friendly-functions.
931
904
  */
932
905
  const fingerprintRanges = (deps) => (ownerId, buckets, upperBound = InfiniteUpperBound) => {
933
906
  const bucketsJson = JSON.stringify(buckets);
@@ -937,14 +910,11 @@ const fingerprintRanges = (deps) => (ownerId, buckets, upperBound = InfiniteUppe
937
910
  select max(l) from evolu_timestamp where ownerId = ${ownerId}
938
911
  ),
939
912
  c0(c) as (select value as c from json_each(${bucketsJson})),
940
- c1(c, b, nt, nc, nh1, nh2, ft, tt, dl, ic, h1, h2) as (
913
+ c1(c, b, nt, ft, tt, dl, ic, h1, h2) as (
941
914
  select
942
915
  c,
943
916
  1,
944
917
  null,
945
- null,
946
- null,
947
- null,
948
918
  zeroblob(0),
949
919
  X'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
950
920
  ml,
@@ -956,60 +926,50 @@ const fingerprintRanges = (deps) => (ownerId, buckets, upperBound = InfiniteUppe
956
926
  ml
957
927
  union all
958
928
  select
959
- c,
960
- not b,
929
+ c1.c,
930
+ not c1.b,
961
931
  iif(
962
- b,
932
+ c1.b,
963
933
  (
964
934
  select t
965
935
  from evolu_timestamp
966
- where l = dl and t > ft and t < tt and ownerId = ${ownerId}
936
+ where
937
+ l = c1.dl
938
+ and t > c1.ft
939
+ and t < c1.tt
940
+ and ownerId = ${ownerId}
967
941
  order by t
968
942
  limit 1
969
943
  ),
970
944
  null
971
945
  ),
946
+ iif(c1.b, c1.ft, iif(c1.ic + node.c <= c1.c, c1.nt, c1.ft)),
972
947
  iif(
973
- b,
974
- (
975
- select c
976
- from evolu_timestamp
977
- where l = dl and t > ft and t < tt and ownerId = ${ownerId}
978
- order by t
979
- limit 1
980
- ),
981
- null
948
+ c1.b,
949
+ c1.tt,
950
+ iif(c1.ic + node.c <= c1.c, c1.tt, ifnull(c1.nt, c1.tt))
982
951
  ),
952
+ iif(c1.b, c1.dl, iif(c1.ic + node.c <= c1.c, c1.dl, c1.dl - 1)),
983
953
  iif(
984
- b,
985
- (
986
- select h1
987
- from evolu_timestamp
988
- where l = dl and t > ft and t < tt and ownerId = ${ownerId}
989
- order by t
990
- limit 1
991
- ),
992
- null
954
+ c1.b,
955
+ c1.ic,
956
+ iif(c1.ic + node.c <= c1.c, c1.ic + node.c, c1.ic)
993
957
  ),
994
958
  iif(
995
- b,
996
- (
997
- select h2
998
- from evolu_timestamp
999
- where l = dl and t > ft and t < tt and ownerId = ${ownerId}
1000
- order by t
1001
- limit 1
1002
- ),
1003
- null
959
+ c1.b,
960
+ c1.h1,
961
+ iif(c1.ic + node.c <= c1.c, ${x("c1.h1", "node.h1")}, c1.h1)
1004
962
  ),
1005
- iif(b, ft, iif(ic + nc <= c, nt, ft)),
1006
- iif(b, tt, iif(ic + nc <= c, tt, ifnull(nt, tt))),
1007
- iif(b, dl, iif(ic + nc <= c, dl, dl - 1)),
1008
- iif(b, ic, iif(ic + nc <= c, ic + nc, ic)),
1009
- iif(b, h1, iif(ic + nc <= c, ${x("h1", "nh1")}, h1)),
1010
- iif(b, h2, iif(ic + nc <= c, ${x("h2", "nh2")}, h2))
1011
- from c1
1012
- where iif(b, 1, ic != c)
963
+ iif(
964
+ c1.b,
965
+ c1.h2,
966
+ iif(c1.ic + node.c <= c1.c, ${x("c1.h2", "node.h2")}, c1.h2)
967
+ )
968
+ from
969
+ c1
970
+ left join evolu_timestamp as node
971
+ on not c1.b and node.ownerId = ${ownerId} and node.t = c1.nt
972
+ where iif(c1.b, 1, c1.ic != c1.c)
1013
973
  ),
1014
974
  c2(h1, h2, t, rn) as (
1015
975
  select