orez 0.2.27 → 0.2.29

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