orez 0.2.27 → 0.2.30

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 (157) hide show
  1. package/dist/cf-do/worker.d.ts +3 -0
  2. package/dist/cf-do/worker.d.ts.map +1 -1
  3. package/dist/cf-do/worker.js +37 -15
  4. package/dist/cf-do/worker.js.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +8 -0
  7. package/dist/index.js.map +1 -1
  8. package/package.json +3 -4
  9. package/src/admin/admin-data.test.ts +0 -348
  10. package/src/admin/http-proxy.ts +0 -252
  11. package/src/admin/log-store.ts +0 -192
  12. package/src/admin/server.ts +0 -471
  13. package/src/admin/ui.ts +0 -1322
  14. package/src/bench/proxy-throughput.bench.ts +0 -343
  15. package/src/bench/serial-mutations.bench.ts +0 -270
  16. package/src/browser.ts +0 -203
  17. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  18. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  19. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  20. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  21. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
  22. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  23. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  24. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  25. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
  26. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
  27. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
  28. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
  29. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
  30. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
  31. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
  32. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
  33. package/src/cf-do/ARCHITECTURE.md +0 -93
  34. package/src/cf-do/CHAT_E2E.md +0 -213
  35. package/src/cf-do/watermark.test.ts +0 -103
  36. package/src/cf-do/watermark.ts +0 -118
  37. package/src/cf-do/worker.ts +0 -1041
  38. package/src/cf-do/wrangler.toml +0 -11
  39. package/src/cf-pglite/README.md +0 -19
  40. package/src/change-tracking.ts +0 -25
  41. package/src/child-process.test.ts +0 -147
  42. package/src/child-process.ts +0 -90
  43. package/src/cli-entry.ts +0 -72
  44. package/src/cli.test.ts +0 -40
  45. package/src/cli.ts +0 -1214
  46. package/src/config.ts +0 -150
  47. package/src/do-sql-tracking.test.ts +0 -19
  48. package/src/do-sql-tracking.ts +0 -19
  49. package/src/index.ts +0 -1215
  50. package/src/integration/integration.test.ts +0 -517
  51. package/src/integration/native-binary.guard.test.ts +0 -13
  52. package/src/integration/native-startup.test.ts +0 -44
  53. package/src/integration/replication-latency.test.ts +0 -428
  54. package/src/integration/restore-live-stress.test.ts +0 -433
  55. package/src/integration/restore-reset.test.ts +0 -400
  56. package/src/integration/restore.test.ts +0 -274
  57. package/src/integration/test-permissions.ts +0 -147
  58. package/src/load-config.ts +0 -46
  59. package/src/log.ts +0 -96
  60. package/src/mutex.ts +0 -47
  61. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  62. package/src/pg-proxy-browser.ts +0 -2022
  63. package/src/pg-proxy-do-backend.test.ts +0 -3890
  64. package/src/pg-proxy-do-backend.ts +0 -7191
  65. package/src/pg-proxy.ts +0 -1087
  66. package/src/pg-sqlite-compiler/README.md +0 -53
  67. package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
  68. package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
  69. package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
  70. package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
  71. package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
  72. package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
  73. package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
  74. package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
  75. package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
  76. package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
  77. package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
  78. package/src/pg-sqlite-compiler/index.ts +0 -73
  79. package/src/pg-sqlite-compiler/integration.test.ts +0 -136
  80. package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
  81. package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
  82. package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
  83. package/src/pg-sqlite-compiler/passes/index.ts +0 -49
  84. package/src/pg-sqlite-compiler/passes/types.ts +0 -156
  85. package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
  86. package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
  87. package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
  88. package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
  89. package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
  90. package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
  91. package/src/pg-sqlite-compiler/types.ts +0 -63
  92. package/src/pglite-ipc.test.ts +0 -116
  93. package/src/pglite-ipc.ts +0 -266
  94. package/src/pglite-manager.ts +0 -557
  95. package/src/pglite-web-proxy.test.ts +0 -57
  96. package/src/pglite-web-proxy.ts +0 -221
  97. package/src/pglite-web-worker.ts +0 -152
  98. package/src/pglite-worker-thread.ts +0 -253
  99. package/src/port.ts +0 -25
  100. package/src/process-title.ts +0 -9
  101. package/src/recovery.ts +0 -155
  102. package/src/replication/change-tracker.test.ts +0 -357
  103. package/src/replication/change-tracker.ts +0 -279
  104. package/src/replication/handler.test.ts +0 -511
  105. package/src/replication/handler.ts +0 -1190
  106. package/src/replication/pgoutput-encoder.test.ts +0 -697
  107. package/src/replication/pgoutput-encoder.ts +0 -373
  108. package/src/replication/tcp-replication.test.ts +0 -876
  109. package/src/replication/zero-compat.test.ts +0 -1150
  110. package/src/restore-stress.test.ts +0 -188
  111. package/src/s3-local.ts +0 -203
  112. package/src/shim/hooks.mjs +0 -120
  113. package/src/shim/register.mjs +0 -4
  114. package/src/sqlite-mode/apply-mode.ts +0 -224
  115. package/src/sqlite-mode/index.ts +0 -15
  116. package/src/sqlite-mode/native-binary.ts +0 -89
  117. package/src/sqlite-mode/package-resolve.ts +0 -17
  118. package/src/sqlite-mode/resolve-mode.ts +0 -80
  119. package/src/sqlite-mode/shim-template.ts +0 -159
  120. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  121. package/src/sqlite-mode/types.ts +0 -30
  122. package/src/vite-plugin.ts +0 -67
  123. package/src/wasm-sqlite.test.ts +0 -537
  124. package/src/worker/browser-admin.ts +0 -52
  125. package/src/worker/browser-build-config.test.ts +0 -71
  126. package/src/worker/browser-build-config.ts +0 -109
  127. package/src/worker/browser-embed-admin.test.ts +0 -75
  128. package/src/worker/browser-embed.ts +0 -345
  129. package/src/worker/cf-patches.ts +0 -384
  130. package/src/worker/embed-integration.test.ts +0 -321
  131. package/src/worker/index.ts +0 -138
  132. package/src/worker/shims/fastify.test.ts +0 -255
  133. package/src/worker/shims/fastify.ts +0 -306
  134. package/src/worker/shims/http-service.test.ts +0 -355
  135. package/src/worker/shims/http-service.ts +0 -293
  136. package/src/worker/shims/node-stub.ts +0 -290
  137. package/src/worker/shims/oxfmt.ts +0 -3
  138. package/src/worker/shims/postgres-browser.ts +0 -59
  139. package/src/worker/shims/postgres-socket.test.ts +0 -576
  140. package/src/worker/shims/postgres-socket.ts +0 -310
  141. package/src/worker/shims/postgres.test.ts +0 -364
  142. package/src/worker/shims/postgres.ts +0 -1454
  143. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  144. package/src/worker/shims/sqlite-browser.ts +0 -175
  145. package/src/worker/shims/sqlite.test.ts +0 -786
  146. package/src/worker/shims/sqlite.ts +0 -978
  147. package/src/worker/shims/stream-browser.ts +0 -15
  148. package/src/worker/shims/ws-browser.test.ts +0 -205
  149. package/src/worker/shims/ws-browser.ts +0 -248
  150. package/src/worker/shims/ws.test.ts +0 -288
  151. package/src/worker/shims/ws.ts +0 -467
  152. package/src/worker/shims/zero-process-env.ts +0 -11
  153. package/src/worker/types.ts +0 -75
  154. package/src/worker/worker-integration.test.ts +0 -223
  155. package/src/worker/worker.test.ts +0 -136
  156. package/src/worker/zero-cache-embed-cf.ts +0 -463
  157. 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
- })