orez 0.2.26 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/cf-do/worker.d.ts.map +1 -1
  2. package/dist/cf-do/worker.js +9 -1
  3. package/dist/cf-do/worker.js.map +1 -1
  4. package/dist/pg-proxy-do-backend.d.ts +2 -0
  5. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  6. package/dist/pg-proxy-do-backend.js +49 -7
  7. package/dist/pg-proxy-do-backend.js.map +1 -1
  8. package/dist/pg-sqlite-compiler/catalog/seed.d.ts +67 -0
  9. package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +1 -0
  10. package/dist/pg-sqlite-compiler/catalog/seed.js +436 -0
  11. package/dist/pg-sqlite-compiler/catalog/seed.js.map +1 -0
  12. package/dist/pg-sqlite-compiler/index.d.ts +12 -0
  13. package/dist/pg-sqlite-compiler/index.d.ts.map +1 -0
  14. package/dist/pg-sqlite-compiler/index.js +59 -0
  15. package/dist/pg-sqlite-compiler/index.js.map +1 -0
  16. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +48 -0
  17. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +1 -0
  18. package/dist/pg-sqlite-compiler/passes/ast-utils.js +93 -0
  19. package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +1 -0
  20. package/dist/pg-sqlite-compiler/passes/catalog.d.ts +34 -0
  21. package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +1 -0
  22. package/dist/pg-sqlite-compiler/passes/catalog.js +30 -0
  23. package/dist/pg-sqlite-compiler/passes/catalog.js.map +1 -0
  24. package/dist/pg-sqlite-compiler/passes/datetime.d.ts +21 -0
  25. package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +1 -0
  26. package/dist/pg-sqlite-compiler/passes/datetime.js +53 -0
  27. package/dist/pg-sqlite-compiler/passes/datetime.js.map +1 -0
  28. package/dist/pg-sqlite-compiler/passes/index.d.ts +21 -0
  29. package/dist/pg-sqlite-compiler/passes/index.d.ts.map +1 -0
  30. package/dist/pg-sqlite-compiler/passes/index.js +39 -0
  31. package/dist/pg-sqlite-compiler/passes/index.js.map +1 -0
  32. package/dist/pg-sqlite-compiler/passes/types.d.ts +41 -0
  33. package/dist/pg-sqlite-compiler/passes/types.d.ts.map +1 -0
  34. package/dist/pg-sqlite-compiler/passes/types.js +103 -0
  35. package/dist/pg-sqlite-compiler/passes/types.js.map +1 -0
  36. package/dist/pg-sqlite-compiler/test/oracle.d.ts +34 -0
  37. package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +1 -0
  38. package/dist/pg-sqlite-compiler/test/oracle.js +204 -0
  39. package/dist/pg-sqlite-compiler/test/oracle.js.map +1 -0
  40. package/dist/pg-sqlite-compiler/types.d.ts +55 -0
  41. package/dist/pg-sqlite-compiler/types.d.ts.map +1 -0
  42. package/dist/pg-sqlite-compiler/types.js +2 -0
  43. package/dist/pg-sqlite-compiler/types.js.map +1 -0
  44. package/package.json +8 -4
  45. package/src/admin/admin-data.test.ts +0 -348
  46. package/src/admin/http-proxy.ts +0 -252
  47. package/src/admin/log-store.ts +0 -192
  48. package/src/admin/server.ts +0 -471
  49. package/src/admin/ui.ts +0 -1322
  50. package/src/bench/proxy-throughput.bench.ts +0 -343
  51. package/src/bench/serial-mutations.bench.ts +0 -270
  52. package/src/browser.ts +0 -203
  53. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  54. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  55. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  56. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  57. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
  58. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
  59. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
  60. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
  61. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
  62. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
  63. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
  64. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
  65. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
  66. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
  67. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
  68. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
  69. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
  70. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
  71. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.sqlite +0 -0
  72. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  73. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  74. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  75. package/src/cf-do/ARCHITECTURE.md +0 -83
  76. package/src/cf-do/watermark.test.ts +0 -103
  77. package/src/cf-do/watermark.ts +0 -118
  78. package/src/cf-do/worker.ts +0 -1033
  79. package/src/cf-do/wrangler.toml +0 -11
  80. package/src/cf-pglite/README.md +0 -19
  81. package/src/change-tracking.ts +0 -25
  82. package/src/child-process.test.ts +0 -147
  83. package/src/child-process.ts +0 -90
  84. package/src/cli-entry.ts +0 -72
  85. package/src/cli.test.ts +0 -38
  86. package/src/cli.ts +0 -1214
  87. package/src/config.ts +0 -150
  88. package/src/do-sql-tracking.test.ts +0 -19
  89. package/src/do-sql-tracking.ts +0 -19
  90. package/src/index.ts +0 -1215
  91. package/src/integration/integration.test.ts +0 -517
  92. package/src/integration/native-binary.guard.test.ts +0 -13
  93. package/src/integration/native-startup.test.ts +0 -44
  94. package/src/integration/replication-latency.test.ts +0 -428
  95. package/src/integration/restore-live-stress.test.ts +0 -433
  96. package/src/integration/restore-reset.test.ts +0 -400
  97. package/src/integration/restore.test.ts +0 -274
  98. package/src/integration/test-permissions.ts +0 -147
  99. package/src/load-config.ts +0 -46
  100. package/src/log.ts +0 -96
  101. package/src/mutex.ts +0 -47
  102. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  103. package/src/pg-proxy-browser.ts +0 -2022
  104. package/src/pg-proxy-do-backend.test.ts +0 -3890
  105. package/src/pg-proxy-do-backend.ts +0 -7157
  106. package/src/pg-proxy.ts +0 -1087
  107. package/src/pglite-ipc.test.ts +0 -116
  108. package/src/pglite-ipc.ts +0 -266
  109. package/src/pglite-manager.ts +0 -557
  110. package/src/pglite-web-proxy.test.ts +0 -57
  111. package/src/pglite-web-proxy.ts +0 -221
  112. package/src/pglite-web-worker.ts +0 -152
  113. package/src/pglite-worker-thread.ts +0 -253
  114. package/src/port.ts +0 -25
  115. package/src/process-title.ts +0 -9
  116. package/src/recovery.ts +0 -155
  117. package/src/replication/change-tracker.test.ts +0 -357
  118. package/src/replication/change-tracker.ts +0 -279
  119. package/src/replication/handler.test.ts +0 -511
  120. package/src/replication/handler.ts +0 -1190
  121. package/src/replication/pgoutput-encoder.test.ts +0 -697
  122. package/src/replication/pgoutput-encoder.ts +0 -373
  123. package/src/replication/tcp-replication.test.ts +0 -876
  124. package/src/replication/zero-compat.test.ts +0 -1150
  125. package/src/restore-stress.test.ts +0 -188
  126. package/src/s3-local.ts +0 -203
  127. package/src/shim/hooks.mjs +0 -120
  128. package/src/shim/register.mjs +0 -4
  129. package/src/sqlite-mode/apply-mode.ts +0 -224
  130. package/src/sqlite-mode/index.ts +0 -15
  131. package/src/sqlite-mode/native-binary.ts +0 -89
  132. package/src/sqlite-mode/package-resolve.ts +0 -17
  133. package/src/sqlite-mode/resolve-mode.ts +0 -80
  134. package/src/sqlite-mode/shim-template.ts +0 -159
  135. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  136. package/src/sqlite-mode/types.ts +0 -30
  137. package/src/vite-plugin.ts +0 -67
  138. package/src/wasm-sqlite.test.ts +0 -537
  139. package/src/worker/browser-admin.ts +0 -52
  140. package/src/worker/browser-build-config.test.ts +0 -71
  141. package/src/worker/browser-build-config.ts +0 -109
  142. package/src/worker/browser-embed-admin.test.ts +0 -75
  143. package/src/worker/browser-embed.ts +0 -345
  144. package/src/worker/cf-patches.ts +0 -384
  145. package/src/worker/embed-integration.test.ts +0 -321
  146. package/src/worker/index.ts +0 -138
  147. package/src/worker/shims/fastify.test.ts +0 -255
  148. package/src/worker/shims/fastify.ts +0 -306
  149. package/src/worker/shims/http-service.test.ts +0 -355
  150. package/src/worker/shims/http-service.ts +0 -293
  151. package/src/worker/shims/node-stub.ts +0 -290
  152. package/src/worker/shims/oxfmt.ts +0 -3
  153. package/src/worker/shims/postgres-browser.ts +0 -59
  154. package/src/worker/shims/postgres-socket.test.ts +0 -576
  155. package/src/worker/shims/postgres-socket.ts +0 -310
  156. package/src/worker/shims/postgres.test.ts +0 -364
  157. package/src/worker/shims/postgres.ts +0 -1454
  158. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  159. package/src/worker/shims/sqlite-browser.ts +0 -175
  160. package/src/worker/shims/sqlite.test.ts +0 -786
  161. package/src/worker/shims/sqlite.ts +0 -978
  162. package/src/worker/shims/stream-browser.ts +0 -15
  163. package/src/worker/shims/ws-browser.test.ts +0 -205
  164. package/src/worker/shims/ws-browser.ts +0 -248
  165. package/src/worker/shims/ws.test.ts +0 -288
  166. package/src/worker/shims/ws.ts +0 -467
  167. package/src/worker/shims/zero-process-env.ts +0 -11
  168. package/src/worker/types.ts +0 -75
  169. package/src/worker/worker-integration.test.ts +0 -223
  170. package/src/worker/worker.test.ts +0 -136
  171. package/src/worker/zero-cache-embed-cf.ts +0 -463
  172. package/src/worker/zero-cache-embed.ts +0 -277
@@ -1,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
- })