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