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.
- package/package.json +3 -4
- package/src/admin/admin-data.test.ts +0 -348
- package/src/admin/http-proxy.ts +0 -252
- package/src/admin/log-store.ts +0 -192
- package/src/admin/server.ts +0 -471
- package/src/admin/ui.ts +0 -1322
- package/src/bench/proxy-throughput.bench.ts +0 -343
- package/src/bench/serial-mutations.bench.ts +0 -270
- package/src/browser.ts +0 -203
- package/src/cf-do/.wrangler/cache/cf.json +0 -1
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
- package/src/cf-do/ARCHITECTURE.md +0 -93
- package/src/cf-do/CHAT_E2E.md +0 -213
- package/src/cf-do/watermark.test.ts +0 -103
- package/src/cf-do/watermark.ts +0 -118
- package/src/cf-do/worker.ts +0 -1041
- package/src/cf-do/wrangler.toml +0 -11
- package/src/cf-pglite/README.md +0 -19
- package/src/change-tracking.ts +0 -25
- package/src/child-process.test.ts +0 -147
- package/src/child-process.ts +0 -90
- package/src/cli-entry.ts +0 -72
- package/src/cli.test.ts +0 -40
- package/src/cli.ts +0 -1214
- package/src/config.ts +0 -150
- package/src/do-sql-tracking.test.ts +0 -19
- package/src/do-sql-tracking.ts +0 -19
- package/src/index.ts +0 -1215
- package/src/integration/integration.test.ts +0 -517
- package/src/integration/native-binary.guard.test.ts +0 -13
- package/src/integration/native-startup.test.ts +0 -44
- package/src/integration/replication-latency.test.ts +0 -428
- package/src/integration/restore-live-stress.test.ts +0 -433
- package/src/integration/restore-reset.test.ts +0 -400
- package/src/integration/restore.test.ts +0 -274
- package/src/integration/test-permissions.ts +0 -147
- package/src/load-config.ts +0 -46
- package/src/log.ts +0 -96
- package/src/mutex.ts +0 -47
- package/src/pg-proxy-browser.singledb.test.ts +0 -233
- package/src/pg-proxy-browser.ts +0 -2022
- package/src/pg-proxy-do-backend.test.ts +0 -3890
- package/src/pg-proxy-do-backend.ts +0 -7191
- package/src/pg-proxy.ts +0 -1087
- package/src/pg-sqlite-compiler/README.md +0 -53
- package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
- package/src/pg-sqlite-compiler/index.ts +0 -73
- package/src/pg-sqlite-compiler/integration.test.ts +0 -136
- package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
- package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
- package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
- package/src/pg-sqlite-compiler/passes/index.ts +0 -49
- package/src/pg-sqlite-compiler/passes/types.ts +0 -156
- package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
- package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
- package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
- package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
- package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
- package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
- package/src/pg-sqlite-compiler/types.ts +0 -63
- package/src/pglite-ipc.test.ts +0 -116
- package/src/pglite-ipc.ts +0 -266
- package/src/pglite-manager.ts +0 -557
- package/src/pglite-web-proxy.test.ts +0 -57
- package/src/pglite-web-proxy.ts +0 -221
- package/src/pglite-web-worker.ts +0 -152
- package/src/pglite-worker-thread.ts +0 -253
- package/src/port.ts +0 -25
- package/src/process-title.ts +0 -9
- package/src/recovery.ts +0 -155
- package/src/replication/change-tracker.test.ts +0 -357
- package/src/replication/change-tracker.ts +0 -279
- package/src/replication/handler.test.ts +0 -511
- package/src/replication/handler.ts +0 -1190
- package/src/replication/pgoutput-encoder.test.ts +0 -697
- package/src/replication/pgoutput-encoder.ts +0 -373
- package/src/replication/tcp-replication.test.ts +0 -876
- package/src/replication/zero-compat.test.ts +0 -1150
- package/src/restore-stress.test.ts +0 -188
- package/src/s3-local.ts +0 -203
- package/src/shim/hooks.mjs +0 -120
- package/src/shim/register.mjs +0 -4
- package/src/sqlite-mode/apply-mode.ts +0 -224
- package/src/sqlite-mode/index.ts +0 -15
- package/src/sqlite-mode/native-binary.ts +0 -89
- package/src/sqlite-mode/package-resolve.ts +0 -17
- package/src/sqlite-mode/resolve-mode.ts +0 -80
- package/src/sqlite-mode/shim-template.ts +0 -159
- package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
- package/src/sqlite-mode/types.ts +0 -30
- package/src/vite-plugin.ts +0 -67
- package/src/wasm-sqlite.test.ts +0 -537
- package/src/worker/browser-admin.ts +0 -52
- package/src/worker/browser-build-config.test.ts +0 -71
- package/src/worker/browser-build-config.ts +0 -109
- package/src/worker/browser-embed-admin.test.ts +0 -75
- package/src/worker/browser-embed.ts +0 -345
- package/src/worker/cf-patches.ts +0 -384
- package/src/worker/embed-integration.test.ts +0 -321
- package/src/worker/index.ts +0 -138
- package/src/worker/shims/fastify.test.ts +0 -255
- package/src/worker/shims/fastify.ts +0 -306
- package/src/worker/shims/http-service.test.ts +0 -355
- package/src/worker/shims/http-service.ts +0 -293
- package/src/worker/shims/node-stub.ts +0 -290
- package/src/worker/shims/oxfmt.ts +0 -3
- package/src/worker/shims/postgres-browser.ts +0 -59
- package/src/worker/shims/postgres-socket.test.ts +0 -576
- package/src/worker/shims/postgres-socket.ts +0 -310
- package/src/worker/shims/postgres.test.ts +0 -364
- package/src/worker/shims/postgres.ts +0 -1454
- package/src/worker/shims/sqlite-browser.test.ts +0 -233
- package/src/worker/shims/sqlite-browser.ts +0 -175
- package/src/worker/shims/sqlite.test.ts +0 -786
- package/src/worker/shims/sqlite.ts +0 -978
- package/src/worker/shims/stream-browser.ts +0 -15
- package/src/worker/shims/ws-browser.test.ts +0 -205
- package/src/worker/shims/ws-browser.ts +0 -248
- package/src/worker/shims/ws.test.ts +0 -288
- package/src/worker/shims/ws.ts +0 -467
- package/src/worker/shims/zero-process-env.ts +0 -11
- package/src/worker/types.ts +0 -75
- package/src/worker/worker-integration.test.ts +0 -223
- package/src/worker/worker.test.ts +0 -136
- package/src/worker/zero-cache-embed-cf.ts +0 -463
- 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
|
-
})
|