orez 0.1.7 → 0.1.9
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.
- package/README.md +2 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -6
- package/dist/index.js.map +1 -1
- package/dist/integration/test-permissions.d.ts +2 -0
- package/dist/integration/test-permissions.d.ts.map +1 -1
- package/dist/integration/test-permissions.js +28 -0
- package/dist/integration/test-permissions.js.map +1 -1
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +76 -1
- package/dist/pg-proxy.js.map +1 -1
- package/dist/sqlite-mode/native-binary.js +1 -1
- package/dist/sqlite-mode/native-binary.js.map +1 -1
- package/dist/sqlite-mode/package-resolve.d.ts +6 -0
- package/dist/sqlite-mode/package-resolve.d.ts.map +1 -0
- package/dist/sqlite-mode/package-resolve.js +20 -0
- package/dist/sqlite-mode/package-resolve.js.map +1 -0
- package/dist/sqlite-mode/resolve-mode.d.ts +12 -7
- package/dist/sqlite-mode/resolve-mode.d.ts.map +1 -1
- package/dist/sqlite-mode/resolve-mode.js +27 -23
- package/dist/sqlite-mode/resolve-mode.js.map +1 -1
- package/package.json +2 -2
- package/src/cli.ts +7 -1
- package/src/config.ts +2 -0
- package/src/index.ts +69 -6
- package/src/integration/integration.test.ts +49 -42
- package/src/integration/native-startup.test.ts +1 -1
- package/src/integration/restore-live-stress.test.ts +61 -65
- package/src/integration/restore-reset.test.ts +63 -66
- package/src/integration/test-permissions.ts +36 -0
- package/src/pg-proxy.ts +84 -1
- package/src/sqlite-mode/native-binary.ts +1 -1
- package/src/sqlite-mode/package-resolve.ts +17 -0
- package/src/sqlite-mode/resolve-mode.ts +31 -21
- package/src/sqlite-mode/sqlite-mode.test.ts +11 -5
|
@@ -18,11 +18,26 @@ import WebSocket from 'ws'
|
|
|
18
18
|
import { execDumpFile } from '../cli.js'
|
|
19
19
|
import { startZeroLite } from '../index.js'
|
|
20
20
|
import { installChangeTracking } from '../replication/change-tracker.js'
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
ensureTablesInPublications,
|
|
23
|
+
hasNonNullPermissions,
|
|
24
|
+
installAllowAllPermissions,
|
|
25
|
+
} from './test-permissions.js'
|
|
22
26
|
|
|
23
27
|
import type { PGlite } from '@electric-sql/pglite'
|
|
24
28
|
|
|
25
29
|
const SYNC_PROTOCOL_VERSION = 45
|
|
30
|
+
const LIVE_CLIENT_SCHEMA = {
|
|
31
|
+
tables: {
|
|
32
|
+
restore_live_probe: {
|
|
33
|
+
columns: {
|
|
34
|
+
id: { type: 'string' },
|
|
35
|
+
value: { type: 'string' },
|
|
36
|
+
},
|
|
37
|
+
primaryKey: ['id'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
26
41
|
|
|
27
42
|
function encodeSecProtocols(
|
|
28
43
|
initConnectionMessage: unknown,
|
|
@@ -128,78 +143,52 @@ function connectAndSubscribe(
|
|
|
128
143
|
query: Record<string, unknown>
|
|
129
144
|
): Promise<WebSocket> {
|
|
130
145
|
return new Promise((resolve, reject) => {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
const initConnectionMessage: [string, Record<string, unknown>] = [
|
|
147
|
+
'initConnection',
|
|
148
|
+
{
|
|
149
|
+
desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
|
|
150
|
+
clientSchema: LIVE_CLIENT_SCHEMA,
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
const secProtocol = encodeSecProtocols(initConnectionMessage, undefined)
|
|
154
|
+
const ws = new WebSocket(
|
|
134
155
|
`ws://127.0.0.1:${port}/sync/v${SYNC_PROTOCOL_VERSION}/connect` +
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const bootstrapProtocol = encodeSecProtocols(
|
|
140
|
-
['initConnection', { desiredQueriesPatch: [] }],
|
|
141
|
-
undefined
|
|
156
|
+
`?clientGroupID=restore-live-cg-${Date.now()}` +
|
|
157
|
+
`&clientID=restore-live-client` +
|
|
158
|
+
`&wsid=ws1&schemaVersion=1&baseCookie=&ts=${Date.now()}&lmid=0`,
|
|
159
|
+
secProtocol
|
|
142
160
|
)
|
|
143
|
-
const bootstrapWs = new WebSocket(`${urlBase}&wsid=bootstrap`, bootstrapProtocol)
|
|
144
161
|
|
|
145
|
-
|
|
146
|
-
|
|
162
|
+
let settled = false
|
|
163
|
+
const failTimer = setTimeout(() => {
|
|
164
|
+
if (settled) return
|
|
165
|
+
settled = true
|
|
147
166
|
try {
|
|
148
|
-
|
|
167
|
+
ws.close()
|
|
149
168
|
} catch {}
|
|
150
|
-
reject(
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const bootstrapTimer = setTimeout(() => {
|
|
154
|
-
fail(new Error('bootstrap websocket timeout'))
|
|
169
|
+
reject(new Error('websocket connected but no downstream messages'))
|
|
155
170
|
}, 7000)
|
|
156
|
-
bootstrapWs.once('error', fail)
|
|
157
|
-
bootstrapWs.once('message', () => {
|
|
158
|
-
clearTimeout(bootstrapTimer)
|
|
159
|
-
try {
|
|
160
|
-
bootstrapWs.close()
|
|
161
|
-
} catch {}
|
|
162
171
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
},
|
|
168
|
-
]
|
|
169
|
-
const secProtocol = encodeSecProtocols(initConnectionMessage, undefined)
|
|
170
|
-
const ws = new WebSocket(`${urlBase}&wsid=ws1`, secProtocol)
|
|
171
|
-
|
|
172
|
-
let settled = false
|
|
173
|
-
const failTimer = setTimeout(() => {
|
|
174
|
-
if (settled) return
|
|
175
|
-
settled = true
|
|
176
|
-
try {
|
|
177
|
-
ws.close()
|
|
178
|
-
} catch {}
|
|
179
|
-
reject(new Error('websocket connected but no downstream messages'))
|
|
180
|
-
}, 7000)
|
|
181
|
-
|
|
182
|
-
ws.on('message', (data) => {
|
|
183
|
-
const msg = JSON.parse(data.toString())
|
|
184
|
-
downstream.enqueue(msg)
|
|
185
|
-
if (!settled) {
|
|
186
|
-
settled = true
|
|
187
|
-
clearTimeout(failTimer)
|
|
188
|
-
resolve(ws)
|
|
189
|
-
}
|
|
190
|
-
})
|
|
191
|
-
ws.once('error', (err) => {
|
|
192
|
-
if (settled) return
|
|
172
|
+
ws.on('message', (data) => {
|
|
173
|
+
const msg = JSON.parse(data.toString())
|
|
174
|
+
downstream.enqueue(msg)
|
|
175
|
+
if (!settled) {
|
|
193
176
|
settled = true
|
|
194
177
|
clearTimeout(failTimer)
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
178
|
+
resolve(ws)
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
ws.once('error', (err) => {
|
|
182
|
+
if (settled) return
|
|
183
|
+
settled = true
|
|
184
|
+
clearTimeout(failTimer)
|
|
185
|
+
reject(err)
|
|
186
|
+
})
|
|
187
|
+
ws.once('close', () => {
|
|
188
|
+
if (settled) return
|
|
189
|
+
settled = true
|
|
190
|
+
clearTimeout(failTimer)
|
|
191
|
+
reject(new Error('websocket closed before initial downstream message'))
|
|
203
192
|
})
|
|
204
193
|
})
|
|
205
194
|
}
|
|
@@ -299,6 +288,7 @@ describe('live restore stress with connected frontend', { timeout: 360_000 }, ()
|
|
|
299
288
|
let zeroPort: number
|
|
300
289
|
let shutdown: () => Promise<void>
|
|
301
290
|
let restartZero: (() => Promise<void>) | undefined
|
|
291
|
+
let resetZeroFull: (() => Promise<void>) | undefined
|
|
302
292
|
let dataDir: string
|
|
303
293
|
let dumpFile: string
|
|
304
294
|
|
|
@@ -330,6 +320,7 @@ describe('live restore stress with connected frontend', { timeout: 360_000 }, ()
|
|
|
330
320
|
zeroPort = started.zeroPort
|
|
331
321
|
shutdown = started.stop
|
|
332
322
|
restartZero = started.restartZero
|
|
323
|
+
resetZeroFull = started.resetZeroFull
|
|
333
324
|
await waitForZero(zeroPort, 90_000)
|
|
334
325
|
}, 180_000)
|
|
335
326
|
|
|
@@ -352,8 +343,13 @@ describe('live restore stress with connected frontend', { timeout: 360_000 }, ()
|
|
|
352
343
|
value TEXT NOT NULL
|
|
353
344
|
)
|
|
354
345
|
`)
|
|
346
|
+
await ensureTablesInPublications(db, ['restore_live_probe'])
|
|
355
347
|
await installAllowAllPermissions(db, ['restore_live_probe'])
|
|
356
|
-
|
|
348
|
+
expect(await hasNonNullPermissions(db)).toBe(true)
|
|
349
|
+
if (resetZeroFull) {
|
|
350
|
+
await resetZeroFull()
|
|
351
|
+
await waitForZero(zeroPort, 90_000)
|
|
352
|
+
} else if (restartZero) {
|
|
357
353
|
await restartZero()
|
|
358
354
|
await waitForZero(zeroPort, 60_000)
|
|
359
355
|
}
|
|
@@ -18,10 +18,25 @@ import WebSocket from 'ws'
|
|
|
18
18
|
|
|
19
19
|
import { execDumpFile } from '../cli.js'
|
|
20
20
|
import { startZeroLite } from '../index.js'
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
ensureTablesInPublications,
|
|
23
|
+
hasNonNullPermissions,
|
|
24
|
+
installAllowAllPermissions,
|
|
25
|
+
} from './test-permissions.js'
|
|
22
26
|
|
|
23
27
|
// zero-cache protocol version (from @rocicorp/zero/out/zero-protocol/src/protocol-version.js)
|
|
24
28
|
const PROTOCOL_VERSION = 45
|
|
29
|
+
const RESET_CLIENT_SCHEMA = {
|
|
30
|
+
tables: {
|
|
31
|
+
reset_probe: {
|
|
32
|
+
columns: {
|
|
33
|
+
id: { type: 'string' },
|
|
34
|
+
value: { type: 'string' },
|
|
35
|
+
},
|
|
36
|
+
primaryKey: ['id'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
25
40
|
|
|
26
41
|
// encode initConnection message for sec-websocket-protocol header
|
|
27
42
|
// matches zero-protocol's encodeSecProtocols implementation
|
|
@@ -117,6 +132,7 @@ describe('restore/reset integration regression', { timeout: 150_000 }, () => {
|
|
|
117
132
|
let zeroPort: number
|
|
118
133
|
let shutdown: () => Promise<void>
|
|
119
134
|
let restartZero: (() => Promise<void>) | undefined
|
|
135
|
+
let resetZeroFull: (() => Promise<void>) | undefined
|
|
120
136
|
let dataDir: string
|
|
121
137
|
let dumpFile: string
|
|
122
138
|
let dumpFileIsTemp = false
|
|
@@ -143,6 +159,7 @@ describe('restore/reset integration regression', { timeout: 150_000 }, () => {
|
|
|
143
159
|
zeroPort = started.zeroPort
|
|
144
160
|
shutdown = started.stop
|
|
145
161
|
restartZero = started.restartZero
|
|
162
|
+
resetZeroFull = started.resetZeroFull
|
|
146
163
|
|
|
147
164
|
await waitForZero(zeroPort, 90_000)
|
|
148
165
|
}, 120_000)
|
|
@@ -205,6 +222,7 @@ describe('restore/reset integration regression', { timeout: 150_000 }, () => {
|
|
|
205
222
|
AFTER INSERT OR UPDATE OR DELETE ON public.reset_probe
|
|
206
223
|
FOR EACH STATEMENT EXECUTE FUNCTION public._zero_notify_change();
|
|
207
224
|
`)
|
|
225
|
+
await ensureTablesInPublications(db, ['reset_probe'])
|
|
208
226
|
const pubName = process.env.ZERO_APP_PUBLICATIONS?.trim()
|
|
209
227
|
if (pubName) {
|
|
210
228
|
const quotedPub = '"' + pubName.replace(/"/g, '""') + '"'
|
|
@@ -213,7 +231,11 @@ describe('restore/reset integration regression', { timeout: 150_000 }, () => {
|
|
|
213
231
|
.catch(() => {})
|
|
214
232
|
}
|
|
215
233
|
await installAllowAllPermissions(db, ['reset_probe'])
|
|
216
|
-
|
|
234
|
+
expect(await hasNonNullPermissions(db)).toBe(true)
|
|
235
|
+
if (resetZeroFull) {
|
|
236
|
+
await resetZeroFull()
|
|
237
|
+
await waitForZero(zeroPort, 90_000)
|
|
238
|
+
} else if (restartZero) {
|
|
217
239
|
await restartZero()
|
|
218
240
|
await waitForZero(zeroPort, 60_000)
|
|
219
241
|
}
|
|
@@ -259,79 +281,54 @@ function connectAndSubscribe(
|
|
|
259
281
|
const ts = Date.now()
|
|
260
282
|
const clientGroupID = `restore-reset-cg-${ts}`
|
|
261
283
|
const clientID = 'restore-reset-client'
|
|
262
|
-
const
|
|
284
|
+
const initConnectionMessage: [string, Record<string, unknown>] = [
|
|
285
|
+
'initConnection',
|
|
286
|
+
{
|
|
287
|
+
desiredQueriesPatch: [{ op: 'put', hash: 'q1', ast: query }],
|
|
288
|
+
clientSchema: RESET_CLIENT_SCHEMA,
|
|
289
|
+
},
|
|
290
|
+
]
|
|
291
|
+
const secProtocol = encodeSecProtocols(initConnectionMessage, undefined)
|
|
292
|
+
const ws = new WebSocket(
|
|
263
293
|
`ws://127.0.0.1:${port}/sync/v${PROTOCOL_VERSION}/connect` +
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// bootstrap the client group first so the query connection is not "new group"
|
|
267
|
-
const bootstrapProtocol = encodeSecProtocols(
|
|
268
|
-
['initConnection', { desiredQueriesPatch: [] }],
|
|
269
|
-
undefined
|
|
294
|
+
`?clientGroupID=${clientGroupID}&clientID=${clientID}&wsid=ws1&schemaVersion=1&baseCookie=&ts=${ts}&lmid=0`,
|
|
295
|
+
secProtocol
|
|
270
296
|
)
|
|
271
|
-
const bootstrapWs = new WebSocket(`${urlBase}&wsid=bootstrap`, bootstrapProtocol)
|
|
272
|
-
const bootstrapTimer = setTimeout(() => {
|
|
273
|
-
fail(new Error('bootstrap websocket timeout'))
|
|
274
|
-
}, 7000)
|
|
275
|
-
|
|
276
|
-
const fail = (err: unknown) => {
|
|
277
|
-
clearTimeout(bootstrapTimer)
|
|
278
|
-
try {
|
|
279
|
-
bootstrapWs.close()
|
|
280
|
-
} catch {}
|
|
281
|
-
reject(err)
|
|
282
|
-
}
|
|
283
297
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
298
|
+
let settled = false
|
|
299
|
+
let sawMessage = false
|
|
300
|
+
const failTimer = setTimeout(() => {
|
|
301
|
+
if (settled) return
|
|
302
|
+
settled = true
|
|
287
303
|
try {
|
|
288
|
-
|
|
304
|
+
ws.close()
|
|
289
305
|
} catch {}
|
|
306
|
+
reject(new Error('websocket connected but no downstream messages'))
|
|
307
|
+
}, 7000)
|
|
290
308
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
]
|
|
297
|
-
const secProtocol = encodeSecProtocols(initConnectionMessage, undefined)
|
|
298
|
-
const ws = new WebSocket(`${urlBase}&wsid=ws1`, secProtocol)
|
|
299
|
-
|
|
300
|
-
let settled = false
|
|
301
|
-
let sawMessage = false
|
|
302
|
-
const failTimer = setTimeout(() => {
|
|
303
|
-
if (settled) return
|
|
304
|
-
settled = true
|
|
305
|
-
try {
|
|
306
|
-
ws.close()
|
|
307
|
-
} catch {}
|
|
308
|
-
reject(new Error('websocket connected but no downstream messages'))
|
|
309
|
-
}, 7000)
|
|
310
|
-
|
|
311
|
-
ws.on('message', (data) => {
|
|
312
|
-
const msg = JSON.parse(data.toString())
|
|
313
|
-
downstream.enqueue(msg)
|
|
314
|
-
if (!sawMessage && !settled) {
|
|
315
|
-
sawMessage = true
|
|
316
|
-
settled = true
|
|
317
|
-
clearTimeout(failTimer)
|
|
318
|
-
resolve(ws)
|
|
319
|
-
}
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
ws.once('error', (err) => {
|
|
323
|
-
if (settled) return
|
|
309
|
+
ws.on('message', (data) => {
|
|
310
|
+
const msg = JSON.parse(data.toString())
|
|
311
|
+
downstream.enqueue(msg)
|
|
312
|
+
if (!sawMessage && !settled) {
|
|
313
|
+
sawMessage = true
|
|
324
314
|
settled = true
|
|
325
315
|
clearTimeout(failTimer)
|
|
326
|
-
|
|
327
|
-
}
|
|
316
|
+
resolve(ws)
|
|
317
|
+
}
|
|
318
|
+
})
|
|
328
319
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
320
|
+
ws.once('error', (err) => {
|
|
321
|
+
if (settled) return
|
|
322
|
+
settled = true
|
|
323
|
+
clearTimeout(failTimer)
|
|
324
|
+
reject(err)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
ws.once('close', () => {
|
|
328
|
+
if (settled) return
|
|
329
|
+
settled = true
|
|
330
|
+
clearTimeout(failTimer)
|
|
331
|
+
reject(new Error('websocket closed before initial downstream message'))
|
|
335
332
|
})
|
|
336
333
|
})
|
|
337
334
|
}
|
|
@@ -84,6 +84,42 @@ export async function installAllowAllPermissions(
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
export async function hasNonNullPermissions(db: DbLike): Promise<boolean> {
|
|
88
|
+
const schemas = await findPermissionsSchemas(db)
|
|
89
|
+
for (const schema of schemas) {
|
|
90
|
+
const quotedSchema = '"' + schema.replace(/"/g, '""') + '"'
|
|
91
|
+
const result = await db.query<{ has_permissions: boolean }>(
|
|
92
|
+
`SELECT (permissions IS NOT NULL) AS has_permissions
|
|
93
|
+
FROM ${quotedSchema}.permissions
|
|
94
|
+
WHERE lock = true
|
|
95
|
+
LIMIT 1`
|
|
96
|
+
)
|
|
97
|
+
if (result.rows[0]?.has_permissions) return true
|
|
98
|
+
}
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function ensureTablesInPublications(
|
|
103
|
+
db: DbLike,
|
|
104
|
+
tables: string[]
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
const pubs = await db.query<{ pubname: string }>(
|
|
107
|
+
`SELECT pubname
|
|
108
|
+
FROM pg_publication
|
|
109
|
+
WHERE pubname NOT LIKE '%metadata%'
|
|
110
|
+
ORDER BY pubname`
|
|
111
|
+
)
|
|
112
|
+
for (const { pubname } of pubs.rows) {
|
|
113
|
+
const quotedPub = '"' + pubname.replace(/"/g, '""') + '"'
|
|
114
|
+
for (const table of tables) {
|
|
115
|
+
const quotedTable = '"' + table.replace(/"/g, '""') + '"'
|
|
116
|
+
await db
|
|
117
|
+
.exec(`ALTER PUBLICATION ${quotedPub} ADD TABLE "public".${quotedTable}`)
|
|
118
|
+
.catch(() => {})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
87
123
|
function parsePermissions(value: unknown): { tables?: Record<string, unknown> } {
|
|
88
124
|
if (!value) return {}
|
|
89
125
|
if (typeof value === 'string') {
|
package/src/pg-proxy.ts
CHANGED
|
@@ -261,6 +261,43 @@ function readInt32BE(data: Uint8Array, offset: number): number {
|
|
|
261
261
|
)
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// pglite transaction state warnings to suppress (benign, but noisy)
|
|
265
|
+
// 25001: "there is already a transaction in progress"
|
|
266
|
+
// 25P01: "there is no transaction in progress"
|
|
267
|
+
const SUPPRESS_NOTICE_CODES = new Set(['25001', '25P01'])
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* extract SQLSTATE code from a NoticeResponse message.
|
|
271
|
+
* returns null if not a NoticeResponse or code not found.
|
|
272
|
+
*/
|
|
273
|
+
function extractNoticeCode(
|
|
274
|
+
data: Uint8Array,
|
|
275
|
+
offset: number,
|
|
276
|
+
totalLen: number
|
|
277
|
+
): string | null {
|
|
278
|
+
if (data[offset] !== 0x4e) return null // not a NoticeResponse
|
|
279
|
+
|
|
280
|
+
let pos = offset + 5 // skip type byte + length
|
|
281
|
+
const end = offset + totalLen
|
|
282
|
+
|
|
283
|
+
while (pos < end) {
|
|
284
|
+
const fieldType = data[pos++]
|
|
285
|
+
if (fieldType === 0) break // terminator
|
|
286
|
+
|
|
287
|
+
// find null-terminated string
|
|
288
|
+
const strStart = pos
|
|
289
|
+
while (pos < end && data[pos] !== 0) pos++
|
|
290
|
+
if (pos >= end) break
|
|
291
|
+
|
|
292
|
+
if (fieldType === 0x43) {
|
|
293
|
+
// 'C' = SQLSTATE code
|
|
294
|
+
return new TextDecoder().decode(data.subarray(strStart, pos))
|
|
295
|
+
}
|
|
296
|
+
pos++ // skip null terminator
|
|
297
|
+
}
|
|
298
|
+
return null
|
|
299
|
+
}
|
|
300
|
+
|
|
264
301
|
/**
|
|
265
302
|
* strip ReadyForQuery messages from a response buffer.
|
|
266
303
|
*/
|
|
@@ -297,6 +334,48 @@ function stripReadyForQuery(data: Uint8Array): Uint8Array {
|
|
|
297
334
|
return result
|
|
298
335
|
}
|
|
299
336
|
|
|
337
|
+
/**
|
|
338
|
+
* strip NoticeResponse messages with specific SQLSTATE codes from a response buffer.
|
|
339
|
+
* pglite emits benign transaction state warnings that we suppress to reduce noise.
|
|
340
|
+
*/
|
|
341
|
+
function stripTransactionWarnings(data: Uint8Array): Uint8Array {
|
|
342
|
+
if (data.length === 0) return data
|
|
343
|
+
|
|
344
|
+
const parts: Uint8Array[] = []
|
|
345
|
+
let offset = 0
|
|
346
|
+
let stripped = false
|
|
347
|
+
|
|
348
|
+
while (offset < data.length) {
|
|
349
|
+
if (offset + 5 > data.length) break
|
|
350
|
+
const msgLen = readInt32BE(data, offset + 1)
|
|
351
|
+
const totalLen = 1 + msgLen
|
|
352
|
+
|
|
353
|
+
if (totalLen <= 0 || offset + totalLen > data.length) break
|
|
354
|
+
|
|
355
|
+
const code = extractNoticeCode(data, offset, totalLen)
|
|
356
|
+
if (code && SUPPRESS_NOTICE_CODES.has(code)) {
|
|
357
|
+
stripped = true
|
|
358
|
+
} else {
|
|
359
|
+
parts.push(data.subarray(offset, offset + totalLen))
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
offset += totalLen
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!stripped) return data
|
|
366
|
+
if (parts.length === 0) return new Uint8Array(0)
|
|
367
|
+
if (parts.length === 1) return parts[0]
|
|
368
|
+
|
|
369
|
+
const total = parts.reduce((sum, p) => sum + p.length, 0)
|
|
370
|
+
const result = new Uint8Array(total)
|
|
371
|
+
let pos = 0
|
|
372
|
+
for (const p of parts) {
|
|
373
|
+
result.set(p, pos)
|
|
374
|
+
pos += p.length
|
|
375
|
+
}
|
|
376
|
+
return result
|
|
377
|
+
}
|
|
378
|
+
|
|
300
379
|
export async function startPgProxy(
|
|
301
380
|
dbInput: PGlite | PGliteInstances,
|
|
302
381
|
config: ZeroLiteConfig
|
|
@@ -436,6 +515,9 @@ export async function startPgProxy(
|
|
|
436
515
|
throw err
|
|
437
516
|
}
|
|
438
517
|
|
|
518
|
+
// strip benign transaction state warnings from pglite
|
|
519
|
+
result = stripTransactionWarnings(result)
|
|
520
|
+
|
|
439
521
|
// strip ReadyForQuery from non-Sync/non-SimpleQuery responses
|
|
440
522
|
if (data[0] !== 0x53 && data[0] !== 0x51) {
|
|
441
523
|
result = stripReadyForQuery(result)
|
|
@@ -510,9 +592,10 @@ async function handleReplicationMessage(
|
|
|
510
592
|
data = interceptQuery(data)
|
|
511
593
|
|
|
512
594
|
// fall through to pglite for unrecognized queries
|
|
513
|
-
|
|
595
|
+
const result = await db.execProtocolRaw(data, {
|
|
514
596
|
throwOnError: false,
|
|
515
597
|
})
|
|
598
|
+
return stripTransactionWarnings(result)
|
|
516
599
|
} finally {
|
|
517
600
|
mutex.release()
|
|
518
601
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, resolve } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import { resolvePackage } from './resolve
|
|
4
|
+
import { resolvePackage } from './package-resolve.js'
|
|
5
5
|
|
|
6
6
|
const NATIVE_BINARY_RELATIVE_PATHS = ['build/Release/better_sqlite3.node']
|
|
7
7
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* resolve a package entry path
|
|
5
|
+
* import.meta.resolve doesn't work in vitest, so we fall back to require.resolve
|
|
6
|
+
*/
|
|
7
|
+
export function resolvePackage(pkg: string): string {
|
|
8
|
+
try {
|
|
9
|
+
const resolved = import.meta.resolve(pkg)
|
|
10
|
+
if (resolved) return resolved.replace('file://', '')
|
|
11
|
+
} catch {}
|
|
12
|
+
try {
|
|
13
|
+
const require = createRequire(import.meta.url)
|
|
14
|
+
return require.resolve(pkg)
|
|
15
|
+
} catch {}
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
@@ -1,33 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* mode resolution - canonical place to determine sqlite mode from config/env
|
|
3
|
+
*
|
|
4
|
+
* priority:
|
|
5
|
+
* 1. explicit --disable-wasm-sqlite flag → native
|
|
6
|
+
* 2. explicit --force-wasm-sqlite flag → wasm
|
|
7
|
+
* 3. native binary available → native (auto-detect)
|
|
8
|
+
* 4. fallback → wasm
|
|
3
9
|
*/
|
|
4
10
|
|
|
5
|
-
import {
|
|
11
|
+
import { inspectNativeSqliteBinary } from './native-binary.js'
|
|
12
|
+
import { resolvePackage } from './package-resolve.js'
|
|
6
13
|
|
|
7
14
|
import type { SqliteMode, SqliteModeConfig } from './types.js'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* resolve a package entry path
|
|
11
|
-
* import.meta.resolve doesn't work in vitest, so we fall back to require.resolve
|
|
12
|
-
*/
|
|
13
|
-
export function resolvePackage(pkg: string): string {
|
|
14
|
-
try {
|
|
15
|
-
const resolved = import.meta.resolve(pkg)
|
|
16
|
-
if (resolved) return resolved.replace('file://', '')
|
|
17
|
-
} catch {}
|
|
18
|
-
try {
|
|
19
|
-
const require = createRequire(import.meta.url)
|
|
20
|
-
return require.resolve(pkg)
|
|
21
|
-
} catch {}
|
|
22
|
-
return ''
|
|
23
|
-
}
|
|
15
|
+
export { resolvePackage } from './package-resolve.js'
|
|
24
16
|
|
|
25
17
|
/**
|
|
26
18
|
* resolve sqlite mode from config
|
|
27
19
|
* single source of truth for mode selection
|
|
20
|
+
*
|
|
21
|
+
* @param disableWasmSqlite - explicit flag to force native mode
|
|
22
|
+
* @param forceWasmSqlite - explicit flag to force wasm mode (overrides auto-detect)
|
|
28
23
|
*/
|
|
29
|
-
export function resolveSqliteMode(
|
|
30
|
-
|
|
24
|
+
export function resolveSqliteMode(
|
|
25
|
+
disableWasmSqlite: boolean,
|
|
26
|
+
forceWasmSqlite: boolean = false
|
|
27
|
+
): SqliteMode {
|
|
28
|
+
// explicit native request
|
|
29
|
+
if (disableWasmSqlite) return 'native'
|
|
30
|
+
|
|
31
|
+
// explicit wasm request
|
|
32
|
+
if (forceWasmSqlite) return 'wasm'
|
|
33
|
+
|
|
34
|
+
// auto-detect: prefer native if binary is available
|
|
35
|
+
const nativeCheck = inspectNativeSqliteBinary()
|
|
36
|
+
if (nativeCheck.found) return 'native'
|
|
37
|
+
|
|
38
|
+
// fallback to wasm
|
|
39
|
+
return 'wasm'
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
/**
|
|
@@ -35,9 +44,10 @@ export function resolveSqliteMode(disableWasmSqlite: boolean): SqliteMode {
|
|
|
35
44
|
* returns null if required packages aren't installed
|
|
36
45
|
*/
|
|
37
46
|
export function resolveSqliteModeConfig(
|
|
38
|
-
disableWasmSqlite: boolean
|
|
47
|
+
disableWasmSqlite: boolean,
|
|
48
|
+
forceWasmSqlite: boolean = false
|
|
39
49
|
): SqliteModeConfig | null {
|
|
40
|
-
const mode = resolveSqliteMode(disableWasmSqlite)
|
|
50
|
+
const mode = resolveSqliteMode(disableWasmSqlite, forceWasmSqlite)
|
|
41
51
|
const zeroSqlitePath = resolvePackage('@rocicorp/zero-sqlite3') || undefined
|
|
42
52
|
|
|
43
53
|
// native mode may still need zero-sqlite3 path for restoring from a prior shim
|
|
@@ -28,16 +28,22 @@ describe('sqlite mode types', () => {
|
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
describe('sqlite mode resolution', () => {
|
|
31
|
-
it('resolves
|
|
32
|
-
expect(resolveSqliteMode(false)).toBe('
|
|
31
|
+
it('resolves native mode when disableWasmSqlite is true', () => {
|
|
32
|
+
expect(resolveSqliteMode(true, false)).toBe('native')
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
it('resolves
|
|
36
|
-
expect(resolveSqliteMode(true)).toBe('
|
|
35
|
+
it('resolves wasm mode when forceWasmSqlite is true', () => {
|
|
36
|
+
expect(resolveSqliteMode(false, true)).toBe('wasm')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('auto-detects mode based on native binary availability', () => {
|
|
40
|
+
// when neither flag is set, mode depends on whether native binary exists
|
|
41
|
+
const mode = resolveSqliteMode(false, false)
|
|
42
|
+
expect(['wasm', 'native']).toContain(mode)
|
|
37
43
|
})
|
|
38
44
|
|
|
39
45
|
it('returns config with mode for native', () => {
|
|
40
|
-
const config = resolveSqliteModeConfig(true)
|
|
46
|
+
const config = resolveSqliteModeConfig(true, false)
|
|
41
47
|
expect(config).not.toBeNull()
|
|
42
48
|
expect(config?.mode).toBe('native')
|
|
43
49
|
})
|