orez 0.2.26 → 0.2.29

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 (172) hide show
  1. package/dist/cf-do/worker.d.ts.map +1 -1
  2. package/dist/cf-do/worker.js +9 -1
  3. package/dist/cf-do/worker.js.map +1 -1
  4. package/dist/pg-proxy-do-backend.d.ts +2 -0
  5. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  6. package/dist/pg-proxy-do-backend.js +49 -7
  7. package/dist/pg-proxy-do-backend.js.map +1 -1
  8. package/dist/pg-sqlite-compiler/catalog/seed.d.ts +67 -0
  9. package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +1 -0
  10. package/dist/pg-sqlite-compiler/catalog/seed.js +436 -0
  11. package/dist/pg-sqlite-compiler/catalog/seed.js.map +1 -0
  12. package/dist/pg-sqlite-compiler/index.d.ts +12 -0
  13. package/dist/pg-sqlite-compiler/index.d.ts.map +1 -0
  14. package/dist/pg-sqlite-compiler/index.js +59 -0
  15. package/dist/pg-sqlite-compiler/index.js.map +1 -0
  16. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +48 -0
  17. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +1 -0
  18. package/dist/pg-sqlite-compiler/passes/ast-utils.js +93 -0
  19. package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +1 -0
  20. package/dist/pg-sqlite-compiler/passes/catalog.d.ts +34 -0
  21. package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +1 -0
  22. package/dist/pg-sqlite-compiler/passes/catalog.js +30 -0
  23. package/dist/pg-sqlite-compiler/passes/catalog.js.map +1 -0
  24. package/dist/pg-sqlite-compiler/passes/datetime.d.ts +21 -0
  25. package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +1 -0
  26. package/dist/pg-sqlite-compiler/passes/datetime.js +53 -0
  27. package/dist/pg-sqlite-compiler/passes/datetime.js.map +1 -0
  28. package/dist/pg-sqlite-compiler/passes/index.d.ts +21 -0
  29. package/dist/pg-sqlite-compiler/passes/index.d.ts.map +1 -0
  30. package/dist/pg-sqlite-compiler/passes/index.js +39 -0
  31. package/dist/pg-sqlite-compiler/passes/index.js.map +1 -0
  32. package/dist/pg-sqlite-compiler/passes/types.d.ts +41 -0
  33. package/dist/pg-sqlite-compiler/passes/types.d.ts.map +1 -0
  34. package/dist/pg-sqlite-compiler/passes/types.js +103 -0
  35. package/dist/pg-sqlite-compiler/passes/types.js.map +1 -0
  36. package/dist/pg-sqlite-compiler/test/oracle.d.ts +34 -0
  37. package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +1 -0
  38. package/dist/pg-sqlite-compiler/test/oracle.js +204 -0
  39. package/dist/pg-sqlite-compiler/test/oracle.js.map +1 -0
  40. package/dist/pg-sqlite-compiler/types.d.ts +55 -0
  41. package/dist/pg-sqlite-compiler/types.d.ts.map +1 -0
  42. package/dist/pg-sqlite-compiler/types.js +2 -0
  43. package/dist/pg-sqlite-compiler/types.js.map +1 -0
  44. package/package.json +8 -4
  45. package/src/admin/admin-data.test.ts +0 -348
  46. package/src/admin/http-proxy.ts +0 -252
  47. package/src/admin/log-store.ts +0 -192
  48. package/src/admin/server.ts +0 -471
  49. package/src/admin/ui.ts +0 -1322
  50. package/src/bench/proxy-throughput.bench.ts +0 -343
  51. package/src/bench/serial-mutations.bench.ts +0 -270
  52. package/src/browser.ts +0 -203
  53. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  54. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  55. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  56. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  57. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
  58. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
  59. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
  60. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
  61. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
  62. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
  63. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
  64. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
  65. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
  66. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
  67. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
  68. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
  69. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
  70. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
  71. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.sqlite +0 -0
  72. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  73. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  74. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  75. package/src/cf-do/ARCHITECTURE.md +0 -83
  76. package/src/cf-do/watermark.test.ts +0 -103
  77. package/src/cf-do/watermark.ts +0 -118
  78. package/src/cf-do/worker.ts +0 -1033
  79. package/src/cf-do/wrangler.toml +0 -11
  80. package/src/cf-pglite/README.md +0 -19
  81. package/src/change-tracking.ts +0 -25
  82. package/src/child-process.test.ts +0 -147
  83. package/src/child-process.ts +0 -90
  84. package/src/cli-entry.ts +0 -72
  85. package/src/cli.test.ts +0 -38
  86. package/src/cli.ts +0 -1214
  87. package/src/config.ts +0 -150
  88. package/src/do-sql-tracking.test.ts +0 -19
  89. package/src/do-sql-tracking.ts +0 -19
  90. package/src/index.ts +0 -1215
  91. package/src/integration/integration.test.ts +0 -517
  92. package/src/integration/native-binary.guard.test.ts +0 -13
  93. package/src/integration/native-startup.test.ts +0 -44
  94. package/src/integration/replication-latency.test.ts +0 -428
  95. package/src/integration/restore-live-stress.test.ts +0 -433
  96. package/src/integration/restore-reset.test.ts +0 -400
  97. package/src/integration/restore.test.ts +0 -274
  98. package/src/integration/test-permissions.ts +0 -147
  99. package/src/load-config.ts +0 -46
  100. package/src/log.ts +0 -96
  101. package/src/mutex.ts +0 -47
  102. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  103. package/src/pg-proxy-browser.ts +0 -2022
  104. package/src/pg-proxy-do-backend.test.ts +0 -3890
  105. package/src/pg-proxy-do-backend.ts +0 -7157
  106. package/src/pg-proxy.ts +0 -1087
  107. package/src/pglite-ipc.test.ts +0 -116
  108. package/src/pglite-ipc.ts +0 -266
  109. package/src/pglite-manager.ts +0 -557
  110. package/src/pglite-web-proxy.test.ts +0 -57
  111. package/src/pglite-web-proxy.ts +0 -221
  112. package/src/pglite-web-worker.ts +0 -152
  113. package/src/pglite-worker-thread.ts +0 -253
  114. package/src/port.ts +0 -25
  115. package/src/process-title.ts +0 -9
  116. package/src/recovery.ts +0 -155
  117. package/src/replication/change-tracker.test.ts +0 -357
  118. package/src/replication/change-tracker.ts +0 -279
  119. package/src/replication/handler.test.ts +0 -511
  120. package/src/replication/handler.ts +0 -1190
  121. package/src/replication/pgoutput-encoder.test.ts +0 -697
  122. package/src/replication/pgoutput-encoder.ts +0 -373
  123. package/src/replication/tcp-replication.test.ts +0 -876
  124. package/src/replication/zero-compat.test.ts +0 -1150
  125. package/src/restore-stress.test.ts +0 -188
  126. package/src/s3-local.ts +0 -203
  127. package/src/shim/hooks.mjs +0 -120
  128. package/src/shim/register.mjs +0 -4
  129. package/src/sqlite-mode/apply-mode.ts +0 -224
  130. package/src/sqlite-mode/index.ts +0 -15
  131. package/src/sqlite-mode/native-binary.ts +0 -89
  132. package/src/sqlite-mode/package-resolve.ts +0 -17
  133. package/src/sqlite-mode/resolve-mode.ts +0 -80
  134. package/src/sqlite-mode/shim-template.ts +0 -159
  135. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  136. package/src/sqlite-mode/types.ts +0 -30
  137. package/src/vite-plugin.ts +0 -67
  138. package/src/wasm-sqlite.test.ts +0 -537
  139. package/src/worker/browser-admin.ts +0 -52
  140. package/src/worker/browser-build-config.test.ts +0 -71
  141. package/src/worker/browser-build-config.ts +0 -109
  142. package/src/worker/browser-embed-admin.test.ts +0 -75
  143. package/src/worker/browser-embed.ts +0 -345
  144. package/src/worker/cf-patches.ts +0 -384
  145. package/src/worker/embed-integration.test.ts +0 -321
  146. package/src/worker/index.ts +0 -138
  147. package/src/worker/shims/fastify.test.ts +0 -255
  148. package/src/worker/shims/fastify.ts +0 -306
  149. package/src/worker/shims/http-service.test.ts +0 -355
  150. package/src/worker/shims/http-service.ts +0 -293
  151. package/src/worker/shims/node-stub.ts +0 -290
  152. package/src/worker/shims/oxfmt.ts +0 -3
  153. package/src/worker/shims/postgres-browser.ts +0 -59
  154. package/src/worker/shims/postgres-socket.test.ts +0 -576
  155. package/src/worker/shims/postgres-socket.ts +0 -310
  156. package/src/worker/shims/postgres.test.ts +0 -364
  157. package/src/worker/shims/postgres.ts +0 -1454
  158. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  159. package/src/worker/shims/sqlite-browser.ts +0 -175
  160. package/src/worker/shims/sqlite.test.ts +0 -786
  161. package/src/worker/shims/sqlite.ts +0 -978
  162. package/src/worker/shims/stream-browser.ts +0 -15
  163. package/src/worker/shims/ws-browser.test.ts +0 -205
  164. package/src/worker/shims/ws-browser.ts +0 -248
  165. package/src/worker/shims/ws.test.ts +0 -288
  166. package/src/worker/shims/ws.ts +0 -467
  167. package/src/worker/shims/zero-process-env.ts +0 -11
  168. package/src/worker/types.ts +0 -75
  169. package/src/worker/worker-integration.test.ts +0 -223
  170. package/src/worker/worker.test.ts +0 -136
  171. package/src/worker/zero-cache-embed-cf.ts +0 -463
  172. package/src/worker/zero-cache-embed.ts +0 -277
