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,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
- })