orez 0.2.26 → 0.2.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cf-do/worker.d.ts.map +1 -1
- package/dist/cf-do/worker.js +9 -1
- package/dist/cf-do/worker.js.map +1 -1
- package/dist/pg-proxy-do-backend.d.ts +2 -0
- package/dist/pg-proxy-do-backend.d.ts.map +1 -1
- package/dist/pg-proxy-do-backend.js +49 -7
- package/dist/pg-proxy-do-backend.js.map +1 -1
- package/dist/pg-sqlite-compiler/catalog/seed.d.ts +67 -0
- package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/catalog/seed.js +436 -0
- package/dist/pg-sqlite-compiler/catalog/seed.js.map +1 -0
- package/dist/pg-sqlite-compiler/index.d.ts +12 -0
- package/dist/pg-sqlite-compiler/index.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/index.js +59 -0
- package/dist/pg-sqlite-compiler/index.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +48 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.js +93 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/catalog.d.ts +34 -0
- package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/catalog.js +30 -0
- package/dist/pg-sqlite-compiler/passes/catalog.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/datetime.d.ts +21 -0
- package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/datetime.js +53 -0
- package/dist/pg-sqlite-compiler/passes/datetime.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/index.d.ts +21 -0
- package/dist/pg-sqlite-compiler/passes/index.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/index.js +39 -0
- package/dist/pg-sqlite-compiler/passes/index.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/types.d.ts +41 -0
- package/dist/pg-sqlite-compiler/passes/types.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/types.js +103 -0
- package/dist/pg-sqlite-compiler/passes/types.js.map +1 -0
- package/dist/pg-sqlite-compiler/test/oracle.d.ts +34 -0
- package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/test/oracle.js +204 -0
- package/dist/pg-sqlite-compiler/test/oracle.js.map +1 -0
- package/dist/pg-sqlite-compiler/types.d.ts +55 -0
- package/dist/pg-sqlite-compiler/types.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/types.js +2 -0
- package/dist/pg-sqlite-compiler/types.js.map +1 -0
- package/package.json +8 -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/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.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/ARCHITECTURE.md +0 -83
- package/src/cf-do/watermark.test.ts +0 -103
- package/src/cf-do/watermark.ts +0 -118
- package/src/cf-do/worker.ts +0 -1033
- 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 -38
- 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 -7157
- package/src/pg-proxy.ts +0 -1087
- 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,511 +0,0 @@
|
|
|
1
|
-
import { PGlite } from '@electric-sql/pglite'
|
|
2
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import { Mutex } from '../mutex'
|
|
5
|
-
import { installChangeTracking } from './change-tracker'
|
|
6
|
-
import {
|
|
7
|
-
extractStartLsn,
|
|
8
|
-
handleReplicationQuery,
|
|
9
|
-
handleStartReplication,
|
|
10
|
-
lsnFromString,
|
|
11
|
-
resetReplicationState,
|
|
12
|
-
signalReplicationChange,
|
|
13
|
-
type ReplicationWriter,
|
|
14
|
-
} from './handler'
|
|
15
|
-
|
|
16
|
-
// parse wire protocol RowDescription+DataRow response into columns/values
|
|
17
|
-
function parseResponse(buf: Uint8Array): { columns: string[]; values: string[] } | null {
|
|
18
|
-
if (buf[0] !== 0x54) return null // RowDescription
|
|
19
|
-
|
|
20
|
-
const dv = new DataView(buf.buffer, buf.byteOffset)
|
|
21
|
-
let pos = 7
|
|
22
|
-
const numFields = dv.getInt16(5)
|
|
23
|
-
const columns: string[] = []
|
|
24
|
-
for (let i = 0; i < numFields; i++) {
|
|
25
|
-
let end = pos
|
|
26
|
-
while (buf[end] !== 0) end++
|
|
27
|
-
columns.push(new TextDecoder().decode(buf.subarray(pos, end)))
|
|
28
|
-
pos = end + 1 + 4 + 2 + 4 + 2 + 4 + 2
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (buf[pos] !== 0x44) return { columns, values: [] }
|
|
32
|
-
pos += 7
|
|
33
|
-
const values: string[] = []
|
|
34
|
-
for (let i = 0; i < numFields; i++) {
|
|
35
|
-
const len = dv.getInt32(pos)
|
|
36
|
-
pos += 4
|
|
37
|
-
values.push(new TextDecoder().decode(buf.subarray(pos, pos + len)))
|
|
38
|
-
pos += len
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return { columns, values }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
describe('handleReplicationQuery', () => {
|
|
45
|
-
let db: PGlite
|
|
46
|
-
|
|
47
|
-
beforeEach(async () => {
|
|
48
|
-
db = new PGlite()
|
|
49
|
-
await db.waitReady
|
|
50
|
-
await installChangeTracking(db)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
afterEach(async () => {
|
|
54
|
-
await db.close()
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('IDENTIFY_SYSTEM returns system info', async () => {
|
|
58
|
-
const res = await handleReplicationQuery('IDENTIFY_SYSTEM', db)
|
|
59
|
-
expect(res).not.toBeNull()
|
|
60
|
-
|
|
61
|
-
const parsed = parseResponse(res!)
|
|
62
|
-
expect(parsed!.columns).toEqual(['systemid', 'timeline', 'xlogpos', 'dbname'])
|
|
63
|
-
expect(parsed!.values[0]).toBe('1234567890')
|
|
64
|
-
expect(parsed!.values[1]).toBe('1')
|
|
65
|
-
expect(parsed!.values[3]).toBe('postgres')
|
|
66
|
-
// xlogpos should be a valid LSN format
|
|
67
|
-
expect(parsed!.values[2]).toMatch(/^[0-9A-F]+\/[0-9A-F]+$/)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('CREATE_REPLICATION_SLOT persists and returns slot info', async () => {
|
|
71
|
-
const res = await handleReplicationQuery(
|
|
72
|
-
'CREATE_REPLICATION_SLOT "test_slot" TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT',
|
|
73
|
-
db
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
const parsed = parseResponse(res!)
|
|
77
|
-
expect(parsed!.values[0]).toBe('test_slot')
|
|
78
|
-
expect(parsed!.values[3]).toBe('pgoutput')
|
|
79
|
-
|
|
80
|
-
const slots = await db.query<{ slot_name: string }>(
|
|
81
|
-
`SELECT slot_name FROM _orez._zero_replication_slots WHERE slot_name = 'test_slot'`
|
|
82
|
-
)
|
|
83
|
-
expect(slots.rows).toHaveLength(1)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('DROP_REPLICATION_SLOT removes slot', async () => {
|
|
87
|
-
await handleReplicationQuery(
|
|
88
|
-
'CREATE_REPLICATION_SLOT "drop_me" TEMPORARY LOGICAL pgoutput',
|
|
89
|
-
db
|
|
90
|
-
)
|
|
91
|
-
await handleReplicationQuery('DROP_REPLICATION_SLOT "drop_me"', db)
|
|
92
|
-
|
|
93
|
-
const slots = await db.query<{ count: string }>(
|
|
94
|
-
`SELECT count(*) as count FROM _orez._zero_replication_slots WHERE slot_name = 'drop_me'`
|
|
95
|
-
)
|
|
96
|
-
expect(Number(slots.rows[0].count)).toBe(0)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('wal_level query returns logical', async () => {
|
|
100
|
-
const res = await handleReplicationQuery(
|
|
101
|
-
"SELECT current_setting('wal_level'), version()",
|
|
102
|
-
db
|
|
103
|
-
)
|
|
104
|
-
expect(res).not.toBeNull()
|
|
105
|
-
const parsed = parseResponse(res!)
|
|
106
|
-
expect(parsed!.values[0]).toBe('logical')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('ALTER ROLE returns success', async () => {
|
|
110
|
-
const res = await handleReplicationQuery('ALTER ROLE user REPLICATION', db)
|
|
111
|
-
expect(res).not.toBeNull()
|
|
112
|
-
// should contain CommandComplete
|
|
113
|
-
expect(res![0]).toBe(0x43) // 'C'
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('returns null for unknown queries', async () => {
|
|
117
|
-
expect(await handleReplicationQuery('SELECT 1', db)).toBeNull()
|
|
118
|
-
})
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
describe('handleStartReplication', () => {
|
|
122
|
-
let db: PGlite
|
|
123
|
-
let replicationPromise: Promise<void>
|
|
124
|
-
const testMutex = new Mutex()
|
|
125
|
-
|
|
126
|
-
beforeEach(async () => {
|
|
127
|
-
resetReplicationState()
|
|
128
|
-
db = new PGlite()
|
|
129
|
-
await db.waitReady
|
|
130
|
-
await db.exec(`
|
|
131
|
-
CREATE TABLE public.items (
|
|
132
|
-
id SERIAL PRIMARY KEY,
|
|
133
|
-
name TEXT NOT NULL,
|
|
134
|
-
value INTEGER
|
|
135
|
-
)
|
|
136
|
-
`)
|
|
137
|
-
await installChangeTracking(db)
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
afterEach(async () => {
|
|
141
|
-
// closing db causes poll loop to exit with 'closed' error
|
|
142
|
-
await db.close()
|
|
143
|
-
// wake handler from idle sleep so it hits the closed db and exits
|
|
144
|
-
signalReplicationChange()
|
|
145
|
-
// wait for the replication promise to settle
|
|
146
|
-
await replicationPromise?.catch(() => {})
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
function createWriter() {
|
|
150
|
-
const written: Uint8Array[] = []
|
|
151
|
-
const writer: ReplicationWriter = {
|
|
152
|
-
write(data: Uint8Array) {
|
|
153
|
-
written.push(new Uint8Array(data))
|
|
154
|
-
},
|
|
155
|
-
}
|
|
156
|
-
return { written, writer }
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// extract all pgoutput message types from a (possibly batched) buffer.
|
|
160
|
-
// each CopyData frame: 0x64 + int32(len) + payload
|
|
161
|
-
// XLogData payload: 0x77 + 24 bytes header + actual message type byte
|
|
162
|
-
function extractPayloadTypes(buf: Uint8Array): number[] {
|
|
163
|
-
const types: number[] = []
|
|
164
|
-
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
165
|
-
let pos = 0
|
|
166
|
-
while (pos < buf.length) {
|
|
167
|
-
if (buf[pos] !== 0x64) break // not CopyData
|
|
168
|
-
const len = dv.getInt32(pos + 1)
|
|
169
|
-
if (buf[pos + 5] === 0x77 && pos + 30 < buf.length) {
|
|
170
|
-
types.push(buf[pos + 30])
|
|
171
|
-
}
|
|
172
|
-
pos += 1 + len
|
|
173
|
-
}
|
|
174
|
-
return types
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function countCopyDataFrames(buf: Uint8Array): number {
|
|
178
|
-
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
179
|
-
let pos = 0
|
|
180
|
-
let count = 0
|
|
181
|
-
while (pos < buf.length) {
|
|
182
|
-
if (buf[pos] !== 0x64) return count
|
|
183
|
-
const len = dv.getInt32(pos + 1)
|
|
184
|
-
pos += 1 + len
|
|
185
|
-
count++
|
|
186
|
-
}
|
|
187
|
-
return count
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
it('sends CopyBothResponse first', async () => {
|
|
191
|
-
const { written, writer } = createWriter()
|
|
192
|
-
|
|
193
|
-
replicationPromise = handleStartReplication(
|
|
194
|
-
'START_REPLICATION SLOT "s" LOGICAL 0/0',
|
|
195
|
-
writer,
|
|
196
|
-
db,
|
|
197
|
-
testMutex
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
await new Promise((r) => setTimeout(r, 200))
|
|
201
|
-
|
|
202
|
-
expect(written.length).toBeGreaterThan(0)
|
|
203
|
-
expect(written[0][0]).toBe(0x57) // 'W' CopyBothResponse
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('sends keepalives', async () => {
|
|
207
|
-
const { written, writer } = createWriter()
|
|
208
|
-
|
|
209
|
-
replicationPromise = handleStartReplication(
|
|
210
|
-
'START_REPLICATION SLOT "s" LOGICAL 0/0',
|
|
211
|
-
writer,
|
|
212
|
-
db,
|
|
213
|
-
testMutex
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
await new Promise((r) => setTimeout(r, 700))
|
|
217
|
-
|
|
218
|
-
const keepalives = written.filter((msg) => msg[0] === 0x64 && msg[5] === 0x6b)
|
|
219
|
-
expect(keepalives.length).toBeGreaterThan(0)
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
it('streams INSERT as BEGIN+RELATION+INSERT+COMMIT', async () => {
|
|
223
|
-
const { written, writer } = createWriter()
|
|
224
|
-
|
|
225
|
-
replicationPromise = handleStartReplication(
|
|
226
|
-
'START_REPLICATION SLOT "s" LOGICAL 0/0',
|
|
227
|
-
writer,
|
|
228
|
-
db,
|
|
229
|
-
testMutex
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
233
|
-
await db.exec(`INSERT INTO public.items (name, value) VALUES ('streamed', 123)`)
|
|
234
|
-
signalReplicationChange()
|
|
235
|
-
await new Promise((r) => setTimeout(r, 700))
|
|
236
|
-
|
|
237
|
-
const types = written.flatMap(extractPayloadTypes)
|
|
238
|
-
|
|
239
|
-
expect(types).toContain(0x42) // BEGIN
|
|
240
|
-
expect(types).toContain(0x52) // RELATION
|
|
241
|
-
expect(types).toContain(0x49) // INSERT
|
|
242
|
-
expect(types).toContain(0x43) // COMMIT
|
|
243
|
-
|
|
244
|
-
// order: BEGIN before RELATION before INSERT before COMMIT
|
|
245
|
-
const beginIdx = types.indexOf(0x42)
|
|
246
|
-
const relIdx = types.indexOf(0x52)
|
|
247
|
-
const insIdx = types.indexOf(0x49)
|
|
248
|
-
const comIdx = types.indexOf(0x43)
|
|
249
|
-
expect(beginIdx).toBeLessThan(relIdx)
|
|
250
|
-
expect(relIdx).toBeLessThan(insIdx)
|
|
251
|
-
expect(insIdx).toBeLessThan(comIdx)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('writes one CopyData frame per socket chunk', async () => {
|
|
255
|
-
const { written, writer } = createWriter()
|
|
256
|
-
|
|
257
|
-
replicationPromise = handleStartReplication(
|
|
258
|
-
'START_REPLICATION SLOT "s" LOGICAL 0/0',
|
|
259
|
-
writer,
|
|
260
|
-
db,
|
|
261
|
-
testMutex
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
265
|
-
await db.exec(`INSERT INTO public.items (name, value) VALUES ('chunked', 123)`)
|
|
266
|
-
signalReplicationChange()
|
|
267
|
-
await new Promise((r) => setTimeout(r, 700))
|
|
268
|
-
|
|
269
|
-
const copyDataWrites = written.filter((msg) => msg[0] === 0x64)
|
|
270
|
-
expect(copyDataWrites.length).toBeGreaterThanOrEqual(4)
|
|
271
|
-
for (const msg of copyDataWrites) {
|
|
272
|
-
expect(countCopyDataFrames(msg)).toBe(1)
|
|
273
|
-
}
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
it('streams UPDATE and DELETE operations', async () => {
|
|
277
|
-
const { written, writer } = createWriter()
|
|
278
|
-
|
|
279
|
-
replicationPromise = handleStartReplication(
|
|
280
|
-
'START_REPLICATION SLOT "s" LOGICAL 0/0',
|
|
281
|
-
writer,
|
|
282
|
-
db,
|
|
283
|
-
testMutex
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
287
|
-
|
|
288
|
-
await db.exec(`INSERT INTO public.items (name, value) VALUES ('mut', 1)`)
|
|
289
|
-
signalReplicationChange()
|
|
290
|
-
await new Promise((r) => setTimeout(r, 700))
|
|
291
|
-
|
|
292
|
-
await db.exec(`UPDATE public.items SET value = 2 WHERE name = 'mut'`)
|
|
293
|
-
signalReplicationChange()
|
|
294
|
-
await new Promise((r) => setTimeout(r, 700))
|
|
295
|
-
|
|
296
|
-
await db.exec(`DELETE FROM public.items WHERE name = 'mut'`)
|
|
297
|
-
signalReplicationChange()
|
|
298
|
-
await new Promise((r) => setTimeout(r, 700))
|
|
299
|
-
|
|
300
|
-
const types = written.flatMap(extractPayloadTypes)
|
|
301
|
-
expect(types).toContain(0x49) // INSERT
|
|
302
|
-
expect(types).toContain(0x55) // UPDATE
|
|
303
|
-
expect(types).toContain(0x44) // DELETE
|
|
304
|
-
}, 10_000)
|
|
305
|
-
|
|
306
|
-
it('only sends RELATION once per table', async () => {
|
|
307
|
-
const { written, writer } = createWriter()
|
|
308
|
-
|
|
309
|
-
replicationPromise = handleStartReplication(
|
|
310
|
-
'START_REPLICATION SLOT "s" LOGICAL 0/0',
|
|
311
|
-
writer,
|
|
312
|
-
db,
|
|
313
|
-
testMutex
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
317
|
-
|
|
318
|
-
await db.exec(`INSERT INTO public.items (name, value) VALUES ('a', 1)`)
|
|
319
|
-
signalReplicationChange()
|
|
320
|
-
await new Promise((r) => setTimeout(r, 700))
|
|
321
|
-
|
|
322
|
-
await db.exec(`INSERT INTO public.items (name, value) VALUES ('b', 2)`)
|
|
323
|
-
signalReplicationChange()
|
|
324
|
-
await new Promise((r) => setTimeout(r, 700))
|
|
325
|
-
|
|
326
|
-
const types = written.flatMap(extractPayloadTypes)
|
|
327
|
-
const relationCount = types.filter((t) => t === 0x52).length
|
|
328
|
-
expect(relationCount).toBe(1)
|
|
329
|
-
}, 10_000)
|
|
330
|
-
|
|
331
|
-
it('sends RELATION for each distinct table', async () => {
|
|
332
|
-
await db.exec(`CREATE TABLE public.other (id SERIAL PRIMARY KEY, label TEXT)`)
|
|
333
|
-
await installChangeTracking(db)
|
|
334
|
-
|
|
335
|
-
const { written, writer } = createWriter()
|
|
336
|
-
|
|
337
|
-
replicationPromise = handleStartReplication(
|
|
338
|
-
'START_REPLICATION SLOT "s" LOGICAL 0/0',
|
|
339
|
-
writer,
|
|
340
|
-
db,
|
|
341
|
-
testMutex
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
345
|
-
|
|
346
|
-
await db.exec(`INSERT INTO public.items (name, value) VALUES ('a', 1)`)
|
|
347
|
-
await db.exec(`INSERT INTO public.other (label) VALUES ('b')`)
|
|
348
|
-
signalReplicationChange()
|
|
349
|
-
await new Promise((r) => setTimeout(r, 700))
|
|
350
|
-
|
|
351
|
-
const types = written.flatMap(extractPayloadTypes)
|
|
352
|
-
const relationCount = types.filter((t) => t === 0x52).length
|
|
353
|
-
expect(relationCount).toBe(2)
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
it('handles rapid sequential inserts', async () => {
|
|
357
|
-
const { written, writer } = createWriter()
|
|
358
|
-
|
|
359
|
-
replicationPromise = handleStartReplication(
|
|
360
|
-
'START_REPLICATION SLOT "s" LOGICAL 0/0',
|
|
361
|
-
writer,
|
|
362
|
-
db,
|
|
363
|
-
testMutex
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
367
|
-
|
|
368
|
-
for (let i = 0; i < 20; i++) {
|
|
369
|
-
await db.exec(`INSERT INTO public.items (name, value) VALUES ('r${i}', ${i})`)
|
|
370
|
-
}
|
|
371
|
-
signalReplicationChange()
|
|
372
|
-
|
|
373
|
-
// wait for handler to process
|
|
374
|
-
await new Promise((r) => setTimeout(r, 1500))
|
|
375
|
-
|
|
376
|
-
const inserts = written.flatMap(extractPayloadTypes).filter((t) => t === 0x49)
|
|
377
|
-
expect(inserts.length).toBe(20)
|
|
378
|
-
}, 10_000)
|
|
379
|
-
|
|
380
|
-
it('each transaction has matching BEGIN and COMMIT', async () => {
|
|
381
|
-
const { written, writer } = createWriter()
|
|
382
|
-
|
|
383
|
-
replicationPromise = handleStartReplication(
|
|
384
|
-
'START_REPLICATION SLOT "s" LOGICAL 0/0',
|
|
385
|
-
writer,
|
|
386
|
-
db,
|
|
387
|
-
testMutex
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
391
|
-
|
|
392
|
-
await db.exec(`INSERT INTO public.items (name, value) VALUES ('tx1', 1)`)
|
|
393
|
-
signalReplicationChange()
|
|
394
|
-
await new Promise((r) => setTimeout(r, 700))
|
|
395
|
-
|
|
396
|
-
await db.exec(`INSERT INTO public.items (name, value) VALUES ('tx2', 2)`)
|
|
397
|
-
signalReplicationChange()
|
|
398
|
-
await new Promise((r) => setTimeout(r, 700))
|
|
399
|
-
|
|
400
|
-
const types = written.flatMap(extractPayloadTypes)
|
|
401
|
-
const begins = types.filter((t) => t === 0x42).length
|
|
402
|
-
const commits = types.filter((t) => t === 0x43).length
|
|
403
|
-
expect(begins).toBe(commits)
|
|
404
|
-
expect(begins).toBeGreaterThanOrEqual(1)
|
|
405
|
-
}, 10_000)
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
describe('InProcessWriter', () => {
|
|
409
|
-
it('routes data to callback', async () => {
|
|
410
|
-
const { InProcessWriter } = await import('./handler')
|
|
411
|
-
const received: Uint8Array[] = []
|
|
412
|
-
const writer = new InProcessWriter((data) => received.push(data))
|
|
413
|
-
|
|
414
|
-
const msg = new Uint8Array([1, 2, 3])
|
|
415
|
-
writer.write(msg)
|
|
416
|
-
expect(received).toHaveLength(1)
|
|
417
|
-
expect(received[0]).toEqual(msg)
|
|
418
|
-
expect(writer.closed).toBe(false)
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
it('stops delivering after close', async () => {
|
|
422
|
-
const { InProcessWriter } = await import('./handler')
|
|
423
|
-
const received: Uint8Array[] = []
|
|
424
|
-
const writer = new InProcessWriter((data) => received.push(data))
|
|
425
|
-
|
|
426
|
-
writer.write(new Uint8Array([1]))
|
|
427
|
-
writer.close()
|
|
428
|
-
writer.write(new Uint8Array([2]))
|
|
429
|
-
|
|
430
|
-
expect(received).toHaveLength(1)
|
|
431
|
-
expect(writer.closed).toBe(true)
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
it('implements ReplicationWriter interface', async () => {
|
|
435
|
-
const { InProcessWriter } = await import('./handler')
|
|
436
|
-
const writer = new InProcessWriter(() => {})
|
|
437
|
-
|
|
438
|
-
// type check: can assign to ReplicationWriter
|
|
439
|
-
const rw: ReplicationWriter = writer
|
|
440
|
-
expect(rw.write).toBeDefined()
|
|
441
|
-
expect(rw.closed).toBe(false)
|
|
442
|
-
})
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
describe('lsnFromString', () => {
|
|
446
|
-
it('parses 0/0 to 0n', () => {
|
|
447
|
-
expect(lsnFromString('0/0')).toBe(0n)
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
it('parses simple LSN', () => {
|
|
451
|
-
expect(lsnFromString('0/1000000')).toBe(0x1000000n)
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
it('combines high and low halves', () => {
|
|
455
|
-
expect(lsnFromString('1/0')).toBe(0x100000000n)
|
|
456
|
-
expect(lsnFromString('1/1')).toBe(0x100000001n)
|
|
457
|
-
expect(lsnFromString('A/B')).toBe(0xa0000000bn)
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
it('is case-insensitive', () => {
|
|
461
|
-
expect(lsnFromString('0/ff')).toBe(0xffn)
|
|
462
|
-
expect(lsnFromString('0/FF')).toBe(0xffn)
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
it('tolerates surrounding whitespace', () => {
|
|
466
|
-
expect(lsnFromString(' 0/100 ')).toBe(0x100n)
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
it('returns null for malformed input', () => {
|
|
470
|
-
expect(lsnFromString('0')).toBeNull()
|
|
471
|
-
expect(lsnFromString('0/')).toBeNull()
|
|
472
|
-
expect(lsnFromString('/0')).toBeNull()
|
|
473
|
-
expect(lsnFromString('xyz')).toBeNull()
|
|
474
|
-
expect(lsnFromString('')).toBeNull()
|
|
475
|
-
})
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
describe('extractStartLsn', () => {
|
|
479
|
-
it('extracts from a basic START_REPLICATION query', () => {
|
|
480
|
-
expect(extractStartLsn('START_REPLICATION SLOT "zero" LOGICAL 0/01000300')).toBe(
|
|
481
|
-
0x1000300n
|
|
482
|
-
)
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
it('handles trailing options', () => {
|
|
486
|
-
expect(
|
|
487
|
-
extractStartLsn(
|
|
488
|
-
`START_REPLICATION SLOT "zero" LOGICAL 0/01000300 (proto_version '4', publication_names 'orez_zero_public')`
|
|
489
|
-
)
|
|
490
|
-
).toBe(0x1000300n)
|
|
491
|
-
})
|
|
492
|
-
|
|
493
|
-
it('handles 0/0 (fresh slot)', () => {
|
|
494
|
-
expect(extractStartLsn('START_REPLICATION SLOT "zero" LOGICAL 0/0')).toBe(0n)
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
it('handles quoted LSN', () => {
|
|
498
|
-
expect(extractStartLsn(`START_REPLICATION SLOT "zero" LOGICAL '0/01000300'`)).toBe(
|
|
499
|
-
0x1000300n
|
|
500
|
-
)
|
|
501
|
-
})
|
|
502
|
-
|
|
503
|
-
it('is case-insensitive on the keyword', () => {
|
|
504
|
-
expect(extractStartLsn('start_replication slot "z" logical 0/abc')).toBe(0xabcn)
|
|
505
|
-
})
|
|
506
|
-
|
|
507
|
-
it('returns null when no LSN is present', () => {
|
|
508
|
-
expect(extractStartLsn('START_REPLICATION SLOT "z"')).toBeNull()
|
|
509
|
-
expect(extractStartLsn('IDENTIFY_SYSTEM')).toBeNull()
|
|
510
|
-
})
|
|
511
|
-
})
|