package/src/recovery.ts DELETED
@@ -1,155 +0,0 @@
1
- /**
2
- * recovery helpers for zero state corruption and other startup issues.
3
- * centralizes error detection and recovery logic to avoid scattering it throughout the codebase.
4
- */
5
-
6
- import { mkdirSync, rmSync } from 'node:fs'
7
- import { resolve } from 'node:path'
8
-
9
- import {
10
- isChildProcessRunning,
11
- killProcessTree,
12
- waitForChildProcessExit,
13
- } from './child-process.js'
14
- import { log } from './log.js'
15
- import { createPGliteWorker } from './pglite-manager.js'
16
-
17
- import type { PGlite } from '@electric-sql/pglite'
18
- import type { ChildProcess } from 'node:child_process'
19
-
20
- export interface RecoveryContext {
21
- config: { dataDir: string; useWorkerThreads?: boolean }
22
- instances: {
23
- postgres: PGlite
24
- cvr: PGlite
25
- cdb: PGlite
26
- }
27
- zeroCacheProcess: ChildProcess | null
28
- }
29
-
30
- /**
31
- * detect CDC corruption errors from zero-cache output.
32
- * these occur when zero-cache crashes mid-transaction, leaving duplicate
33
- * watermark entries in the changeLog table.
34
- */
35
- export function hasCdcCorruptionSignature(details: string): boolean {
36
- if (!details) return false
37
- // duplicate key in changeLog table (CDC state corruption)
38
- if (details.includes('changeLog_pkey') && details.includes('duplicate key')) {
39
- return true
40
- }
41
- // duplicate key with watermark pattern
42
- if (details.includes('23505') && details.includes('watermark')) {
43
- return true
44
- }
45
- return false
46
- }
47
-
48
- /**
49
- * recover from CDC corruption by resetting CVR/CDB state.
50
- * this is called when zero-cache fails to start due to duplicate changeLog entries.
51
- */
52
- export async function recoverFromCdcCorruption(ctx: RecoveryContext): Promise<void> {
53
- const { config, instances, zeroCacheProcess } = ctx
54
-
55
- log.orez('detected CDC state corruption, auto-recovering...')
56
-
57
- // kill the failed zero-cache process
58
- if (isChildProcessRunning(zeroCacheProcess)) {
59
- if (zeroCacheProcess.pid) killProcessTree(zeroCacheProcess.pid, 'SIGKILL')
60
- else zeroCacheProcess.kill('SIGKILL')
61
- await waitForChildProcessExit(zeroCacheProcess, 1000)
62
- }
63
-
64
- // close and delete CVR/CDB instances
65
- await instances.cvr.close().catch(() => {})
66
- await instances.cdb.close().catch(() => {})
67
-
68
- for (const dir of ['pgdata-cvr', 'pgdata-cdb']) {
69
- try {
70
- rmSync(resolve(config.dataDir, dir), { recursive: true, force: true })
71
- } catch {}
72
- }
73
- log.orez('deleted corrupted CVR/CDB data')
74
-
75
- // delete replica file
76
- const replicaPath = resolve(config.dataDir, 'zero-replica.db')
77
- for (const suffix of ['', '-shm', '-wal', '-wal2']) {
78
- try {
79
- rmSync(replicaPath + suffix, { force: true })
80
- } catch {}
81
- }
82
-
83
- // recreate CVR/CDB instances
84
- if (config.useWorkerThreads) {
85
- const cvrProxy = createPGliteWorker(resolve(config.dataDir, 'pgdata-cvr'), 'cvr')
86
- const cdbProxy = createPGliteWorker(resolve(config.dataDir, 'pgdata-cdb'), 'cdb')
87
- await Promise.all([cvrProxy.waitReady, cdbProxy.waitReady])
88
- instances.cvr = cvrProxy as unknown as PGlite
89
- instances.cdb = cdbProxy as unknown as PGlite
90
- } else {
91
- const { PGlite: PGliteCtor } = await import('@electric-sql/pglite')
92
- mkdirSync(resolve(config.dataDir, 'pgdata-cvr'), { recursive: true })
93
- mkdirSync(resolve(config.dataDir, 'pgdata-cdb'), { recursive: true })
94
- instances.cvr = new PGliteCtor({
95
- dataDir: resolve(config.dataDir, 'pgdata-cvr'),
96
- relaxedDurability: true,
97
- })
98
- instances.cdb = new PGliteCtor({
99
- dataDir: resolve(config.dataDir, 'pgdata-cdb'),
100
- relaxedDurability: true,
101
- })
102
- await instances.cvr.waitReady
103
- await instances.cdb.waitReady
104
- }
105
- log.orez('recreated CVR/CDB instances')
106
-
107
- // clear upstream replication tracking
108
- const db = instances.postgres
109
- await db.exec(`TRUNCATE _orez._zero_changes`).catch(() => {})
110
- await db.exec(`TRUNCATE _orez._zero_replication_slots`).catch(() => {})
111
- await db.exec(`ALTER SEQUENCE _orez._zero_watermark RESTART WITH 1`).catch(() => {})
112
-
113
- // drop stale shard schemas
114
- const shardSchemas = await db.query<{ schemaname: string }>(
115
- `SELECT DISTINCT schemaname FROM pg_tables
116
- WHERE tablename IN ('clients', 'replicas', 'mutations')
117
- AND schemaname NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'public', '_orez')
118
- AND schemaname NOT LIKE 'pg_%'`
119
- )
120
- for (const { schemaname } of shardSchemas.rows) {
121
- await db.exec(`DROP SCHEMA IF EXISTS "${schemaname.replace(/"/g, '""')}" CASCADE`)
122
- }
123
-
124
- log.orez('CDC corruption recovery complete')
125
- }
126
-
127
- /**
128
- * proactively clean CDC state on startup to prevent duplicate key errors.
129
- * this handles cases where orez was killed (SIGKILL) mid-transaction,
130
- * leaving stale watermarks in the changeLog table.
131
- *
132
- * in dev mode, it's safe to drop all CDC schemas - zero-cache will recreate them.
133
- */
134
- export async function cleanCdcStateOnStartup(cdb: PGlite): Promise<void> {
135
- try {
136
- // find all CDC schemas (e.g. chat_0/cdc, startchat_0/cdc)
137
- const result = await cdb.query<{ nspname: string }>(
138
- `SELECT nspname FROM pg_namespace WHERE nspname LIKE '%/cdc'`
139
- )
140
-
141
- if (result.rows.length === 0) {
142
- return // no CDC schemas to clean
143
- }
144
-
145
- for (const { nspname } of result.rows) {
146
- const quoted = '"' + nspname.replace(/"/g, '""') + '"'
147
- await cdb.exec(`DROP SCHEMA IF EXISTS ${quoted} CASCADE`)
148
- }
149
-
150
- log.debug.orez(`cleaned ${result.rows.length} CDC schema(s) on startup`)
151
- } catch (err: any) {
152
- // non-fatal - zero-cache might still work
153
- log.debug.orez(`CDC cleanup warning: ${err?.message || err}`)
154
- }
155
- }
@@ -1,357 +0,0 @@
1
- import { PGlite } from '@electric-sql/pglite'
2
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
3
-
4
- import {
5
- installChangeTracking,
6
- installTriggersOnShardTables,
7
- resetShardSchemaCache,
8
- purgeConsumedChanges,
9
- getChangesSince,
10
- getCurrentWatermark,
11
- } from './change-tracker'
12
-
13
- describe('change-tracker', () => {
14
- let db: PGlite
15
-
16
- beforeEach(async () => {
17
- db = new PGlite()
18
- await db.waitReady
19
- await db.exec(`
20
- CREATE TABLE public.items (
21
- id SERIAL PRIMARY KEY,
22
- name TEXT NOT NULL,
23
- value INTEGER
24
- )
25
- `)
26
- await installChangeTracking(db)
27
- })
28
-
29
- afterEach(async () => {
30
- await db.close()
31
- })
32
-
33
- it('captures INSERT', async () => {
34
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('a', 1)`)
35
-
36
- const changes = await getChangesSince(db, 0)
37
- expect(changes).toHaveLength(1)
38
- expect(changes[0].op).toBe('INSERT')
39
- expect(changes[0].table_name).toBe('public.items')
40
- expect(changes[0].row_data).toMatchObject({ name: 'a', value: 1 })
41
- expect(changes[0].old_data).toBeNull()
42
- })
43
-
44
- it('captures UPDATE with old + new data', async () => {
45
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('a', 1)`)
46
- await db.exec(`UPDATE public.items SET value = 99 WHERE name = 'a'`)
47
-
48
- const changes = await getChangesSince(db, 0)
49
- expect(changes).toHaveLength(2)
50
- expect(changes[1].op).toBe('UPDATE')
51
- expect(changes[1].row_data).toMatchObject({ value: 99 })
52
- expect(changes[1].old_data).toMatchObject({ value: 1 })
53
- })
54
-
55
- it('captures DELETE with old data', async () => {
56
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('a', 1)`)
57
- await db.exec(`DELETE FROM public.items WHERE name = 'a'`)
58
-
59
- const changes = await getChangesSince(db, 0)
60
- expect(changes).toHaveLength(2)
61
- expect(changes[1].op).toBe('DELETE')
62
- expect(changes[1].old_data).toMatchObject({ name: 'a', value: 1 })
63
- expect(changes[1].row_data).toBeNull()
64
- })
65
-
66
- it('watermarks increase monotonically', async () => {
67
- for (let i = 0; i < 5; i++) {
68
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('item${i}', ${i})`)
69
- }
70
-
71
- const changes = await getChangesSince(db, 0)
72
- expect(changes).toHaveLength(5)
73
- for (let i = 1; i < changes.length; i++) {
74
- expect(changes[i].watermark).toBeGreaterThan(changes[i - 1].watermark)
75
- }
76
- })
77
-
78
- it('getChangesSince filters by watermark', async () => {
79
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('a', 1)`)
80
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('b', 2)`)
81
-
82
- const all = await getChangesSince(db, 0)
83
- const afterFirst = await getChangesSince(db, all[0].watermark)
84
-
85
- expect(afterFirst).toHaveLength(1)
86
- expect(afterFirst[0].row_data).toMatchObject({ name: 'b' })
87
- })
88
-
89
- it('respects limit', async () => {
90
- for (let i = 0; i < 10; i++) {
91
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('x', ${i})`)
92
- }
93
-
94
- const limited = await getChangesSince(db, 0, 3)
95
- expect(limited).toHaveLength(3)
96
- })
97
-
98
- it('getCurrentWatermark returns 0 before any inserts', async () => {
99
- const wm = await getCurrentWatermark(db)
100
- expect(wm).toBe(0)
101
- })
102
-
103
- it('getCurrentWatermark advances', async () => {
104
- // first insert consumes the initial sequence value
105
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('x', 1)`)
106
- const before = await getCurrentWatermark(db)
107
- expect(before).toBeGreaterThan(0)
108
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('y', 2)`)
109
- const after = await getCurrentWatermark(db)
110
- expect(after).toBeGreaterThan(before)
111
- })
112
-
113
- it('tracks multiple tables', async () => {
114
- await db.exec(`CREATE TABLE public.other (id SERIAL PRIMARY KEY, label TEXT)`)
115
- await installChangeTracking(db) // reinstall picks up new table
116
-
117
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('a', 1)`)
118
- await db.exec(`INSERT INTO public.other (label) VALUES ('b')`)
119
-
120
- const changes = await getChangesSince(db, 0)
121
- const tables = new Set(changes.map((c) => c.table_name))
122
- expect(tables).toContain('public.items')
123
- expect(tables).toContain('public.other')
124
- })
125
-
126
- it('handles rapid inserts (50 rows)', async () => {
127
- for (let i = 0; i < 50; i++) {
128
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('r${i}', ${i})`)
129
- }
130
-
131
- const changes = await getChangesSince(db, 0)
132
- expect(changes).toHaveLength(50)
133
-
134
- for (let i = 1; i < changes.length; i++) {
135
- expect(changes[i].watermark).toBeGreaterThan(changes[i - 1].watermark)
136
- }
137
- })
138
-
139
- it('does not track internal _zero_ tables', async () => {
140
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('x', 1)`)
141
-
142
- const changes = await getChangesSince(db, 0)
143
- // only the items insert, not the _zero_changes insert that the trigger itself caused
144
- const internalChanges = changes.filter((c) => c.table_name.startsWith('_zero_'))
145
- expect(internalChanges).toHaveLength(0)
146
- })
147
-
148
- it('respects empty configured publication (tracks no public tables)', async () => {
149
- const prev = process.env.ZERO_APP_PUBLICATIONS
150
- process.env.ZERO_APP_PUBLICATIONS = 'zero_scope'
151
- try {
152
- await db.exec(`CREATE PUBLICATION "zero_scope"`)
153
- await installChangeTracking(db) // reinstall picks up publication scope
154
- await db.exec(`TRUNCATE _orez._zero_changes`)
155
-
156
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('x', 1)`)
157
- const changes = await getChangesSince(db, 0)
158
- expect(changes).toHaveLength(0)
159
- } finally {
160
- if (prev === undefined) delete process.env.ZERO_APP_PUBLICATIONS
161
- else process.env.ZERO_APP_PUBLICATIONS = prev
162
- }
163
- })
164
-
165
- it('handles NULL column values', async () => {
166
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('nulltest', NULL)`)
167
-
168
- const changes = await getChangesSince(db, 0)
169
- expect(changes[0].row_data).toMatchObject({ name: 'nulltest', value: null })
170
- })
171
-
172
- it('handles multi-row update', async () => {
173
- await db.exec(
174
- `INSERT INTO public.items (name, value) VALUES ('a', 1), ('b', 2), ('c', 3)`
175
- )
176
- await db.exec(`UPDATE public.items SET value = value * 10`)
177
-
178
- const changes = await getChangesSince(db, 0)
179
- const updates = changes.filter((c) => c.op === 'UPDATE')
180
- expect(updates).toHaveLength(3)
181
- })
182
-
183
- it('preserves change ordering across mixed operations', async () => {
184
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('a', 1)`)
185
- await db.exec(`UPDATE public.items SET value = 2 WHERE name = 'a'`)
186
- await db.exec(`INSERT INTO public.items (name, value) VALUES ('b', 3)`)
187
- await db.exec(`DELETE FROM public.items WHERE name = 'a'`)
188
-
189
- const changes = await getChangesSince(db, 0)
190
- const ops = changes.map((c) => c.op)
191
- expect(ops).toEqual(['INSERT', 'UPDATE', 'INSERT', 'DELETE'])
192
- })
193
-
194
- it('tracks tables with special characters in names', async () => {
195
- await db.exec(`CREATE TABLE public."my""table" (id SERIAL PRIMARY KEY, val TEXT)`)
196
- await installChangeTracking(db)
197
-
198
- await db.exec(`INSERT INTO public."my""table" (val) VALUES ('works')`)
199
-
200
- const changes = await getChangesSince(db, 0)
201
- const special = changes.filter((c) => c.table_name === 'public.my"table')
202
- expect(special).toHaveLength(1)
203
- expect(special[0].op).toBe('INSERT')
204
- expect(special[0].row_data).toMatchObject({ val: 'works' })
205
- })
206
- })
207
-
208
- describe('shard table tracking', () => {
209
- let db: PGlite
210
-
211
- beforeEach(async () => {
212
- resetShardSchemaCache()
213
- db = new PGlite()
214
- await db.waitReady
215
- await installChangeTracking(db)
216
- })
217
-
218
- afterEach(async () => {
219
- await db.close()
220
- })
221
-
222
- it('only tracks mutation-confirmation tables in shard schemas', async () => {
223
- // zero-cache creates shard schemas like chat_0 with clients, replicas, mutations.
224
- // clients advance lmid and mutations carry server results; replicas stays internal.
225
- // with "Unknown table chat_0.replicas" because they aren't in zero's schema.
226
- await db.exec(`
227
- CREATE SCHEMA chat_0;
228
- CREATE TABLE chat_0.clients (
229
- "clientGroupID" TEXT NOT NULL,
230
- "clientID" TEXT NOT NULL,
231
- "lastMutationID" BIGINT,
232
- "userID" TEXT,
233
- PRIMARY KEY ("clientGroupID", "clientID")
234
- );
235
- CREATE TABLE chat_0.replicas (
236
- id TEXT PRIMARY KEY,
237
- version TEXT,
238
- cookie TEXT
239
- );
240
- CREATE TABLE chat_0.mutations (
241
- id TEXT PRIMARY KEY,
242
- "clientID" TEXT,
243
- name TEXT,
244
- args JSONB
245
- );
246
- `)
247
-
248
- await installTriggersOnShardTables(db)
249
-
250
- // insert into all three tables
251
- await db.exec(
252
- `INSERT INTO chat_0.clients ("clientGroupID", "clientID", "lastMutationID") VALUES ('cg1', 'c1', 1)`
253
- )
254
- await db.exec(`INSERT INTO chat_0.replicas (id, version) VALUES ('r1', 'v1')`)
255
- await db.exec(
256
- `INSERT INTO chat_0.mutations (id, "clientID", name) VALUES ('m1', 'c1', 'sendMessage')`
257
- )
258
-
259
- const changes = await getChangesSince(db, 0)
260
- const tables = changes.map((c) => c.table_name)
261
-
262
- // only mutation-confirmation tables should be tracked
263
- expect(tables).toContain('chat_0.clients')
264
- expect(tables).toContain('chat_0.mutations')
265
- expect(tables).not.toContain('chat_0.replicas')
266
- })
267
-
268
- it('purges consumed changes to prevent OOM', async () => {
269
- // _zero_changes accumulates forever in 0.0.37. with wasm pglite,
270
- // this eventually causes OOM. we need a purge mechanism.
271
- await db.exec(`
272
- CREATE TABLE public.items (id SERIAL PRIMARY KEY, val TEXT)
273
- `)
274
- await installChangeTracking(db)
275
-
276
- // insert some data
277
- for (let i = 0; i < 10; i++) {
278
- await db.exec(`INSERT INTO public.items (val) VALUES ('item${i}')`)
279
- }
280
-
281
- const changes = await getChangesSince(db, 0)
282
- expect(changes).toHaveLength(10)
283
- const lastWatermark = changes[changes.length - 1].watermark
284
-
285
- // purge consumed changes up to the watermark we've processed
286
- await purgeConsumedChanges(db, lastWatermark)
287
-
288
- // after purge, no changes before that watermark should remain
289
- const remaining = await getChangesSince(db, 0)
290
- expect(remaining).toHaveLength(0)
291
- })
292
-
293
- it('tracks tables created after initial installChangeTracking', async () => {
294
- // simulate zero-cache creating shard schema AFTER replication starts.
295
- // in production, zero-cache creates chat_0 schema + clients table
296
- // after the replication connection is already established.
297
- // the change tracker must pick up these new tables.
298
- await db.exec(`
299
- CREATE SCHEMA chat_0;
300
- CREATE TABLE chat_0.clients (
301
- "clientGroupID" TEXT NOT NULL,
302
- "clientID" TEXT NOT NULL,
303
- "lastMutationID" BIGINT,
304
- PRIMARY KEY ("clientGroupID", "clientID")
305
- );
306
- `)
307
-
308
- // re-running installTriggersOnShardTables should pick up new tables
309
- await installTriggersOnShardTables(db)
310
-
311
- await db.exec(
312
- `INSERT INTO chat_0.clients ("clientGroupID", "clientID", "lastMutationID") VALUES ('cg1', 'c1', 1)`
313
- )
314
-
315
- const changes = await getChangesSince(db, 0)
316
- expect(changes).toHaveLength(1)
317
- expect(changes[0].table_name).toBe('chat_0.clients')
318
- })
319
-
320
- it('tracks shard tables created after the schema was scanned', async () => {
321
- await db.exec(`CREATE SCHEMA chat_0`)
322
-
323
- // first scan sees the schema but no internal tables yet.
324
- await installTriggersOnShardTables(db)
325
-
326
- await db.exec(`
327
- CREATE TABLE chat_0.clients (
328
- "clientGroupID" TEXT NOT NULL,
329
- "clientID" TEXT NOT NULL,
330
- "lastMutationID" BIGINT,
331
- PRIMARY KEY ("clientGroupID", "clientID")
332
- );
333
- CREATE TABLE chat_0.mutations (
334
- "clientGroupID" TEXT NOT NULL,
335
- "clientID" TEXT NOT NULL,
336
- "mutationID" BIGINT NOT NULL,
337
- result JSONB,
338
- PRIMARY KEY ("clientGroupID", "clientID", "mutationID")
339
- );
340
- `)
341
-
342
- await installTriggersOnShardTables(db)
343
-
344
- await db.exec(
345
- `INSERT INTO chat_0.clients ("clientGroupID", "clientID", "lastMutationID") VALUES ('cg1', 'c1', 1)`
346
- )
347
- await db.exec(
348
- `INSERT INTO chat_0.mutations ("clientGroupID", "clientID", "mutationID", result) VALUES ('cg1', 'c1', 1, '{}')`
349
- )
350
-
351
- const changes = await getChangesSince(db, 0)
352
- expect(changes.map((change) => change.table_name).sort()).toEqual([
353
- 'chat_0.clients',
354
- 'chat_0.mutations',
355
- ])
356
- })
357
- })