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
@@ -1,400 +0,0 @@
1
- /**
2
- * regression test for restore/reset integration.
3
- *
4
- * covers the real integration boundary that previously regressed:
5
- * - restore data through wire protocol
6
- * - trigger full zero-state reset via pid-file + SIGUSR1 (same path as pg_restore)
7
- * - verify zero-cache restarts and live replication still works
8
- */
9
-
10
- import { existsSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
11
- import { homedir, tmpdir } from 'node:os'
12
- import { join } from 'node:path'
13
-
14
- import { loadModule } from 'pgsql-parser'
15
- import postgres from 'postgres'
16
- import { afterAll, beforeAll, describe, expect, test } from 'vitest'
17
- import WebSocket from 'ws'
18
-
19
- import { execDumpFile } from '../cli.js'
20
- import { startZeroLite } from '../index.js'
21
- import {
22
- ensureTablesInPublications,
23
- hasNonNullPermissions,
24
- installAllowAllPermissions,
25
- } from './test-permissions.js'
26
-
27
- // zero-cache protocol version (from @rocicorp/zero/out/zero-protocol/src/protocol-version.js)
28
- const PROTOCOL_VERSION = 45
29
- const RESET_CLIENT_SCHEMA = {
30
- tables: {
31
- reset_probe: {
32
- columns: {
33
- id: { type: 'string' },
34
- value: { type: 'string' },
35
- },
36
- primaryKey: ['id'],
37
- },
38
- },
39
- }
40
-
41
- // encode initConnection message for sec-websocket-protocol header
42
- // matches zero-protocol's encodeSecProtocols implementation
43
- function encodeSecProtocols(
44
- initConnectionMessage: unknown,
45
- authToken: string | undefined
46
- ): string {
47
- const payload = JSON.stringify({ initConnectionMessage, authToken })
48
- return encodeURIComponent(Buffer.from(payload, 'utf-8').toString('base64'))
49
- }
50
-
51
- import type { PGlite } from '@electric-sql/pglite'
52
-
53
- class Queue<T> {
54
- private items: T[] = []
55
- private waiters: Array<{
56
- resolve: (v: T) => void
57
- timer?: ReturnType<typeof setTimeout>
58
- }> = []
59
-
60
- enqueue(item: T) {
61
- const waiter = this.waiters.shift()
62
- if (waiter) {
63
- if (waiter.timer) clearTimeout(waiter.timer)
64
- waiter.resolve(item)
65
- } else {
66
- this.items.push(item)
67
- }
68
- }
69
-
70
- dequeue(fallback?: T, timeoutMs = 10000): Promise<T> {
71
- if (this.items.length > 0) {
72
- return Promise.resolve(this.items.shift()!)
73
- }
74
- return new Promise<T>((resolve) => {
75
- const waiter: { resolve: (v: T) => void; timer?: ReturnType<typeof setTimeout> } = {
76
- resolve,
77
- }
78
- if (fallback !== undefined) {
79
- waiter.timer = setTimeout(() => {
80
- const idx = this.waiters.indexOf(waiter)
81
- if (idx >= 0) this.waiters.splice(idx, 1)
82
- resolve(fallback)
83
- }, timeoutMs)
84
- }
85
- this.waiters.push(waiter)
86
- })
87
- }
88
- }
89
-
90
- function generateFallbackDump(): string {
91
- return [
92
- 'SET statement_timeout = 0;',
93
- "SET client_encoding = 'UTF8';",
94
- 'SET standard_conforming_strings = on;',
95
- '',
96
- 'CREATE TABLE IF NOT EXISTS restore_seed (',
97
- ' id integer PRIMARY KEY,',
98
- ' note text NOT NULL',
99
- ');',
100
- '',
101
- "INSERT INTO restore_seed (id, note) VALUES (1, 'seeded by fallback dump');",
102
- '',
103
- ].join('\n')
104
- }
105
-
106
- function resolveDumpFile(): { path: string; cleanup: boolean } {
107
- const envDump = process.env.OREZ_RESTORE_SQL_DUMP
108
- if (envDump && existsSync(envDump)) {
109
- return { path: envDump, cleanup: false }
110
- }
111
-
112
- const chatCandidates = [
113
- join(homedir(), 'chat', 'tmp', 'restore.sql'),
114
- join(homedir(), 'chat', 'tmp', 'backup.sql'),
115
- join(homedir(), 'chat', 'restore.sql'),
116
- join(homedir(), 'chat', 'backup.sql'),
117
- ]
118
- for (const candidate of chatCandidates) {
119
- if (existsSync(candidate)) {
120
- return { path: candidate, cleanup: false }
121
- }
122
- }
123
-
124
- const tmpDump = join(tmpdir(), `orez-restore-reset-${Date.now()}.sql`)
125
- writeFileSync(tmpDump, generateFallbackDump())
126
- return { path: tmpDump, cleanup: true }
127
- }
128
-
129
- describe('restore/reset integration regression', { timeout: 150_000 }, () => {
130
- let db: PGlite
131
- let pgPort: number
132
- let zeroPort: number
133
- let shutdown: () => Promise<void>
134
- let restartZero: (() => Promise<void>) | undefined
135
- let resetZeroFull: (() => Promise<void>) | undefined
136
- let dataDir: string
137
- let dumpFile: string
138
- let dumpFileIsTemp = false
139
-
140
- beforeAll(async () => {
141
- await loadModule()
142
-
143
- const dump = resolveDumpFile()
144
- dumpFile = dump.path
145
- dumpFileIsTemp = dump.cleanup
146
-
147
- dataDir = `.orez-restore-reset-test-${Date.now()}`
148
-
149
- const started = await startZeroLite({
150
- pgPort: 27000 + Math.floor(Math.random() * 1000),
151
- zeroPort: 28000 + Math.floor(Math.random() * 1000),
152
- dataDir,
153
- logLevel: 'warn',
154
- skipZeroCache: false,
155
- })
156
-
157
- db = started.db
158
- pgPort = started.pgPort
159
- zeroPort = started.zeroPort
160
- shutdown = started.stop
161
- restartZero = started.restartZero
162
- resetZeroFull = started.resetZeroFull
163
-
164
- await waitForZero(zeroPort, 90_000)
165
- }, 120_000)
166
-
167
- afterAll(async () => {
168
- if (shutdown) await shutdown()
169
- if (dataDir) {
170
- try {
171
- rmSync(dataDir, { recursive: true, force: true })
172
- } catch {}
173
- }
174
- if (dumpFileIsTemp && dumpFile) {
175
- try {
176
- unlinkSync(dumpFile)
177
- } catch {}
178
- }
179
- })
180
-
181
- test('wire restore + pid signal full reset keeps zero-cache healthy', async () => {
182
- const sql = postgres({
183
- host: '127.0.0.1',
184
- port: pgPort,
185
- user: 'user',
186
- password: 'password',
187
- database: 'postgres',
188
- max: 1,
189
- })
190
-
191
- try {
192
- const wireDb = { exec: (query: string) => sql.unsafe(query) as Promise<unknown> }
193
- await execDumpFile(wireDb, dumpFile)
194
- } finally {
195
- await sql.end({ timeout: 1 }).catch(() => {})
196
- }
197
-
198
- // mirror pg_restore behavior: read pid file and signal SIGUSR1 for full reset
199
- const pidFile = join(dataDir, 'orez.pid')
200
- const pid = Number(readFileSync(pidFile, 'utf-8').trim())
201
- expect(pid).toBeGreaterThan(0)
202
- process.kill(pid, 'SIGUSR1')
203
-
204
- await waitForZero(zeroPort, 90_000)
205
-
206
- // prove zero-cache is alive after reset and still streams live writes
207
- await db.exec(`
208
- CREATE TABLE IF NOT EXISTS reset_probe (
209
- id text PRIMARY KEY,
210
- value text NOT NULL
211
- );
212
-
213
- -- install change tracking trigger on the new table
214
- DROP TRIGGER IF EXISTS _zero_change_trigger ON public.reset_probe;
215
- CREATE TRIGGER _zero_change_trigger
216
- AFTER INSERT OR UPDATE OR DELETE ON public.reset_probe
217
- FOR EACH ROW EXECUTE FUNCTION public._zero_track_change();
218
-
219
- -- install notify trigger for real-time notifications
220
- DROP TRIGGER IF EXISTS _zero_notify_trigger ON public.reset_probe;
221
- CREATE TRIGGER _zero_notify_trigger
222
- AFTER INSERT OR UPDATE OR DELETE ON public.reset_probe
223
- FOR EACH STATEMENT EXECUTE FUNCTION public._zero_notify_change();
224
- `)
225
- await ensureTablesInPublications(db, ['reset_probe'])
226
- const pubName = process.env.ZERO_APP_PUBLICATIONS?.trim()
227
- if (pubName) {
228
- const quotedPub = '"' + pubName.replace(/"/g, '""') + '"'
229
- await db
230
- .exec(`ALTER PUBLICATION ${quotedPub} ADD TABLE "public"."reset_probe"`)
231
- .catch(() => {})
232
- }
233
- await installAllowAllPermissions(db, ['reset_probe'])
234
- expect(await hasNonNullPermissions(db)).toBe(true)
235
- if (resetZeroFull) {
236
- await resetZeroFull()
237
- await waitForZero(zeroPort, 90_000)
238
- } else if (restartZero) {
239
- await restartZero()
240
- await waitForZero(zeroPort, 60_000)
241
- }
242
-
243
- const downstream = new Queue<unknown>()
244
- const ws = await connectAndSubscribeWithRetry(zeroPort, downstream, {
245
- table: 'reset_probe',
246
- orderBy: [['id', 'asc']],
247
- })
248
-
249
- try {
250
- await drainInitialPokes(downstream)
251
-
252
- await db.query(`INSERT INTO reset_probe (id, value) VALUES ($1, $2)`, [
253
- `post-reset-${Date.now()}`,
254
- 'ok',
255
- ])
256
-
257
- const poke = await waitForPokePart(downstream, 30_000)
258
- expect(poke.rowsPatch).toEqual(
259
- expect.arrayContaining([
260
- expect.objectContaining({
261
- op: 'put',
262
- tableName: 'reset_probe',
263
- value: expect.objectContaining({
264
- value: 'ok',
265
- }),
266
- }),
267
- ])
268
- )
269
- } finally {
270
- ws.close()
271
- }
272
- })
273
- })
274
-
275
- function connectAndSubscribe(
276
- port: number,
277
- downstream: Queue<unknown>,
278
- query: Record<string, unknown>
279
- ): Promise<WebSocket> {
280
- return new Promise((resolve, reject) => {
281
- const ts = Date.now()
282
- const clientGroupID = `restore-reset-cg-${ts}`
283
- const clientID = 'restore-reset-client'
284
- const initConnectionMessage: [string, Record<string, unknown>] = [
285
- 'initConnection',
286
- {
287
- desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
288
- clientSchema: RESET_CLIENT_SCHEMA,
289
- },
290
- ]
291
- const secProtocol = encodeSecProtocols(initConnectionMessage, undefined)
292
- const ws = new WebSocket(
293
- `ws://127.0.0.1:${port}/sync/v${PROTOCOL_VERSION}/connect` +
294
- `?clientGroupID=${clientGroupID}&clientID=${clientID}&wsid=ws1&schemaVersion=1&baseCookie=&ts=${ts}&lmid=0`,
295
- secProtocol
296
- )
297
-
298
- let settled = false
299
- let sawMessage = false
300
- const failTimer = setTimeout(() => {
301
- if (settled) return
302
- settled = true
303
- try {
304
- ws.close()
305
- } catch {}
306
- reject(new Error('websocket connected but no downstream messages'))
307
- }, 7000)
308
-
309
- ws.on('message', (data) => {
310
- const msg = JSON.parse(data.toString())
311
- downstream.enqueue(msg)
312
- if (!sawMessage && !settled) {
313
- sawMessage = true
314
- settled = true
315
- clearTimeout(failTimer)
316
- resolve(ws)
317
- }
318
- })
319
-
320
- ws.once('error', (err) => {
321
- if (settled) return
322
- settled = true
323
- clearTimeout(failTimer)
324
- reject(err)
325
- })
326
-
327
- ws.once('close', () => {
328
- if (settled) return
329
- settled = true
330
- clearTimeout(failTimer)
331
- reject(new Error('websocket closed before initial downstream message'))
332
- })
333
- })
334
- }
335
-
336
- async function connectAndSubscribeWithRetry(
337
- port: number,
338
- downstream: Queue<unknown>,
339
- query: Record<string, unknown>,
340
- timeoutMs = 30_000
341
- ): Promise<WebSocket> {
342
- const deadline = Date.now() + timeoutMs
343
- let lastErr: unknown
344
- while (Date.now() < deadline) {
345
- try {
346
- return await connectAndSubscribe(port, downstream, query)
347
- } catch (err) {
348
- lastErr = err
349
- await new Promise((r) => setTimeout(r, 300))
350
- }
351
- }
352
- throw new Error(
353
- `timed out connecting websocket after reset: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
354
- )
355
- }
356
-
357
- async function drainInitialPokes(downstream: Queue<unknown>) {
358
- let settled = false
359
- const timeout = Date.now() + 30_000
360
-
361
- while (!settled && Date.now() < timeout) {
362
- const msg = (await downstream.dequeue('timeout' as any, 3000)) as any
363
- if (msg === 'timeout') {
364
- settled = true
365
- } else if (Array.isArray(msg) && msg[0] === 'pokeEnd') {
366
- const next = (await downstream.dequeue('timeout' as any, 2000)) as any
367
- if (next === 'timeout') {
368
- settled = true
369
- }
370
- }
371
- }
372
- }
373
-
374
- async function waitForPokePart(
375
- downstream: Queue<unknown>,
376
- timeoutMs = 10_000
377
- ): Promise<Record<string, any>> {
378
- const deadline = Date.now() + timeoutMs
379
- while (Date.now() < deadline) {
380
- const remaining = Math.max(1000, deadline - Date.now())
381
- const msg = (await downstream.dequeue('timeout' as any, remaining)) as any
382
- if (msg === 'timeout') throw new Error('timed out waiting for pokePart')
383
- if (Array.isArray(msg) && msg[0] === 'pokePart' && msg[1]?.rowsPatch) {
384
- return msg[1]
385
- }
386
- }
387
- throw new Error('timed out waiting for pokePart')
388
- }
389
-
390
- async function waitForZero(port: number, timeoutMs = 30_000) {
391
- const deadline = Date.now() + timeoutMs
392
- while (Date.now() < deadline) {
393
- try {
394
- const res = await fetch(`http://localhost:${port}/`)
395
- if (res.ok || res.status === 404) return
396
- } catch {}
397
- await new Promise((r) => setTimeout(r, 500))
398
- }
399
- throw new Error(`zero-cache not ready on port ${port} after ${timeoutMs}ms`)
400
- }
@@ -1,274 +0,0 @@
1
- /**
2
- * integration test for pg_restore through a running orez instance.
3
- *
4
- * generates a pg_dump-style SQL file, starts fresh orez, restores via wire
5
- * protocol, then verifies data via wire queries + zero-cache websocket sync.
6
- */
7
-
8
- import { writeFileSync, unlinkSync, rmSync } from 'node:fs'
9
- import { tmpdir } from 'node:os'
10
- import { join } from 'node:path'
11
-
12
- import { loadModule } from 'pgsql-parser'
13
- import postgres from 'postgres'
14
- import { describe, test, expect, beforeAll, afterAll } from 'vitest'
15
-
16
- import { execDumpFile } from '../cli.js'
17
- import { startZeroLite } from '../index.js'
18
-
19
- import type { PGlite } from '@electric-sql/pglite'
20
-
21
- // generate a pg_dump-style SQL file with our test schema + data
22
- function generateDump(): string {
23
- const lines: string[] = []
24
-
25
- // preamble (mimics pg_dump)
26
- lines.push('SET statement_timeout = 0;')
27
- lines.push("SET client_encoding = 'UTF8';")
28
- lines.push('SET standard_conforming_strings = on;')
29
- lines.push('')
30
-
31
- // tables
32
- lines.push(`CREATE TABLE items (
33
- id integer NOT NULL,
34
- name text NOT NULL,
35
- data text,
36
- score integer DEFAULT 0
37
- );`)
38
- lines.push('')
39
- lines.push(
40
- `CREATE SEQUENCE items_id_seq AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;`
41
- )
42
- lines.push(`ALTER SEQUENCE items_id_seq OWNED BY items.id;`)
43
- lines.push(
44
- `ALTER TABLE ONLY items ALTER COLUMN id SET DEFAULT nextval('items_id_seq'::regclass);`
45
- )
46
- lines.push('')
47
-
48
- lines.push(`CREATE TABLE tags (
49
- id integer NOT NULL,
50
- item_id integer,
51
- label text NOT NULL
52
- );`)
53
- lines.push('')
54
- lines.push(
55
- `CREATE SEQUENCE tags_id_seq AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1;`
56
- )
57
- lines.push(`ALTER SEQUENCE tags_id_seq OWNED BY tags.id;`)
58
- lines.push(
59
- `ALTER TABLE ONLY tags ALTER COLUMN id SET DEFAULT nextval('tags_id_seq'::regclass);`
60
- )
61
- lines.push('')
62
-
63
- // view + function
64
- lines.push(`CREATE VIEW item_summary AS
65
- SELECT i.id, i.name, count(t.id) AS tag_count
66
- FROM items i LEFT JOIN tags t ON t.item_id = i.id
67
- GROUP BY i.id, i.name;`)
68
- lines.push('')
69
- lines.push(
70
- `CREATE FUNCTION item_count() RETURNS integer LANGUAGE sql AS $$SELECT count(*)::integer FROM items$$;`
71
- )
72
- lines.push('')
73
-
74
- // COPY items data (200 rows)
75
- lines.push('COPY items (id, name, data, score) FROM stdin;')
76
- for (let i = 0; i < 200; i++) {
77
- const id = i + 1
78
- const name = i % 7 === 0 ? `O'Brien's item #${i}` : `item-${i}`
79
- const data = i % 11 === 0 ? '\\N' : `data-${'x'.repeat(100)}-${i}`
80
- const score = i * 10
81
- // COPY text format: tab-separated, \N for NULL, backslash escapes
82
- lines.push(`${id}\t${escapeCopy(name)}\t${data}\t${score}`)
83
- }
84
- lines.push('\\.')
85
- lines.push('')
86
-
87
- // COPY tags data (50 rows)
88
- lines.push('COPY tags (id, item_id, label) FROM stdin;')
89
- for (let i = 0; i < 50; i++) {
90
- lines.push(`${i + 1}\t${(i % 200) + 1}\ttag-${i}`)
91
- }
92
- lines.push('\\.')
93
- lines.push('')
94
-
95
- // constraints (pg_dump adds these after data)
96
- lines.push('ALTER TABLE ONLY items ADD CONSTRAINT items_pkey PRIMARY KEY (id);')
97
- lines.push('ALTER TABLE ONLY tags ADD CONSTRAINT tags_pkey PRIMARY KEY (id);')
98
- lines.push(
99
- 'ALTER TABLE ONLY tags ADD CONSTRAINT tags_item_id_fkey FOREIGN KEY (item_id) REFERENCES items(id);'
100
- )
101
- lines.push('')
102
-
103
- // sequence values
104
- lines.push(`SELECT pg_catalog.setval('items_id_seq', 200, true);`)
105
- lines.push(`SELECT pg_catalog.setval('tags_id_seq', 50, true);`)
106
- lines.push('')
107
-
108
- return lines.join('\n')
109
- }
110
-
111
- // escape a value for COPY text format
112
- function escapeCopy(val: string): string {
113
- return val.replace(/\\/g, '\\\\').replace(/\t/g, '\\t').replace(/\n/g, '\\n')
114
- }
115
-
116
- describe('restore integration', { timeout: 120_000 }, () => {
117
- let db: PGlite
118
- let pgPort: number
119
- let zeroPort: number
120
- let shutdown: () => Promise<void>
121
- let dataDir: string
122
- let dumpFile: string
123
-
124
- beforeAll(async () => {
125
- await loadModule()
126
-
127
- // write dump file
128
- dumpFile = join(tmpdir(), `orez-restore-test-${Date.now()}.sql`)
129
- writeFileSync(dumpFile, generateDump())
130
- dataDir = `.orez-restore-test-${Date.now()}`
131
-
132
- // start orez without zero-cache (restore doesn't need sync)
133
- const fresh = await startZeroLite({
134
- pgPort: 25000 + Math.floor(Math.random() * 1000),
135
- zeroPort: 26000 + Math.floor(Math.random() * 1000),
136
- dataDir,
137
- logLevel: 'warn',
138
- skipZeroCache: true,
139
- })
140
-
141
- db = fresh.db
142
- pgPort = fresh.pgPort
143
- zeroPort = fresh.zeroPort
144
- shutdown = fresh.stop
145
-
146
- // restore via wire protocol
147
- const sql = postgres({
148
- host: '127.0.0.1',
149
- port: pgPort,
150
- user: 'user',
151
- password: 'password',
152
- database: 'postgres',
153
- max: 1,
154
- })
155
-
156
- const wireDb = { exec: (query: string) => sql.unsafe(query) as Promise<unknown> }
157
- const result = await execDumpFile(wireDb, dumpFile)
158
- console.log(
159
- `[restore-test] restored: ${result.executed} executed, ${result.skipped} skipped`
160
- )
161
- await sql.end()
162
- }, 60_000)
163
-
164
- afterAll(async () => {
165
- if (shutdown) await shutdown()
166
- if (dataDir) {
167
- try {
168
- rmSync(dataDir, { recursive: true, force: true })
169
- } catch {}
170
- }
171
- if (dumpFile) {
172
- try {
173
- unlinkSync(dumpFile)
174
- } catch {}
175
- }
176
- })
177
-
178
- test('tables exist and row counts match', async () => {
179
- const sql = wireClient()
180
- try {
181
- const items = await sql`SELECT count(*) as cnt FROM items`
182
- expect(Number(items[0].cnt)).toBe(200)
183
-
184
- const tags = await sql`SELECT count(*) as cnt FROM tags`
185
- expect(Number(tags[0].cnt)).toBe(50)
186
- } finally {
187
- await sql.end()
188
- }
189
- })
190
-
191
- test('data integrity preserved (quotes, nulls, large values)', async () => {
192
- const sql = wireClient()
193
- try {
194
- const quoted =
195
- await sql`SELECT name FROM items WHERE name LIKE ${"O'Brien%"} LIMIT 1`
196
- expect(quoted[0].name).toContain("O'Brien")
197
-
198
- const nulls = await sql`SELECT count(*) as cnt FROM items WHERE data IS NULL`
199
- expect(Number(nulls[0].cnt)).toBeGreaterThan(0)
200
-
201
- const scores = await sql`SELECT min(score) as lo, max(score) as hi FROM items`
202
- expect(Number(scores[0].lo)).toBe(0)
203
- expect(Number(scores[0].hi)).toBe(1990)
204
- } finally {
205
- await sql.end()
206
- }
207
- })
208
-
209
- test('views work after restore', async () => {
210
- const sql = wireClient()
211
- try {
212
- const summary = await sql`SELECT * FROM item_summary ORDER BY id LIMIT 3`
213
- expect(summary.length).toBe(3)
214
- expect(summary[0]).toHaveProperty('name')
215
- expect(summary[0]).toHaveProperty('tag_count')
216
- } finally {
217
- await sql.end()
218
- }
219
- })
220
-
221
- test('functions work after restore', async () => {
222
- const sql = wireClient()
223
- try {
224
- const result = await sql`SELECT item_count() as cnt`
225
- expect(Number(result[0].cnt)).toBe(200)
226
- } finally {
227
- await sql.end()
228
- }
229
- })
230
-
231
- test('foreign keys intact', async () => {
232
- const sql = wireClient()
233
- try {
234
- const joined =
235
- await sql`SELECT t.label, i.name FROM tags t JOIN items i ON i.id = t.item_id LIMIT 1`
236
- expect(joined.length).toBe(1)
237
-
238
- // FK enforced — inserting with nonexistent item_id should fail
239
- try {
240
- await sql`INSERT INTO tags (item_id, label) VALUES (99999, 'bad')`
241
- expect.unreachable('should have thrown FK violation')
242
- } catch (err: any) {
243
- expect(err.message).toContain('foreign key')
244
- }
245
- } finally {
246
- await sql.end()
247
- }
248
- })
249
-
250
- test('new inserts via wire protocol work after restore', async () => {
251
- const sql = wireClient()
252
- try {
253
- await sql`INSERT INTO items (name, score) VALUES ('post-restore', 9999)`
254
- const result = await sql`SELECT * FROM items WHERE name = 'post-restore'`
255
- expect(result.length).toBe(1)
256
- expect(Number(result[0].score)).toBe(9999)
257
- } finally {
258
- await sql.end()
259
- }
260
- })
261
-
262
- // --- helpers ---
263
-
264
- function wireClient() {
265
- return postgres({
266
- host: '127.0.0.1',
267
- port: pgPort,
268
- user: 'user',
269
- password: 'password',
270
- database: 'postgres',
271
- max: 1,
272
- })
273
- }
274
- })