orez 0.2.27 → 0.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/cf-do/worker.d.ts +3 -0
  2. package/dist/cf-do/worker.d.ts.map +1 -1
  3. package/dist/cf-do/worker.js +37 -15
  4. package/dist/cf-do/worker.js.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +8 -0
  7. package/dist/index.js.map +1 -1
  8. package/package.json +3 -4
  9. package/src/admin/admin-data.test.ts +0 -348
  10. package/src/admin/http-proxy.ts +0 -252
  11. package/src/admin/log-store.ts +0 -192
  12. package/src/admin/server.ts +0 -471
  13. package/src/admin/ui.ts +0 -1322
  14. package/src/bench/proxy-throughput.bench.ts +0 -343
  15. package/src/bench/serial-mutations.bench.ts +0 -270
  16. package/src/browser.ts +0 -203
  17. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  18. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  19. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  20. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  21. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
  22. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  23. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  24. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  25. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
  26. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
  27. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
  28. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
  29. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
  30. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
  31. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
  32. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
  33. package/src/cf-do/ARCHITECTURE.md +0 -93
  34. package/src/cf-do/CHAT_E2E.md +0 -213
  35. package/src/cf-do/watermark.test.ts +0 -103
  36. package/src/cf-do/watermark.ts +0 -118
  37. package/src/cf-do/worker.ts +0 -1041
  38. package/src/cf-do/wrangler.toml +0 -11
  39. package/src/cf-pglite/README.md +0 -19
  40. package/src/change-tracking.ts +0 -25
  41. package/src/child-process.test.ts +0 -147
  42. package/src/child-process.ts +0 -90
  43. package/src/cli-entry.ts +0 -72
  44. package/src/cli.test.ts +0 -40
  45. package/src/cli.ts +0 -1214
  46. package/src/config.ts +0 -150
  47. package/src/do-sql-tracking.test.ts +0 -19
  48. package/src/do-sql-tracking.ts +0 -19
  49. package/src/index.ts +0 -1215
  50. package/src/integration/integration.test.ts +0 -517
  51. package/src/integration/native-binary.guard.test.ts +0 -13
  52. package/src/integration/native-startup.test.ts +0 -44
  53. package/src/integration/replication-latency.test.ts +0 -428
  54. package/src/integration/restore-live-stress.test.ts +0 -433
  55. package/src/integration/restore-reset.test.ts +0 -400
  56. package/src/integration/restore.test.ts +0 -274
  57. package/src/integration/test-permissions.ts +0 -147
  58. package/src/load-config.ts +0 -46
  59. package/src/log.ts +0 -96
  60. package/src/mutex.ts +0 -47
  61. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  62. package/src/pg-proxy-browser.ts +0 -2022
  63. package/src/pg-proxy-do-backend.test.ts +0 -3890
  64. package/src/pg-proxy-do-backend.ts +0 -7191
  65. package/src/pg-proxy.ts +0 -1087
  66. package/src/pg-sqlite-compiler/README.md +0 -53
  67. package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
  68. package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
  69. package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
  70. package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
  71. package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
  72. package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
  73. package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
  74. package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
  75. package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
  76. package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
  77. package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
  78. package/src/pg-sqlite-compiler/index.ts +0 -73
  79. package/src/pg-sqlite-compiler/integration.test.ts +0 -136
  80. package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
  81. package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
  82. package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
  83. package/src/pg-sqlite-compiler/passes/index.ts +0 -49
  84. package/src/pg-sqlite-compiler/passes/types.ts +0 -156
  85. package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
  86. package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
  87. package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
  88. package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
  89. package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
  90. package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
  91. package/src/pg-sqlite-compiler/types.ts +0 -63
  92. package/src/pglite-ipc.test.ts +0 -116
  93. package/src/pglite-ipc.ts +0 -266
  94. package/src/pglite-manager.ts +0 -557
  95. package/src/pglite-web-proxy.test.ts +0 -57
  96. package/src/pglite-web-proxy.ts +0 -221
  97. package/src/pglite-web-worker.ts +0 -152
  98. package/src/pglite-worker-thread.ts +0 -253
  99. package/src/port.ts +0 -25
  100. package/src/process-title.ts +0 -9
  101. package/src/recovery.ts +0 -155
  102. package/src/replication/change-tracker.test.ts +0 -357
  103. package/src/replication/change-tracker.ts +0 -279
  104. package/src/replication/handler.test.ts +0 -511
  105. package/src/replication/handler.ts +0 -1190
  106. package/src/replication/pgoutput-encoder.test.ts +0 -697
  107. package/src/replication/pgoutput-encoder.ts +0 -373
  108. package/src/replication/tcp-replication.test.ts +0 -876
  109. package/src/replication/zero-compat.test.ts +0 -1150
  110. package/src/restore-stress.test.ts +0 -188
  111. package/src/s3-local.ts +0 -203
  112. package/src/shim/hooks.mjs +0 -120
  113. package/src/shim/register.mjs +0 -4
  114. package/src/sqlite-mode/apply-mode.ts +0 -224
  115. package/src/sqlite-mode/index.ts +0 -15
  116. package/src/sqlite-mode/native-binary.ts +0 -89
  117. package/src/sqlite-mode/package-resolve.ts +0 -17
  118. package/src/sqlite-mode/resolve-mode.ts +0 -80
  119. package/src/sqlite-mode/shim-template.ts +0 -159
  120. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  121. package/src/sqlite-mode/types.ts +0 -30
  122. package/src/vite-plugin.ts +0 -67
  123. package/src/wasm-sqlite.test.ts +0 -537
  124. package/src/worker/browser-admin.ts +0 -52
  125. package/src/worker/browser-build-config.test.ts +0 -71
  126. package/src/worker/browser-build-config.ts +0 -109
  127. package/src/worker/browser-embed-admin.test.ts +0 -75
  128. package/src/worker/browser-embed.ts +0 -345
  129. package/src/worker/cf-patches.ts +0 -384
  130. package/src/worker/embed-integration.test.ts +0 -321
  131. package/src/worker/index.ts +0 -138
  132. package/src/worker/shims/fastify.test.ts +0 -255
  133. package/src/worker/shims/fastify.ts +0 -306
  134. package/src/worker/shims/http-service.test.ts +0 -355
  135. package/src/worker/shims/http-service.ts +0 -293
  136. package/src/worker/shims/node-stub.ts +0 -290
  137. package/src/worker/shims/oxfmt.ts +0 -3
  138. package/src/worker/shims/postgres-browser.ts +0 -59
  139. package/src/worker/shims/postgres-socket.test.ts +0 -576
  140. package/src/worker/shims/postgres-socket.ts +0 -310
  141. package/src/worker/shims/postgres.test.ts +0 -364
  142. package/src/worker/shims/postgres.ts +0 -1454
  143. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  144. package/src/worker/shims/sqlite-browser.ts +0 -175
  145. package/src/worker/shims/sqlite.test.ts +0 -786
  146. package/src/worker/shims/sqlite.ts +0 -978
  147. package/src/worker/shims/stream-browser.ts +0 -15
  148. package/src/worker/shims/ws-browser.test.ts +0 -205
  149. package/src/worker/shims/ws-browser.ts +0 -248
  150. package/src/worker/shims/ws.test.ts +0 -288
  151. package/src/worker/shims/ws.ts +0 -467
  152. package/src/worker/shims/zero-process-env.ts +0 -11
  153. package/src/worker/types.ts +0 -75
  154. package/src/worker/worker-integration.test.ts +0 -223
  155. package/src/worker/worker.test.ts +0 -136
  156. package/src/worker/zero-cache-embed-cf.ts +0 -463
  157. package/src/worker/zero-cache-embed.ts +0 -277
