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,3890 +0,0 @@
1
- import { createServer, type Server } from 'node:http'
2
-
3
- import { afterEach, describe, expect, test } from 'vitest'
4
-
5
- import { DoBackend } from './pg-proxy-do-backend.js'
6
-
7
- const encoder = new TextEncoder()
8
- let servers: Server[] = []
9
-
10
- function cstr(s: string): Uint8Array {
11
- const encoded = encoder.encode(s)
12
- const out = new Uint8Array(encoded.length + 1)
13
- out.set(encoded)
14
- return out
15
- }
16
-
17
- function i16(v: number): Uint8Array {
18
- const out = new Uint8Array(2)
19
- new DataView(out.buffer).setInt16(0, v)
20
- return out
21
- }
22
-
23
- function i32(v: number): Uint8Array {
24
- const out = new Uint8Array(4)
25
- new DataView(out.buffer).setInt32(0, v)
26
- return out
27
- }
28
-
29
- function concat(...parts: Uint8Array[]): Uint8Array {
30
- const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0))
31
- let offset = 0
32
- for (const part of parts) {
33
- out.set(part, offset)
34
- offset += part.length
35
- }
36
- return out
37
- }
38
-
39
- function msg(type: number, payload: Uint8Array): Uint8Array {
40
- return concat(new Uint8Array([type]), i32(payload.length + 4), payload)
41
- }
42
-
43
- function parseMessage(sql: string, name = '', paramOIDs: number[] = []): Uint8Array {
44
- return msg(
45
- 0x50,
46
- concat(
47
- cstr(name),
48
- cstr(sql),
49
- i16(paramOIDs.length),
50
- ...paramOIDs.map((oid) => {
51
- const out = new Uint8Array(4)
52
- new DataView(out.buffer).setUint32(0, oid)
53
- return out
54
- })
55
- )
56
- )
57
- }
58
-
59
- function describeStatement(name = ''): Uint8Array {
60
- return msg(0x44, concat(encoder.encode('S'), cstr(name)))
61
- }
62
-
63
- function describePortal(name = ''): Uint8Array {
64
- return msg(0x44, concat(encoder.encode('P'), cstr(name)))
65
- }
66
-
67
- function bindStatement(statement = '', portal = ''): Uint8Array {
68
- return msg(0x42, concat(cstr(portal), cstr(statement), i16(0), i16(0), i16(0)))
69
- }
70
-
71
- function bindStatementParams(params: unknown[], statement = '', portal = ''): Uint8Array {
72
- return msg(
73
- 0x42,
74
- concat(
75
- cstr(portal),
76
- cstr(statement),
77
- i16(0),
78
- i16(params.length),
79
- ...params.map((param) => {
80
- if (param === null || param === undefined) return i32(-1)
81
- const encoded = encoder.encode(String(param))
82
- return concat(i32(encoded.length), encoded)
83
- }),
84
- i16(0)
85
- )
86
- )
87
- }
88
-
89
- function executePortal(portal = ''): Uint8Array {
90
- return msg(0x45, concat(cstr(portal), i32(0)))
91
- }
92
-
93
- function closePortal(name = ''): Uint8Array {
94
- return msg(0x43, concat(encoder.encode('P'), cstr(name)))
95
- }
96
-
97
- function closeStatement(name = ''): Uint8Array {
98
- return msg(0x43, concat(encoder.encode('S'), cstr(name)))
99
- }
100
-
101
- function messageTypes(data: Uint8Array): string[] {
102
- const types: string[] = []
103
- for (let offset = 0; offset < data.length; ) {
104
- types.push(String.fromCharCode(data[offset]))
105
- const len = new DataView(data.buffer, data.byteOffset + offset + 1, 4).getInt32(0)
106
- offset += 1 + len
107
- }
108
- return types
109
- }
110
-
111
- function readyForQueryStatuses(data: Uint8Array): string[] {
112
- const statuses: string[] = []
113
- for (let offset = 0; offset < data.length; ) {
114
- const type = data[offset]
115
- const len = new DataView(data.buffer, data.byteOffset + offset + 1, 4).getInt32(0)
116
- if (type === 0x5a) statuses.push(String.fromCharCode(data[offset + 5]))
117
- offset += 1 + len
118
- }
119
- return statuses
120
- }
121
-
122
- function rowDescriptionOids(data: Uint8Array): Record<string, number> {
123
- const oids: Record<string, number> = {}
124
- for (let offset = 0; offset < data.length; ) {
125
- const type = data[offset]
126
- const len = new DataView(data.buffer, data.byteOffset + offset + 1, 4).getInt32(0)
127
- if (type === 0x54) {
128
- const view = new DataView(data.buffer, data.byteOffset + offset, 1 + len)
129
- const count = view.getInt16(5)
130
- let pos = 7
131
- for (let i = 0; i < count; i++) {
132
- const nameStart = pos
133
- while (pos < 1 + len && data[offset + pos] !== 0) pos++
134
- const name = new TextDecoder().decode(
135
- data.subarray(offset + nameStart, offset + pos)
136
- )
137
- pos++
138
- pos += 6
139
- oids[name] = view.getUint32(pos)
140
- pos += 12
141
- }
142
- }
143
- offset += 1 + len
144
- }
145
- return oids
146
- }
147
-
148
- function rowDescriptionNames(data: Uint8Array): string[] {
149
- for (let offset = 0; offset < data.length; ) {
150
- const type = data[offset]
151
- const len = new DataView(data.buffer, data.byteOffset + offset + 1, 4).getInt32(0)
152
- if (type === 0x54) {
153
- const view = new DataView(data.buffer, data.byteOffset + offset, 1 + len)
154
- const count = view.getInt16(5)
155
- let pos = 7
156
- const names: string[] = []
157
- for (let i = 0; i < count; i++) {
158
- const nameStart = pos
159
- while (pos < 1 + len && data[offset + pos] !== 0) pos++
160
- names.push(
161
- new TextDecoder().decode(data.subarray(offset + nameStart, offset + pos))
162
- )
163
- pos += 19
164
- }
165
- return names
166
- }
167
- offset += 1 + len
168
- }
169
- return []
170
- }
171
-
172
- function parameterDescriptionOids(data: Uint8Array): number[] {
173
- for (let offset = 0; offset < data.length; ) {
174
- const type = data[offset]
175
- const len = new DataView(data.buffer, data.byteOffset + offset + 1, 4).getInt32(0)
176
- if (type === 0x74) {
177
- const view = new DataView(data.buffer, data.byteOffset + offset, 1 + len)
178
- const count = view.getInt16(5)
179
- return Array.from({ length: count }, (_, index) => view.getUint32(7 + index * 4))
180
- }
181
- offset += 1 + len
182
- }
183
- return []
184
- }
185
-
186
- function dataRowValues(data: Uint8Array): (string | null)[][] {
187
- const rows: (string | null)[][] = []
188
- for (let offset = 0; offset < data.length; ) {
189
- const type = data[offset]
190
- const len = new DataView(data.buffer, data.byteOffset + offset + 1, 4).getInt32(0)
191
- if (type === 0x44) {
192
- const view = new DataView(data.buffer, data.byteOffset + offset, 1 + len)
193
- const count = view.getInt16(5)
194
- let pos = 7
195
- const row: (string | null)[] = []
196
- for (let i = 0; i < count; i++) {
197
- const valueLength = view.getInt32(pos)
198
- pos += 4
199
- if (valueLength === -1) {
200
- row.push(null)
201
- continue
202
- }
203
- row.push(
204
- new TextDecoder().decode(
205
- data.subarray(offset + pos, offset + pos + valueLength)
206
- )
207
- )
208
- pos += valueLength
209
- }
210
- rows.push(row)
211
- }
212
- offset += 1 + len
213
- }
214
- return rows
215
- }
216
-
217
- function compactSQL(sql: string): string {
218
- return sql.replace(/\s+/g, ' ').trim()
219
- }
220
-
221
- function sqlContaining(sqls: string[], needle: string): string {
222
- const found = [...sqls].reverse().find((sql) => compactSQL(sql).includes(needle))
223
- expect(found).toBeDefined()
224
- return found ?? ''
225
- }
226
-
227
- function startDoHttp(
228
- handler: (sql: string, url: URL) => { rows?: unknown[]; columns?: string[] } | Response
229
- ): Promise<{
230
- url: string
231
- requests: URL[]
232
- sqls: string[]
233
- params: unknown[][]
234
- bodies: any[]
235
- }> {
236
- const requests: URL[] = []
237
- const sqls: string[] = []
238
- const params: unknown[][] = []
239
- const bodies: any[] = []
240
- const server = createServer(async (req, res) => {
241
- const url = new URL(req.url || '/', 'http://127.0.0.1')
242
- requests.push(url)
243
- let body = ''
244
- req.on('data', (chunk) => {
245
- body += chunk
246
- })
247
- req.on('end', () => {
248
- const parsed = body ? JSON.parse(body) : {}
249
- bodies.push(parsed)
250
- if (url.pathname === '/batch') {
251
- const statements = Array.isArray(parsed.statements) ? parsed.statements : []
252
- sqls.push(
253
- ...statements.map((statement: any) =>
254
- typeof statement === 'string' ? statement : statement.sql
255
- )
256
- )
257
- res.setHeader('content-type', 'application/json')
258
- res.end(
259
- JSON.stringify({
260
- results: statements.map(() => ({ rows: [], columns: [] })),
261
- })
262
- )
263
- return
264
- }
265
- const sql = parsed.sql || ''
266
- sqls.push(sql)
267
- params.push(Array.isArray(parsed.params) ? parsed.params : [])
268
- const result = handler(sql, url)
269
- if (result instanceof Response) {
270
- res.statusCode = result.status
271
- result.text().then((text) => res.end(text))
272
- return
273
- }
274
- res.setHeader('content-type', 'application/json')
275
- res.end(JSON.stringify({ rows: result.rows ?? [], columns: result.columns ?? [] }))
276
- })
277
- })
278
- servers.push(server)
279
-
280
- return new Promise((resolve, reject) => {
281
- server.once('error', reject)
282
- server.listen(0, '127.0.0.1', () => {
283
- const addr = server.address()
284
- if (!addr || typeof addr === 'string') {
285
- reject(new Error('server did not bind to a tcp port'))
286
- return
287
- }
288
- resolve({
289
- url: `http://127.0.0.1:${addr.port}`,
290
- requests,
291
- sqls,
292
- params,
293
- bodies,
294
- })
295
- })
296
- })
297
- }
298
-
299
- afterEach(async () => {
300
- await Promise.all(
301
- servers.map(
302
- (server) =>
303
- new Promise<void>((resolve) => {
304
- server.close(() => resolve())
305
- })
306
- )
307
- )
308
- servers = []
309
- })
310
-
311
- describe('DoBackend', () => {
312
- test('sends the configured Durable Object namespace on every SQL request', async () => {
313
- const http = await startDoHttp(() => ({ rows: [{ ok: 1 }], columns: ['ok'] }))
314
- const backend = new DoBackend(http.url, 'postgres', 'chat-test-namespace')
315
- await backend.waitReady
316
-
317
- await backend.query('SELECT 1 AS ok')
318
-
319
- expect(http.requests.length).toBeGreaterThanOrEqual(2)
320
- expect(http.requests.every((url) => url.searchParams.get('db') === 'postgres')).toBe(
321
- true
322
- )
323
- expect(
324
- http.requests.every((url) => url.searchParams.get('ns') === 'chat-test-namespace')
325
- ).toBe(true)
326
- })
327
-
328
- test('describes prepared statements with parameter and row metadata', async () => {
329
- const http = await startDoHttp((sql) => {
330
- if (sql.includes('_orez_describe')) return { rows: [], columns: ['value'] }
331
- return { rows: [{ value: 'ok' }], columns: ['value'] }
332
- })
333
- const backend = new DoBackend(http.url, 'postgres', 'describe-test')
334
- await backend.waitReady
335
-
336
- expect(
337
- messageTypes(
338
- await backend.execProtocolRaw(parseMessage('SELECT $1 AS value', '', [25]))
339
- )
340
- ).toEqual(['1'])
341
- expect(messageTypes(await backend.execProtocolRaw(describeStatement()))).toEqual([
342
- 't',
343
- 'T',
344
- ])
345
- })
346
-
347
- test('returns row metadata for zero-row selects', async () => {
348
- const http = await startDoHttp(() => ({ rows: [], columns: ['id'] }))
349
- const backend = new DoBackend(http.url, 'postgres', 'zero-row-test')
350
- await backend.waitReady
351
-
352
- const result = await backend.execProtocolRaw(
353
- msg(0x51, cstr('SELECT id FROM message WHERE 1 = 0'))
354
- )
355
-
356
- expect(messageTypes(result)).toEqual(['T', 'C', 'Z'])
357
- })
358
-
359
- test('rewrites pg_column_size totals as ordinary SQL instead of catalog probes', async () => {
360
- const http = await startDoHttp((sql) => {
361
- if (compactSQL(sql).includes('length')) {
362
- return { rows: [{ totalBytes: 42 }], columns: ['totalBytes'] }
363
- }
364
- return { rows: [], columns: [] }
365
- })
366
- const backend = new DoBackend(http.url, 'postgres', 'pg-column-size-test')
367
- await backend.waitReady
368
-
369
- await backend.execProtocolRaw(
370
- parseMessage(`
371
- SELECT (
372
- SUM(COALESCE(pg_column_size("id"), 0)) +
373
- SUM(COALESCE(pg_column_size("payload"), 0))
374
- ) AS "totalBytes"
375
- FROM public.message
376
- `)
377
- )
378
- await backend.execProtocolRaw(bindStatement())
379
- const result = await backend.execProtocolRaw(executePortal())
380
-
381
- const sent = compactSQL(http.sqls.at(-1) || '')
382
- expect(messageTypes(result)).toEqual(['T', 'D', 'C'])
383
- expect(dataRowValues(result)).toEqual([['42']])
384
- expect(sent).toContain('length')
385
- expect(sent).toContain('FROM message')
386
- expect(sent).not.toContain('pg_column_size')
387
- expect(sent).not.toContain('public.message')
388
- })
389
-
390
- test('streams COPY TO STDOUT from a parsed select query', async () => {
391
- const http = await startDoHttp(() => ({
392
- rows: [{ id: 'm1', deleted: 0 }],
393
- columns: ['id', 'deleted'],
394
- }))
395
- const backend = new DoBackend(http.url, 'postgres', 'copy-test')
396
- await backend.waitReady
397
- await backend.exec(`
398
- CREATE TABLE public.message (
399
- id text PRIMARY KEY,
400
- deleted boolean NOT NULL DEFAULT false
401
- )
402
- `)
403
-
404
- const result = await backend.execProtocolRaw(
405
- msg(0x51, cstr('COPY (SELECT id, deleted FROM public.message) TO STDOUT'))
406
- )
407
-
408
- expect(messageTypes(result)).toEqual(['H', 'd', 'c', 'C', 'Z'])
409
- expect(new TextDecoder().decode(result)).toContain('m1\tf\n')
410
- expect(http.sqls.some((sql) => compactSQL(sql).includes('FROM message'))).toBe(true)
411
- })
412
-
413
- test('formats timestamp typed rows as postgres text for DataRow and COPY', async () => {
414
- const http = await startDoHttp((sql) => {
415
- if (compactSQL(sql).startsWith('SELECT')) {
416
- return {
417
- rows: [
418
- { id: 'm1', createdAt: '2026-05-25T20:17:28.377Z' },
419
- { id: 'm2', createdAt: '1779746873949.0' },
420
- ],
421
- columns: ['id', 'createdAt'],
422
- }
423
- }
424
- return { rows: [], columns: [] }
425
- })
426
- const backend = new DoBackend(http.url, 'postgres', 'timestamp-format-test')
427
- await backend.waitReady
428
-
429
- await backend.exec(`
430
- CREATE TABLE public.message (
431
- id text PRIMARY KEY,
432
- "createdAt" timestamptz NOT NULL
433
- )
434
- `)
435
-
436
- const select = await backend.execProtocolRaw(
437
- msg(0x51, cstr('SELECT id, "createdAt" FROM public.message'))
438
- )
439
- const copy = await backend.execProtocolRaw(
440
- msg(0x51, cstr('COPY (SELECT id, "createdAt" FROM public.message) TO STDOUT'))
441
- )
442
-
443
- expect(rowDescriptionOids(select)).toMatchObject({ createdAt: 1184 })
444
- expect(dataRowValues(select)).toEqual([
445
- ['m1', '2026-05-25 20:17:28.377+00'],
446
- ['m2', '2026-05-25 22:07:53.949+00'],
447
- ])
448
- expect(new TextDecoder().decode(copy)).toContain('m1\t2026-05-25 20:17:28.377+00\n')
449
- expect(new TextDecoder().decode(copy)).toContain('m2\t2026-05-25 22:07:53.949+00\n')
450
- })
451
-
452
- test('normalizes timestamp typed parameters before sending them to SQLite', async () => {
453
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
454
- const backend = new DoBackend(http.url, 'postgres', 'timestamp-param-test')
455
- await backend.waitReady
456
-
457
- await backend.exec(`
458
- CREATE TABLE public.event (
459
- id text PRIMARY KEY,
460
- "createdAt" timestamptz NOT NULL
461
- )
462
- `)
463
- await backend.exec(`
464
- CREATE TABLE public.notification (
465
- id text PRIMARY KEY,
466
- "seenAt" timestamptz
467
- )
468
- `)
469
-
470
- await backend.execProtocolRaw(
471
- parseMessage(
472
- 'INSERT INTO public.event (id, "createdAt") VALUES ($1, $2)',
473
- 'insert_timestamp'
474
- )
475
- )
476
- const insertDescribe = await backend.execProtocolRaw(
477
- describeStatement('insert_timestamp')
478
- )
479
- expect(parameterDescriptionOids(insertDescribe)).toEqual([25, 1184])
480
- await backend.execProtocolRaw(
481
- bindStatementParams(['e1', 1779746873949], 'insert_timestamp')
482
- )
483
- await backend.execProtocolRaw(executePortal())
484
-
485
- await backend.execProtocolRaw(
486
- parseMessage(
487
- 'UPDATE public.event SET "createdAt" = $1 WHERE id = $2',
488
- 'update_timestamp'
489
- )
490
- )
491
- const updateDescribe = await backend.execProtocolRaw(
492
- describeStatement('update_timestamp')
493
- )
494
- expect(parameterDescriptionOids(updateDescribe)).toEqual([1184, 25])
495
- await backend.execProtocolRaw(
496
- bindStatementParams([1779746874950, 'e1'], 'update_timestamp')
497
- )
498
- await backend.execProtocolRaw(executePortal())
499
-
500
- await backend.execProtocolRaw(
501
- parseMessage(
502
- 'INSERT INTO public.notification (id, "seenAt") VALUES ($1, $2)',
503
- 'insert_null_timestamp'
504
- )
505
- )
506
- const nullDescribe = await backend.execProtocolRaw(
507
- describeStatement('insert_null_timestamp')
508
- )
509
- expect(parameterDescriptionOids(nullDescribe)).toEqual([25, 1184])
510
- await backend.execProtocolRaw(
511
- bindStatementParams(['n1', null], 'insert_null_timestamp')
512
- )
513
- await backend.execProtocolRaw(executePortal())
514
-
515
- expect(http.params.at(-3)).toEqual(['e1', '2026-05-25 22:07:53.949+00'])
516
- expect(http.params.at(-2)).toEqual(['2026-05-25 22:07:54.950+00', 'e1'])
517
- expect(http.params.at(-1)).toEqual(['n1', null])
518
- })
519
-
520
- test('normalizes boolean parameters before sending them to SQLite', async () => {
521
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
522
- const backend = new DoBackend(http.url, 'postgres', 'boolean-param-test')
523
- await backend.waitReady
524
-
525
- await backend.exec(`
526
- CREATE TABLE public.flag_probe (
527
- id text PRIMARY KEY,
528
- enabled boolean NOT NULL
529
- )
530
- `)
531
- await backend.execProtocolRaw(
532
- parseMessage('INSERT INTO public.flag_probe (id, enabled) VALUES ($1, $2)')
533
- )
534
- await backend.execProtocolRaw(bindStatementParams(['f1', false]))
535
- await backend.execProtocolRaw(executePortal())
536
- await backend.execProtocolRaw(bindStatementParams(['t1', true]))
537
- await backend.execProtocolRaw(executePortal())
538
-
539
- expect(http.params.at(-2)).toEqual(['f1', 0])
540
- expect(http.params.at(-1)).toEqual(['t1', 1])
541
- })
542
-
543
- test('normalizes boolean parameters through text cast chains', async () => {
544
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
545
- const backend = new DoBackend(http.url, 'postgres', 'boolean-text-cast-param-test')
546
- await backend.waitReady
547
-
548
- await backend.query(
549
- 'SELECT id FROM public.flag_probe WHERE enabled = $1::text::boolean OR enabled = $1::text::boolean',
550
- ['false']
551
- )
552
-
553
- expect(http.params.at(-1)).toEqual([0, 0])
554
- })
555
-
556
- test('infers JSON parameters in inserts and ON CONFLICT updates', async () => {
557
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
558
- const backend = new DoBackend(http.url, 'postgres', 'json-param-test')
559
- await backend.waitReady
560
-
561
- await backend.exec(`
562
- CREATE TABLE public.client_state (
563
- id text PRIMARY KEY,
564
- "clientSchema" jsonb
565
- )
566
- `)
567
-
568
- await backend.execProtocolRaw(
569
- parseMessage(
570
- `INSERT INTO public.client_state (id, "clientSchema")
571
- VALUES ($1, $2)
572
- ON CONFLICT (id) DO UPDATE SET "clientSchema" = $3`,
573
- 'json_upsert'
574
- )
575
- )
576
- const describe = await backend.execProtocolRaw(describeStatement('json_upsert'))
577
- expect(parameterDescriptionOids(describe)).toEqual([25, 3802, 3802])
578
-
579
- await backend.query(
580
- `INSERT INTO public.client_state (id, "clientSchema")
581
- VALUES ($1, $2)
582
- ON CONFLICT (id) DO UPDATE SET "clientSchema" = $3`,
583
- ['cg', { tables: { insert: true } }, { tables: { update: true } }]
584
- )
585
-
586
- expect(http.params.at(-1)).toEqual([
587
- 'cg',
588
- '{"tables":{"insert":true}}',
589
- '{"tables":{"update":true}}',
590
- ])
591
- })
592
-
593
- test('returns aliased current_setting catalog values needed by zero-cache', async () => {
594
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
595
- const backend = new DoBackend(http.url, 'postgres', 'current-setting-test')
596
- await backend.waitReady
597
-
598
- const result = await (backend as any).handleCatalogQuery(`
599
- SELECT current_setting('wal_level') as "walLevel",
600
- current_setting('server_version_num') as "version";
601
- `)
602
-
603
- expect(result.fields.map((field: any) => field.name)).toEqual(['walLevel', 'version'])
604
- expect(result.rows).toEqual([{ walLevel: 'logical', version: '160000' }])
605
-
606
- const rewrittenResult = await (backend as any).handleCatalogQuery(`
607
- SELECT 'logical'::text as "walLevel",
608
- current_setting('server_version_num') as "version";
609
- `)
610
-
611
- expect(rewrittenResult.fields.map((field: any) => field.name)).toEqual([
612
- 'walLevel',
613
- 'version',
614
- ])
615
- expect(rewrittenResult.rows).toEqual([{ walLevel: 'logical', version: '160000' }])
616
- })
617
-
618
- test('projects pg_settings expressions from synthesized settings rows', async () => {
619
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
620
- const backend = new DoBackend(http.url, 'postgres', 'pg-settings-test')
621
- await backend.waitReady
622
-
623
- const result = await (backend as any).handleCatalogQuery(`
624
- SELECT EXTRACT(EPOCH FROM (setting || unit)::interval) * 1000
625
- AS "walSenderTimeoutMs"
626
- FROM pg_settings
627
- WHERE name = 'wal_sender_timeout'
628
- `)
629
-
630
- expect(result.fields).toEqual([{ name: 'walSenderTimeoutMs', oid: 701 }])
631
- expect(result.rows).toEqual([{ walSenderTimeoutMs: 60000 }])
632
- })
633
-
634
- test('returns requested pg_publication rows for zero-cache validation', async () => {
635
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
636
- const backend = new DoBackend(http.url, 'postgres', 'publication-test')
637
- await backend.waitReady
638
- await backend.exec('CREATE PUBLICATION zero_chat FOR TABLE message')
639
-
640
- const result = await (backend as any).handleCatalogQuery(`
641
- SELECT pubname FROM pg_publication WHERE pubname IN ('zero_chat')
642
- `)
643
-
644
- expect(result.fields.map((field: any) => field.name)).toEqual(['pubname'])
645
- expect(result.rows).toEqual([{ pubname: 'zero_chat' }])
646
- })
647
-
648
- test('executes the portal named by extended-protocol Execute', async () => {
649
- const http = await startDoHttp(() => ({ rows: [], columns: ['value'] }))
650
- const backend = new DoBackend(http.url, 'postgres', 'portal-execute-test')
651
- await backend.waitReady
652
- await backend.exec('CREATE PUBLICATION zero_chat FOR TABLE message')
653
-
654
- await backend.execProtocolRaw(
655
- parseMessage('SELECT value FROM ordinary WHERE id = $1', 'ordinary')
656
- )
657
- await backend.execProtocolRaw(
658
- bindStatementParams(['ignore'], 'ordinary', 'ordinary_portal')
659
- )
660
- await backend.execProtocolRaw(
661
- parseMessage(
662
- 'SELECT pubname FROM pg_publication WHERE pubname IN ($1)',
663
- 'publication'
664
- )
665
- )
666
- await backend.execProtocolRaw(
667
- bindStatementParams(['zero_chat'], 'publication', 'publication_portal')
668
- )
669
-
670
- const result = await backend.execProtocolRaw(executePortal('publication_portal'))
671
-
672
- expect(messageTypes(result)).toEqual(['T', 'D', 'C'])
673
- expect(new TextDecoder().decode(result)).toContain('zero_chat')
674
- expect(http.sqls.some((sql) => compactSQL(sql).includes('FROM ordinary'))).toBe(false)
675
- })
676
-
677
- test('describes and closes bound portals by portal name', async () => {
678
- const http = await startDoHttp(() => ({ rows: [], columns: ['value'] }))
679
- const backend = new DoBackend(http.url, 'postgres', 'portal-describe-test')
680
- await backend.waitReady
681
-
682
- await backend.execProtocolRaw(parseMessage('SELECT $1 AS value', 'statement', [25]))
683
- await backend.execProtocolRaw(bindStatementParams(['ok'], 'statement', 'portal'))
684
-
685
- expect(messageTypes(await backend.execProtocolRaw(describePortal('portal')))).toEqual(
686
- ['T']
687
- )
688
- expect(messageTypes(await backend.execProtocolRaw(closePortal('portal')))).toEqual([
689
- '3',
690
- ])
691
- expect(messageTypes(await backend.execProtocolRaw(describePortal('portal')))).toEqual(
692
- ['n']
693
- )
694
-
695
- await backend.execProtocolRaw(bindStatementParams(['ok'], 'statement', 'portal'))
696
- expect(
697
- messageTypes(await backend.execProtocolRaw(closeStatement('statement')))
698
- ).toEqual(['3'])
699
- expect(messageTypes(await backend.execProtocolRaw(describePortal('portal')))).toEqual(
700
- ['n']
701
- )
702
- })
703
-
704
- test('returns publication flags with boolean oids for zero-cache schema checks', async () => {
705
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
706
- const backend = new DoBackend(http.url, 'postgres', 'publication-flags-test')
707
- await backend.waitReady
708
- await backend.exec(`
709
- CREATE PUBLICATION zero_chat FOR TABLE message;
710
- CREATE PUBLICATION _zero_metadata FOR TABLE "_zero"."clients";
711
- `)
712
-
713
- const result = await (backend as any).handleCatalogQuery(`
714
- SELECT pubname,pubinsert,pubupdate,pubdelete,pubtruncate FROM pg_publication pb
715
- WHERE pb.pubname IN ('zero_chat','_zero_metadata')
716
- ORDER BY pubname
717
- `)
718
-
719
- expect(result.fields).toEqual([
720
- { name: 'pubname', oid: undefined },
721
- { name: 'pubinsert', oid: 16 },
722
- { name: 'pubupdate', oid: 16 },
723
- { name: 'pubdelete', oid: 16 },
724
- { name: 'pubtruncate', oid: 16 },
725
- ])
726
- expect(result.rows).toEqual([
727
- {
728
- pubname: '_zero_metadata',
729
- pubinsert: 't',
730
- pubupdate: 't',
731
- pubdelete: 't',
732
- pubtruncate: 't',
733
- },
734
- {
735
- pubname: 'zero_chat',
736
- pubinsert: 't',
737
- pubupdate: 't',
738
- pubdelete: 't',
739
- pubtruncate: 't',
740
- },
741
- ])
742
- })
743
-
744
- test('preserves selected field metadata for empty catalog probes', async () => {
745
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
746
- const backend = new DoBackend(http.url, 'postgres', 'pg-class-probe-test')
747
- await backend.waitReady
748
-
749
- const result = await (backend as any).handleCatalogQuery(`
750
- SELECT nspname, relname FROM pg_class
751
- JOIN pg_namespace ON relnamespace = pg_namespace.oid
752
- WHERE nspname = 'chat_0' AND relname = 'versionHistory'
753
- `)
754
-
755
- expect(result.rows).toEqual([])
756
- expect(result.fields.map((field: any) => field.name)).toEqual(['nspname', 'relname'])
757
- })
758
-
759
- test('synthesizes advisory lock catalog calls with one null row', async () => {
760
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
761
- const backend = new DoBackend(http.url, 'postgres', 'advisory-lock-test')
762
- await backend.waitReady
763
-
764
- const result = await (backend as any).handleCatalogQuery(`
765
- SELECT pg_advisory_xact_lock(hashtext('migrate-schema:chat_0'))
766
- `)
767
-
768
- expect(result.fields.map((field: any) => field.name)).toEqual([
769
- 'pg_advisory_xact_lock',
770
- ])
771
- expect(result.rows).toEqual([{ pg_advisory_xact_lock: null }])
772
- })
773
-
774
- test('synthesizes logical message lag-report probes', async () => {
775
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
776
- const backend = new DoBackend(http.url, 'postgres', 'logical-message-test')
777
- await backend.waitReady
778
-
779
- const result = await (backend as any).handleCatalogQuery(`
780
- WITH CTE AS (SELECT extract(epoch from now()) * 1000 AS "commitTimeMs")
781
- SELECT "commitTimeMs", pg_logical_emit_message(
782
- false,
783
- 'zero/0',
784
- json_build_object(
785
- 'id', 'lag-1'::text,
786
- 'sendTimeMs', 1::int8,
787
- 'commitTimeMs', "commitTimeMs"
788
- )::text
789
- ) as lsn FROM CTE;
790
- `)
791
-
792
- expect(result.fields.map((field: any) => field.name)).toEqual(['commitTimeMs', 'lsn'])
793
- expect(result.rows[0]).toEqual({
794
- commitTimeMs: expect.any(Number),
795
- lsn: '0/1',
796
- })
797
-
798
- const wire = await backend.execProtocolRaw(
799
- msg(
800
- 0x51,
801
- cstr(`
802
- WITH CTE AS (SELECT extract(epoch from now()) * 1000 AS "commitTimeMs")
803
- SELECT "commitTimeMs", pg_logical_emit_message(
804
- false,
805
- 'zero/0',
806
- json_build_object(
807
- 'id', 'lag-1'::text,
808
- 'sendTimeMs', 1::int8,
809
- 'commitTimeMs', "commitTimeMs"
810
- )::text
811
- ) as lsn FROM CTE;
812
- `)
813
- )
814
- )
815
- expect(rowDescriptionOids(wire)).toMatchObject({
816
- commitTimeMs: 701,
817
- lsn: 25,
818
- })
819
- })
820
-
821
- test('synthesizes pg_tables from sqlite schema for publication setup', async () => {
822
- const http = await startDoHttp((sql) => {
823
- if (sql.includes('sqlite_master')) {
824
- return {
825
- rows: [
826
- { name: 'message', sql: 'CREATE TABLE message (id varchar PRIMARY KEY)' },
827
- { name: 'user', sql: 'CREATE TABLE user (id varchar PRIMARY KEY)' },
828
- {
829
- name: 'public_migrations',
830
- sql: 'CREATE TABLE public_migrations (id integer PRIMARY KEY)',
831
- },
832
- ],
833
- columns: ['name', 'sql'],
834
- }
835
- }
836
- if (sql.includes('PRAGMA table_info("message")')) {
837
- return {
838
- rows: [
839
- { cid: 0, name: 'id', type: 'varchar', notnull: 1, dflt_value: null, pk: 1 },
840
- ],
841
- columns: ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'],
842
- }
843
- }
844
- return { rows: [], columns: [] }
845
- })
846
- const backend = new DoBackend(http.url, 'postgres', 'pg-tables-test')
847
- await backend.waitReady
848
-
849
- const result = await (backend as any).handleCatalogQuery(`
850
- SELECT tablename FROM pg_tables
851
- WHERE schemaname = 'public'
852
- AND tablename != ALL('{"user","migrations"}')
853
- `)
854
-
855
- expect(result.rows).toEqual([{ tablename: 'message' }])
856
- })
857
-
858
- test('synthesizes information_schema.columns from parsed DDL metadata', async () => {
859
- const http = await startDoHttp((sql) => {
860
- if (sql.includes('sqlite_master')) {
861
- return {
862
- rows: [
863
- {
864
- name: 'message',
865
- sql: 'CREATE TABLE message (id varchar, payload text, enabled integer, tags text)',
866
- },
867
- ],
868
- columns: ['name', 'sql'],
869
- }
870
- }
871
- if (sql.includes('PRAGMA table_info("message")')) {
872
- return {
873
- rows: [
874
- { cid: 0, name: 'id', type: 'varchar', notnull: 1, dflt_value: null, pk: 1 },
875
- {
876
- cid: 1,
877
- name: 'payload',
878
- type: 'text',
879
- notnull: 0,
880
- dflt_value: null,
881
- pk: 0,
882
- },
883
- {
884
- cid: 2,
885
- name: 'enabled',
886
- type: 'integer',
887
- notnull: 1,
888
- dflt_value: '0',
889
- pk: 0,
890
- },
891
- { cid: 3, name: 'tags', type: 'text', notnull: 0, dflt_value: null, pk: 0 },
892
- ],
893
- columns: ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'],
894
- }
895
- }
896
- return { rows: [], columns: [] }
897
- })
898
- const backend = new DoBackend(http.url, 'postgres', 'information-schema-test')
899
- await backend.waitReady
900
- await backend.exec(`
901
- CREATE TABLE public.message (
902
- id varchar(64) PRIMARY KEY,
903
- payload jsonb,
904
- enabled boolean NOT NULL DEFAULT false,
905
- tags text[]
906
- )
907
- `)
908
-
909
- const result = await (backend as any).handleCatalogQuery(`
910
- SELECT
911
- c.table_schema::text AS schema,
912
- c.table_name::text AS table,
913
- c.column_name::text AS column,
914
- c.data_type::text AS "dataType",
915
- c.character_maximum_length AS length,
916
- c.numeric_precision AS precision,
917
- c.numeric_scale AS scale,
918
- t.typtype::text AS typtype,
919
- t.typname::text AS typename,
920
- CASE WHEN t.typelem <> 0 THEN et.typtype::text ELSE NULL END AS "elemTyptype",
921
- CASE WHEN t.typelem <> 0 THEN et.typname::text ELSE NULL END AS "elemTypname"
922
- FROM information_schema.columns c
923
- JOIN pg_catalog.pg_type t ON c.udt_name = t.typname
924
- LEFT JOIN pg_catalog.pg_type et ON t.typelem = et.oid
925
- JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid
926
- WHERE (c.table_schema, c.table_name) IN (('public'::text, 'message'::text))
927
- `)
928
-
929
- expect(result.rows).toEqual([
930
- expect.objectContaining({
931
- column: 'id',
932
- dataType: 'character varying',
933
- length: 64,
934
- typtype: 'b',
935
- typename: 'varchar',
936
- elemTyptype: null,
937
- }),
938
- expect.objectContaining({
939
- column: 'payload',
940
- dataType: 'jsonb',
941
- typename: 'jsonb',
942
- }),
943
- expect.objectContaining({
944
- column: 'enabled',
945
- dataType: 'boolean',
946
- typename: 'bool',
947
- }),
948
- expect.objectContaining({
949
- column: 'tags',
950
- dataType: 'ARRAY',
951
- typename: '_text',
952
- elemTyptype: 'b',
953
- elemTypname: 'text',
954
- }),
955
- ])
956
- })
957
-
958
- test('tracks parser-backed publication membership without private table lists', async () => {
959
- const http = await startDoHttp((sql) => {
960
- if (sql.includes('sqlite_master')) {
961
- return {
962
- rows: [
963
- { name: 'message', sql: 'CREATE TABLE message (id text)' },
964
- { name: 'account', sql: 'CREATE TABLE account (id text)' },
965
- ],
966
- columns: ['name', 'sql'],
967
- }
968
- }
969
- if (sql.includes('PRAGMA table_info("message")')) {
970
- return {
971
- rows: [
972
- { cid: 0, name: 'id', type: 'text', notnull: 1, dflt_value: null, pk: 1 },
973
- ],
974
- columns: ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'],
975
- }
976
- }
977
- if (sql.includes('PRAGMA table_info("account")')) {
978
- return {
979
- rows: [
980
- { cid: 0, name: 'id', type: 'text', notnull: 1, dflt_value: null, pk: 1 },
981
- ],
982
- columns: ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'],
983
- }
984
- }
985
- return { rows: [], columns: [] }
986
- })
987
- const backend = new DoBackend(http.url, 'postgres', 'publication-membership-test')
988
- await backend.waitReady
989
- await backend.exec('CREATE PUBLICATION zero_chat FOR TABLE message')
990
-
991
- const result = await (backend as any).handleCatalogQuery(`
992
- SELECT schemaname, tablename, pubname
993
- FROM pg_publication_tables
994
- WHERE pubname IN ('zero_chat')
995
- `)
996
-
997
- expect(result.rows).toEqual([
998
- { schemaname: 'public', tablename: 'message', pubname: 'zero_chat' },
999
- ])
1000
- })
1001
-
1002
- test('synthesizes zero-cache publication metadata result sets', async () => {
1003
- const http = await startDoHttp((sql) => {
1004
- if (sql.includes('sqlite_master')) {
1005
- return {
1006
- rows: [
1007
- { name: 'message', sql: 'CREATE TABLE message (id varchar PRIMARY KEY)' },
1008
- ],
1009
- columns: ['name', 'sql'],
1010
- }
1011
- }
1012
- if (sql.includes('PRAGMA table_info("message")')) {
1013
- return {
1014
- rows: [
1015
- { cid: 0, name: 'id', type: 'varchar', notnull: 1, dflt_value: null, pk: 1 },
1016
- {
1017
- cid: 1,
1018
- name: 'deleted',
1019
- type: 'INTEGER',
1020
- notnull: 1,
1021
- dflt_value: '0',
1022
- pk: 0,
1023
- },
1024
- ],
1025
- columns: ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'],
1026
- }
1027
- }
1028
- return { rows: [], columns: [] }
1029
- })
1030
- const backend = new DoBackend(http.url, 'postgres', 'publication-metadata-test')
1031
- await backend.waitReady
1032
- await backend.exec(`
1033
- CREATE TABLE message (
1034
- id varchar PRIMARY KEY,
1035
- deleted boolean NOT NULL DEFAULT false
1036
- );
1037
- CREATE PUBLICATION zero_chat FOR TABLE message;
1038
- `)
1039
-
1040
- const results = await (backend as any).handleCatalogQueries(`
1041
- SELECT schemaname AS "schema", tablename AS "table",
1042
- json_object_agg(pubname, attnames) AS "publications"
1043
- FROM pg_publication_tables pb
1044
- WHERE pb.pubname IN ('zero_chat')
1045
- GROUP BY schemaname, tablename;
1046
-
1047
- WITH published_columns AS (
1048
- SELECT attname FROM pg_attribute
1049
- JOIN pg_publication_tables pb ON attname = ANY(pb.attnames)
1050
- WHERE pb.pubname IN ('zero_chat')
1051
- )
1052
- SELECT COALESCE(json_agg("table"), '[]'::json) as "tables" FROM published_columns;
1053
-
1054
- WITH indexed_columns AS (
1055
- SELECT pg_indexes.indexname FROM pg_indexes
1056
- JOIN pg_index ON true
1057
- )
1058
- SELECT COALESCE(json_agg("index"), '[]'::json) as "indexes" FROM indexed_columns;
1059
- `)
1060
-
1061
- expect(results[0].rows).toEqual([
1062
- {
1063
- schema: 'public',
1064
- table: 'message',
1065
- publications: { zero_chat: ['id', 'deleted'] },
1066
- },
1067
- ])
1068
- expect(results[1].rows[0].tables).toEqual([
1069
- expect.objectContaining({
1070
- name: 'message',
1071
- primaryKey: ['id'],
1072
- columns: expect.objectContaining({
1073
- id: expect.objectContaining({ dataType: 'character varying', typeOID: 1043 }),
1074
- deleted: expect.objectContaining({ dataType: 'boolean', typeOID: 16 }),
1075
- }),
1076
- }),
1077
- ])
1078
- expect(results[2]).toEqual({
1079
- rows: [
1080
- {
1081
- indexes: [
1082
- expect.objectContaining({
1083
- schema: 'public',
1084
- tableName: 'message',
1085
- name: 'message_id_pkey',
1086
- unique: true,
1087
- isPrimaryKey: true,
1088
- isImmediate: true,
1089
- columns: { id: 'ASC' },
1090
- }),
1091
- ],
1092
- },
1093
- ],
1094
- fields: [{ name: 'indexes', oid: 114 }],
1095
- })
1096
- })
1097
-
1098
- test('tracks published table writes with parser-derived returning SQL', async () => {
1099
- const http = await startDoHttp((sql) => {
1100
- if (compactSQL(sql).includes('RETURNING *')) {
1101
- return { rows: [{ id: 't1', body: 'hello' }], columns: ['id', 'body'] }
1102
- }
1103
- return { rows: [], columns: [] }
1104
- })
1105
- const backend = new DoBackend(http.url, 'postgres', 'write-tracking-test')
1106
- await backend.waitReady
1107
- await backend.exec('CREATE PUBLICATION zero_chat FOR TABLE task_item')
1108
-
1109
- const result = await backend.execProtocolRaw(
1110
- msg(0x51, cstr("INSERT INTO task_item (id, body) VALUES ('t1', 'hello')"))
1111
- )
1112
-
1113
- expect(messageTypes(result)).toEqual(['C', 'Z'])
1114
- const tracked = http.bodies.find((body) => body.track)
1115
- expect(tracked.track).toEqual({
1116
- tableName: 'public.task_item',
1117
- operation: 'INSERT',
1118
- returnRows: false,
1119
- })
1120
- expect(compactSQL(tracked.sql)).toContain('RETURNING *')
1121
- })
1122
-
1123
- test('signals replication immediately after tracked writes', async () => {
1124
- const http = await startDoHttp((sql) => {
1125
- if (compactSQL(sql).includes('RETURNING *')) {
1126
- return { rows: [{ id: 't1', body: 'hello' }], columns: ['id', 'body'] }
1127
- }
1128
- return { rows: [], columns: [] }
1129
- })
1130
- const backend = new DoBackend(http.url, 'postgres', 'write-signal-test')
1131
- await backend.waitReady
1132
- await backend.exec('CREATE PUBLICATION zero_chat FOR TABLE task_item')
1133
-
1134
- const globalObject = globalThis as any
1135
- const previousWakeup = globalObject.__orez_signal_replication
1136
- let wakeups = 0
1137
- globalObject.__orez_signal_replication = () => {
1138
- wakeups++
1139
- }
1140
- try {
1141
- await backend.query("INSERT INTO task_item (id, body) VALUES ('t1', 'hello')")
1142
- } finally {
1143
- if (previousWakeup === undefined) {
1144
- delete globalObject.__orez_signal_replication
1145
- } else {
1146
- globalObject.__orez_signal_replication = previousWakeup
1147
- }
1148
- }
1149
-
1150
- expect(wakeups).toBe(1)
1151
- })
1152
-
1153
- test('tracks full published rows while preserving client RETURNING projection', async () => {
1154
- const http = await startDoHttp((sql) => {
1155
- const compact = compactSQL(sql)
1156
- if (compact.includes('RETURNING *')) {
1157
- return {
1158
- rows: [{ id: 't1', body: 'hello', __orez_returning_1: 'HELLO' }],
1159
- columns: ['id', 'body', '__orez_returning_1'],
1160
- }
1161
- }
1162
- return { rows: [], columns: [] }
1163
- })
1164
- const backend = new DoBackend(http.url, 'postgres', 'write-returning-test')
1165
- await backend.waitReady
1166
- await backend.exec('CREATE TABLE task_item (id TEXT PRIMARY KEY, body TEXT)')
1167
- await backend.exec('CREATE PUBLICATION zero_chat FOR TABLE task_item')
1168
-
1169
- const result = await backend.execProtocolRaw(
1170
- msg(
1171
- 0x51,
1172
- cstr(
1173
- `INSERT INTO task_item (id, body)
1174
- VALUES ('t1', 'hello')
1175
- RETURNING "id", upper("body") AS "bodyUpper"`
1176
- )
1177
- )
1178
- )
1179
-
1180
- expect(messageTypes(result)).toEqual(['T', 'D', 'C', 'Z'])
1181
- expect(rowDescriptionNames(result)).toEqual(['id', 'bodyUpper'])
1182
- expect(dataRowValues(result)).toEqual([['t1', 'HELLO']])
1183
-
1184
- const tracked = http.bodies.find((body) => body.track)
1185
- expect(tracked.track).toEqual({
1186
- tableName: 'public.task_item',
1187
- operation: 'INSERT',
1188
- returnRows: true,
1189
- rowColumns: ['id', 'body'],
1190
- })
1191
- expect(compactSQL(tracked.sql)).toContain('RETURNING *')
1192
- expect(compactSQL(tracked.sql)).toContain('__orez_returning_1')
1193
- })
1194
-
1195
- test('synthesizes primary-key rows for zero-cache relation metadata queries', async () => {
1196
- const http = await startDoHttp((sql) => {
1197
- if (sql.includes('sqlite_master')) {
1198
- return {
1199
- rows: [{ name: 'server', sql: 'CREATE TABLE server (id varchar PRIMARY KEY)' }],
1200
- columns: ['name', 'sql'],
1201
- }
1202
- }
1203
- if (sql.includes('PRAGMA table_info("server")')) {
1204
- return {
1205
- rows: [
1206
- { cid: 0, name: 'id', type: 'varchar', notnull: 1, dflt_value: null, pk: 1 },
1207
- {
1208
- cid: 1,
1209
- name: 'name',
1210
- type: 'varchar',
1211
- notnull: 1,
1212
- dflt_value: null,
1213
- pk: 0,
1214
- },
1215
- ],
1216
- columns: ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'],
1217
- }
1218
- }
1219
- return { rows: [], columns: [] }
1220
- })
1221
- const backend = new DoBackend(http.url, 'postgres', 'relation-metadata-test')
1222
- await backend.waitReady
1223
-
1224
- const result = await backend.query<{
1225
- kind: string
1226
- table_schema: string
1227
- table_name: string
1228
- column_name: string
1229
- data_type: string | null
1230
- ordinal_position: number
1231
- }>(
1232
- `SELECT 'pk' AS kind, tc.table_schema, tc.table_name, kcu.column_name, NULL AS data_type, kcu.ordinal_position
1233
- FROM information_schema.table_constraints tc
1234
- JOIN information_schema.key_column_usage kcu
1235
- ON tc.constraint_name = kcu.constraint_name
1236
- AND tc.table_schema = kcu.table_schema
1237
- WHERE tc.constraint_type = 'PRIMARY KEY'
1238
- AND tc.table_schema = ANY($1)
1239
- UNION ALL
1240
- SELECT 'col' AS kind, table_schema, table_name, column_name, data_type, ordinal_position
1241
- FROM information_schema.columns
1242
- WHERE table_schema = ANY($1)
1243
- ORDER BY table_schema, table_name, kind, ordinal_position`,
1244
- [['public']]
1245
- )
1246
-
1247
- expect(result.rows).toContainEqual({
1248
- kind: 'pk',
1249
- table_schema: 'public',
1250
- table_name: 'server',
1251
- column_name: 'id',
1252
- data_type: null,
1253
- ordinal_position: 1,
1254
- })
1255
- expect(result.rows).toContainEqual({
1256
- kind: 'col',
1257
- table_schema: 'public',
1258
- table_name: 'server',
1259
- column_name: 'name',
1260
- data_type: 'character varying',
1261
- ordinal_position: 2,
1262
- })
1263
- })
1264
-
1265
- test('does not track unpublished public table writes', async () => {
1266
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
1267
- const backend = new DoBackend(http.url, 'postgres', 'unpublished-write-test')
1268
- await backend.waitReady
1269
-
1270
- await backend.execProtocolRaw(
1271
- msg(0x51, cstr("INSERT INTO private_note (id) VALUES ('n1')"))
1272
- )
1273
-
1274
- expect(http.bodies.some((body) => body.track)).toBe(false)
1275
- })
1276
-
1277
- test('synthesizes primary and unique index metadata for published tables', async () => {
1278
- const http = await startDoHttp((sql) => {
1279
- if (sql.includes('sqlite_master')) {
1280
- return {
1281
- rows: [
1282
- {
1283
- name: 'account',
1284
- sql: 'CREATE TABLE account (id text PRIMARY KEY, email text NOT NULL UNIQUE)',
1285
- },
1286
- ],
1287
- columns: ['name', 'sql'],
1288
- }
1289
- }
1290
- if (sql.includes('PRAGMA table_info("account")')) {
1291
- return {
1292
- rows: [
1293
- { cid: 0, name: 'id', type: 'text', notnull: 1, dflt_value: null, pk: 1 },
1294
- {
1295
- cid: 1,
1296
- name: 'email',
1297
- type: 'text',
1298
- notnull: 1,
1299
- dflt_value: null,
1300
- pk: 0,
1301
- },
1302
- ],
1303
- columns: ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'],
1304
- }
1305
- }
1306
- if (sql.includes('PRAGMA index_list("account")')) {
1307
- return {
1308
- rows: [
1309
- {
1310
- seq: 0,
1311
- name: 'sqlite_autoindex_account_2',
1312
- unique: 1,
1313
- origin: 'u',
1314
- partial: 0,
1315
- },
1316
- {
1317
- seq: 1,
1318
- name: 'sqlite_autoindex_account_1',
1319
- unique: 1,
1320
- origin: 'pk',
1321
- partial: 0,
1322
- },
1323
- ],
1324
- columns: ['seq', 'name', 'unique', 'origin', 'partial'],
1325
- }
1326
- }
1327
- if (sql.includes('PRAGMA index_xinfo("sqlite_autoindex_account_2")')) {
1328
- return {
1329
- rows: [
1330
- { seqno: 0, cid: 1, name: 'email', desc: 0, key: 1 },
1331
- { seqno: 1, cid: -1, name: null, desc: 0, key: 0 },
1332
- ],
1333
- columns: ['seqno', 'cid', 'name', 'desc', 'key'],
1334
- }
1335
- }
1336
- if (sql.includes('PRAGMA index_xinfo("sqlite_autoindex_account_1")')) {
1337
- return {
1338
- rows: [
1339
- { seqno: 0, cid: 0, name: 'id', desc: 0, key: 1 },
1340
- { seqno: 1, cid: -1, name: null, desc: 0, key: 0 },
1341
- ],
1342
- columns: ['seqno', 'cid', 'name', 'desc', 'key'],
1343
- }
1344
- }
1345
- return { rows: [], columns: [] }
1346
- })
1347
- const backend = new DoBackend(http.url, 'postgres', 'published-index-test')
1348
- await backend.waitReady
1349
- await backend.exec('CREATE PUBLICATION zero_chat FOR TABLE account')
1350
-
1351
- const result = await (backend as any).handleCatalogQuery(`
1352
- WITH indexed_columns AS (
1353
- SELECT pg_indexes.indexname FROM pg_indexes
1354
- JOIN pg_index ON true
1355
- JOIN pg_publication_tables pb ON true
1356
- WHERE pb.pubname IN ('zero_chat')
1357
- )
1358
- SELECT COALESCE(json_agg("index"), '[]'::json) as "indexes"
1359
- FROM indexed_columns;
1360
- `)
1361
-
1362
- expect(result).toEqual({
1363
- rows: [
1364
- {
1365
- indexes: [
1366
- expect.objectContaining({
1367
- schema: 'public',
1368
- tableName: 'account',
1369
- name: 'account_id_pkey',
1370
- unique: true,
1371
- isPrimaryKey: true,
1372
- isImmediate: true,
1373
- columns: { id: 'ASC' },
1374
- }),
1375
- expect.objectContaining({
1376
- schema: 'public',
1377
- tableName: 'account',
1378
- name: 'account_email_key',
1379
- unique: true,
1380
- isPrimaryKey: false,
1381
- isImmediate: true,
1382
- columns: { email: 'ASC' },
1383
- }),
1384
- ],
1385
- },
1386
- ],
1387
- fields: [{ name: 'indexes', oid: 114 }],
1388
- })
1389
- })
1390
-
1391
- test('uses parsed ADD COLUMN primary-key metadata when SQLite cannot alter the physical key', async () => {
1392
- const http = await startDoHttp((sql) => {
1393
- if (sql.includes('sqlite_master')) {
1394
- return {
1395
- rows: [
1396
- {
1397
- name: 'appInstall',
1398
- sql: 'CREATE TABLE "appInstall" ("serverId" text, "creatorId" text, "appId" text, "id" text, PRIMARY KEY ("serverId", "creatorId", "appId"))',
1399
- },
1400
- ],
1401
- columns: ['name', 'sql'],
1402
- }
1403
- }
1404
- if (sql.includes('PRAGMA table_info("appInstall")')) {
1405
- return {
1406
- rows: [
1407
- {
1408
- cid: 0,
1409
- name: 'serverId',
1410
- type: 'text',
1411
- notnull: 0,
1412
- dflt_value: null,
1413
- pk: 1,
1414
- },
1415
- {
1416
- cid: 1,
1417
- name: 'creatorId',
1418
- type: 'text',
1419
- notnull: 0,
1420
- dflt_value: null,
1421
- pk: 2,
1422
- },
1423
- {
1424
- cid: 2,
1425
- name: 'appId',
1426
- type: 'text',
1427
- notnull: 0,
1428
- dflt_value: null,
1429
- pk: 3,
1430
- },
1431
- { cid: 3, name: 'id', type: 'text', notnull: 0, dflt_value: null, pk: 0 },
1432
- ],
1433
- columns: ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'],
1434
- }
1435
- }
1436
- if (sql.includes('PRAGMA index_list("appInstall")')) {
1437
- return {
1438
- rows: [
1439
- {
1440
- seq: 0,
1441
- name: 'sqlite_autoindex_appInstall_1',
1442
- unique: 1,
1443
- origin: 'pk',
1444
- partial: 0,
1445
- },
1446
- ],
1447
- columns: ['seq', 'name', 'unique', 'origin', 'partial'],
1448
- }
1449
- }
1450
- return { rows: [], columns: [] }
1451
- })
1452
- const backend = new DoBackend(http.url, 'postgres', 'alter-primary-key-test')
1453
- await backend.waitReady
1454
- await backend.exec(`
1455
- ALTER TABLE "appInstall"
1456
- ADD COLUMN "id" varchar PRIMARY KEY NOT NULL;
1457
- CREATE PUBLICATION zero_chat FOR TABLE "appInstall";
1458
- `)
1459
-
1460
- const results = await (backend as any).handleCatalogQueries(`
1461
- WITH published_columns AS (
1462
- SELECT attname FROM pg_attribute
1463
- JOIN pg_publication_tables pb ON attname = ANY(pb.attnames)
1464
- WHERE pb.pubname IN ('zero_chat')
1465
- )
1466
- SELECT COALESCE(json_agg("table"), '[]'::json) as "tables" FROM published_columns;
1467
-
1468
- WITH indexed_columns AS (
1469
- SELECT pg_indexes.indexname FROM pg_indexes
1470
- JOIN pg_index ON true
1471
- )
1472
- SELECT COALESCE(json_agg("index"), '[]'::json) as "indexes" FROM indexed_columns;
1473
- `)
1474
-
1475
- expect(results[0].rows[0].tables).toEqual([
1476
- expect.objectContaining({
1477
- name: 'appInstall',
1478
- primaryKey: ['id'],
1479
- columns: expect.objectContaining({
1480
- id: expect.objectContaining({ notNull: true }),
1481
- }),
1482
- }),
1483
- ])
1484
- expect(results[1].rows[0].indexes).toEqual([
1485
- expect.objectContaining({
1486
- tableName: 'appInstall',
1487
- unique: true,
1488
- isPrimaryKey: true,
1489
- columns: { id: 'ASC' },
1490
- }),
1491
- ])
1492
- })
1493
-
1494
- test('does not convert backend SQL errors into empty result sets', async () => {
1495
- const http = await startDoHttp(() => new Response('boom', { status: 500 }))
1496
- const backend = new DoBackend(http.url, 'postgres', 'error-test')
1497
- await backend.waitReady
1498
-
1499
- await expect(backend.query('SELECT broken')).rejects.toThrow('HTTP 500: boom')
1500
- })
1501
-
1502
- test('flattens public schema table references before sending SQL to DO', async () => {
1503
- const http = await startDoHttp(() => ({ rows: [{ count: 0 }], columns: ['count'] }))
1504
- const backend = new DoBackend(http.url, 'postgres', 'schema-flatten-test')
1505
- await backend.waitReady
1506
-
1507
- await backend.query('SELECT count(*) FROM public.migrations')
1508
-
1509
- expect(
1510
- http.sqls.some((sql) => compactSQL(sql).includes('FROM public_migrations'))
1511
- ).toBe(true)
1512
- expect(
1513
- http.sqls.some((sql) => compactSQL(sql).includes('FROM public.migrations'))
1514
- ).toBe(false)
1515
- })
1516
-
1517
- test('flushes transactional writes before reads so migrations can see DDL', async () => {
1518
- const http = await startDoHttp((sql) => {
1519
- if (/select\s+name\s+from\s+public_migrations/i.test(sql)) {
1520
- return { rows: [], columns: ['name'] }
1521
- }
1522
- return { rows: [], columns: [] }
1523
- })
1524
- const backend = new DoBackend(http.url, 'postgres', 'transaction-read-test')
1525
- await backend.waitReady
1526
-
1527
- await backend.execProtocolRaw(msg(0x51, cstr('BEGIN')))
1528
- await backend.execProtocolRaw(
1529
- msg(
1530
- 0x51,
1531
- cstr(`
1532
- CREATE TABLE IF NOT EXISTS public.migrations (
1533
- id SERIAL PRIMARY KEY,
1534
- name VARCHAR(255) NOT NULL
1535
- )
1536
- `)
1537
- )
1538
- )
1539
- await backend.execProtocolRaw(msg(0x51, cstr('SELECT name FROM public.migrations')))
1540
-
1541
- const createIndex = http.sqls.findIndex((sql) =>
1542
- /create\s+table\s+if\s+not\s+exists\s+public_migrations/i.test(sql)
1543
- )
1544
- const selectIndex = http.sqls.findIndex((sql) =>
1545
- /select\s+name\s+from\s+public_migrations/i.test(sql)
1546
- )
1547
- expect(createIndex).toBeGreaterThanOrEqual(0)
1548
- expect(selectIndex).toBeGreaterThan(createIndex)
1549
- await backend.execProtocolRaw(msg(0x51, cstr('ROLLBACK')))
1550
- })
1551
-
1552
- test('intercepts parser-recognized transaction variants before DO execution', async () => {
1553
- // The Durable Object refuses raw BEGIN/SAVEPOINT (it requires the JS-side
1554
- // transaction API). All PG transaction control statements must be handled
1555
- // locally and never reach the DO as SQL.
1556
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
1557
- const backend = new DoBackend(http.url, 'postgres', 'transaction-variant-test')
1558
- await backend.waitReady
1559
-
1560
- const begin = await backend.execProtocolRaw(
1561
- msg(0x51, cstr('BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ'))
1562
- )
1563
- await backend.execProtocolRaw(msg(0x51, cstr('SAVEPOINT zero_migrate')))
1564
- await backend.execProtocolRaw(msg(0x51, cstr('RELEASE SAVEPOINT zero_migrate')))
1565
- await backend.execProtocolRaw(msg(0x51, cstr('COMMIT')))
1566
-
1567
- expect(messageTypes(begin)).toEqual(['C', 'Z'])
1568
- const sent = http.sqls.map((sql) => sql.trim().toUpperCase())
1569
- expect(sent.some((sql) => sql.startsWith('BEGIN'))).toBe(false)
1570
- expect(sent.some((sql) => sql.startsWith('COMMIT'))).toBe(false)
1571
- expect(sent.some((sql) => sql.startsWith('SAVEPOINT'))).toBe(false)
1572
- expect(sent.some((sql) => sql.startsWith('RELEASE'))).toBe(false)
1573
- })
1574
-
1575
- test('reports ReadyForQuery transaction status while a transaction is open', async () => {
1576
- const http = await startDoHttp(() => ({ rows: [], columns: ['ok'] }))
1577
- const backend = new DoBackend(http.url, 'postgres', 'transaction-status-test')
1578
- await backend.waitReady
1579
-
1580
- const begin = await backend.execProtocolRaw(msg(0x51, cstr('BEGIN')))
1581
- const select = await backend.execProtocolRaw(msg(0x51, cstr('SELECT 1 AS ok')))
1582
- const commit = await backend.execProtocolRaw(msg(0x51, cstr('COMMIT')))
1583
-
1584
- expect(readyForQueryStatuses(begin)).toEqual(['T'])
1585
- expect(readyForQueryStatuses(select)).toEqual(['T'])
1586
- expect(readyForQueryStatuses(commit)).toEqual(['I'])
1587
- })
1588
-
1589
- test('keeps extended-protocol Sync in transaction state after BEGIN', async () => {
1590
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
1591
- const backend = new DoBackend(
1592
- http.url,
1593
- 'postgres',
1594
- 'extended-transaction-status-test'
1595
- )
1596
- await backend.waitReady
1597
-
1598
- await backend.execProtocolRaw(parseMessage('BEGIN'))
1599
- await backend.execProtocolRaw(bindStatement())
1600
- await backend.execProtocolRaw(executePortal())
1601
- const sync = await backend.execProtocolRaw(msg(0x53, new Uint8Array(0)))
1602
-
1603
- expect(readyForQueryStatuses(sync)).toEqual(['T'])
1604
- await backend.execProtocolRaw(parseMessage('ROLLBACK'))
1605
- await backend.execProtocolRaw(bindStatement())
1606
- await backend.execProtocolRaw(executePortal())
1607
- })
1608
-
1609
- test('returns command completion for extended transaction starts', async () => {
1610
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
1611
- const backend = new DoBackend(http.url, 'postgres', 'extended-begin-test')
1612
- await backend.waitReady
1613
-
1614
- await backend.execProtocolRaw(parseMessage('BEGIN'))
1615
- await backend.execProtocolRaw(bindStatement())
1616
- const result = await backend.execProtocolRaw(executePortal())
1617
-
1618
- expect(messageTypes(result)).toEqual(['C'])
1619
- expect(http.sqls.some((sql) => compactSQL(sql).startsWith('BEGIN'))).toBe(false)
1620
- await backend.execProtocolRaw(parseMessage('ROLLBACK'))
1621
- await backend.execProtocolRaw(bindStatement())
1622
- await backend.execProtocolRaw(executePortal())
1623
- })
1624
-
1625
- test('clears the rewrite cache on transaction rollback', async () => {
1626
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
1627
- const backend = new DoBackend(http.url, 'postgres', 'rewrite-cache-rollback-test')
1628
- await backend.waitReady
1629
-
1630
- await backend.exec('SELECT 1')
1631
- const beforeCount = http.sqls.filter((sql) => compactSQL(sql) === 'SELECT 1').length
1632
-
1633
- await backend.execProtocolRaw(msg(0x51, cstr('BEGIN')))
1634
- await backend.execProtocolRaw(msg(0x51, cstr('ROLLBACK')))
1635
-
1636
- await backend.exec('SELECT 1')
1637
- // BEGIN / ROLLBACK don't reach the DO; the cache invalidation should
1638
- // re-issue the SELECT.
1639
- expect(http.sqls.filter((sql) => compactSQL(sql) === 'SELECT 1').length).toBe(
1640
- beforeCount + 1
1641
- )
1642
- })
1643
-
1644
- test('snapshots extended transaction writes for rollback', async () => {
1645
- const http = await startDoHttp((sql) => {
1646
- if (sql.includes('sqlite_master')) {
1647
- return { rows: [{ ok: 1 }], columns: ['ok'] }
1648
- }
1649
- if (compactSQL(sql).includes('RETURNING *')) {
1650
- return {
1651
- rows: [{ clientGroupID: 'cg', clientID: 'client-a', lastMutationID: 1 }],
1652
- columns: ['clientGroupID', 'clientID', 'lastMutationID'],
1653
- }
1654
- }
1655
- return { rows: [], columns: [] }
1656
- })
1657
- const backend = new DoBackend(http.url, 'postgres', 'extended-tx-session-test')
1658
- await backend.waitReady
1659
-
1660
- await backend.execProtocolRaw(parseMessage('BEGIN'))
1661
- await backend.execProtocolRaw(bindStatement())
1662
- await backend.execProtocolRaw(executePortal())
1663
-
1664
- await backend.execProtocolRaw(
1665
- parseMessage(`
1666
- INSERT INTO "chat_0"."clients" AS current
1667
- ("clientGroupID", "clientID", "lastMutationID")
1668
- VALUES ('cg', 'client-a', 1)
1669
- ON CONFLICT ("clientGroupID", "clientID")
1670
- DO UPDATE SET "lastMutationID" = current."lastMutationID" + 1
1671
- RETURNING "lastMutationID"
1672
- `)
1673
- )
1674
- await backend.execProtocolRaw(bindStatement())
1675
- const result = await backend.execProtocolRaw(executePortal())
1676
- expect(dataRowValues(result)).toEqual([['1']])
1677
-
1678
- await backend.execProtocolRaw(parseMessage('ROLLBACK'))
1679
- await backend.execProtocolRaw(bindStatement())
1680
- await backend.execProtocolRaw(executePortal())
1681
-
1682
- expect(
1683
- http.sqls.some((sql) =>
1684
- /CREATE TABLE "_orez_tx_.*_chat_0_clients" AS SELECT \* FROM "chat_0_clients"/.test(
1685
- sql
1686
- )
1687
- )
1688
- ).toBe(true)
1689
- expect(http.requests.some((url) => url.pathname === '/batch')).toBe(true)
1690
- expect(
1691
- http.bodies.some(
1692
- (body) =>
1693
- Array.isArray(body.statements) &&
1694
- body.statements.some((sql: string) =>
1695
- /INSERT OR REPLACE INTO "chat_0_clients" SELECT \* FROM "_orez_tx_.*_chat_0_clients"/.test(
1696
- sql
1697
- )
1698
- )
1699
- )
1700
- ).toBe(true)
1701
- })
1702
-
1703
- test('restores rollback snapshots without firing table triggers', async () => {
1704
- const triggerSQL =
1705
- 'CREATE TRIGGER "clients_hash" AFTER INSERT ON "chat_0_clients" BEGIN SELECT md5(new."clientID"); END'
1706
- const http = await startDoHttp((sql) => {
1707
- const compact = compactSQL(sql)
1708
- if (compact.includes("WHERE type = 'trigger'")) {
1709
- return {
1710
- rows: [{ name: 'clients_hash', sql: triggerSQL }],
1711
- columns: ['name', 'sql'],
1712
- }
1713
- }
1714
- if (sql.includes('sqlite_master')) {
1715
- return { rows: [{ ok: 1 }], columns: ['ok'] }
1716
- }
1717
- if (compact.includes('RETURNING *')) {
1718
- return {
1719
- rows: [{ clientGroupID: 'cg', clientID: 'client-a', lastMutationID: 1 }],
1720
- columns: ['clientGroupID', 'clientID', 'lastMutationID'],
1721
- }
1722
- }
1723
- return { rows: [], columns: [] }
1724
- })
1725
- const backend = new DoBackend(
1726
- http.url,
1727
- 'postgres',
1728
- 'extended-tx-trigger-restore-test'
1729
- )
1730
- await backend.waitReady
1731
-
1732
- await backend.execProtocolRaw(parseMessage('BEGIN'))
1733
- await backend.execProtocolRaw(bindStatement())
1734
- await backend.execProtocolRaw(executePortal())
1735
-
1736
- await backend.execProtocolRaw(
1737
- parseMessage(`
1738
- INSERT INTO "chat_0"."clients" AS current
1739
- ("clientGroupID", "clientID", "lastMutationID")
1740
- VALUES ('cg', 'client-a', 1)
1741
- ON CONFLICT ("clientGroupID", "clientID")
1742
- DO UPDATE SET "lastMutationID" = current."lastMutationID" + 1
1743
- RETURNING "lastMutationID"
1744
- `)
1745
- )
1746
- await backend.execProtocolRaw(bindStatement())
1747
- await backend.execProtocolRaw(executePortal())
1748
-
1749
- await backend.execProtocolRaw(parseMessage('ROLLBACK'))
1750
- await backend.execProtocolRaw(bindStatement())
1751
- await backend.execProtocolRaw(executePortal())
1752
-
1753
- const batch = http.bodies.find(
1754
- (body) =>
1755
- Array.isArray(body.statements) &&
1756
- body.statements.some((sql: string) =>
1757
- sql.includes('INSERT OR REPLACE INTO "chat_0_clients"')
1758
- )
1759
- )
1760
- expect(batch).toBeTruthy()
1761
- const statements = batch.statements.map(compactSQL)
1762
- const dropIndex = statements.indexOf('DROP TRIGGER IF EXISTS "clients_hash"')
1763
- const insertIndex = statements.findIndex((sql: string) =>
1764
- sql.includes('INSERT OR REPLACE INTO "chat_0_clients"')
1765
- )
1766
- const recreateIndex = statements.indexOf(triggerSQL)
1767
- expect(dropIndex).toBeGreaterThanOrEqual(0)
1768
- expect(insertIndex).toBeGreaterThan(dropIndex)
1769
- expect(recreateIndex).toBeGreaterThan(insertIndex)
1770
- })
1771
-
1772
- test('returns command completion for parser-skipped extended statements', async () => {
1773
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
1774
- const backend = new DoBackend(http.url, 'postgres', 'extended-noop-test')
1775
- await backend.waitReady
1776
-
1777
- await backend.execProtocolRaw(
1778
- parseMessage(`
1779
- CREATE OR REPLACE FUNCTION chat.set_permissions_hash()
1780
- RETURNS TRIGGER AS $$
1781
- BEGIN
1782
- RETURN NEW;
1783
- END;
1784
- $$ LANGUAGE plpgsql
1785
- `)
1786
- )
1787
-
1788
- expect(messageTypes(await backend.execProtocolRaw(describeStatement()))).toEqual([
1789
- 't',
1790
- 'n',
1791
- ])
1792
- await backend.execProtocolRaw(bindStatement())
1793
- const result = await backend.execProtocolRaw(executePortal())
1794
-
1795
- expect(messageTypes(result)).toEqual(['C'])
1796
- expect(http.sqls.some((sql) => sql.includes('CREATE OR REPLACE FUNCTION'))).toBe(
1797
- false
1798
- )
1799
- })
1800
-
1801
- test('rewrites DEFAULT values in inserts by omitting those columns', async () => {
1802
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
1803
- const backend = new DoBackend(http.url, 'postgres', 'insert-default-test')
1804
- await backend.waitReady
1805
-
1806
- await backend.exec(`
1807
- INSERT INTO reaction(id, value, keyword, "createdAt", "updatedAt")
1808
- VALUES ('1', 'wave', 'wave', DEFAULT, DEFAULT)
1809
- ON CONFLICT DO NOTHING;
1810
- `)
1811
-
1812
- const sent = compactSQL(http.sqls.at(-1) || '')
1813
- expect(sent).toContain('INSERT INTO reaction')
1814
- expect(sent).toContain('id')
1815
- expect(sent).toContain('value')
1816
- expect(sent).toContain('keyword')
1817
- expect(sent).toContain('ON CONFLICT DO NOTHING')
1818
- expect(sent).not.toContain('DEFAULT')
1819
- expect(sent).not.toContain('createdAt')
1820
- expect(sent).not.toContain('updatedAt')
1821
- })
1822
-
1823
- test('rewrites json_to_recordset range functions to SQLite json_each', async () => {
1824
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
1825
- const backend = new DoBackend(http.url, 'postgres', 'json-recordset-test')
1826
- await backend.waitReady
1827
-
1828
- await backend.query(
1829
- `
1830
- INSERT INTO "chat_0/cvr_queries" (
1831
- "clientGroupID",
1832
- "queryHash",
1833
- "clientAST",
1834
- "queryName",
1835
- "queryArgs",
1836
- "patchVersion",
1837
- "transformationHash",
1838
- "transformationVersion",
1839
- "internal",
1840
- "deleted"
1841
- )
1842
- SELECT
1843
- "clientGroupID",
1844
- "queryHash",
1845
- "clientAST",
1846
- "queryName",
1847
- CASE
1848
- WHEN "queryArgs" IS NULL THEN NULL
1849
- ELSE "queryArgs"::json
1850
- END,
1851
- "patchVersion",
1852
- "transformationHash",
1853
- "transformationVersion",
1854
- "internal",
1855
- "deleted"
1856
- FROM json_to_recordset($1) AS x(
1857
- "clientGroupID" TEXT,
1858
- "queryHash" TEXT,
1859
- "clientAST" JSONB,
1860
- "queryName" TEXT,
1861
- "queryArgs" TEXT,
1862
- "patchVersion" TEXT,
1863
- "transformationHash" TEXT,
1864
- "transformationVersion" TEXT,
1865
- "internal" BOOLEAN,
1866
- "deleted" BOOLEAN
1867
- )
1868
- ON CONFLICT ("clientGroupID", "queryHash") DO UPDATE SET
1869
- "clientAST" = excluded."clientAST",
1870
- "queryName" = excluded."queryName"
1871
- `,
1872
- [
1873
- JSON.stringify([
1874
- {
1875
- clientGroupID: 'cg1',
1876
- queryHash: 'hash1',
1877
- clientAST: { table: 'message' },
1878
- queryName: 'messages',
1879
- queryArgs: null,
1880
- patchVersion: '01',
1881
- transformationHash: 'th',
1882
- transformationVersion: 'tv',
1883
- internal: false,
1884
- deleted: false,
1885
- },
1886
- ]),
1887
- ]
1888
- )
1889
-
1890
- const sent = compactSQL(http.sqls.at(-1) || '')
1891
- expect(sent).toContain('FROM json_each(?)')
1892
- expect(sent).toContain(`json_extract(value, '$.clientGroupID') AS "clientGroupID"`)
1893
- expect(sent).toContain('WHERE 1 ON CONFLICT')
1894
- expect(sent).not.toContain('json_to_recordset')
1895
- })
1896
-
1897
- test('infers JSON parameter oid for json_to_recordset inputs', async () => {
1898
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
1899
- const backend = new DoBackend(http.url, 'postgres', 'json-recordset-oid-test')
1900
- await backend.waitReady
1901
-
1902
- await backend.execProtocolRaw(
1903
- parseMessage(
1904
- `
1905
- SELECT *
1906
- FROM json_to_recordset($1) AS x(
1907
- "clientAST" JSONB,
1908
- "deleted" BOOLEAN
1909
- )
1910
- `,
1911
- '',
1912
- [0]
1913
- )
1914
- )
1915
-
1916
- const describe = await backend.execProtocolRaw(describeStatement())
1917
-
1918
- expect(parameterDescriptionOids(describe)).toEqual([114])
1919
- })
1920
-
1921
- test('normalizes PostgreSQL array literal params used as JSON documents', async () => {
1922
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
1923
- const backend = new DoBackend(http.url, 'postgres', 'json-param-normalize-test')
1924
- await backend.waitReady
1925
-
1926
- await backend.query(
1927
- `
1928
- SELECT "clientGroupID"
1929
- FROM json_to_recordset($1) AS x("clientGroupID" TEXT)
1930
- `,
1931
- ['{"{\\"clientGroupID\\":\\"cg1\\"}"}']
1932
- )
1933
-
1934
- expect(compactSQL(http.sqls.at(-1) || '')).toContain('FROM json_each(?)')
1935
- expect(http.params.at(-1)).toEqual(['[{"clientGroupID":"cg1"}]'])
1936
- })
1937
-
1938
- test('rewrites Zero timestamp and row JSON helpers for SQLite', async () => {
1939
- const http = await startDoHttp(() => ({ rows: [], columns: ['zql_result'] }))
1940
- const backend = new DoBackend(http.url, 'postgres', 'zql-helper-rewrite-test')
1941
- await backend.waitReady
1942
-
1943
- await backend.query(`
1944
- SELECT row_to_json(zql_root) AS zql_result
1945
- FROM (
1946
- SELECT
1947
- "userPublic_0".id AS id,
1948
- EXTRACT(EPOCH FROM "userPublic_0"."joinedAt") * 1000 AS "joinedAt"
1949
- FROM "userPublic" AS "userPublic_0"
1950
- ) zql_root
1951
- `)
1952
-
1953
- const select = compactSQL(http.sqls.at(-1) || '')
1954
- expect(select).toContain(`"json_object"('id'`)
1955
- expect(select).toContain(`zql_root.id`)
1956
- expect(select).toContain(`'joinedAt'`)
1957
- expect(select).toContain(`zql_root."joinedAt"`)
1958
- expect(select).toContain(`strftime('%s', "userPublic_0"."joinedAt") * 1000`)
1959
- expect(select).not.toContain('row_to_json')
1960
- expect(select).not.toContain('EXTRACT')
1961
-
1962
- await backend.query(
1963
- `
1964
- INSERT INTO "userPublic" ("joinedAt")
1965
- VALUES (to_timestamp($1::text::numeric / 1000.0) AT TIME ZONE 'UTC')
1966
- `,
1967
- [123000]
1968
- )
1969
-
1970
- const insert = compactSQL(http.sqls.at(-1) || '')
1971
- expect(insert).toContain(`datetime(? / 1000.0, 'unixepoch')`)
1972
- expect(insert).not.toContain('to_timestamp')
1973
- expect(insert).not.toContain('AT TIME ZONE')
1974
- })
1975
-
1976
- test('respects explicit text casts on JSON result expressions', async () => {
1977
- const http = await startDoHttp(() => ({
1978
- rows: [{ zql_result: [{ id: 'u1' }] }],
1979
- columns: ['zql_result'],
1980
- }))
1981
- const backend = new DoBackend(http.url, 'postgres', 'zql-text-result-test')
1982
- await backend.waitReady
1983
-
1984
- await backend.execProtocolRaw(
1985
- parseMessage(`
1986
- SELECT COALESCE(json_agg(row_to_json(zql_root)), '[]'::json)::text AS zql_result
1987
- FROM (
1988
- SELECT id
1989
- FROM "userPublic"
1990
- ) zql_root
1991
- `)
1992
- )
1993
- await backend.execProtocolRaw(bindStatement())
1994
- const result = await backend.execProtocolRaw(executePortal())
1995
-
1996
- expect(rowDescriptionOids(result)).toMatchObject({ zql_result: 25 })
1997
- expect(dataRowValues(result)).toEqual([[`[{"id":"u1"}]`]])
1998
- })
1999
-
2000
- test('flushes simple-protocol transaction writes before extended statements', async () => {
2001
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2002
- const backend = new DoBackend(http.url, 'postgres', 'mixed-protocol-test')
2003
- await backend.waitReady
2004
-
2005
- await backend.execProtocolRaw(msg(0x51, cstr('BEGIN')))
2006
- await backend.execProtocolRaw(
2007
- msg(0x51, cstr('CREATE TABLE reaction (id TEXT PRIMARY KEY)'))
2008
- )
2009
- await backend.execProtocolRaw(parseMessage("INSERT INTO reaction(id) VALUES ('1')"))
2010
- await backend.execProtocolRaw(bindStatement())
2011
- await backend.execProtocolRaw(executePortal())
2012
-
2013
- const createIndex = http.sqls.findIndex((sql) =>
2014
- /create\s+table\s+(if\s+not\s+exists\s+)?reaction/i.test(sql)
2015
- )
2016
- const insertIndex = http.sqls.findIndex((sql) =>
2017
- /insert\s+into\s+reaction/i.test(sql)
2018
- )
2019
- expect(createIndex).toBeGreaterThanOrEqual(0)
2020
- expect(insertIndex).toBeGreaterThan(createIndex)
2021
- await backend.execProtocolRaw(msg(0x51, cstr('ROLLBACK')))
2022
- })
2023
-
2024
- test('sends extended-protocol params as bound DO parameters', async () => {
2025
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2026
- const backend = new DoBackend(http.url, 'postgres', 'param-inline-test')
2027
- await backend.waitReady
2028
-
2029
- await backend.execProtocolRaw(
2030
- parseMessage('INSERT INTO docs(id, content) VALUES ($1, $2)')
2031
- )
2032
- await backend.execProtocolRaw(
2033
- bindStatementParams(['doc_start-doc/intro', "body keeps $1 and 'quote'"])
2034
- )
2035
- await backend.execProtocolRaw(executePortal())
2036
-
2037
- const sent = compactSQL(http.sqls.at(-1) || '')
2038
- expect(sent).toContain('VALUES ( ?, ? )')
2039
- expect(http.params.at(-1)).toEqual([
2040
- 'doc_start-doc/intro',
2041
- "body keeps $1 and 'quote'",
2042
- ])
2043
- expect(sent).not.toContain("body keeps 'doc_start-doc/intro'")
2044
- })
2045
-
2046
- test('rewrites PG ALL array comparisons to SQLite json_each subqueries', async () => {
2047
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2048
- const backend = new DoBackend(http.url, 'postgres', 'all-array-test')
2049
- await backend.waitReady
2050
-
2051
- await backend.execProtocolRaw(
2052
- parseMessage(
2053
- `DELETE FROM search_documents
2054
- WHERE id LIKE 'doc_start-doc/%'
2055
- AND type = 'doc'
2056
- AND id != ALL($1)`
2057
- )
2058
- )
2059
- await backend.execProtocolRaw(
2060
- bindStatementParams(['{"doc_start-doc/intro","doc_start-doc/api"}'])
2061
- )
2062
- await backend.execProtocolRaw(executePortal())
2063
-
2064
- const sent = compactSQL(http.sqls.at(-1) || '')
2065
- expect(sent).toContain('NOT (id IN (SELECT value FROM json_each')
2066
- expect(sent).toContain('json_each(?)')
2067
- expect(http.params.at(-1)).toEqual(['["doc_start-doc/intro","doc_start-doc/api"]'])
2068
- expect(sent).not.toContain('ALL')
2069
- })
2070
-
2071
- test('rewrites JSONB array element filters to SQLite json_each', async () => {
2072
- const http = await startDoHttp(() => ({ rows: [], columns: ['value'] }))
2073
- const backend = new DoBackend(http.url, 'postgres', 'jsonb-array-elements-test')
2074
- await backend.waitReady
2075
-
2076
- await backend.execProtocolRaw(
2077
- parseMessage(
2078
- `SELECT value
2079
- FROM jsonb_array_elements_text($1::text::jsonb)`
2080
- )
2081
- )
2082
- await backend.execProtocolRaw(bindStatementParams(['{"data","chat"}']))
2083
- await backend.execProtocolRaw(executePortal())
2084
-
2085
- const sent = compactSQL(http.sqls.at(-1) || '')
2086
- expect(sent).toContain('FROM json_each(?)')
2087
- expect(http.params.at(-1)).toEqual(['["data","chat"]'])
2088
- expect(sent).not.toContain('jsonb_array_elements_text')
2089
- })
2090
-
2091
- test('collapses json_each over ARRAY subqueries from plural filters', async () => {
2092
- const http = await startDoHttp(() => ({ rows: [], columns: ['id'] }))
2093
- const backend = new DoBackend(http.url, 'postgres', 'jsonb-array-filter-test')
2094
- await backend.waitReady
2095
-
2096
- await backend.execProtocolRaw(
2097
- parseMessage(
2098
- `SELECT id
2099
- FROM app
2100
- WHERE id IN (
2101
- SELECT value
2102
- FROM jsonb_array_elements_text(
2103
- ARRAY(
2104
- SELECT value::text
2105
- FROM jsonb_array_elements_text($1::text::jsonb)
2106
- )
2107
- )
2108
- )`
2109
- )
2110
- )
2111
- await backend.execProtocolRaw(bindStatementParams(['{"data","chat"}']))
2112
- await backend.execProtocolRaw(executePortal())
2113
-
2114
- const sent = compactSQL(http.sqls.at(-1) || '')
2115
- expect(sent).toContain('IN (SELECT value FROM json_each(?))')
2116
- expect(sent).not.toContain('ARRAY')
2117
- expect(http.params.at(-1)).toEqual(['["data","chat"]'])
2118
- })
2119
-
2120
- test('rewrites PG array column declarations to SQLite text columns', async () => {
2121
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2122
- const backend = new DoBackend(http.url, 'postgres', 'array-column-test')
2123
- await backend.waitReady
2124
-
2125
- await backend.exec(`
2126
- CREATE TABLE "chat_0"."shardConfig" (
2127
- "publications" TEXT[] NOT NULL,
2128
- "ddlDetection" BOOL NOT NULL
2129
- );
2130
- `)
2131
-
2132
- const sent = compactSQL(
2133
- sqlContaining(http.sqls, 'CREATE TABLE IF NOT EXISTS "chat_0_shardConfig"')
2134
- )
2135
- expect(sent).toContain('CREATE TABLE IF NOT EXISTS "chat_0_shardConfig"')
2136
- expect(sent).toContain('publications text NOT NULL')
2137
- expect(sent).not.toContain('text[]')
2138
- })
2139
-
2140
- test('rewrites PG array constructors to JSON text literals', async () => {
2141
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2142
- const backend = new DoBackend(http.url, 'postgres', 'array-constructor-test')
2143
- await backend.waitReady
2144
-
2145
- await backend.exec(`
2146
- INSERT INTO "chat_0"."shardConfig" ("publications", "ddlDetection")
2147
- VALUES (ARRAY['zero_chat', '_zero_metadata'], false);
2148
- `)
2149
-
2150
- const sent = compactSQL(http.sqls.at(-1) || '')
2151
- expect(sent).toContain(`'["zero_chat","_zero_metadata"]'`)
2152
- expect(sent).not.toContain('ARRAY')
2153
- })
2154
-
2155
- test('rewrites PG sequences to readable SQLite sequence tables', async () => {
2156
- const http = await startDoHttp((sql) => {
2157
- if (compactSQL(sql).startsWith('SELECT')) {
2158
- return {
2159
- rows: [{ last_value: 1, is_called: 0 }],
2160
- columns: ['last_value', 'is_called'],
2161
- }
2162
- }
2163
- return { rows: [], columns: [] }
2164
- })
2165
- const backend = new DoBackend(http.url, 'postgres', 'sequence-table-test')
2166
- await backend.waitReady
2167
-
2168
- await backend.exec('CREATE SEQUENCE IF NOT EXISTS _orez._zero_watermark')
2169
- const result = await backend.execProtocolRaw(
2170
- msg(0x51, cstr('SELECT last_value, is_called FROM _orez._zero_watermark'))
2171
- )
2172
-
2173
- const sent = compactSQL(http.sqls.join('; '))
2174
- expect(sent).toContain('CREATE TABLE IF NOT EXISTS "_orez___zero_watermark"')
2175
- expect(sent).toContain('INSERT OR IGNORE INTO "_orez___zero_watermark"')
2176
- expect(rowDescriptionOids(result)).toMatchObject({
2177
- last_value: 20,
2178
- is_called: 16,
2179
- })
2180
- expect(dataRowValues(result)).toEqual([['1', 'f']])
2181
- })
2182
-
2183
- test('sends high-level query params as bound DO parameters', async () => {
2184
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2185
- const backend = new DoBackend(http.url, 'postgres', 'query-param-test')
2186
- await backend.waitReady
2187
-
2188
- await backend.query(
2189
- `INSERT INTO _orez._zero_replication_slots (
2190
- slot_name,
2191
- restart_lsn,
2192
- confirmed_flush_lsn
2193
- ) VALUES ($1, $2, $3)`,
2194
- ['slot_1', '0/16B6C50', '0/16B6C50']
2195
- )
2196
-
2197
- const sent = compactSQL(http.sqls.at(-1) || '')
2198
- expect(sent).toContain('VALUES ( ?, ?, ? )')
2199
- expect(sent).not.toContain('$1')
2200
- expect(http.params.at(-1)).toEqual(['slot_1', '0/16B6C50', '0/16B6C50'])
2201
- })
2202
-
2203
- test('returns JSON and boolean type metadata for rewritten SQLite rows', async () => {
2204
- const http = await startDoHttp((sql) => {
2205
- if (compactSQL(sql).startsWith('SELECT')) {
2206
- return {
2207
- rows: [
2208
- {
2209
- publications: '["_chat_metadata_0","zero_chat"]',
2210
- ddlDetection: 0,
2211
- },
2212
- ],
2213
- columns: ['publications', 'ddlDetection'],
2214
- }
2215
- }
2216
- return { rows: [], columns: [] }
2217
- })
2218
- const backend = new DoBackend(http.url, 'postgres', 'json-field-oid-test')
2219
- await backend.waitReady
2220
-
2221
- await backend.exec(`
2222
- CREATE TABLE "_zero"."shardConfig" (
2223
- "publications" TEXT[] NOT NULL,
2224
- "ddlDetection" BOOL NOT NULL
2225
- );
2226
- `)
2227
-
2228
- const result = await backend.execProtocolRaw(
2229
- msg(0x51, cstr('SELECT "publications", "ddlDetection" FROM "_zero_shardConfig"'))
2230
- )
2231
-
2232
- expect(messageTypes(result)).toEqual(['T', 'D', 'C', 'Z'])
2233
- expect(rowDescriptionOids(result)).toMatchObject({
2234
- publications: 114,
2235
- ddlDetection: 16,
2236
- })
2237
- expect(dataRowValues(result)).toEqual([['["_chat_metadata_0","zero_chat"]', 'f']])
2238
- })
2239
-
2240
- test('falls back to zero-cache metadata column types when durable metadata is absent', async () => {
2241
- const http = await startDoHttp((sql) => {
2242
- const compact = compactSQL(sql)
2243
- if (
2244
- compact.includes('_orez_pg_metadata') ||
2245
- compact.includes('sqlite_master') ||
2246
- compact.startsWith('PRAGMA')
2247
- ) {
2248
- return { rows: [], columns: [] }
2249
- }
2250
- if (compact.startsWith('SELECT')) {
2251
- return {
2252
- rows: [
2253
- {
2254
- slot: 'slot_1',
2255
- version: '01',
2256
- publications: '["_chat_metadata_0","zero_chat"]',
2257
- ddlDetection: 1,
2258
- },
2259
- ],
2260
- columns: ['slot', 'version', 'publications', 'ddlDetection'],
2261
- }
2262
- }
2263
- return { rows: [], columns: [] }
2264
- })
2265
- const backend = new DoBackend(http.url, 'postgres', 'missing-metadata-oid-test')
2266
- await backend.waitReady
2267
-
2268
- const result = await backend.execProtocolRaw(
2269
- msg(
2270
- 0x51,
2271
- cstr(`
2272
- SELECT * FROM "chat_0".replicas
2273
- JOIN "chat_0"."shardConfig" ON true
2274
- WHERE version = '01'
2275
- `)
2276
- )
2277
- )
2278
-
2279
- expect(rowDescriptionOids(result)).toMatchObject({
2280
- publications: 114,
2281
- ddlDetection: 16,
2282
- })
2283
- expect(dataRowValues(result)).toEqual([
2284
- ['slot_1', '01', '["_chat_metadata_0","zero_chat"]', 't'],
2285
- ])
2286
- })
2287
-
2288
- test('resolves JSON metadata for columns from joined tables', async () => {
2289
- const http = await startDoHttp((sql) => {
2290
- if (compactSQL(sql).startsWith('SELECT')) {
2291
- return {
2292
- rows: [
2293
- {
2294
- slot: 'slot_1',
2295
- version: '01',
2296
- publications: '["_chat_metadata_0","zero_chat"]',
2297
- ddlDetection: 1,
2298
- },
2299
- ],
2300
- columns: ['slot', 'version', 'publications', 'ddlDetection'],
2301
- }
2302
- }
2303
- return { rows: [], columns: [] }
2304
- })
2305
- const backend = new DoBackend(http.url, 'postgres', 'join-field-oid-test')
2306
- await backend.waitReady
2307
-
2308
- await backend.exec(`
2309
- CREATE TABLE "chat_0".replicas (
2310
- "slot" text PRIMARY KEY,
2311
- "version" text NOT NULL
2312
- );
2313
- CREATE TABLE "chat_0"."shardConfig" (
2314
- "publications" TEXT[] NOT NULL,
2315
- "ddlDetection" BOOL NOT NULL
2316
- );
2317
- CREATE TABLE "chat_0/cdc"."replicationConfig" (
2318
- "publications" text NOT NULL
2319
- );
2320
- `)
2321
-
2322
- const result = await backend.execProtocolRaw(
2323
- msg(
2324
- 0x51,
2325
- cstr(`
2326
- SELECT * FROM "chat_0".replicas
2327
- JOIN "chat_0"."shardConfig" ON true
2328
- WHERE version = '01'
2329
- `)
2330
- )
2331
- )
2332
-
2333
- expect(rowDescriptionOids(result)).toMatchObject({
2334
- publications: 114,
2335
- ddlDetection: 16,
2336
- })
2337
- expect(dataRowValues(result)).toEqual([
2338
- ['slot_1', '01', '["_chat_metadata_0","zero_chat"]', 't'],
2339
- ])
2340
-
2341
- await backend.execProtocolRaw(
2342
- parseMessage(
2343
- `
2344
- SELECT * FROM "chat_0".replicas
2345
- JOIN "chat_0"."shardConfig" ON true
2346
- WHERE version = $1
2347
- `,
2348
- 'replica-at-version',
2349
- [25]
2350
- )
2351
- )
2352
- const described = await backend.execProtocolRaw(
2353
- describeStatement('replica-at-version')
2354
- )
2355
- expect(rowDescriptionOids(described)).toMatchObject({
2356
- publications: 114,
2357
- ddlDetection: 16,
2358
- })
2359
- })
2360
-
2361
- test('hydrates persisted PG column metadata for existing DO tables', async () => {
2362
- const metadataRows: Record<string, unknown>[] = []
2363
- const http = await startDoHttp((sql) => {
2364
- const compact = compactSQL(sql)
2365
- if (compact.startsWith('CREATE TABLE IF NOT EXISTS "_orez_pg_metadata"')) {
2366
- return { rows: [], columns: [] }
2367
- }
2368
- if (compact.startsWith('SELECT kind, key, subkey, value FROM')) {
2369
- return {
2370
- rows: metadataRows,
2371
- columns: ['kind', 'key', 'subkey', 'value'],
2372
- }
2373
- }
2374
- if (compact.startsWith('DELETE FROM "_orez_pg_metadata"')) {
2375
- metadataRows.length = 0
2376
- return { rows: [], columns: [] }
2377
- }
2378
- if (compact.startsWith('INSERT OR REPLACE INTO "_orez_pg_metadata"')) {
2379
- return { rows: [], columns: [] }
2380
- }
2381
- return { rows: [], columns: [] }
2382
- })
2383
-
2384
- const first = new DoBackend(http.url, 'postgres', 'durable-metadata-test')
2385
- await first.waitReady
2386
- await first.exec(`
2387
- CREATE TABLE "chat_0".replicas (
2388
- "slot" text PRIMARY KEY,
2389
- "version" text NOT NULL
2390
- );
2391
- CREATE TABLE "chat_0"."shardConfig" (
2392
- "publications" TEXT[] NOT NULL,
2393
- "ddlDetection" BOOL NOT NULL
2394
- );
2395
- `)
2396
-
2397
- for (const body of http.bodies) {
2398
- if (
2399
- typeof body.sql === 'string' &&
2400
- compactSQL(body.sql).startsWith('INSERT OR REPLACE INTO "_orez_pg_metadata"')
2401
- ) {
2402
- const [kind, key, subkey, value] = body.params
2403
- metadataRows.push({ kind, key, subkey, value })
2404
- }
2405
- }
2406
-
2407
- const second = new DoBackend(http.url, 'postgres', 'durable-metadata-test')
2408
- await second.waitReady
2409
- await second.execProtocolRaw(
2410
- parseMessage(
2411
- `
2412
- SELECT * FROM "chat_0".replicas
2413
- JOIN "chat_0"."shardConfig" ON true
2414
- WHERE version = $1
2415
- `,
2416
- 'replica-at-version',
2417
- [25]
2418
- )
2419
- )
2420
- const described = await second.execProtocolRaw(
2421
- describeStatement('replica-at-version')
2422
- )
2423
-
2424
- expect(rowDescriptionOids(described)).toMatchObject({
2425
- publications: 114,
2426
- ddlDetection: 16,
2427
- })
2428
- })
2429
-
2430
- test('repairs internal metadata publication from shardConfig rows', async () => {
2431
- const http = await startDoHttp((sql) => {
2432
- const compact = compactSQL(sql)
2433
- if (compact.startsWith('CREATE TABLE IF NOT EXISTS "_orez_pg_metadata"')) {
2434
- return { rows: [], columns: [] }
2435
- }
2436
- if (compact.startsWith('SELECT kind, key, subkey, value FROM')) {
2437
- return { rows: [], columns: ['kind', 'key', 'subkey', 'value'] }
2438
- }
2439
- if (compact.startsWith('DELETE FROM "_orez_pg_metadata"')) {
2440
- return { rows: [], columns: [] }
2441
- }
2442
- if (compact.startsWith('INSERT OR REPLACE INTO "_orez_pg_metadata"')) {
2443
- return { rows: [], columns: [] }
2444
- }
2445
- if (compact.includes('sqlite_master')) {
2446
- return {
2447
- rows: [
2448
- {
2449
- name: 'todo_permissions',
2450
- sql: 'CREATE TABLE todo_permissions (permissions text, hash text)',
2451
- },
2452
- {
2453
- name: 'todo_0_clients',
2454
- sql: 'CREATE TABLE todo_0_clients (clientGroupID text, clientID text)',
2455
- },
2456
- {
2457
- name: 'todo_0_mutations',
2458
- sql: 'CREATE TABLE todo_0_mutations (clientGroupID text, mutation text)',
2459
- },
2460
- {
2461
- name: 'todo_0_shardConfig',
2462
- sql: 'CREATE TABLE todo_0_shardConfig (publications text)',
2463
- },
2464
- ],
2465
- columns: ['name', 'sql'],
2466
- }
2467
- }
2468
- if (compact === 'SELECT publications FROM "todo_0_shardConfig" LIMIT 1') {
2469
- return {
2470
- rows: [{ publications: '["_todo_metadata_0","zero_todo"]' }],
2471
- columns: ['publications'],
2472
- }
2473
- }
2474
- if (compact.includes('PRAGMA table_info("todo_permissions")')) {
2475
- return {
2476
- rows: [
2477
- { cid: 0, name: 'permissions', type: 'text', notnull: 0, pk: 0 },
2478
- { cid: 1, name: 'hash', type: 'text', notnull: 0, pk: 0 },
2479
- ],
2480
- columns: ['cid', 'name', 'type', 'notnull', 'pk'],
2481
- }
2482
- }
2483
- if (compact.includes('PRAGMA table_info("todo_0_clients")')) {
2484
- return {
2485
- rows: [
2486
- { cid: 0, name: 'clientGroupID', type: 'text', notnull: 1, pk: 1 },
2487
- { cid: 1, name: 'clientID', type: 'text', notnull: 1, pk: 2 },
2488
- ],
2489
- columns: ['cid', 'name', 'type', 'notnull', 'pk'],
2490
- }
2491
- }
2492
- if (compact.includes('PRAGMA table_info("todo_0_mutations")')) {
2493
- return {
2494
- rows: [
2495
- { cid: 0, name: 'clientGroupID', type: 'text', notnull: 1, pk: 1 },
2496
- { cid: 1, name: 'mutation', type: 'text', notnull: 1, pk: 0 },
2497
- ],
2498
- columns: ['cid', 'name', 'type', 'notnull', 'pk'],
2499
- }
2500
- }
2501
- return { rows: [], columns: [] }
2502
- })
2503
- const backend = new DoBackend(http.url, 'postgres', 'metadata-publication-test')
2504
- await backend.waitReady
2505
-
2506
- expect(
2507
- http.bodies.some(
2508
- (body) =>
2509
- body.params?.[0] === 'publication' && body.params?.[1] === '_todo_metadata_0'
2510
- )
2511
- ).toBe(true)
2512
-
2513
- const publications = await (backend as any).handleCatalogQuery(`
2514
- SELECT pubname FROM pg_publication
2515
- WHERE pubname IN ('_todo_metadata_0', 'zero_todo')
2516
- `)
2517
- expect(publications.rows).toEqual([{ pubname: '_todo_metadata_0' }])
2518
-
2519
- const publicationTables = await (backend as any).handleCatalogQuery(`
2520
- SELECT pubname, schemaname, tablename
2521
- FROM pg_publication_tables
2522
- WHERE pubname IN ('_todo_metadata_0')
2523
- `)
2524
- expect(publicationTables.rows).toEqual(
2525
- expect.arrayContaining([
2526
- {
2527
- pubname: '_todo_metadata_0',
2528
- schemaname: 'todo',
2529
- tablename: 'permissions',
2530
- },
2531
- {
2532
- pubname: '_todo_metadata_0',
2533
- schemaname: 'todo_0',
2534
- tablename: 'clients',
2535
- },
2536
- {
2537
- pubname: '_todo_metadata_0',
2538
- schemaname: 'todo_0',
2539
- tablename: 'mutations',
2540
- },
2541
- ])
2542
- )
2543
- expect(publicationTables.rows).toHaveLength(3)
2544
- })
2545
-
2546
- test('repairing internal metadata publication preserves app publications', async () => {
2547
- const metadataRows: Record<string, unknown>[] = [
2548
- {
2549
- kind: 'publication',
2550
- key: 'zero_todo',
2551
- subkey: '',
2552
- value: JSON.stringify({
2553
- name: 'zero_todo',
2554
- allTables: false,
2555
- schemas: [],
2556
- tables: [['todo', { table: 'todo', schema: 'public', tableName: 'todo' }]],
2557
- }),
2558
- },
2559
- ]
2560
- let http: Awaited<ReturnType<typeof startDoHttp>>
2561
- http = await startDoHttp((sql) => {
2562
- const compact = compactSQL(sql)
2563
- if (compact.startsWith('CREATE TABLE IF NOT EXISTS "_orez_pg_metadata"')) {
2564
- return { rows: [], columns: [] }
2565
- }
2566
- if (compact.startsWith('SELECT kind, key, subkey, value FROM')) {
2567
- return {
2568
- rows: metadataRows,
2569
- columns: ['kind', 'key', 'subkey', 'value'],
2570
- }
2571
- }
2572
- if (compact.startsWith('INSERT OR REPLACE INTO "_orez_pg_metadata"')) {
2573
- const params = http.bodies.at(-1)?.params ?? []
2574
- const [kind, key, subkey, value] =
2575
- params.length === 3 ? [params[0], params[1], '', params[2]] : params
2576
- const existing = metadataRows.findIndex(
2577
- (row) => row.kind === kind && row.key === key && row.subkey === subkey
2578
- )
2579
- const row = { kind, key, subkey, value }
2580
- if (existing >= 0) metadataRows[existing] = row
2581
- else metadataRows.push(row)
2582
- return { rows: [], columns: [] }
2583
- }
2584
- if (compact.includes('sqlite_master')) {
2585
- return {
2586
- rows: [
2587
- {
2588
- name: 'todo_0_shardConfig',
2589
- sql: 'CREATE TABLE todo_0_shardConfig (publications text)',
2590
- },
2591
- ],
2592
- columns: ['name', 'sql'],
2593
- }
2594
- }
2595
- if (compact === 'SELECT publications FROM "todo_0_shardConfig" LIMIT 1') {
2596
- return {
2597
- rows: [{ publications: '["_todo_metadata_0","zero_todo"]' }],
2598
- columns: ['publications'],
2599
- }
2600
- }
2601
- return { rows: [], columns: [] }
2602
- })
2603
- const backend = new DoBackend(http.url, 'postgres', 'metadata-merge-test')
2604
- await backend.waitReady
2605
-
2606
- const publications = await (backend as any).handleCatalogQuery(`
2607
- SELECT pubname FROM pg_publication
2608
- WHERE pubname IN ('_todo_metadata_0', 'zero_todo')
2609
- ORDER BY pubname
2610
- `)
2611
- expect(publications.rows).toEqual([
2612
- { pubname: '_todo_metadata_0' },
2613
- { pubname: 'zero_todo' },
2614
- ])
2615
- expect(metadataRows.map((row) => row.key).sort()).toEqual([
2616
- '_todo_metadata_0',
2617
- 'zero_todo',
2618
- ])
2619
- })
2620
-
2621
- test('infers JSON parameter oids from parsed insert target columns', async () => {
2622
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2623
- const backend = new DoBackend(http.url, 'postgres', 'json-param-oid-test')
2624
- await backend.waitReady
2625
-
2626
- await backend.exec(`
2627
- CREATE TABLE "chat_0".replicas (
2628
- "slot" text PRIMARY KEY,
2629
- "version" text NOT NULL,
2630
- "initialSchema" JSON NOT NULL,
2631
- "initialSyncContext" JSON
2632
- )
2633
- `)
2634
-
2635
- await backend.execProtocolRaw(
2636
- parseMessage(
2637
- `INSERT INTO "chat_0".replicas
2638
- ("slot", "version", "initialSchema", "initialSyncContext")
2639
- VALUES ($1, $2, $3, $4)`,
2640
- '',
2641
- [0, 0, 0, 0]
2642
- )
2643
- )
2644
- const describe = await backend.execProtocolRaw(describeStatement())
2645
-
2646
- expect(parameterDescriptionOids(describe)).toEqual([25, 25, 114, 114])
2647
-
2648
- await backend.execProtocolRaw(
2649
- bindStatementParams(
2650
- [
2651
- 'slot_1',
2652
- '0/16B6C50',
2653
- '{"tables":[{"name":"message"}],"indexes":[]}',
2654
- '{"requestID":"req_1"}',
2655
- ],
2656
- '',
2657
- 'replica_insert'
2658
- )
2659
- )
2660
- await backend.execProtocolRaw(executePortal('replica_insert'))
2661
-
2662
- expect(compactSQL(http.sqls.at(-1) || '')).toContain('VALUES ( ?, ?, ?, ? )')
2663
- expect(http.params.at(-1)).toEqual([
2664
- 'slot_1',
2665
- '0/16B6C50',
2666
- '{"tables":[{"name":"message"}],"indexes":[]}',
2667
- '{"requestID":"req_1"}',
2668
- ])
2669
- })
2670
-
2671
- test('infers zero-cache JSON parameter oids without durable metadata', async () => {
2672
- const http = await startDoHttp((sql) => {
2673
- const compact = compactSQL(sql)
2674
- if (
2675
- compact.includes('_orez_pg_metadata') ||
2676
- compact.includes('sqlite_master') ||
2677
- compact.startsWith('PRAGMA')
2678
- ) {
2679
- return { rows: [], columns: [] }
2680
- }
2681
- return { rows: [], columns: [] }
2682
- })
2683
- const backend = new DoBackend(http.url, 'postgres', 'json-param-fallback-test')
2684
- await backend.waitReady
2685
-
2686
- await backend.execProtocolRaw(
2687
- parseMessage(
2688
- `INSERT INTO "chat_0".replicas
2689
- ("slot", "version", "initialSchema", "initialSyncContext")
2690
- VALUES ($1, $2, $3, $4)`,
2691
- 'replica-insert-no-metadata',
2692
- [0, 0, 0, 0]
2693
- )
2694
- )
2695
- let describe = await backend.execProtocolRaw(
2696
- describeStatement('replica-insert-no-metadata')
2697
- )
2698
- expect(parameterDescriptionOids(describe)).toEqual([0, 0, 114, 114])
2699
-
2700
- await backend.execProtocolRaw(
2701
- parseMessage(
2702
- `UPDATE "chat_0".replicas
2703
- SET "subscriberContext" = $1
2704
- WHERE slot = $2`,
2705
- 'replica-update-no-metadata',
2706
- [0, 0]
2707
- )
2708
- )
2709
- describe = await backend.execProtocolRaw(
2710
- describeStatement('replica-update-no-metadata')
2711
- )
2712
- expect(parameterDescriptionOids(describe)).toEqual([114, 0])
2713
- })
2714
-
2715
- test('infers fallback insert params for ON CONFLICT without durable metadata', async () => {
2716
- const http = await startDoHttp((sql) => {
2717
- const compact = compactSQL(sql)
2718
- if (
2719
- compact.includes('_orez_pg_metadata') ||
2720
- compact.includes('sqlite_master') ||
2721
- compact.startsWith('PRAGMA')
2722
- ) {
2723
- return { rows: [], columns: [] }
2724
- }
2725
- return { rows: [], columns: [] }
2726
- })
2727
- const backend = new DoBackend(http.url, 'postgres', 'json-upsert-no-metadata')
2728
- await backend.waitReady
2729
-
2730
- await backend.execProtocolRaw(
2731
- parseMessage(
2732
- `INSERT INTO "chat_0".replicas
2733
- ("slot", "version", "initialSchema", "initialSyncContext")
2734
- VALUES ($1, $2, $3, $4)
2735
- ON CONFLICT ("slot") DO UPDATE SET "initialSchema" = $5`,
2736
- 'replica-upsert-no-metadata',
2737
- [0, 0, 0, 0, 0]
2738
- )
2739
- )
2740
- const describe = await backend.execProtocolRaw(
2741
- describeStatement('replica-upsert-no-metadata')
2742
- )
2743
-
2744
- expect(parameterDescriptionOids(describe)).toEqual([0, 0, 114, 114, 114])
2745
- })
2746
-
2747
- test('splits Drizzle statement-breakpoint batches and drops PG constraint alters', async () => {
2748
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2749
- const backend = new DoBackend(http.url, 'postgres', 'statement-batch-test')
2750
- await backend.waitReady
2751
-
2752
- await backend.exec(`
2753
- CREATE TABLE "parent" ("id" text PRIMARY KEY);
2754
- --> statement-breakpoint
2755
- ALTER TABLE "child" ADD CONSTRAINT "child_parent_fk" FOREIGN KEY ("parentId") REFERENCES "public"."parent"("id");
2756
- --> statement-breakpoint
2757
- ALTER TABLE "child" ADD PRIMARY KEY("id");
2758
- --> statement-breakpoint
2759
- CREATE TABLE "child" ("id" text PRIMARY KEY, "parentId" text);
2760
- `)
2761
-
2762
- const sent = http.sqls.join('; ')
2763
- expect(sent).toContain('CREATE TABLE IF NOT EXISTS parent')
2764
- expect(sent).toContain('CREATE TABLE IF NOT EXISTS child')
2765
- expect(sent).not.toContain('ADD CONSTRAINT')
2766
- expect(sent).not.toContain('statement-breakpoint')
2767
- })
2768
-
2769
- test('splits semicolon batches inside Drizzle breakpoint chunks', async () => {
2770
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2771
- const backend = new DoBackend(http.url, 'postgres', 'semicolon-batch-test')
2772
- await backend.waitReady
2773
-
2774
- await backend.exec(`
2775
- ALTER TABLE "serverApp" DROP COLUMN "id";
2776
- ALTER TABLE "serverApp" ADD CONSTRAINT "serverApp_pk" PRIMARY KEY("serverId","creatorId");
2777
- --> statement-breakpoint
2778
- INSERT INTO "log" ("message") VALUES ('keeps ; inside strings');
2779
- `)
2780
-
2781
- const sent = http.sqls.at(-1) || ''
2782
- const compact = compactSQL(sent)
2783
- expect(compact).toContain('ALTER TABLE "serverApp" DROP COLUMN id')
2784
- expect(compact).toContain(
2785
- `INSERT INTO log ( message ) VALUES ( 'keeps ; inside strings' )`
2786
- )
2787
- expect(sent).not.toContain('ADD CONSTRAINT')
2788
- expect(sent).not.toContain('PRIMARY KEY("serverId"')
2789
- })
2790
-
2791
- test('rewrites btree indexes and drops unsupported PG index methods', async () => {
2792
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2793
- const backend = new DoBackend(http.url, 'postgres', 'index-rewrite-test')
2794
- await backend.waitReady
2795
-
2796
- await backend.exec(`
2797
- CREATE INDEX "idx_message_channel" ON "message" USING btree ("channelId","order");
2798
- --> statement-breakpoint
2799
- CREATE INDEX "idx_message_search" ON "message" USING gin ("content" gin_trgm_ops);
2800
- `)
2801
-
2802
- const sent = http.sqls.at(-1) || ''
2803
- expect(compactSQL(sent)).toContain(
2804
- 'CREATE INDEX IF NOT EXISTS idx_message_channel ON message ("channelId", "order")'
2805
- )
2806
- expect(sent).not.toContain('USING')
2807
- expect(sent).not.toContain('idx_message_search')
2808
- })
2809
-
2810
- test('drops PG null ordering from index elements', async () => {
2811
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2812
- const backend = new DoBackend(http.url, 'postgres', 'index-nulls-order-test')
2813
- await backend.waitReady
2814
-
2815
- await backend.exec(`
2816
- CREATE INDEX queries_patch_version
2817
- ON "chat_0/cvr".queries ("patchVersion" NULLS FIRST);
2818
- `)
2819
-
2820
- const sent = compactSQL(http.sqls.at(-1) || '')
2821
- expect(sent).toContain(
2822
- 'CREATE INDEX IF NOT EXISTS queries_patch_version ON "chat_0/cvr_queries" ("patchVersion")'
2823
- )
2824
- expect(sent).not.toContain('NULLS FIRST')
2825
- expect(sent).not.toContain('"chat_0/cvr".')
2826
- })
2827
-
2828
- test('normalizes unsupported ALTER TABLE ADD COLUMN constraints', async () => {
2829
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2830
- const backend = new DoBackend(http.url, 'postgres', 'alter-add-column-test')
2831
- await backend.waitReady
2832
-
2833
- await backend.exec(
2834
- 'ALTER TABLE "appInstall" ADD COLUMN IF NOT EXISTS "id" varchar PRIMARY KEY NOT NULL DEFAULT md5(random()::text);'
2835
- )
2836
-
2837
- const sent = compactSQL(
2838
- sqlContaining(http.sqls, 'ALTER TABLE "appInstall" ADD COLUMN id varchar')
2839
- )
2840
- expect(sent).toContain('ALTER TABLE "appInstall" ADD COLUMN id varchar')
2841
- expect(sent).not.toContain('IF NOT EXISTS')
2842
- expect(sent).not.toContain('PRIMARY KEY')
2843
- expect(sent).not.toContain('NOT NULL')
2844
- expect(sent).not.toContain('md5')
2845
- })
2846
-
2847
- test('tracks parser metadata through table and column renames', async () => {
2848
- const http = await startDoHttp((sql) => {
2849
- const compact = compactSQL(sql)
2850
- if (compact.includes("sqlite_master WHERE type = 'table'")) {
2851
- return {
2852
- rows: [{ name: 'app', sql: 'CREATE TABLE "app" ("madeAt" text, meta text)' }],
2853
- columns: ['name', 'sql'],
2854
- }
2855
- }
2856
- if (compact.includes('PRAGMA table_info("app")')) {
2857
- return {
2858
- rows: [
2859
- {
2860
- cid: 0,
2861
- name: 'madeAt',
2862
- type: 'text',
2863
- notnull: 0,
2864
- dflt_value: null,
2865
- pk: 0,
2866
- },
2867
- {
2868
- cid: 1,
2869
- name: 'meta',
2870
- type: 'text',
2871
- notnull: 0,
2872
- dflt_value: null,
2873
- pk: 0,
2874
- },
2875
- ],
2876
- columns: ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'],
2877
- }
2878
- }
2879
- return { rows: [], columns: [] }
2880
- })
2881
- const backend = new DoBackend(http.url, 'postgres', 'rename-metadata-test')
2882
- await backend.waitReady
2883
-
2884
- await backend.exec(`
2885
- CREATE TABLE "plugin" (
2886
- "createdAt" timestamp,
2887
- "meta" jsonb
2888
- );
2889
- `)
2890
- await backend.exec('ALTER TABLE "plugin" RENAME TO "app";')
2891
- await backend.exec('ALTER TABLE "app" RENAME COLUMN "createdAt" TO "madeAt";')
2892
-
2893
- const result = await (backend as any).handleCatalogQuery(`
2894
- SELECT c.column_name::text AS column,
2895
- c.data_type::text AS "dataType",
2896
- t.typname::text AS typename
2897
- FROM information_schema.columns c
2898
- JOIN pg_catalog.pg_type t ON c.udt_name = t.typname
2899
- LEFT JOIN pg_catalog.pg_type et ON t.typelem = et.oid
2900
- JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid
2901
- WHERE (c.table_schema, c.table_name) IN (('public'::text, 'app'::text))
2902
- `)
2903
-
2904
- expect(result.rows).toEqual([
2905
- {
2906
- column: 'madeAt',
2907
- dataType: 'timestamp without time zone',
2908
- typename: 'timestamp',
2909
- },
2910
- { column: 'meta', dataType: 'jsonb', typename: 'jsonb' },
2911
- ])
2912
- })
2913
-
2914
- test('tracks ALTER COLUMN TYPE as catalog metadata without SQLite DDL', async () => {
2915
- const http = await startDoHttp((sql) => {
2916
- const compact = compactSQL(sql)
2917
- if (compact.includes("sqlite_master WHERE type = 'table'")) {
2918
- return {
2919
- rows: [{ name: 'data', sql: 'CREATE TABLE data (value text)' }],
2920
- columns: ['name', 'sql'],
2921
- }
2922
- }
2923
- if (compact.includes('PRAGMA table_info("data")')) {
2924
- return {
2925
- rows: [
2926
- {
2927
- cid: 0,
2928
- name: 'value',
2929
- type: 'text',
2930
- notnull: 0,
2931
- dflt_value: null,
2932
- pk: 0,
2933
- },
2934
- ],
2935
- columns: ['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk'],
2936
- }
2937
- }
2938
- return { rows: [], columns: [] }
2939
- })
2940
- const backend = new DoBackend(http.url, 'postgres', 'alter-type-metadata-test')
2941
- await backend.waitReady
2942
-
2943
- await backend.exec('CREATE TABLE data (value text);')
2944
- await backend.exec(
2945
- 'ALTER TABLE data ALTER COLUMN value SET DATA TYPE jsonb USING value::jsonb;'
2946
- )
2947
-
2948
- const result = await (backend as any).handleCatalogQuery(`
2949
- SELECT c.column_name::text AS column,
2950
- c.data_type::text AS "dataType",
2951
- t.typname::text AS typename
2952
- FROM information_schema.columns c
2953
- JOIN pg_catalog.pg_type t ON c.udt_name = t.typname
2954
- LEFT JOIN pg_catalog.pg_type et ON t.typelem = et.oid
2955
- JOIN pg_catalog.pg_namespace n ON t.typnamespace = n.oid
2956
- WHERE (c.table_schema, c.table_name) IN (('public'::text, 'data'::text))
2957
- `)
2958
-
2959
- expect(http.sqls.some((sql) => compactSQL(sql).includes('ALTER COLUMN'))).toBe(false)
2960
- expect(result.rows).toEqual([
2961
- { column: 'value', dataType: 'jsonb', typename: 'jsonb' },
2962
- ])
2963
- })
2964
-
2965
- test('normalizes pgvector and generated tsvector columns in create table', async () => {
2966
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2967
- const backend = new DoBackend(http.url, 'postgres', 'search-table-test')
2968
- await backend.waitReady
2969
-
2970
- await backend.exec(`
2971
- CREATE TABLE IF NOT EXISTS search_documents (
2972
- id text PRIMARY KEY,
2973
- title text,
2974
- content text,
2975
- search_vector tsvector GENERATED ALWAYS AS (
2976
- setweight(to_tsvector('english', coalesce(title, '')), 'A')
2977
- ) STORED,
2978
- embedding vector(384)
2979
- );
2980
- `)
2981
-
2982
- const sent = compactSQL(
2983
- sqlContaining(http.sqls, 'CREATE TABLE IF NOT EXISTS search_documents')
2984
- )
2985
- expect(sent).toContain('search_vector text')
2986
- expect(sent).toContain('embedding text')
2987
- expect(sent).not.toContain('GENERATED')
2988
- expect(sent).not.toContain('to_tsvector')
2989
- expect(sent).not.toContain('vector(384)')
2990
- })
2991
-
2992
- test('makes create-table statements idempotent for repeated migrations', async () => {
2993
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
2994
- const backend = new DoBackend(http.url, 'postgres', 'create-table-idempotent-test')
2995
- await backend.waitReady
2996
-
2997
- await backend.exec(`
2998
- CREATE TABLE "privateChatsStats" (
2999
- "id" text PRIMARY KEY,
3000
- "createdAt" timestamptz DEFAULT now()
3001
- );
3002
- `)
3003
-
3004
- const sent = compactSQL(
3005
- sqlContaining(http.sqls, 'CREATE TABLE IF NOT EXISTS "privateChatsStats"')
3006
- )
3007
- expect(sent).toContain('CREATE TABLE IF NOT EXISTS "privateChatsStats"')
3008
- expect(sent).toContain('"createdAt" text DEFAULT CURRENT_TIMESTAMP')
3009
- })
3010
-
3011
- test('drops foreign-key constraints while flattening schema-qualified create table DDL', async () => {
3012
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3013
- const backend = new DoBackend(http.url, 'postgres', 'cvr-foreign-key-test')
3014
- await backend.waitReady
3015
-
3016
- await backend.exec(`
3017
- CREATE TABLE "chat_0/cvr".rows (
3018
- "clientGroupID" TEXT,
3019
- "rowKey" JSONB,
3020
- PRIMARY KEY ("clientGroupID", "rowKey"),
3021
- CONSTRAINT fk_rows_client_group
3022
- FOREIGN KEY("clientGroupID")
3023
- REFERENCES "chat_0/cvr"."rowsVersion" ("clientGroupID")
3024
- ON DELETE CASCADE
3025
- );
3026
- `)
3027
-
3028
- const sent = compactSQL(
3029
- sqlContaining(http.sqls, 'CREATE TABLE IF NOT EXISTS "chat_0/cvr_rows"')
3030
- )
3031
- expect(sent).toContain('CREATE TABLE IF NOT EXISTS "chat_0/cvr_rows"')
3032
- expect(sent).toContain('"rowKey" text')
3033
- expect(sent).not.toContain('FOREIGN KEY')
3034
- expect(sent).not.toContain('REFERENCES')
3035
- expect(sent).not.toContain('"chat_0/cvr".')
3036
- })
3037
-
3038
- test('rewrites temporary create-table-as statements to persistent SQLite tables', async () => {
3039
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3040
- const backend = new DoBackend(http.url, 'postgres', 'temp-table-as-test')
3041
- await backend.waitReady
3042
-
3043
- await backend.exec(`
3044
- CREATE TEMP TABLE app_id_mapping AS
3045
- SELECT id AS old_id, uid AS new_id
3046
- FROM public.app;
3047
- `)
3048
-
3049
- const sent = compactSQL(http.sqls.at(-1) || '')
3050
- expect(sent).toContain('CREATE TABLE app_id_mapping AS SELECT')
3051
- expect(sent).toContain('FROM app')
3052
- expect(sent).not.toContain('TEMP')
3053
- })
3054
-
3055
- test('normalizes multiline ALTER TABLE ADD COLUMN modifiers', async () => {
3056
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3057
- const backend = new DoBackend(http.url, 'postgres', 'multiline-add-column-test')
3058
- await backend.waitReady
3059
-
3060
- await backend.exec(`
3061
- ALTER TABLE "userPublic"
3062
- ADD COLUMN IF NOT EXISTS "hasOnboarded" BOOLEAN NOT NULL DEFAULT false;
3063
- `)
3064
-
3065
- const sent = compactSQL(
3066
- sqlContaining(
3067
- http.sqls,
3068
- 'ALTER TABLE "userPublic" ADD COLUMN "hasOnboarded" integer DEFAULT 0'
3069
- )
3070
- )
3071
- expect(sent).toContain(
3072
- 'ALTER TABLE "userPublic" ADD COLUMN "hasOnboarded" integer DEFAULT 0'
3073
- )
3074
- expect(sent).not.toContain('IF NOT EXISTS')
3075
- expect(sent).not.toContain('NOT NULL')
3076
- })
3077
-
3078
- test('splits multi-command ALTER TABLE statements for SQLite', async () => {
3079
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3080
- const backend = new DoBackend(http.url, 'postgres', 'multi-add-column-test')
3081
- await backend.waitReady
3082
-
3083
- await backend.exec(`
3084
- ALTER TABLE search_documents
3085
- ADD COLUMN server_id text,
3086
- ADD COLUMN channel_id text;
3087
- `)
3088
-
3089
- const sent = http.sqls.join('; ')
3090
- expect(compactSQL(sent)).toContain(
3091
- 'ALTER TABLE search_documents ADD COLUMN server_id text'
3092
- )
3093
- expect(compactSQL(sent)).toContain(
3094
- 'ALTER TABLE search_documents ADD COLUMN channel_id text'
3095
- )
3096
- expect(compactSQL(sent)).not.toContain('server_id text, ADD COLUMN')
3097
- })
3098
-
3099
- test('skips ADD COLUMN IF NOT EXISTS when parser metadata finds the column', async () => {
3100
- const http = await startDoHttp((sql) => {
3101
- if (sql.toLowerCase().includes('pragma table_info')) {
3102
- return { rows: [{ name: 'hasOnboarded' }], columns: ['name'] }
3103
- }
3104
- return { rows: [], columns: [] }
3105
- })
3106
- const backend = new DoBackend(http.url, 'postgres', 'conditional-add-column-test')
3107
- await backend.waitReady
3108
-
3109
- await backend.execProtocolRaw(msg(0x51, cstr('BEGIN')))
3110
- await backend.execProtocolRaw(
3111
- msg(
3112
- 0x51,
3113
- cstr(`
3114
- ALTER TABLE "userPublic"
3115
- ADD COLUMN IF NOT EXISTS "hasOnboarded" BOOLEAN NOT NULL DEFAULT false;
3116
- `)
3117
- )
3118
- )
3119
- await backend.execProtocolRaw(msg(0x51, cstr('COMMIT')))
3120
-
3121
- expect(http.sqls.some((sql) => compactSQL(sql).includes('PRAGMA table_info'))).toBe(
3122
- true
3123
- )
3124
- expect(http.sqls.some((sql) => compactSQL(sql).includes('ADD COLUMN'))).toBe(false)
3125
- })
3126
-
3127
- test('skips plain ADD COLUMN when parser metadata finds the column', async () => {
3128
- const http = await startDoHttp((sql) => {
3129
- if (sql.toLowerCase().includes('pragma table_info')) {
3130
- return { rows: [{ name: 'latestMessageOrder' }], columns: ['name'] }
3131
- }
3132
- return { rows: [], columns: [] }
3133
- })
3134
- const backend = new DoBackend(http.url, 'postgres', 'plain-add-column-test')
3135
- await backend.waitReady
3136
-
3137
- await backend.execProtocolRaw(msg(0x51, cstr('BEGIN')))
3138
- await backend.execProtocolRaw(
3139
- msg(
3140
- 0x51,
3141
- cstr(`
3142
- ALTER TABLE channel
3143
- ADD COLUMN "latestMessageOrder" varchar;
3144
- `)
3145
- )
3146
- )
3147
- await backend.execProtocolRaw(msg(0x51, cstr('COMMIT')))
3148
-
3149
- expect(http.sqls.some((sql) => compactSQL(sql).includes('PRAGMA table_info'))).toBe(
3150
- true
3151
- )
3152
- expect(http.sqls.some((sql) => compactSQL(sql).includes('ADD COLUMN'))).toBe(false)
3153
- })
3154
-
3155
- test('skips DROP COLUMN IF EXISTS when parser metadata cannot find the column', async () => {
3156
- const http = await startDoHttp((sql) => {
3157
- if (sql.toLowerCase().includes('pragma table_info')) {
3158
- return { rows: [{ name: 'id' }], columns: ['name'] }
3159
- }
3160
- return { rows: [], columns: [] }
3161
- })
3162
- const backend = new DoBackend(http.url, 'postgres', 'conditional-drop-column-test')
3163
- await backend.waitReady
3164
-
3165
- await backend.execProtocolRaw(msg(0x51, cstr('BEGIN')))
3166
- await backend.execProtocolRaw(
3167
- msg(
3168
- 0x51,
3169
- cstr(`
3170
- ALTER TABLE search_documents
3171
- DROP COLUMN IF EXISTS message_order;
3172
- `)
3173
- )
3174
- )
3175
- await backend.execProtocolRaw(msg(0x51, cstr('COMMIT')))
3176
-
3177
- expect(http.sqls.some((sql) => compactSQL(sql).includes('PRAGMA table_info'))).toBe(
3178
- true
3179
- )
3180
- expect(http.sqls.some((sql) => compactSQL(sql).includes('DROP COLUMN'))).toBe(false)
3181
- })
3182
-
3183
- test('keeps table-qualified column refs while flattening schemas', async () => {
3184
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3185
- const backend = new DoBackend(http.url, 'postgres', 'update-from-test')
3186
- await backend.waitReady
3187
-
3188
- await backend.exec(`
3189
- UPDATE thread
3190
- SET "serverId" = channel."serverId"
3191
- FROM channel
3192
- WHERE thread."channelId" = channel.id
3193
- AND public.thread."deleted" = false;
3194
- `)
3195
-
3196
- const sent = compactSQL(http.sqls.at(-1) || '')
3197
- expect(sent).toContain('channel."serverId"')
3198
- expect(sent).toContain('thread."channelId" = channel.id')
3199
- expect(sent).toContain('thread.deleted = 0')
3200
- expect(sent).not.toContain('channel_id')
3201
- expect(sent).not.toContain('thread_channelId')
3202
- })
3203
-
3204
- test('rewrites PG least and greatest scalar functions to SQLite min and max', async () => {
3205
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3206
- const backend = new DoBackend(http.url, 'postgres', 'least-greatest-test')
3207
- await backend.waitReady
3208
-
3209
- await backend.exec(`
3210
- UPDATE "thread"
3211
- SET "replyCount" = LEAST((SELECT COUNT(*)::INTEGER FROM "message"), 11),
3212
- "order" = GREATEST("order", 0);
3213
- `)
3214
-
3215
- const sent = compactSQL(http.sqls.at(-1) || '')
3216
- expect(sent).toContain('"replyCount" = min')
3217
- expect(sent).toContain('"order" = max')
3218
- expect(sent).not.toContain('LEAST')
3219
- expect(sent).not.toContain('GREATEST')
3220
- })
3221
-
3222
- test('rewrites PG starts_with scalar function to SQLite instr predicate', async () => {
3223
- const http = await startDoHttp((sql) => {
3224
- if (sql.includes('instr(')) return { rows: [{ users: 3 }], columns: ['users'] }
3225
- return { rows: [], columns: [] }
3226
- })
3227
- const backend = new DoBackend(http.url, 'postgres', 'starts-with-test')
3228
- await backend.waitReady
3229
-
3230
- const result = await backend.query(
3231
- `SELECT count(*) FILTER (WHERE starts_with("profileID", 'p')) AS users
3232
- FROM "chat_0/cvr_instances"`
3233
- )
3234
-
3235
- expect(result.rows).toEqual([{ users: 3 }])
3236
- const sent = http.sqls.at(-1) ?? ''
3237
- expect(sent).not.toContain('starts_with')
3238
- expect(sent).toContain('instr("profileID", \'p\') = 1')
3239
- })
3240
-
3241
- test('rewrites DISTINCT ON selects with a window function for SQLite', async () => {
3242
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3243
- const backend = new DoBackend(http.url, 'postgres', 'distinct-on-test')
3244
- await backend.waitReady
3245
-
3246
- await backend.exec(`
3247
- UPDATE "agentConfig" ac
3248
- SET "systemPrompt" = sub."chatPrompt"
3249
- FROM (
3250
- SELECT DISTINCT ON (c."serverId")
3251
- c."serverId",
3252
- c."chatPrompt"
3253
- FROM channel c
3254
- WHERE c."chatPrompt" IS NOT NULL
3255
- AND c."chatPrompt" != ''
3256
- ORDER BY c."serverId", c."updatedAt" DESC
3257
- ) sub
3258
- WHERE ac."serverId" = sub."serverId"
3259
- AND ac.type = 'builtin';
3260
- `)
3261
-
3262
- const sent = compactSQL(http.sqls.at(-1) || '')
3263
- expect(sent).toContain('row_number() OVER')
3264
- expect(sent).toContain('PARTITION BY c."serverId"')
3265
- expect(sent).toContain('ORDER BY c."serverId", c."updatedAt" DESC')
3266
- expect(sent).toContain('_orez_rn = 1')
3267
- expect(sent).not.toContain('DISTINCT ON')
3268
- })
3269
-
3270
- test('strips PostgreSQL row-locking clauses from SELECTs for SQLite', async () => {
3271
- const http = await startDoHttp(() => ({ rows: [], columns: ['clientGroupID'] }))
3272
- const backend = new DoBackend(http.url, 'postgres', 'select-locking-clause-test')
3273
- await backend.waitReady
3274
-
3275
- await backend.exec(`
3276
- SELECT "clientGroupID"
3277
- FROM "chat_0/cvr_instances"
3278
- WHERE NOT "deleted"
3279
- ORDER BY "lastActive" ASC
3280
- LIMIT 10
3281
- FOR UPDATE SKIP LOCKED
3282
- `)
3283
-
3284
- const sent = compactSQL(http.sqls.at(-1) || '')
3285
- expect(sent).toContain('ORDER BY "lastActive" ASC')
3286
- expect(sent).toContain('LIMIT 10')
3287
- expect(sent).not.toContain('FOR UPDATE')
3288
- expect(sent).not.toContain('SKIP LOCKED')
3289
- })
3290
-
3291
- test('rewrites LIKE with PostgreSQL escape semantics to SQLite like()', async () => {
3292
- const http = await startDoHttp(() => ({ rows: [], columns: ['slot'] }))
3293
- const backend = new DoBackend(http.url, 'postgres', 'like-escape-test')
3294
- await backend.waitReady
3295
-
3296
- await backend.query(
3297
- `
3298
- SELECT slot_name AS slot
3299
- FROM _orez._zero_replication_slots
3300
- WHERE slot_name LIKE $1
3301
- `,
3302
- ['chat\\_0\\_%']
3303
- )
3304
-
3305
- const sent = compactSQL(http.sqls.at(-1) || '')
3306
- expect(sent).toContain('"like"(?, slot_name, "char"(92))')
3307
- expect(sent).not.toContain('slot_name LIKE')
3308
- expect(http.params.at(-1)).toEqual(['chat\\_0\\_%'])
3309
- })
3310
-
3311
- test('rewrites PG JSONB helper functions to SQLite JSON1 equivalents', async () => {
3312
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3313
- const backend = new DoBackend(http.url, 'postgres', 'json-function-test')
3314
- await backend.waitReady
3315
-
3316
- await backend.exec(`
3317
- UPDATE task
3318
- SET "numSteps" = CASE
3319
- WHEN steps IS NULL THEN 0
3320
- ELSE jsonb_array_length(steps)
3321
- END;
3322
- `)
3323
-
3324
- const sent = compactSQL(http.sqls.at(-1) || '')
3325
- expect(sent).toContain('json_array_length(steps)')
3326
- expect(sent).not.toContain('jsonb_array_length')
3327
- })
3328
-
3329
- test('rewrites PG JSONB any-key operator to SQLite JSON1 joins', async () => {
3330
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3331
- const backend = new DoBackend(http.url, 'postgres', 'jsonb-any-key-test')
3332
- await backend.waitReady
3333
-
3334
- await backend.query(
3335
- `
3336
- SELECT *
3337
- FROM "chat_0/cvr_rows"
3338
- WHERE
3339
- "clientGroupID" = $1
3340
- AND "patchVersion" > $2
3341
- AND "patchVersion" <= $3
3342
- AND ("refCounts" IS NULL OR NOT ("refCounts" ?| $4))
3343
- `,
3344
- ['cg1', '00:01', '00:02', ['q1', 'q2']]
3345
- )
3346
-
3347
- const sent = compactSQL(http.sqls.at(-1) || '')
3348
- expect(sent).toContain('json_each("refCounts")')
3349
- expect(sent).toContain('json_each(?)')
3350
- expect(sent).toContain('obj.key = keys.value')
3351
- expect(sent).not.toContain('?|')
3352
- expect(http.params.at(-1)).toEqual(['cg1', '00:01', '00:02', '["q1","q2"]'])
3353
- })
3354
-
3355
- test('rewrites JSON object builders and marks their result columns as JSON', async () => {
3356
- const http = await startDoHttp((sql) => {
3357
- if (compactSQL(sql).startsWith('SELECT')) {
3358
- return {
3359
- rows: [
3360
- {
3361
- table: '{"schema":"public","name":"message","metadata":{"rowKey":["id"]}}',
3362
- columns: '{"payload":{"source":"backfill"}}',
3363
- },
3364
- ],
3365
- columns: ['table', 'columns'],
3366
- }
3367
- }
3368
- return { rows: [], columns: [] }
3369
- })
3370
- const backend = new DoBackend(http.url, 'postgres', 'json-object-builder-test')
3371
- await backend.waitReady
3372
-
3373
- const result = await backend.execProtocolRaw(
3374
- msg(
3375
- 0x51,
3376
- cstr(`
3377
- SELECT
3378
- json_build_object(
3379
- 'schema', b."schema",
3380
- 'name', b."table",
3381
- 'metadata', t."metadata"
3382
- ) AS "table",
3383
- json_object_agg(b."column", b."backfill") AS "columns"
3384
- FROM "chat_0/change-streamer_0"."backfilling" AS b
3385
- LEFT JOIN "chat_0/change-streamer_0"."tableMetadata" AS t
3386
- ON (b."schema" = t."schema" AND b."table" = t."table")
3387
- GROUP BY b."schema", b."table", t."metadata"
3388
- `)
3389
- )
3390
- )
3391
-
3392
- const sent = compactSQL(http.sqls.at(-1) || '')
3393
- expect(sent).toContain('json_group_object')
3394
- expect(sent).toContain('json_valid')
3395
- expect(sent).not.toContain('json_build_object')
3396
- expect(sent).not.toContain('json_object_agg')
3397
- expect(rowDescriptionOids(result)).toMatchObject({
3398
- table: 114,
3399
- columns: 114,
3400
- })
3401
- expect(dataRowValues(result)).toEqual([
3402
- [
3403
- '{"schema":"public","name":"message","metadata":{"rowKey":["id"]}}',
3404
- '{"payload":{"source":"backfill"}}',
3405
- ],
3406
- ])
3407
- })
3408
-
3409
- test('skips unsupported regexp_replace updates when the target table is empty', async () => {
3410
- const http = await startDoHttp((sql) => {
3411
- if (compactSQL(sql).includes('SELECT 1 AS ok FROM "message" LIMIT 1')) {
3412
- return { rows: [], columns: ['ok'] }
3413
- }
3414
- return { rows: [], columns: [] }
3415
- })
3416
- const backend = new DoBackend(http.url, 'postgres', 'regexp-empty-update-test')
3417
- await backend.waitReady
3418
-
3419
- await backend.execProtocolRaw(msg(0x51, cstr('BEGIN')))
3420
- await backend.execProtocolRaw(
3421
- msg(
3422
- 0x51,
3423
- cstr(`
3424
- UPDATE message
3425
- SET content = regexp_replace(content, '<@{([^:}]+):([^:}]+):([^}]+)}>', E'<@{\\\\1%\\\\2%\\\\3}>', 'g')
3426
- WHERE content LIKE '%<@{%:%:%}%';
3427
- `)
3428
- )
3429
- )
3430
- await backend.execProtocolRaw(msg(0x51, cstr('COMMIT')))
3431
-
3432
- expect(
3433
- http.sqls.some((sql) =>
3434
- compactSQL(sql).includes('SELECT 1 AS ok FROM "message" LIMIT 1')
3435
- )
3436
- ).toBe(true)
3437
- expect(http.sqls.some((sql) => sql.includes('regexp_replace'))).toBe(false)
3438
- })
3439
-
3440
- test('skips CTE backfill inserts when the source table is empty', async () => {
3441
- const http = await startDoHttp((sql) => {
3442
- if (compactSQL(sql).includes('SELECT 1 AS ok FROM "messageReaction" LIMIT 1')) {
3443
- return { rows: [], columns: ['ok'] }
3444
- }
3445
- return { rows: [], columns: [] }
3446
- })
3447
- const backend = new DoBackend(http.url, 'postgres', 'empty-cte-insert-test')
3448
- await backend.waitReady
3449
-
3450
- await backend.execProtocolRaw(msg(0x51, cstr('BEGIN')))
3451
- await backend.execProtocolRaw(
3452
- msg(
3453
- 0x51,
3454
- cstr(`
3455
- INSERT INTO "messageReactionStats"
3456
- WITH ranked_reactions AS (
3457
- SELECT mr."messageId", mr."reactionId"
3458
- FROM "messageReaction" mr
3459
- )
3460
- SELECT "messageId", "reactionId" FROM ranked_reactions;
3461
- `)
3462
- )
3463
- )
3464
- await backend.execProtocolRaw(msg(0x51, cstr('COMMIT')))
3465
-
3466
- expect(
3467
- http.sqls.some((sql) =>
3468
- compactSQL(sql).includes('SELECT 1 AS ok FROM "messageReaction" LIMIT 1')
3469
- )
3470
- ).toBe(true)
3471
- expect(http.sqls.some((sql) => compactSQL(sql).startsWith('INSERT INTO'))).toBe(false)
3472
- })
3473
-
3474
- test('executes zero-cache DELETE RETURNING count CTEs as SQLite deletes', async () => {
3475
- const http = await startDoHttp((sql) => {
3476
- if (compactSQL(sql).startsWith('DELETE FROM "todo_0/cdc_changeLog"')) {
3477
- return {
3478
- rows: [{ __orez_deleted: 1 }, { __orez_deleted: 1 }],
3479
- columns: ['__orez_deleted'],
3480
- }
3481
- }
3482
- return { rows: [], columns: [] }
3483
- })
3484
- const backend = new DoBackend(http.url, 'zero_cdb', 'delete-count-cte-test')
3485
- await backend.waitReady
3486
-
3487
- await backend.execProtocolRaw(
3488
- parseMessage(`
3489
- WITH purged AS (
3490
- DELETE FROM "todo_0/cdc"."changeLog"
3491
- WHERE watermark < $1
3492
- RETURNING watermark, pos
3493
- )
3494
- SELECT COUNT(*) AS deleted
3495
- FROM purged;
3496
- `)
3497
- )
3498
- await backend.execProtocolRaw(bindStatementParams(['a1zs3dw2usxs']))
3499
- const result = await backend.execProtocolRaw(executePortal())
3500
-
3501
- expect(dataRowValues(result)).toEqual([['2']])
3502
- expect(compactSQL(http.sqls.at(-1) || '')).toBe(
3503
- 'DELETE FROM "todo_0/cdc_changeLog" WHERE watermark < ? RETURNING 1 AS "__orez_deleted"'
3504
- )
3505
- expect(http.params.at(-1)).toEqual(['a1zs3dw2usxs'])
3506
- })
3507
-
3508
- test('skips DELETE USING cleanup statements when the target table is empty', async () => {
3509
- const http = await startDoHttp((sql) => {
3510
- if (
3511
- compactSQL(sql).includes(
3512
- 'SELECT 1 AS ok FROM "channelNotificationSetting" LIMIT 1'
3513
- )
3514
- ) {
3515
- return { rows: [], columns: ['ok'] }
3516
- }
3517
- return { rows: [], columns: [] }
3518
- })
3519
- const backend = new DoBackend(http.url, 'postgres', 'empty-delete-using-test')
3520
- await backend.waitReady
3521
-
3522
- await backend.execProtocolRaw(msg(0x51, cstr('BEGIN')))
3523
- await backend.execProtocolRaw(
3524
- msg(
3525
- 0x51,
3526
- cstr(`
3527
- WITH ranked AS (
3528
- SELECT ctid, row_number() OVER (
3529
- PARTITION BY "channelId", "userId"
3530
- ORDER BY "updatedAt" DESC NULLS LAST, "id" DESC
3531
- ) AS rn
3532
- FROM "channelNotificationSetting"
3533
- )
3534
- DELETE FROM "channelNotificationSetting" t
3535
- USING ranked
3536
- WHERE t.ctid = ranked.ctid
3537
- AND ranked.rn > 1;
3538
- `)
3539
- )
3540
- )
3541
- await backend.execProtocolRaw(msg(0x51, cstr('COMMIT')))
3542
-
3543
- expect(
3544
- http.sqls.some((sql) =>
3545
- compactSQL(sql).includes(
3546
- 'SELECT 1 AS ok FROM "channelNotificationSetting" LIMIT 1'
3547
- )
3548
- )
3549
- ).toBe(true)
3550
- expect(http.sqls.some((sql) => compactSQL(sql).startsWith('WITH ranked'))).toBe(false)
3551
- })
3552
-
3553
- test('rewrites TRUNCATE statements to SQLite deletes', async () => {
3554
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3555
- const backend = new DoBackend(http.url, 'postgres', 'truncate-rewrite-test')
3556
- await backend.waitReady
3557
-
3558
- await backend.exec('TRUNCATE public.data, "apiKey";')
3559
-
3560
- const sent = compactSQL(http.sqls.at(-1) || '')
3561
- expect(sent).toContain('DELETE FROM data')
3562
- expect(sent).toContain('DELETE FROM "apiKey"')
3563
- expect(sent).not.toContain('TRUNCATE')
3564
- })
3565
-
3566
- test('translates simple plpgsql row triggers to SQLite triggers', async () => {
3567
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3568
- const backend = new DoBackend(http.url, 'postgres', 'sqlite-trigger-test')
3569
- await backend.waitReady
3570
-
3571
- await backend.exec(`
3572
- CREATE OR REPLACE FUNCTION update_channel_latest_message_order()
3573
- RETURNS TRIGGER AS $$
3574
- BEGIN
3575
- IF NEW.type IS DISTINCT FROM 'draft'
3576
- AND NEW.deleted = false
3577
- AND NEW."isThreadReply" = false
3578
- AND NEW."order" IS NOT NULL THEN
3579
- UPDATE "channel"
3580
- SET "latestMessageOrder" = NEW."order"
3581
- WHERE id = NEW."channelId"
3582
- AND ("latestMessageOrder" IS NULL OR NEW."order" > "latestMessageOrder");
3583
- END IF;
3584
- RETURN NEW;
3585
- END;
3586
- $$ LANGUAGE plpgsql;
3587
-
3588
- DROP TRIGGER IF EXISTS trg_update_channel_latest_message_order ON "message";
3589
- CREATE TRIGGER trg_update_channel_latest_message_order
3590
- AFTER INSERT OR UPDATE ON "message"
3591
- FOR EACH ROW
3592
- EXECUTE FUNCTION update_channel_latest_message_order();
3593
- `)
3594
-
3595
- const sent = http.sqls.map(compactSQL).join('\n')
3596
- expect(sent).toContain(
3597
- 'DROP TRIGGER IF EXISTS "trg_update_channel_latest_message_order_insert"'
3598
- )
3599
- expect(sent).toContain(
3600
- 'CREATE TRIGGER IF NOT EXISTS "trg_update_channel_latest_message_order_insert" AFTER INSERT ON "message"'
3601
- )
3602
- expect(sent).toContain(
3603
- 'CREATE TRIGGER IF NOT EXISTS "trg_update_channel_latest_message_order_update" AFTER UPDATE ON "message"'
3604
- )
3605
- expect(sent).toContain('UPDATE channel SET "latestMessageOrder" = new."order"')
3606
- expect(sent).toContain('new.deleted = 0')
3607
- expect(sent).not.toContain('CREATE OR REPLACE FUNCTION')
3608
- expect(sent).not.toContain('EXECUTE FUNCTION')
3609
- })
3610
-
3611
- test('translates SELECT INTO NEW in plpgsql row triggers', async () => {
3612
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3613
- const backend = new DoBackend(http.url, 'postgres', 'sqlite-select-into-trigger-test')
3614
- await backend.waitReady
3615
-
3616
- await backend.exec(`
3617
- CREATE OR REPLACE FUNCTION update_seen_last_order()
3618
- RETURNS TRIGGER AS $$
3619
- BEGIN
3620
- IF NEW."messageId" IS NOT NULL THEN
3621
- SELECT "order" INTO NEW."lastSeenOrder"
3622
- FROM "message"
3623
- WHERE id = NEW."messageId";
3624
- END IF;
3625
- RETURN NEW;
3626
- END;
3627
- $$ LANGUAGE plpgsql;
3628
-
3629
- CREATE TRIGGER trg_update_seen_last_order
3630
- BEFORE INSERT OR UPDATE ON "seen"
3631
- FOR EACH ROW
3632
- EXECUTE FUNCTION update_seen_last_order();
3633
- `)
3634
-
3635
- const sent = http.sqls.map(compactSQL).join('\n')
3636
- expect(sent).toContain(
3637
- 'CREATE TRIGGER IF NOT EXISTS "trg_update_seen_last_order_insert" AFTER INSERT ON "seen"'
3638
- )
3639
- expect(sent).toContain(
3640
- 'UPDATE "seen" SET "lastSeenOrder" = (SELECT "order" FROM message WHERE id = new."messageId") WHERE rowid = NEW.rowid'
3641
- )
3642
- })
3643
-
3644
- test('translates NEW column assignments in plpgsql row triggers', async () => {
3645
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3646
- const backend = new DoBackend(
3647
- http.url,
3648
- 'postgres',
3649
- 'sqlite-new-assignment-trigger-test'
3650
- )
3651
- await backend.waitReady
3652
-
3653
- await backend.exec(`
3654
- CREATE OR REPLACE FUNCTION update_task_numsteps()
3655
- RETURNS TRIGGER AS $$
3656
- BEGIN
3657
- IF NEW."steps" IS NULL THEN
3658
- NEW."numSteps" = 0;
3659
- ELSE
3660
- NEW."numSteps" = jsonb_array_length(NEW."steps");
3661
- END IF;
3662
- RETURN NEW;
3663
- END;
3664
- $$ LANGUAGE plpgsql;
3665
-
3666
- CREATE TRIGGER task_numsteps_trigger
3667
- BEFORE INSERT OR UPDATE OF "steps" ON "task"
3668
- FOR EACH ROW
3669
- EXECUTE FUNCTION update_task_numsteps();
3670
- `)
3671
-
3672
- const sent = http.sqls.map(compactSQL).join('\n')
3673
- expect(sent).toContain(
3674
- 'CREATE TRIGGER IF NOT EXISTS "task_numsteps_trigger_insert" AFTER INSERT ON "task"'
3675
- )
3676
- expect(sent).toContain(
3677
- 'CREATE TRIGGER IF NOT EXISTS "task_numsteps_trigger_update" AFTER UPDATE ON "task"'
3678
- )
3679
- expect(sent).toContain(
3680
- 'UPDATE "task" SET "numSteps" = 0 WHERE rowid = NEW.rowid AND (new.steps IS NULL)'
3681
- )
3682
- expect(sent).toContain(
3683
- 'UPDATE "task" SET "numSteps" = json_array_length(new.steps) WHERE rowid = NEW.rowid AND (NOT (new.steps IS NULL))'
3684
- )
3685
- })
3686
-
3687
- test('removes update target aliases from compiled SQLite triggers', async () => {
3688
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3689
- const backend = new DoBackend(
3690
- http.url,
3691
- 'postgres',
3692
- 'sqlite-update-alias-trigger-test'
3693
- )
3694
- await backend.waitReady
3695
-
3696
- await backend.exec(`
3697
- CREATE OR REPLACE FUNCTION sync_user_role_permissions_on_insert()
3698
- RETURNS TRIGGER AS $$
3699
- BEGIN
3700
- UPDATE "userRole" ur SET
3701
- "canAdmin" = r."canAdmin"
3702
- FROM "role" r
3703
- WHERE ur."roleId" = r."id"
3704
- AND ur."serverId" = NEW."serverId";
3705
- RETURN NEW;
3706
- END;
3707
- $$ LANGUAGE plpgsql;
3708
-
3709
- CREATE TRIGGER trg_user_role_permission_copy
3710
- AFTER INSERT ON "userRole"
3711
- FOR EACH ROW
3712
- EXECUTE FUNCTION sync_user_role_permissions_on_insert();
3713
- `)
3714
-
3715
- const sent = http.sqls.map(compactSQL).join('\n')
3716
- expect(sent).toContain(
3717
- 'UPDATE "userRole" SET "canAdmin" = r."canAdmin" FROM role AS r WHERE "userRole"."roleId" = r.id'
3718
- )
3719
- expect(sent).toContain('"userRole"."serverId" = new."serverId"')
3720
- expect(sent).not.toContain('UPDATE "userRole" AS ur')
3721
- })
3722
-
3723
- test('rewrites md5 trigger expressions to deterministic SQLite-compatible values', async () => {
3724
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3725
- const backend = new DoBackend(http.url, 'postgres', 'sqlite-md5-trigger-test')
3726
- await backend.waitReady
3727
-
3728
- await backend.exec(`
3729
- CREATE OR REPLACE FUNCTION set_permissions_hash()
3730
- RETURNS TRIGGER AS $$
3731
- BEGIN
3732
- NEW.hash = md5(NEW.permissions::text);
3733
- RETURN NEW;
3734
- END;
3735
- $$ LANGUAGE plpgsql;
3736
-
3737
- CREATE TRIGGER on_set_permissions
3738
- BEFORE INSERT OR UPDATE ON "chat_permissions"
3739
- FOR EACH ROW
3740
- EXECUTE FUNCTION set_permissions_hash();
3741
- `)
3742
-
3743
- const sent = http.sqls.map(compactSQL).join('\n')
3744
- expect(sent).toContain(
3745
- 'UPDATE "chat_permissions" SET "hash" = new.permissions WHERE rowid = NEW.rowid'
3746
- )
3747
- expect(sent).not.toContain('md5(')
3748
- })
3749
-
3750
- test('skips unsupported plpgsql trigger statements instead of throwing', async () => {
3751
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3752
- const backend = new DoBackend(http.url, 'postgres', 'sqlite-unsupported-trigger-test')
3753
- await backend.waitReady
3754
-
3755
- await backend.exec(`
3756
- CREATE OR REPLACE FUNCTION unsupported_trigger()
3757
- RETURNS TRIGGER AS $$
3758
- BEGIN
3759
- invalid plpgsql syntax here;
3760
- RETURN NEW;
3761
- END;
3762
- $$ LANGUAGE plpgsql;
3763
-
3764
- CREATE TRIGGER unsupported_trigger
3765
- BEFORE INSERT ON "task"
3766
- FOR EACH ROW
3767
- EXECUTE FUNCTION unsupported_trigger();
3768
- `)
3769
-
3770
- const sent = http.sqls.map(compactSQL).join('\n')
3771
- expect(sent).not.toContain('CREATE TRIGGER IF NOT EXISTS "unsupported_trigger"')
3772
- })
3773
-
3774
- test('skips plpgsql triggers that require PostgreSQL trigger variables', async () => {
3775
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3776
- const backend = new DoBackend(http.url, 'postgres', 'sqlite-trigger-variable-test')
3777
- await backend.waitReady
3778
-
3779
- await backend.exec(`
3780
- CREATE OR REPLACE FUNCTION refresh_stats()
3781
- RETURNS TRIGGER AS $$
3782
- BEGIN
3783
- IF TG_OP = 'INSERT' THEN
3784
- UPDATE stats SET count = count + 1;
3785
- END IF;
3786
- RETURN NEW;
3787
- END;
3788
- $$ LANGUAGE plpgsql;
3789
-
3790
- CREATE TRIGGER refresh_stats_insert
3791
- AFTER INSERT ON "messageReaction"
3792
- FOR EACH ROW
3793
- EXECUTE FUNCTION refresh_stats();
3794
- `)
3795
-
3796
- const sent = http.sqls.map(compactSQL).join('\n')
3797
- expect(sent).not.toContain('CREATE TRIGGER IF NOT EXISTS "refresh_stats_insert"')
3798
- expect(sent).not.toContain('TG_OP')
3799
- })
3800
-
3801
- test('rewrites trigger drops for SQLite trigger variants', async () => {
3802
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3803
- const backend = new DoBackend(http.url, 'postgres', 'drop-trigger-test')
3804
- await backend.waitReady
3805
-
3806
- await backend.exec(
3807
- 'DROP TRIGGER IF EXISTS "messageReactionInsertTrigger" ON "messageReaction";'
3808
- )
3809
-
3810
- const sent = http.sqls.map(compactSQL).join('\n')
3811
- expect(sent).toContain('DROP TRIGGER IF EXISTS "messageReactionInsertTrigger"')
3812
- expect(sent).toContain('DROP TRIGGER IF EXISTS "messageReactionInsertTrigger_insert"')
3813
- })
3814
-
3815
- test('drops event trigger statements because event trigger creation is skipped', async () => {
3816
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3817
- const backend = new DoBackend(http.url, 'postgres', 'drop-event-trigger-test')
3818
- await backend.waitReady
3819
-
3820
- await backend.exec(`
3821
- DROP EVENT TRIGGER IF EXISTS chat_ddl_start_0;
3822
- CREATE EVENT TRIGGER chat_ddl_start_0
3823
- ON ddl_command_start EXECUTE FUNCTION public._zero_notify_change();
3824
- `)
3825
-
3826
- expect(http.sqls.some((sql) => compactSQL(sql).includes('EVENT TRIGGER'))).toBe(false)
3827
- })
3828
-
3829
- test('drops function statements because function creation is skipped', async () => {
3830
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3831
- const backend = new DoBackend(http.url, 'postgres', 'drop-function-test')
3832
- await backend.waitReady
3833
-
3834
- await backend.exec('DROP FUNCTION IF EXISTS "notifyReactionChange"();')
3835
-
3836
- expect(http.sqls.some((sql) => compactSQL(sql).startsWith('DROP FUNCTION'))).toBe(
3837
- false
3838
- )
3839
- })
3840
-
3841
- test('drops anonymous DO blocks because they only wrap skipped PG DDL', async () => {
3842
- const http = await startDoHttp(() => ({ rows: [], columns: [] }))
3843
- const backend = new DoBackend(http.url, 'postgres', 'do-block-test')
3844
- await backend.waitReady
3845
-
3846
- await backend.exec(`
3847
- DO $$ BEGIN
3848
- IF NOT EXISTS (
3849
- SELECT 1 FROM pg_constraint WHERE conname = 'example_constraint'
3850
- ) THEN
3851
- ALTER TABLE "example" ADD CONSTRAINT "example_constraint" UNIQUE ("id");
3852
- END IF;
3853
- END $$;
3854
- `)
3855
-
3856
- expect(http.sqls.some((sql) => compactSQL(sql).startsWith('DO'))).toBe(false)
3857
- })
3858
-
3859
- test('no-ops direct calls to functions skipped by parser-backed DDL rewrite', async () => {
3860
- const http = await startDoHttp((sql) => {
3861
- if (compactSQL(sql).startsWith('SELECT NULL AS')) {
3862
- return {
3863
- rows: [{ refreshMessageReactionStats: null }],
3864
- columns: ['refreshMessageReactionStats'],
3865
- }
3866
- }
3867
- return { rows: [], columns: [] }
3868
- })
3869
- const backend = new DoBackend(http.url, 'postgres', 'skipped-function-call-test')
3870
- await backend.waitReady
3871
-
3872
- await backend.exec(`
3873
- CREATE OR REPLACE FUNCTION "refreshMessageReactionStats"()
3874
- RETURNS void AS $$
3875
- BEGIN
3876
- END;
3877
- $$ LANGUAGE plpgsql;
3878
- `)
3879
- await backend.exec('SELECT "refreshMessageReactionStats"();')
3880
-
3881
- expect(http.sqls.some((sql) => sql.includes('"refreshMessageReactionStats"()'))).toBe(
3882
- false
3883
- )
3884
- expect(
3885
- http.sqls.some((sql) =>
3886
- compactSQL(sql).includes('SELECT NULL AS "refreshMessageReactionStats"')
3887
- )
3888
- ).toBe(true)
3889
- })
3890
- })