@@ -1,697 +0,0 @@
1
- import { join } from 'node:path'
2
-
3
- import { beforeAll, describe, expect, it } from 'vitest'
4
-
5
- import {
6
- encodeBegin,
7
- encodeCommit,
8
- encodeRelation,
9
- encodeInsert,
10
- encodeUpdate,
11
- encodeDelete,
12
- encodeKeepalive,
13
- wrapXLogData,
14
- wrapCopyData,
15
- getTableOid,
16
- inferColumns,
17
- type ColumnInfo,
18
- } from './pgoutput-encoder'
19
-
20
- // pg epoch: 2000-01-01 in microseconds from unix epoch
21
- const PG_EPOCH_MICROS = 946684800000000n
22
-
23
- // mini decoder helpers
24
- function r16(buf: Uint8Array, off: number) {
25
- return new DataView(buf.buffer, buf.byteOffset).getInt16(off)
26
- }
27
- function r32(buf: Uint8Array, off: number) {
28
- return new DataView(buf.buffer, buf.byteOffset).getInt32(off)
29
- }
30
- function r64(buf: Uint8Array, off: number) {
31
- return new DataView(buf.buffer, buf.byteOffset).getBigInt64(off)
32
- }
33
- function rCStr(buf: Uint8Array, off: number): [string, number] {
34
- let end = off
35
- while (end < buf.length && buf[end] !== 0) end++
36
- return [new TextDecoder().decode(buf.subarray(off, end)), end + 1]
37
- }
38
- function rText(buf: Uint8Array, off: number): [string, number] {
39
- const len = r32(buf, off)
40
- const str = new TextDecoder().decode(buf.subarray(off + 4, off + 4 + len))
41
- return [str, off + 4 + len]
42
- }
43
- function rTupleTextValues(buf: Uint8Array, off: number): [Array<string | null>, number] {
44
- const values: Array<string | null> = []
45
- const count = r16(buf, off)
46
- let pos = off + 2
47
- for (let i = 0; i < count; i++) {
48
- const kind = buf[pos++]
49
- if (kind === 0x6e) {
50
- values.push(null)
51
- continue
52
- }
53
- expect(kind).toBe(0x74)
54
- const len = r32(buf, pos)
55
- pos += 4
56
- values.push(new TextDecoder().decode(buf.subarray(pos, pos + len)))
57
- pos += len
58
- }
59
- return [values, pos]
60
- }
61
-
62
- describe('pgoutput-encoder', () => {
63
- describe('encodeBegin', () => {
64
- it('produces correct binary layout', () => {
65
- const lsn = 0x1000100n
66
- const ts = BigInt(Date.now()) * 1000n
67
- const xid = 42
68
-
69
- const buf = encodeBegin(lsn, ts, xid)
70
-
71
- expect(buf.length).toBe(21)
72
- expect(buf[0]).toBe(0x42) // 'B'
73
- expect(r64(buf, 1)).toBe(lsn)
74
- expect(r64(buf, 9)).toBe(ts - PG_EPOCH_MICROS)
75
- expect(r32(buf, 17)).toBe(xid)
76
- })
77
- })
78
-
79
- describe('encodeCommit', () => {
80
- it('produces correct binary layout', () => {
81
- const lsn = 0x1000100n
82
- const endLsn = 0x1000200n
83
- const ts = BigInt(Date.now()) * 1000n
84
-
85
- const buf = encodeCommit(0, lsn, endLsn, ts)
86
-
87
- expect(buf.length).toBe(26)
88
- expect(buf[0]).toBe(0x43) // 'C'
89
- expect(buf[1]).toBe(0)
90
- expect(r64(buf, 2)).toBe(lsn)
91
- expect(r64(buf, 10)).toBe(endLsn)
92
- expect(r64(buf, 18)).toBe(ts - PG_EPOCH_MICROS)
93
- })
94
- })
95
-
96
- describe('encodeRelation', () => {
97
- it('encodes table with columns', () => {
98
- const cols: ColumnInfo[] = [
99
- { name: 'id', typeOid: 23, typeMod: -1 },
100
- { name: 'name', typeOid: 25, typeMod: -1 },
101
- ]
102
-
103
- const buf = encodeRelation(16384, 'public', 'users', 0x64, cols)
104
-
105
- expect(buf[0]).toBe(0x52) // 'R'
106
- expect(r32(buf, 1)).toBe(16384)
107
-
108
- let pos = 5
109
- const [schema, p1] = rCStr(buf, pos)
110
- expect(schema).toBe('public')
111
- pos = p1
112
-
113
- const [table, p2] = rCStr(buf, pos)
114
- expect(table).toBe('users')
115
- pos = p2
116
-
117
- expect(buf[pos]).toBe(0x64) // replica identity
118
- pos++
119
- expect(r16(buf, pos)).toBe(2)
120
- pos += 2
121
-
122
- // col 0
123
- expect(buf[pos++]).toBe(0) // flags
124
- const [c1, p3] = rCStr(buf, pos)
125
- expect(c1).toBe('id')
126
- pos = p3
127
- expect(r32(buf, pos)).toBe(23)
128
- pos += 4
129
- expect(r32(buf, pos)).toBe(-1)
130
- pos += 4
131
-
132
- // col 1
133
- expect(buf[pos++]).toBe(0)
134
- const [c2, p4] = rCStr(buf, pos)
135
- expect(c2).toBe('name')
136
- pos = p4
137
- expect(r32(buf, pos)).toBe(25)
138
- pos += 4
139
- expect(r32(buf, pos)).toBe(-1)
140
- pos += 4
141
-
142
- expect(pos).toBe(buf.length)
143
- })
144
-
145
- it('handles zero columns', () => {
146
- const buf = encodeRelation(16384, 'public', 'empty', 0x64, [])
147
- expect(buf[0]).toBe(0x52)
148
- let pos = 5
149
- ;[, pos] = rCStr(buf, pos)
150
- ;[, pos] = rCStr(buf, pos)
151
- pos++ // replica identity
152
- expect(r16(buf, pos)).toBe(0)
153
- })
154
- })
155
-
156
- describe('encodeInsert', () => {
157
- const cols: ColumnInfo[] = [
158
- { name: 'id', typeOid: 25, typeMod: -1 },
159
- { name: 'name', typeOid: 25, typeMod: -1 },
160
- ]
161
-
162
- it('encodes row values', () => {
163
- const buf = encodeInsert(16384, { id: '1', name: 'alice' }, cols)
164
-
165
- expect(buf[0]).toBe(0x49) // 'I'
166
- expect(r32(buf, 1)).toBe(16384)
167
- expect(buf[5]).toBe(0x4e) // 'N'
168
-
169
- expect(r16(buf, 6)).toBe(2) // num cols
170
- let pos = 8
171
- expect(buf[pos]).toBe(0x74) // 't'
172
- const [v1, p1] = rText(buf, pos + 1)
173
- expect(v1).toBe('1')
174
- pos = p1 + 1 // +1 for 't' byte
175
- // hmm wait, let me recalculate
176
-
177
- // actually the tuple format is: numCols(2) + for each: type(1) + if text: len(4)+data
178
- // so at offset 8: type byte, then len, then data
179
- pos = 8
180
- expect(buf[pos]).toBe(0x74) // 't'
181
- pos++
182
- expect(r32(buf, pos)).toBe(1) // length of '1'
183
- pos += 4
184
- expect(new TextDecoder().decode(buf.subarray(pos, pos + 1))).toBe('1')
185
- pos += 1
186
-
187
- expect(buf[pos]).toBe(0x74)
188
- pos++
189
- expect(r32(buf, pos)).toBe(5)
190
- pos += 4
191
- expect(new TextDecoder().decode(buf.subarray(pos, pos + 5))).toBe('alice')
192
- })
193
-
194
- it('encodes null values', () => {
195
- const buf = encodeInsert(16384, { id: '1', name: null }, cols)
196
-
197
- let pos = 8 // start of first value
198
- expect(buf[pos]).toBe(0x74) // first val: text
199
- pos += 1 + 4 + 1 // 't' + len + '1'
200
- expect(buf[pos]).toBe(0x6e) // second val: null
201
- })
202
-
203
- it('encodes unicode strings', () => {
204
- const buf = encodeInsert(16384, { id: '1', name: '日本語 🎉' }, cols)
205
-
206
- // find second value
207
- let pos = 8
208
- pos++ // 't'
209
- const len1 = r32(buf, pos)
210
- pos += 4 + len1
211
-
212
- expect(buf[pos]).toBe(0x74)
213
- pos++
214
- const len2 = r32(buf, pos)
215
- pos += 4
216
- const decoded = new TextDecoder().decode(buf.subarray(pos, pos + len2))
217
- expect(decoded).toBe('日本語 🎉')
218
- })
219
-
220
- it('encodes object values as JSON', () => {
221
- const metaCols: ColumnInfo[] = [{ name: 'meta', typeOid: 25, typeMod: -1 }]
222
- const buf = encodeInsert(16384, { meta: { foo: 'bar', n: 42 } }, metaCols)
223
-
224
- let pos = 8 // first value
225
- expect(buf[pos]).toBe(0x74)
226
- pos++
227
- const len = r32(buf, pos)
228
- pos += 4
229
- const decoded = new TextDecoder().decode(buf.subarray(pos, pos + len))
230
- expect(JSON.parse(decoded)).toEqual({ foo: 'bar', n: 42 })
231
- })
232
-
233
- it('encodes empty string', () => {
234
- const buf = encodeInsert(16384, { id: '', name: '' }, cols)
235
-
236
- let pos = 8
237
- expect(buf[pos]).toBe(0x74)
238
- pos++
239
- expect(r32(buf, pos)).toBe(0) // empty string length
240
- })
241
- })
242
-
243
- describe('encodeUpdate', () => {
244
- const cols: ColumnInfo[] = [
245
- { name: 'id', typeOid: 25, typeMod: -1 },
246
- { name: 'val', typeOid: 25, typeMod: -1 },
247
- ]
248
-
249
- it('includes old tuple when provided', () => {
250
- const buf = encodeUpdate(
251
- 16384,
252
- { id: '1', val: 'new' },
253
- { id: '1', val: 'old' },
254
- cols
255
- )
256
-
257
- expect(buf[0]).toBe(0x55) // 'U'
258
- expect(r32(buf, 1)).toBe(16384)
259
- expect(buf[5]).toBe(0x4f) // 'O' old tuple
260
- })
261
-
262
- it('skips old tuple when null', () => {
263
- const buf = encodeUpdate(16384, { id: '1', val: 'new' }, null, cols)
264
-
265
- expect(buf[0]).toBe(0x55)
266
- expect(buf[5]).toBe(0x4e) // 'N' directly
267
- })
268
-
269
- it('old tuple precedes new tuple', () => {
270
- const buf = encodeUpdate(
271
- 16384,
272
- { id: '1', val: 'new' },
273
- { id: '1', val: 'old' },
274
- cols
275
- )
276
-
277
- // 'O' at offset 5, then old tuple, then 'N', then new tuple
278
- expect(buf[5]).toBe(0x4f)
279
- // find 'N' marker after old tuple
280
- // old tuple: numCols(2) + 2 values
281
- let pos = 6 // start of old tuple
282
- const numOldCols = r16(buf, pos)
283
- expect(numOldCols).toBe(2)
284
- pos += 2
285
- // skip values
286
- for (let i = 0; i < numOldCols; i++) {
287
- if (buf[pos] === 0x6e) {
288
- pos++
289
- } else {
290
- pos++ // 't'
291
- const len = r32(buf, pos)
292
- pos += 4 + len
293
- }
294
- }
295
- expect(buf[pos]).toBe(0x4e) // 'N' new tuple marker
296
- })
297
-
298
- it('encodes sqlite integer booleans as postgres bool text', () => {
299
- const boolCols: ColumnInfo[] = [
300
- { name: 'id', typeOid: 25, typeMod: -1, isKey: true },
301
- { name: 'completed', typeOid: 16, typeMod: -1 },
302
- ]
303
- const buf = encodeUpdate(
304
- 16384,
305
- { id: '1', completed: 1 },
306
- { id: '1', completed: 0 },
307
- boolCols
308
- )
309
-
310
- expect(buf[5]).toBe(0x4f) // 'O' old tuple
311
- const [oldValues, afterOld] = rTupleTextValues(buf, 6)
312
- expect(oldValues).toEqual(['1', 'f'])
313
- expect(buf[afterOld]).toBe(0x4e) // 'N' new tuple
314
- const [newValues] = rTupleTextValues(buf, afterOld + 1)
315
- expect(newValues).toEqual(['1', 't'])
316
- })
317
- })
318
-
319
- describe('encodeDelete', () => {
320
- it('encodes with key tuple marker', () => {
321
- const cols: ColumnInfo[] = [{ name: 'id', typeOid: 25, typeMod: -1 }]
322
- const buf = encodeDelete(16384, { id: '42' }, cols)
323
-
324
- expect(buf[0]).toBe(0x44) // 'D'
325
- expect(r32(buf, 1)).toBe(16384)
326
- expect(buf[5]).toBe(0x4b) // 'K'
327
- })
328
- })
329
-
330
- describe('wrapXLogData', () => {
331
- it('wraps payload with wal positions', () => {
332
- const payload = new Uint8Array([1, 2, 3])
333
- const ts = BigInt(Date.now()) * 1000n
334
-
335
- const buf = wrapXLogData(0x100n, 0x200n, ts, payload)
336
-
337
- expect(buf[0]).toBe(0x77) // 'w'
338
- expect(r64(buf, 1)).toBe(0x100n)
339
- expect(r64(buf, 9)).toBe(0x200n)
340
- expect(r64(buf, 17)).toBe(ts - PG_EPOCH_MICROS)
341
- expect(Array.from(buf.subarray(25))).toEqual([1, 2, 3])
342
- })
343
- })
344
-
345
- describe('wrapCopyData', () => {
346
- it('wraps with length prefix', () => {
347
- const inner = new Uint8Array([0xaa, 0xbb])
348
-
349
- const buf = wrapCopyData(inner)
350
-
351
- expect(buf[0]).toBe(0x64) // 'd'
352
- expect(r32(buf, 1)).toBe(4 + 2) // length includes the 4 bytes of the length field itself
353
- expect(Array.from(buf.subarray(5))).toEqual([0xaa, 0xbb])
354
- })
355
- })
356
-
357
- describe('encodeKeepalive', () => {
358
- it('wraps keepalive in CopyData', () => {
359
- const walEnd = 0x1000200n
360
- const ts = BigInt(Date.now()) * 1000n
361
-
362
- const buf = encodeKeepalive(walEnd, ts, false)
363
-
364
- expect(buf[0]).toBe(0x64) // outer CopyData
365
- expect(buf[5]).toBe(0x6b) // inner 'k' keepalive
366
- expect(r64(buf, 6)).toBe(walEnd)
367
- expect(r64(buf, 14)).toBe(ts - PG_EPOCH_MICROS)
368
- expect(buf[22]).toBe(0)
369
- })
370
-
371
- it('sets reply-requested flag', () => {
372
- const buf = encodeKeepalive(0n, BigInt(Date.now()) * 1000n, true)
373
- expect(buf[22]).toBe(1)
374
- })
375
- })
376
-
377
- describe('inferColumns', () => {
378
- it('maps keys to text columns', () => {
379
- const cols = inferColumns({ id: 1, name: 'test', active: true })
380
- expect(cols).toEqual([
381
- { name: 'id', typeOid: 25, typeMod: -1 },
382
- { name: 'name', typeOid: 25, typeMod: -1 },
383
- { name: 'active', typeOid: 25, typeMod: -1 },
384
- ])
385
- })
386
-
387
- it('handles empty object', () => {
388
- expect(inferColumns({})).toEqual([])
389
- })
390
- })
391
-
392
- describe('getTableOid', () => {
393
- it('returns stable oid for same table', () => {
394
- const a = getTableOid('oid_test_stable')
395
- const b = getTableOid('oid_test_stable')
396
- expect(a).toBe(b)
397
- expect(a).toBeGreaterThanOrEqual(16384)
398
- })
399
-
400
- it('returns unique oids for different tables', () => {
401
- const a = getTableOid('oid_test_x')
402
- const b = getTableOid('oid_test_y')
403
- expect(a).not.toBe(b)
404
- })
405
-
406
- it('matches DoBackend catalog oids for flattened schema tables', () => {
407
- expect(getTableOid('public.todo')).toBe(4392680)
408
- expect(getTableOid('todo_0.clients')).toBe(9663976)
409
- expect(getTableOid('todo_0.mutations')).toBe(8519194)
410
- })
411
- })
412
-
413
- // roundtrip tests: encode with orez → parse with zero-cache's parser
414
- // this validates the fundamental contract between orez and zero-cache
415
- describe('roundtrip: orez encoder → zero-cache parser', () => {
416
- // relative path bypasses package.json exports restriction
417
- const parserPath = join(
418
- import.meta.dirname,
419
- '../../node_modules/@rocicorp/zero/out/zero-cache/src/services/change-source/pg/logical-replication/pgoutput-parser.js'
420
- )
421
-
422
- // mock type parsers: unknown OIDs default to String (identity for text)
423
- const typeParsers = { getTypeParser: () => String }
424
-
425
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
426
- let PgoutputParser: any
427
- beforeAll(async () => {
428
- PgoutputParser = (await import(parserPath)).PgoutputParser
429
- })
430
-
431
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
432
- function makeParser(parserOverrides: any = typeParsers) {
433
- return new PgoutputParser(parserOverrides)
434
- }
435
-
436
- function makeBoolAwareParser() {
437
- return makeParser({
438
- getTypeParser: (oid: number) =>
439
- oid === 16 ? (value: string) => value === 't' : String,
440
- })
441
- }
442
-
443
- it('BEGIN roundtrip', () => {
444
- const lsn = 0x1000200n
445
- const ts = BigInt(Date.now()) * 1000n
446
- const parser = makeParser()
447
- const parsed = parser.parse(encodeBegin(lsn, ts, 42))
448
-
449
- expect(parsed.tag).toBe('begin')
450
- expect(parsed.commitLsn).toBe('00000000/01000200')
451
- expect(parsed.xid).toBe(42)
452
- expect(parsed.commitTime).toBe(ts)
453
- })
454
-
455
- it('COMMIT roundtrip', () => {
456
- const lsn = 0x1000200n
457
- const endLsn = 0x1000300n
458
- const ts = BigInt(Date.now()) * 1000n
459
- const parser = makeParser()
460
- const parsed = parser.parse(encodeCommit(0, lsn, endLsn, ts))
461
-
462
- expect(parsed.tag).toBe('commit')
463
- expect(parsed.commitLsn).toBe('00000000/01000200')
464
- expect(parsed.commitEndLsn).toBe('00000000/01000300')
465
- expect(parsed.commitTime).toBe(ts)
466
- })
467
-
468
- it('RELATION roundtrip', () => {
469
- const oid = getTableOid('rt.rel_test')
470
- const cols: ColumnInfo[] = [
471
- { name: 'id', typeOid: 25, typeMod: -1, isKey: true },
472
- { name: 'name', typeOid: 25, typeMod: -1 },
473
- ]
474
- const parser = makeParser()
475
- const parsed = parser.parse(encodeRelation(oid, 'public', 'rel_test', 0x64, cols))
476
-
477
- expect(parsed.tag).toBe('relation')
478
- expect(parsed.schema).toBe('public')
479
- expect(parsed.name).toBe('rel_test')
480
- expect(parsed.columns).toHaveLength(2)
481
- expect(parsed.keyColumns).toEqual(['id'])
482
- })
483
-
484
- it('INSERT roundtrip', () => {
485
- const oid = getTableOid('rt.ins_test')
486
- const cols: ColumnInfo[] = [
487
- { name: 'id', typeOid: 25, typeMod: -1, isKey: true },
488
- { name: 'val', typeOid: 25, typeMod: -1 },
489
- ]
490
- const parser = makeParser()
491
- parser.parse(encodeRelation(oid, 'public', 'ins_test', 0x64, cols))
492
-
493
- const parsed = parser.parse(encodeInsert(oid, { id: 'abc', val: 'hello' }, cols))
494
- expect(parsed.tag).toBe('insert')
495
- expect(parsed.new.id).toBe('abc')
496
- expect(parsed.new.val).toBe('hello')
497
- })
498
-
499
- it('INSERT with null', () => {
500
- const oid = getTableOid('rt.null_test')
501
- const cols: ColumnInfo[] = [
502
- { name: 'id', typeOid: 25, typeMod: -1, isKey: true },
503
- { name: 'opt', typeOid: 25, typeMod: -1 },
504
- ]
505
- const parser = makeParser()
506
- parser.parse(encodeRelation(oid, 'public', 'null_test', 0x64, cols))
507
-
508
- const parsed = parser.parse(encodeInsert(oid, { id: 'x', opt: null }, cols))
509
- expect(parsed.new.opt).toBeNull()
510
- })
511
-
512
- it('UPDATE with old row roundtrip', () => {
513
- const oid = getTableOid('rt.upd_test')
514
- const cols: ColumnInfo[] = [
515
- { name: 'id', typeOid: 25, typeMod: -1, isKey: true },
516
- { name: 'val', typeOid: 25, typeMod: -1 },
517
- ]
518
- const parser = makeParser()
519
- parser.parse(encodeRelation(oid, 'public', 'upd_test', 0x64, cols))
520
-
521
- const parsed = parser.parse(
522
- encodeUpdate(oid, { id: '1', val: 'new' }, { id: '1', val: 'old' }, cols)
523
- )
524
- expect(parsed.tag).toBe('update')
525
- expect(parsed.new.val).toBe('new')
526
- expect(parsed.old.val).toBe('old')
527
- })
528
-
529
- it('UPDATE without old row', () => {
530
- const oid = getTableOid('rt.upd_no_old')
531
- const cols: ColumnInfo[] = [
532
- { name: 'id', typeOid: 25, typeMod: -1, isKey: true },
533
- { name: 'val', typeOid: 25, typeMod: -1 },
534
- ]
535
- const parser = makeParser()
536
- parser.parse(encodeRelation(oid, 'public', 'upd_no_old', 0x64, cols))
537
-
538
- const parsed = parser.parse(encodeUpdate(oid, { id: '1', val: 'v' }, null, cols))
539
- expect(parsed.tag).toBe('update')
540
- expect(parsed.new.val).toBe('v')
541
- expect(parsed.old).toBeNull()
542
- expect(parsed.key).toBeNull()
543
- })
544
-
545
- it('UPDATE roundtrip decodes sqlite boolean integers through zero-cache parser', () => {
546
- const oid = getTableOid('rt.bool_update')
547
- const cols: ColumnInfo[] = [
548
- { name: 'id', typeOid: 25, typeMod: -1, isKey: true },
549
- { name: 'completed', typeOid: 16, typeMod: -1 },
550
- ]
551
- const parser = makeBoolAwareParser()
552
- parser.parse(encodeRelation(oid, 'public', 'bool_update', 0x64, cols))
553
-
554
- const parsed = parser.parse(
555
- encodeUpdate(oid, { id: '1', completed: 1 }, { id: '1', completed: 0 }, cols)
556
- )
557
- expect(parsed.tag).toBe('update')
558
- expect(parsed.old.completed).toBe(false)
559
- expect(parsed.new.completed).toBe(true)
560
- })
561
-
562
- it('DELETE roundtrip', () => {
563
- const oid = getTableOid('rt.del_test')
564
- const cols: ColumnInfo[] = [
565
- { name: 'id', typeOid: 25, typeMod: -1, isKey: true },
566
- { name: 'val', typeOid: 25, typeMod: -1 },
567
- ]
568
- const parser = makeParser()
569
- parser.parse(encodeRelation(oid, 'public', 'del_test', 0x64, cols))
570
-
571
- const parsed = parser.parse(encodeDelete(oid, { id: 'gone', val: 'x' }, cols))
572
- expect(parsed.tag).toBe('delete')
573
- expect(parsed.key.id).toBe('gone')
574
- })
575
-
576
- it('full transaction: BEGIN → RELATION → INSERT → COMMIT', () => {
577
- const parser = makeParser()
578
- const lsn = 0x2000000n
579
- const endLsn = 0x2000100n
580
- const ts = BigInt(Date.now()) * 1000n
581
-
582
- const begin = parser.parse(encodeBegin(lsn, ts, 1))
583
- expect(begin.commitLsn).toBe('00000000/02000000')
584
-
585
- const oid = getTableOid('rt.full_tx')
586
- const cols: ColumnInfo[] = [
587
- { name: 'id', typeOid: 25, typeMod: -1, isKey: true },
588
- { name: 'data', typeOid: 25, typeMod: -1 },
589
- ]
590
- parser.parse(encodeRelation(oid, 'public', 'full_tx', 0x64, cols))
591
-
592
- const ins = parser.parse(encodeInsert(oid, { id: '1', data: 'test' }, cols))
593
- expect(ins.new.id).toBe('1')
594
-
595
- const commit = parser.parse(encodeCommit(0, lsn, endLsn, ts))
596
- expect(commit.commitLsn).toBe(begin.commitLsn)
597
- })
598
-
599
- it('XLogData + CopyData wrapper roundtrip with parser', () => {
600
- const lsn = 0x3000000n
601
- const ts = BigInt(Date.now()) * 1000n
602
- const pgoutput = encodeBegin(lsn, ts, 1)
603
- const xlog = wrapXLogData(lsn, lsn, ts, pgoutput)
604
- const frame = wrapCopyData(xlog)
605
-
606
- // unwrap CopyData
607
- const copyLen = r32(frame, 1)
608
- const inner = frame.subarray(5, 1 + copyLen)
609
-
610
- // parse like stream.js
611
- expect(inner[0]).toBe(0x77)
612
- const streamLsn = new DataView(inner.buffer, inner.byteOffset).getBigUint64(1)
613
- expect(streamLsn).toBe(lsn)
614
-
615
- // parse pgoutput
616
- const parser = makeParser()
617
- const parsed = parser.parse(inner.subarray(25))
618
- expect(parsed.tag).toBe('begin')
619
- expect(parsed.commitLsn).toBe('00000000/03000000')
620
- })
621
-
622
- it('shard schema encoding', () => {
623
- const oid = getTableOid('rt.chat_0.clients')
624
- const cols: ColumnInfo[] = [
625
- { name: 'id', typeOid: 25, typeMod: -1, isKey: true },
626
- { name: 'lastMutationID', typeOid: 20, typeMod: -1 },
627
- ]
628
- const parser = makeParser()
629
- const rel = parser.parse(encodeRelation(oid, 'chat_0', 'clients', 0x64, cols))
630
- expect(rel.schema).toBe('chat_0')
631
- expect(rel.name).toBe('clients')
632
- })
633
-
634
- it('LSN ordering: slot < streaming changes', async () => {
635
- // validates that streaming changes will be seen as "new" by zero-cache
636
- let testLsn = 0x1000000n
637
- const next = () => {
638
- testLsn += 0x100n
639
- return testLsn
640
- }
641
-
642
- const slotLsn = next() // CREATE_REPLICATION_SLOT
643
- const beginLsn = next() // first streaming BEGIN
644
- const commitLsn = next() // first streaming COMMIT
645
-
646
- expect(beginLsn).toBeGreaterThan(slotLsn)
647
- expect(commitLsn).toBeGreaterThan(beginLsn)
648
-
649
- // verify lexi version ordering is preserved
650
- const lexiPath = join(
651
- import.meta.dirname,
652
- '../../node_modules/@rocicorp/zero/out/zero-cache/src/types/lexi-version.js'
653
- )
654
- const { versionToLexi } = await import(lexiPath)
655
- const lsnPath = join(
656
- import.meta.dirname,
657
- '../../node_modules/@rocicorp/zero/out/zero-cache/src/services/change-source/pg/lsn.js'
658
- )
659
- const { toBigInt: lsnToBigInt } = await import(lsnPath)
660
-
661
- const slotHex = `00000000/${slotLsn.toString(16).padStart(8, '0')}`.toUpperCase()
662
- const beginHex = `00000000/${beginLsn.toString(16).padStart(8, '0')}`.toUpperCase()
663
-
664
- const slotVersion = versionToLexi(lsnToBigInt(slotHex))
665
- const beginVersion = versionToLexi(lsnToBigInt(beginHex))
666
-
667
- // lexi versions must maintain ordering
668
- expect(beginVersion > slotVersion).toBe(true)
669
- })
670
- })
671
-
672
- describe('double-wrap: CopyData(XLogData(message))', () => {
673
- // this is the exact framing zero-cache expects for every replication message
674
- it('produces parseable nested structure', () => {
675
- const ts = BigInt(Date.now()) * 1000n
676
- const lsn = 0x1000000n
677
- const inner = encodeBegin(lsn, ts, 1)
678
- const xlog = wrapXLogData(lsn, lsn, ts, inner)
679
- const frame = wrapCopyData(xlog)
680
-
681
- // parse back
682
- expect(frame[0]).toBe(0x64) // CopyData
683
- const copyLen = r32(frame, 1)
684
- expect(frame.length).toBe(1 + copyLen)
685
-
686
- // inside CopyData: XLogData
687
- expect(frame[5]).toBe(0x77) // XLogData
688
- expect(r64(frame, 6)).toBe(lsn) // walStart
689
- expect(r64(frame, 14)).toBe(lsn) // walEnd
690
-
691
- // inside XLogData: Begin
692
- expect(frame[30]).toBe(0x42) // Begin
693
- expect(r64(frame, 31)).toBe(lsn) // begin LSN
694
- expect(r32(frame, 47)).toBe(1) // xid
695
- })
696
- })
697
- })