orez 0.2.27 → 0.2.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -4
- package/src/admin/admin-data.test.ts +0 -348
- package/src/admin/http-proxy.ts +0 -252
- package/src/admin/log-store.ts +0 -192
- package/src/admin/server.ts +0 -471
- package/src/admin/ui.ts +0 -1322
- package/src/bench/proxy-throughput.bench.ts +0 -343
- package/src/bench/serial-mutations.bench.ts +0 -270
- package/src/browser.ts +0 -203
- package/src/cf-do/.wrangler/cache/cf.json +0 -1
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
- package/src/cf-do/ARCHITECTURE.md +0 -93
- package/src/cf-do/CHAT_E2E.md +0 -213
- package/src/cf-do/watermark.test.ts +0 -103
- package/src/cf-do/watermark.ts +0 -118
- package/src/cf-do/worker.ts +0 -1041
- package/src/cf-do/wrangler.toml +0 -11
- package/src/cf-pglite/README.md +0 -19
- package/src/change-tracking.ts +0 -25
- package/src/child-process.test.ts +0 -147
- package/src/child-process.ts +0 -90
- package/src/cli-entry.ts +0 -72
- package/src/cli.test.ts +0 -40
- package/src/cli.ts +0 -1214
- package/src/config.ts +0 -150
- package/src/do-sql-tracking.test.ts +0 -19
- package/src/do-sql-tracking.ts +0 -19
- package/src/index.ts +0 -1215
- package/src/integration/integration.test.ts +0 -517
- package/src/integration/native-binary.guard.test.ts +0 -13
- package/src/integration/native-startup.test.ts +0 -44
- package/src/integration/replication-latency.test.ts +0 -428
- package/src/integration/restore-live-stress.test.ts +0 -433
- package/src/integration/restore-reset.test.ts +0 -400
- package/src/integration/restore.test.ts +0 -274
- package/src/integration/test-permissions.ts +0 -147
- package/src/load-config.ts +0 -46
- package/src/log.ts +0 -96
- package/src/mutex.ts +0 -47
- package/src/pg-proxy-browser.singledb.test.ts +0 -233
- package/src/pg-proxy-browser.ts +0 -2022
- package/src/pg-proxy-do-backend.test.ts +0 -3890
- package/src/pg-proxy-do-backend.ts +0 -7191
- package/src/pg-proxy.ts +0 -1087
- package/src/pg-sqlite-compiler/README.md +0 -53
- package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
- package/src/pg-sqlite-compiler/index.ts +0 -73
- package/src/pg-sqlite-compiler/integration.test.ts +0 -136
- package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
- package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
- package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
- package/src/pg-sqlite-compiler/passes/index.ts +0 -49
- package/src/pg-sqlite-compiler/passes/types.ts +0 -156
- package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
- package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
- package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
- package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
- package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
- package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
- package/src/pg-sqlite-compiler/types.ts +0 -63
- package/src/pglite-ipc.test.ts +0 -116
- package/src/pglite-ipc.ts +0 -266
- package/src/pglite-manager.ts +0 -557
- package/src/pglite-web-proxy.test.ts +0 -57
- package/src/pglite-web-proxy.ts +0 -221
- package/src/pglite-web-worker.ts +0 -152
- package/src/pglite-worker-thread.ts +0 -253
- package/src/port.ts +0 -25
- package/src/process-title.ts +0 -9
- package/src/recovery.ts +0 -155
- package/src/replication/change-tracker.test.ts +0 -357
- package/src/replication/change-tracker.ts +0 -279
- package/src/replication/handler.test.ts +0 -511
- package/src/replication/handler.ts +0 -1190
- package/src/replication/pgoutput-encoder.test.ts +0 -697
- package/src/replication/pgoutput-encoder.ts +0 -373
- package/src/replication/tcp-replication.test.ts +0 -876
- package/src/replication/zero-compat.test.ts +0 -1150
- package/src/restore-stress.test.ts +0 -188
- package/src/s3-local.ts +0 -203
- package/src/shim/hooks.mjs +0 -120
- package/src/shim/register.mjs +0 -4
- package/src/sqlite-mode/apply-mode.ts +0 -224
- package/src/sqlite-mode/index.ts +0 -15
- package/src/sqlite-mode/native-binary.ts +0 -89
- package/src/sqlite-mode/package-resolve.ts +0 -17
- package/src/sqlite-mode/resolve-mode.ts +0 -80
- package/src/sqlite-mode/shim-template.ts +0 -159
- package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
- package/src/sqlite-mode/types.ts +0 -30
- package/src/vite-plugin.ts +0 -67
- package/src/wasm-sqlite.test.ts +0 -537
- package/src/worker/browser-admin.ts +0 -52
- package/src/worker/browser-build-config.test.ts +0 -71
- package/src/worker/browser-build-config.ts +0 -109
- package/src/worker/browser-embed-admin.test.ts +0 -75
- package/src/worker/browser-embed.ts +0 -345
- package/src/worker/cf-patches.ts +0 -384
- package/src/worker/embed-integration.test.ts +0 -321
- package/src/worker/index.ts +0 -138
- package/src/worker/shims/fastify.test.ts +0 -255
- package/src/worker/shims/fastify.ts +0 -306
- package/src/worker/shims/http-service.test.ts +0 -355
- package/src/worker/shims/http-service.ts +0 -293
- package/src/worker/shims/node-stub.ts +0 -290
- package/src/worker/shims/oxfmt.ts +0 -3
- package/src/worker/shims/postgres-browser.ts +0 -59
- package/src/worker/shims/postgres-socket.test.ts +0 -576
- package/src/worker/shims/postgres-socket.ts +0 -310
- package/src/worker/shims/postgres.test.ts +0 -364
- package/src/worker/shims/postgres.ts +0 -1454
- package/src/worker/shims/sqlite-browser.test.ts +0 -233
- package/src/worker/shims/sqlite-browser.ts +0 -175
- package/src/worker/shims/sqlite.test.ts +0 -786
- package/src/worker/shims/sqlite.ts +0 -978
- package/src/worker/shims/stream-browser.ts +0 -15
- package/src/worker/shims/ws-browser.test.ts +0 -205
- package/src/worker/shims/ws-browser.ts +0 -248
- package/src/worker/shims/ws.test.ts +0 -288
- package/src/worker/shims/ws.ts +0 -467
- package/src/worker/shims/zero-process-env.ts +0 -11
- package/src/worker/types.ts +0 -75
- package/src/worker/worker-integration.test.ts +0 -223
- package/src/worker/worker.test.ts +0 -136
- package/src/worker/zero-cache-embed-cf.ts +0 -463
- package/src/worker/zero-cache-embed.ts +0 -277
|
@@ -1,1454 +0,0 @@
|
|
|
1
|
-
// NOTE THIS IS NOT OREZ NODE THIS IS NOT A GOOD REFERENCE BECAUSE ITS OUR EARLY GUESS AT WHAT COULD WORK
|
|
2
|
-
// DO NOT STUDY THIS, THE OTHER STUFF IN SRC IS WHERE YOU EANT TO LOOK
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* postgres shim for cloudflare workers.
|
|
6
|
-
*
|
|
7
|
-
* wraps a PGlite instance to implement the `postgres` npm package API
|
|
8
|
-
* that zero-cache uses. enables bundler aliasing so zero-cache talks to
|
|
9
|
-
* PGlite instead of a real postgres server.
|
|
10
|
-
*
|
|
11
|
-
* usage with bundler alias:
|
|
12
|
-
* alias: { 'postgres': './src/worker/shims/postgres.js' }
|
|
13
|
-
*
|
|
14
|
-
* usage directly:
|
|
15
|
-
* import { createPostgresShim } from 'orez/worker/shims/postgres'
|
|
16
|
-
* const sql = createPostgresShim(pglite)
|
|
17
|
-
* const rows = await sql`SELECT * FROM users WHERE id = ${id}`
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { PassThrough } from 'stream'
|
|
21
|
-
|
|
22
|
-
import { Mutex } from '../../mutex.js'
|
|
23
|
-
import {
|
|
24
|
-
handleStartReplication,
|
|
25
|
-
signalReplicationChange,
|
|
26
|
-
} from '../../replication/handler.js'
|
|
27
|
-
|
|
28
|
-
// debounced signal — matches orez pg-proxy's 8ms debounce.
|
|
29
|
-
// ensures signal fires even during long-running mutagen callbacks.
|
|
30
|
-
let _signalTimer: ReturnType<typeof setTimeout> | null = null
|
|
31
|
-
function debouncedSignal() {
|
|
32
|
-
if (_signalTimer) return
|
|
33
|
-
_signalTimer = setTimeout(() => {
|
|
34
|
-
_signalTimer = null
|
|
35
|
-
signalReplicationChange()
|
|
36
|
-
}, 8)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
import type { PGlite, Results, Transaction } from '@electric-sql/pglite'
|
|
40
|
-
|
|
41
|
-
// -- PostgresError --
|
|
42
|
-
|
|
43
|
-
export class PostgresError extends Error {
|
|
44
|
-
name = 'PostgresError' as const
|
|
45
|
-
severity_local: string
|
|
46
|
-
severity: string
|
|
47
|
-
code: string
|
|
48
|
-
position: string
|
|
49
|
-
file: string
|
|
50
|
-
line: string
|
|
51
|
-
routine: string
|
|
52
|
-
detail?: string
|
|
53
|
-
hint?: string
|
|
54
|
-
schema_name?: string
|
|
55
|
-
table_name?: string
|
|
56
|
-
column_name?: string
|
|
57
|
-
constraint_name?: string
|
|
58
|
-
query: string
|
|
59
|
-
parameters: unknown[]
|
|
60
|
-
|
|
61
|
-
constructor(info: {
|
|
62
|
-
message?: string
|
|
63
|
-
code?: string
|
|
64
|
-
severity?: string
|
|
65
|
-
detail?: string
|
|
66
|
-
hint?: string
|
|
67
|
-
[key: string]: unknown
|
|
68
|
-
}) {
|
|
69
|
-
super(info.message || 'postgres error')
|
|
70
|
-
this.severity_local = (info.severity as string) || 'ERROR'
|
|
71
|
-
this.severity = (info.severity as string) || 'ERROR'
|
|
72
|
-
this.code = (info.code as string) || '00000'
|
|
73
|
-
this.position = (info.position as string) || ''
|
|
74
|
-
this.file = (info.file as string) || ''
|
|
75
|
-
this.line = (info.line as string) || ''
|
|
76
|
-
this.routine = (info.routine as string) || ''
|
|
77
|
-
this.detail = info.detail as string | undefined
|
|
78
|
-
this.hint = info.hint as string | undefined
|
|
79
|
-
this.schema_name = info.schema_name as string | undefined
|
|
80
|
-
this.table_name = info.table_name as string | undefined
|
|
81
|
-
this.column_name = info.column_name as string | undefined
|
|
82
|
-
this.constraint_name = info.constraint_name as string | undefined
|
|
83
|
-
this.query = (info.query as string) || ''
|
|
84
|
-
this.parameters = (info.parameters as unknown[]) || []
|
|
85
|
-
Object.assign(this, info)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// -- Identifier --
|
|
90
|
-
// returned by sql(string) for dynamic identifier escaping
|
|
91
|
-
|
|
92
|
-
class Identifier {
|
|
93
|
-
value: string
|
|
94
|
-
constructor(value: string) {
|
|
95
|
-
this.value = escapeIdentifier(value)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function escapeIdentifier(str: string): string {
|
|
100
|
-
return '"' + str.replace(/"/g, '""').replace(/\./g, '"."') + '"'
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// -- result array --
|
|
104
|
-
// creates an array of rows that also has metadata properties (count, command, columns, statement, state)
|
|
105
|
-
// matching the RowList type from postgres
|
|
106
|
-
|
|
107
|
-
interface ResultMeta {
|
|
108
|
-
count: number
|
|
109
|
-
command: string
|
|
110
|
-
state: { status: string; pid: number; secret: number }
|
|
111
|
-
statement: { name: string; string: string; types: number[]; columns: ColumnMeta[] }
|
|
112
|
-
columns: ColumnMeta[]
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
interface ColumnMeta {
|
|
116
|
-
name: string
|
|
117
|
-
type: number
|
|
118
|
-
table: number
|
|
119
|
-
number: number
|
|
120
|
-
parser?: ((raw: string) => unknown) | undefined
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
type ResultArray<T = Record<string, unknown>> = T[] & ResultMeta
|
|
124
|
-
|
|
125
|
-
function createResultArray<T extends Record<string, unknown>>(
|
|
126
|
-
pgliteResult: Results<T>,
|
|
127
|
-
queryString: string
|
|
128
|
-
): ResultArray<T> {
|
|
129
|
-
// guard against undefined/null results (e.g. DDL on PGlite proxy)
|
|
130
|
-
if (!pgliteResult) {
|
|
131
|
-
const empty = [] as unknown as ResultArray<T>
|
|
132
|
-
empty.count = 0
|
|
133
|
-
empty.command = detectCommand(queryString)
|
|
134
|
-
empty.state = { status: 'idle', pid: 0, secret: 0 }
|
|
135
|
-
empty.statement = { name: '', string: queryString, types: [], columns: [] }
|
|
136
|
-
empty.columns = []
|
|
137
|
-
return empty
|
|
138
|
-
}
|
|
139
|
-
const rows = pgliteResult.rows
|
|
140
|
-
const columns: ColumnMeta[] = (pgliteResult.fields || []).map((f, i) => ({
|
|
141
|
-
name: f.name,
|
|
142
|
-
type: f.dataTypeID,
|
|
143
|
-
table: 0,
|
|
144
|
-
number: i,
|
|
145
|
-
}))
|
|
146
|
-
|
|
147
|
-
// create a proper array with rows as elements
|
|
148
|
-
const result = [...rows] as ResultArray<T>
|
|
149
|
-
|
|
150
|
-
// attach metadata
|
|
151
|
-
const command = detectCommand(queryString)
|
|
152
|
-
// for SELECT queries affectedRows is 0, use row count instead
|
|
153
|
-
result.count =
|
|
154
|
-
command === 'SELECT' || !pgliteResult.affectedRows
|
|
155
|
-
? rows.length
|
|
156
|
-
: pgliteResult.affectedRows
|
|
157
|
-
result.command = command
|
|
158
|
-
result.state = { status: 'idle', pid: 0, secret: 0 }
|
|
159
|
-
result.statement = {
|
|
160
|
-
name: '',
|
|
161
|
-
string: queryString,
|
|
162
|
-
types: [],
|
|
163
|
-
columns,
|
|
164
|
-
}
|
|
165
|
-
result.columns = columns
|
|
166
|
-
|
|
167
|
-
return result
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function detectCommand(sql: string): string {
|
|
171
|
-
const trimmed = sql.trimStart().toUpperCase()
|
|
172
|
-
if (trimmed.startsWith('SELECT')) return 'SELECT'
|
|
173
|
-
if (trimmed.startsWith('INSERT')) return 'INSERT'
|
|
174
|
-
if (trimmed.startsWith('UPDATE')) return 'UPDATE'
|
|
175
|
-
if (trimmed.startsWith('DELETE')) return 'DELETE'
|
|
176
|
-
if (trimmed.startsWith('CREATE')) return 'CREATE'
|
|
177
|
-
if (trimmed.startsWith('DROP')) return 'DROP'
|
|
178
|
-
if (trimmed.startsWith('ALTER')) return 'ALTER'
|
|
179
|
-
return trimmed.split(/\s/)[0] || 'SELECT'
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// -- multi-statement detection --
|
|
183
|
-
// detects if a query string contains multiple SQL statements.
|
|
184
|
-
// strips string literals and comments first to avoid false positives
|
|
185
|
-
// from semicolons inside quoted strings.
|
|
186
|
-
|
|
187
|
-
function hasMultipleStatements(sql: string): boolean {
|
|
188
|
-
// strip dollar-quoted strings ($$ ... $$)
|
|
189
|
-
let stripped = sql.replace(/(\$[a-zA-Z_]*\$)([\s\S]*?)\1/g, '')
|
|
190
|
-
// strip string literals (single-quoted, with '' escape)
|
|
191
|
-
stripped = stripped.replace(/'(?:[^']|'')*'/g, '')
|
|
192
|
-
// strip double-quoted identifiers
|
|
193
|
-
stripped = stripped.replace(/"(?:[^"]|"")*"/g, '')
|
|
194
|
-
// strip -- line comments
|
|
195
|
-
stripped = stripped.replace(/--[^\n]*/g, '')
|
|
196
|
-
// strip /* block comments */
|
|
197
|
-
stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
198
|
-
|
|
199
|
-
// check if there are multiple non-empty statements
|
|
200
|
-
const statements = stripped
|
|
201
|
-
.split(';')
|
|
202
|
-
.map((s) => s.trim())
|
|
203
|
-
.filter((s) => s.length > 0)
|
|
204
|
-
|
|
205
|
-
return statements.length > 1
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// -- parameter serialization --
|
|
209
|
-
// convert js values to postgres-compatible parameter values
|
|
210
|
-
|
|
211
|
-
function serializeParam(value: unknown): unknown {
|
|
212
|
-
if (value === null || value === undefined) return null
|
|
213
|
-
if (value instanceof Identifier) return value // handled in template assembly
|
|
214
|
-
if (typeof value === 'bigint') return value.toString()
|
|
215
|
-
if (
|
|
216
|
-
typeof value === 'object' &&
|
|
217
|
-
!(value instanceof Date) &&
|
|
218
|
-
!Array.isArray(value) &&
|
|
219
|
-
!ArrayBuffer.isView(value)
|
|
220
|
-
) {
|
|
221
|
-
return JSON.stringify(value)
|
|
222
|
-
}
|
|
223
|
-
return value
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// -- template tag to parameterized query conversion --
|
|
227
|
-
// sql`SELECT * FROM foo WHERE id = ${id} AND name = ${name}`
|
|
228
|
-
// becomes: { text: 'SELECT * FROM foo WHERE id = $1 AND name = $2', params: [id, name] }
|
|
229
|
-
|
|
230
|
-
function buildQuery(
|
|
231
|
-
strings: TemplateStringsArray,
|
|
232
|
-
values: unknown[]
|
|
233
|
-
): { text: string; params: unknown[] } {
|
|
234
|
-
const params: unknown[] = []
|
|
235
|
-
let text = ''
|
|
236
|
-
|
|
237
|
-
for (let i = 0; i < strings.length; i++) {
|
|
238
|
-
text += strings[i]
|
|
239
|
-
if (i < values.length) {
|
|
240
|
-
const val = values[i]
|
|
241
|
-
if (val instanceof Identifier) {
|
|
242
|
-
// identifiers are inlined (already escaped)
|
|
243
|
-
text += val.value
|
|
244
|
-
} else if (
|
|
245
|
-
val &&
|
|
246
|
-
typeof val === 'object' &&
|
|
247
|
-
'_isHelper' in val &&
|
|
248
|
-
(val as any)._isHelper
|
|
249
|
-
) {
|
|
250
|
-
// sql(object) helper — expand based on preceding SQL context
|
|
251
|
-
const helper = val as {
|
|
252
|
-
_isHelper: true
|
|
253
|
-
_data: Record<string, any>
|
|
254
|
-
toInsert: () => any
|
|
255
|
-
toUpdate: () => any
|
|
256
|
-
}
|
|
257
|
-
const before = text.trimEnd().toUpperCase()
|
|
258
|
-
if (
|
|
259
|
-
/\)\s*$/.test(before) ||
|
|
260
|
-
/SET\s*$/i.test(before) ||
|
|
261
|
-
/DO\s+UPDATE\s+SET\s*$/i.test(before)
|
|
262
|
-
) {
|
|
263
|
-
// UPDATE SET context: col1 = $1, col2 = $2
|
|
264
|
-
const { set, params: updateParams } = helper.toUpdate()
|
|
265
|
-
// rebase placeholder indices
|
|
266
|
-
const rebasedSet = set.replace(
|
|
267
|
-
/\$(\d+)/g,
|
|
268
|
-
(_: string, n: string) => `$${params.length + Number(n)}`
|
|
269
|
-
)
|
|
270
|
-
text += rebasedSet
|
|
271
|
-
params.push(...updateParams.map(serializeParam))
|
|
272
|
-
} else {
|
|
273
|
-
// INSERT context: (col1, col2) VALUES ($1, $2)
|
|
274
|
-
const {
|
|
275
|
-
columns,
|
|
276
|
-
values: placeholders,
|
|
277
|
-
params: insertParams,
|
|
278
|
-
} = helper.toInsert()
|
|
279
|
-
// rebase placeholder indices
|
|
280
|
-
const rebasedPlaceholders = placeholders.replace(
|
|
281
|
-
/\$(\d+)/g,
|
|
282
|
-
(_: string, n: string) => `$${params.length + Number(n)}`
|
|
283
|
-
)
|
|
284
|
-
text += `(${columns}) VALUES (${rebasedPlaceholders})`
|
|
285
|
-
params.push(...insertParams.map(serializeParam))
|
|
286
|
-
}
|
|
287
|
-
} else if (
|
|
288
|
-
val &&
|
|
289
|
-
typeof val === 'object' &&
|
|
290
|
-
'_isArrayExpansion' in val &&
|
|
291
|
-
(val as any)._isArrayExpansion
|
|
292
|
-
) {
|
|
293
|
-
// sql([1,2,3]) — expand for IN clauses: ($1, $2, $3)
|
|
294
|
-
const arr = (val as any)._values as unknown[]
|
|
295
|
-
const placeholders = arr.map((_, j) => `$${params.length + j + 1}`).join(', ')
|
|
296
|
-
text += `(${placeholders})`
|
|
297
|
-
params.push(...arr.map(serializeParam))
|
|
298
|
-
} else if (Array.isArray(val)) {
|
|
299
|
-
// raw array in template tag
|
|
300
|
-
// check context: json_to_recordset etc. need JSON, everything else needs PG array
|
|
301
|
-
const before = text.trimEnd()
|
|
302
|
-
const needsJson =
|
|
303
|
-
/json_to_record(?:set)?|json_(?:array|build|each|populate)\s*\(\s*$/i.test(
|
|
304
|
-
before
|
|
305
|
-
) ||
|
|
306
|
-
(/\(\s*$/.test(before) &&
|
|
307
|
-
/json/i.test(before.slice(Math.max(0, before.length - 40))))
|
|
308
|
-
if (needsJson) {
|
|
309
|
-
params.push(JSON.stringify(val))
|
|
310
|
-
text += `$${params.length}::json`
|
|
311
|
-
} else {
|
|
312
|
-
// PostgreSQL array literal: {val1,val2,...}
|
|
313
|
-
const pgArray = `{${val
|
|
314
|
-
.map((v: any) => {
|
|
315
|
-
if (v === null || v === undefined) return 'NULL'
|
|
316
|
-
const s = String(v)
|
|
317
|
-
// quote if contains special chars
|
|
318
|
-
if (
|
|
319
|
-
s.includes(',') ||
|
|
320
|
-
s.includes('"') ||
|
|
321
|
-
s.includes('{') ||
|
|
322
|
-
s.includes('}') ||
|
|
323
|
-
s.includes(' ')
|
|
324
|
-
) {
|
|
325
|
-
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
|
|
326
|
-
}
|
|
327
|
-
return s
|
|
328
|
-
})
|
|
329
|
-
.join(',')}}`
|
|
330
|
-
params.push(pgArray)
|
|
331
|
-
text += `$${params.length}`
|
|
332
|
-
}
|
|
333
|
-
} else {
|
|
334
|
-
const serialized = serializeParam(val)
|
|
335
|
-
params.push(serialized)
|
|
336
|
-
// add ::json cast for JSON values (PGlite needs explicit type for json_to_recordset etc.)
|
|
337
|
-
const needsJsonCast =
|
|
338
|
-
typeof serialized === 'string' &&
|
|
339
|
-
typeof val === 'object' &&
|
|
340
|
-
val !== null &&
|
|
341
|
-
(serialized.startsWith('[') || serialized.startsWith('{'))
|
|
342
|
-
text += `$${params.length}${needsJsonCast ? '::json' : ''}`
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return { text, params }
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// -- pending query --
|
|
351
|
-
// wraps a promise to add .simple(), .readable(), .writable(), .execute(), .describe(), .values(), .raw()
|
|
352
|
-
|
|
353
|
-
function createPendingQuery<T>(
|
|
354
|
-
promise: Promise<T>
|
|
355
|
-
): T extends any[]
|
|
356
|
-
? Promise<T> & PendingQueryModifiers
|
|
357
|
-
: Promise<T> & PendingQueryModifiers {
|
|
358
|
-
const pending = promise as any
|
|
359
|
-
|
|
360
|
-
pending.simple = () => pending
|
|
361
|
-
pending.execute = () => pending
|
|
362
|
-
pending.cancel = () => {}
|
|
363
|
-
|
|
364
|
-
pending.describe = () =>
|
|
365
|
-
Promise.reject(new Error('describe() not supported in worker mode'))
|
|
366
|
-
pending.values = () =>
|
|
367
|
-
promise.then((rows: any) => {
|
|
368
|
-
if (!Array.isArray(rows)) return []
|
|
369
|
-
return rows.map((row: any) => Object.values(row))
|
|
370
|
-
})
|
|
371
|
-
pending.raw = () => Promise.reject(new Error('raw() not supported in worker mode'))
|
|
372
|
-
|
|
373
|
-
pending.readable = () => {
|
|
374
|
-
throw new Error('readable() not supported in worker mode')
|
|
375
|
-
}
|
|
376
|
-
pending.writable = () => {
|
|
377
|
-
throw new Error('writable() not supported in worker mode')
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
pending.forEach = (cb: (row: any, result: any) => void) =>
|
|
381
|
-
promise.then((rows: any) => {
|
|
382
|
-
const result = { count: rows.length, command: rows.command || 'SELECT' }
|
|
383
|
-
for (const row of rows) cb(row, result)
|
|
384
|
-
return result
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
// cursor: returns async iterable yielding batches of rows
|
|
388
|
-
pending.cursor = (batchSize: number = 100) => ({
|
|
389
|
-
[Symbol.asyncIterator]() {
|
|
390
|
-
let allRows: any[] | null = null
|
|
391
|
-
let offset = 0
|
|
392
|
-
return {
|
|
393
|
-
async next() {
|
|
394
|
-
if (!allRows) {
|
|
395
|
-
const result = await promise
|
|
396
|
-
allRows = Array.isArray(result) ? result : []
|
|
397
|
-
}
|
|
398
|
-
if (offset >= allRows.length) return { done: true as const, value: undefined }
|
|
399
|
-
const batch = allRows.slice(offset, offset + batchSize)
|
|
400
|
-
offset += batchSize
|
|
401
|
-
return { done: false as const, value: batch }
|
|
402
|
-
},
|
|
403
|
-
}
|
|
404
|
-
},
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
pending.stream = () => {
|
|
408
|
-
throw new Error('stream() is deprecated, use forEach()')
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return pending
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
interface PendingQueryModifiers {
|
|
415
|
-
simple(): this
|
|
416
|
-
readable(): never
|
|
417
|
-
writable(): never
|
|
418
|
-
execute(): this
|
|
419
|
-
cancel(): void
|
|
420
|
-
describe(): Promise<never>
|
|
421
|
-
values(): Promise<never>
|
|
422
|
-
raw(): Promise<never>
|
|
423
|
-
forEach(cb: (row: any, result: any) => void): Promise<any>
|
|
424
|
-
cursor(...args: unknown[]): never
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
type ReplicationCapableDb = {
|
|
428
|
-
query<T>(sql: string, params?: unknown[]): Promise<Results<T>>
|
|
429
|
-
exec(sql: string): Promise<Array<Results> | void>
|
|
430
|
-
listen?: (channel: string, cb: () => void) => Promise<() => Promise<void>>
|
|
431
|
-
closed?: boolean
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function getSharedMutex(target: object): Mutex {
|
|
435
|
-
const existing = (target as any).__orez_mutex as Mutex | undefined
|
|
436
|
-
if (existing) return existing
|
|
437
|
-
const mutex = new Mutex()
|
|
438
|
-
;(target as any).__orez_mutex = mutex
|
|
439
|
-
return mutex
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// -- raw wire protocol helper --
|
|
443
|
-
// bypasses PGlite's JS mutexes (Fe2/ke2) by calling execProtocolRawSync directly.
|
|
444
|
-
// used for replication protocol queries and external queries that would otherwise
|
|
445
|
-
// deadlock against zero-cache's long-running transaction pool.
|
|
446
|
-
function rawQuery(pglite: PGlite, sql: string): any[] {
|
|
447
|
-
const pg = pglite as any
|
|
448
|
-
if (!pg.execProtocolRawSync) return []
|
|
449
|
-
const enc = new TextEncoder()
|
|
450
|
-
const sqlBytes = enc.encode(sql + '\0')
|
|
451
|
-
const len = 4 + sqlBytes.length
|
|
452
|
-
const msg = new Uint8Array(1 + len)
|
|
453
|
-
msg[0] = 0x51 // Q message
|
|
454
|
-
new DataView(msg.buffer).setInt32(1, len)
|
|
455
|
-
msg.set(sqlBytes, 5)
|
|
456
|
-
const result = pg.execProtocolRawSync(msg) as Uint8Array
|
|
457
|
-
// parse wire protocol response
|
|
458
|
-
const rows: any[] = []
|
|
459
|
-
const fields: string[] = []
|
|
460
|
-
let pos = 0
|
|
461
|
-
const dv = new DataView(result.buffer, result.byteOffset, result.byteLength)
|
|
462
|
-
while (pos < result.length) {
|
|
463
|
-
const type = result[pos]
|
|
464
|
-
const msgLen = dv.getInt32(pos + 1) + 1
|
|
465
|
-
if (type === 0x54) {
|
|
466
|
-
// RowDescription
|
|
467
|
-
const nFields = dv.getInt16(pos + 5)
|
|
468
|
-
let fpos = pos + 7
|
|
469
|
-
for (let i = 0; i < nFields; i++) {
|
|
470
|
-
let end = fpos
|
|
471
|
-
while (result[end] !== 0) end++
|
|
472
|
-
fields.push(new TextDecoder().decode(result.subarray(fpos, end)))
|
|
473
|
-
fpos = end + 1 + 18
|
|
474
|
-
}
|
|
475
|
-
} else if (type === 0x44) {
|
|
476
|
-
// DataRow
|
|
477
|
-
const nCols = dv.getInt16(pos + 5)
|
|
478
|
-
const row: any = {}
|
|
479
|
-
let cpos = pos + 7
|
|
480
|
-
for (let i = 0; i < nCols; i++) {
|
|
481
|
-
const colLen = dv.getInt32(cpos)
|
|
482
|
-
cpos += 4
|
|
483
|
-
if (colLen === -1) {
|
|
484
|
-
row[fields[i]] = null
|
|
485
|
-
} else {
|
|
486
|
-
row[fields[i]] = new TextDecoder().decode(result.subarray(cpos, cpos + colLen))
|
|
487
|
-
cpos += colLen
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
rows.push(row)
|
|
491
|
-
} else if (type === 0x45) {
|
|
492
|
-
// ErrorResponse
|
|
493
|
-
break
|
|
494
|
-
}
|
|
495
|
-
pos += msgLen
|
|
496
|
-
}
|
|
497
|
-
return rows
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
function rawExec(pglite: PGlite, sql: string): void {
|
|
501
|
-
const pg = pglite as any
|
|
502
|
-
if (!pg.execProtocolRawSync) return
|
|
503
|
-
const enc = new TextEncoder()
|
|
504
|
-
const sqlBytes = enc.encode(sql + '\0')
|
|
505
|
-
const len = 4 + sqlBytes.length
|
|
506
|
-
const msg = new Uint8Array(1 + len)
|
|
507
|
-
msg[0] = 0x51
|
|
508
|
-
new DataView(msg.buffer).setInt32(1, len)
|
|
509
|
-
msg.set(sqlBytes, 5)
|
|
510
|
-
pg.execProtocolRawSync(msg)
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// create a proxy around PGlite that routes query/exec through raw wire protocol
|
|
514
|
-
// this is needed because the replication handler runs continuously and would
|
|
515
|
-
// deadlock against zero-cache's transaction pool if it used the normal PGlite API
|
|
516
|
-
function createRawDbProxy(pg: PGlite): PGlite {
|
|
517
|
-
if (!(pg as any).execProtocolRawSync) return pg
|
|
518
|
-
return new Proxy(pg, {
|
|
519
|
-
get(target, prop) {
|
|
520
|
-
if (prop === 'query') {
|
|
521
|
-
return async (sql: string, params?: any[]) => {
|
|
522
|
-
// for parameterized queries, fall back to normal query
|
|
523
|
-
// (raw protocol doesn't support parameters easily)
|
|
524
|
-
if (params?.length) return target.query(sql, params)
|
|
525
|
-
const rows = rawQuery(target, sql)
|
|
526
|
-
return { rows, fields: [], affectedRows: 0 }
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
if (prop === 'exec') {
|
|
530
|
-
return async (sql: string) => {
|
|
531
|
-
rawExec(target, sql)
|
|
532
|
-
return []
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
if (prop === 'listen') {
|
|
536
|
-
// skip listen in browser — it's a secondary signal mechanism
|
|
537
|
-
return async () => async () => {}
|
|
538
|
-
}
|
|
539
|
-
if (prop === 'closed') return target.closed
|
|
540
|
-
return (target as any)[prop]
|
|
541
|
-
},
|
|
542
|
-
})
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// -- execute a query, routing multi-statement to exec() --
|
|
546
|
-
// PGlite.query() only handles single statements. multi-statement DDL
|
|
547
|
-
// (schema migrations, etc.) must use exec(). when params are present
|
|
548
|
-
// AND the query is multi-statement, we split and run each individually.
|
|
549
|
-
|
|
550
|
-
// intercept replication-related queries that PGlite can't handle natively.
|
|
551
|
-
// these are sent by zero-cache during initialization (wal_level check,
|
|
552
|
-
// replication slot management, etc.) and need fake responses.
|
|
553
|
-
// uses rawQuery/rawExec to bypass PGlite's transaction mutex.
|
|
554
|
-
async function interceptReplicationQuery(
|
|
555
|
-
text: string,
|
|
556
|
-
pglite: PGlite
|
|
557
|
-
): Promise<ResultArray<any> | null> {
|
|
558
|
-
const upper = text.trimStart().toUpperCase()
|
|
559
|
-
|
|
560
|
-
// wal_level check: zero-cache verifies logical replication is enabled
|
|
561
|
-
if (
|
|
562
|
-
upper.includes('WAL_LEVEL') &&
|
|
563
|
-
(upper.includes('CURRENT_SETTING') || upper.startsWith('SHOW'))
|
|
564
|
-
) {
|
|
565
|
-
if (upper.includes('VERSION')) {
|
|
566
|
-
return fakeResult([{ walLevel: 'logical', version: '170004' }], text)
|
|
567
|
-
}
|
|
568
|
-
return fakeResult([{ walLevel: 'logical' }], text)
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// CREATE_REPLICATION_SLOT: zero-cache creates a slot during initial sync
|
|
572
|
-
if (upper.includes('CREATE_REPLICATION_SLOT')) {
|
|
573
|
-
const match = text.match(/CREATE_REPLICATION_SLOT\s+(?:"([^"]+)"|'([^']+)'|(\S+))/i)
|
|
574
|
-
const slotName = match?.[1] || match?.[2] || match?.[3] || 'zero_slot'
|
|
575
|
-
const lsn = '0/1000100'
|
|
576
|
-
try {
|
|
577
|
-
await pglite.exec(`
|
|
578
|
-
CREATE TABLE IF NOT EXISTS _orez._zero_replication_slots (
|
|
579
|
-
slot_name TEXT PRIMARY KEY, restart_lsn TEXT,
|
|
580
|
-
confirmed_flush_lsn TEXT, wal_status TEXT DEFAULT 'reserved'
|
|
581
|
-
)
|
|
582
|
-
`)
|
|
583
|
-
await pglite.exec(
|
|
584
|
-
`INSERT INTO _orez._zero_replication_slots (slot_name, restart_lsn, confirmed_flush_lsn)
|
|
585
|
-
VALUES ('${slotName.replace(/'/g, "''")}', '${lsn}', '${lsn}')
|
|
586
|
-
ON CONFLICT (slot_name) DO UPDATE SET restart_lsn = '${lsn}'`
|
|
587
|
-
)
|
|
588
|
-
} catch {}
|
|
589
|
-
return fakeResult(
|
|
590
|
-
[
|
|
591
|
-
{
|
|
592
|
-
slot_name: slotName,
|
|
593
|
-
consistent_point: lsn,
|
|
594
|
-
snapshot_name: '00000003-00000001-1',
|
|
595
|
-
output_plugin: 'pgoutput',
|
|
596
|
-
},
|
|
597
|
-
],
|
|
598
|
-
text
|
|
599
|
-
)
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// DROP_REPLICATION_SLOT
|
|
603
|
-
if (upper.startsWith('DROP_REPLICATION_SLOT')) {
|
|
604
|
-
return fakeResult([], text, 'DROP_REPLICATION_SLOT')
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// pg_replication_slots query
|
|
608
|
-
if (upper.includes('PG_REPLICATION_SLOTS') && upper.includes('SELECT')) {
|
|
609
|
-
// if query includes pg_terminate_backend, it's stopExistingReplicationSlotSubscribers
|
|
610
|
-
// — PGlite has no real replication slots or active_pid, return empty result
|
|
611
|
-
if (upper.includes('PG_TERMINATE_BACKEND')) {
|
|
612
|
-
return fakeResult([], text)
|
|
613
|
-
}
|
|
614
|
-
try {
|
|
615
|
-
const result = await pglite.query(
|
|
616
|
-
`SELECT slot_name, restart_lsn as "restartLSN", wal_status as "walStatus"
|
|
617
|
-
FROM _orez._zero_replication_slots`
|
|
618
|
-
)
|
|
619
|
-
return createResultArray(result as any, text)
|
|
620
|
-
} catch {
|
|
621
|
-
return fakeResult([], text)
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// IDENTIFY_SYSTEM
|
|
626
|
-
if (upper === 'IDENTIFY_SYSTEM' || upper === 'IDENTIFY_SYSTEM;') {
|
|
627
|
-
return fakeResult(
|
|
628
|
-
[
|
|
629
|
-
{
|
|
630
|
-
systemid: '1234567890',
|
|
631
|
-
timeline: '1',
|
|
632
|
-
xlogpos: '0/1000100',
|
|
633
|
-
dbname: 'template1',
|
|
634
|
-
},
|
|
635
|
-
],
|
|
636
|
-
text
|
|
637
|
-
)
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// ALTER ROLE ... REPLICATION
|
|
641
|
-
if (upper.startsWith('ALTER ROLE') && upper.includes('REPLICATION')) {
|
|
642
|
-
return fakeResult([], text, 'ALTER ROLE')
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// SET TRANSACTION / SET SESSION / SET LOCAL — PGlite doesn't support SET LOCAL
|
|
646
|
-
if (
|
|
647
|
-
upper.startsWith('SET TRANSACTION') ||
|
|
648
|
-
upper.startsWith('SET SESSION') ||
|
|
649
|
-
upper.startsWith('SET LOCAL')
|
|
650
|
-
) {
|
|
651
|
-
return fakeResult([], text, 'SET')
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// pg_settings query (wal_sender_timeout etc.) — not available in PGlite
|
|
655
|
-
if (upper.includes('PG_SETTINGS') && upper.includes('WAL_SENDER_TIMEOUT')) {
|
|
656
|
-
return fakeResult([{ walSenderTimeoutMs: 60000 }], text)
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// event triggers: PGlite doesn't support them (requires superuser),
|
|
660
|
-
// and DDL detection isn't needed in browser mode
|
|
661
|
-
if (
|
|
662
|
-
upper.includes('EVENT TRIGGER') &&
|
|
663
|
-
(upper.startsWith('DROP') || upper.startsWith('CREATE'))
|
|
664
|
-
) {
|
|
665
|
-
return fakeResult([], text, upper.startsWith('DROP') ? 'DROP' : 'CREATE')
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
return null
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function fakeResult(
|
|
672
|
-
rows: Record<string, unknown>[],
|
|
673
|
-
queryString: string,
|
|
674
|
-
command?: string
|
|
675
|
-
): ResultArray<any> {
|
|
676
|
-
const columns: ColumnMeta[] =
|
|
677
|
-
rows.length > 0
|
|
678
|
-
? Object.keys(rows[0]).map((name, i) => ({ name, type: 25, table: 0, number: i }))
|
|
679
|
-
: []
|
|
680
|
-
const result = [...rows] as ResultArray<any>
|
|
681
|
-
result.count = rows.length
|
|
682
|
-
result.command = command || detectCommand(queryString)
|
|
683
|
-
result.state = { status: 'idle', pid: 0, secret: 0 }
|
|
684
|
-
result.statement = { name: '', string: queryString, types: [], columns }
|
|
685
|
-
result.columns = columns
|
|
686
|
-
return result
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
async function executeQuery(
|
|
690
|
-
executor: {
|
|
691
|
-
query<T>(sql: string, params?: unknown[]): Promise<Results<T>>
|
|
692
|
-
exec(sql: string): Promise<Array<Results>>
|
|
693
|
-
},
|
|
694
|
-
text: string,
|
|
695
|
-
params: unknown[],
|
|
696
|
-
pglite?: PGlite
|
|
697
|
-
): Promise<ResultArray<any>> {
|
|
698
|
-
// intercept replication-related queries before they reach PGlite
|
|
699
|
-
if (pglite) {
|
|
700
|
-
const intercepted = await interceptReplicationQuery(text, pglite)
|
|
701
|
-
if (intercepted) return intercepted
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// strip FK constraints — PGlite doesn't support cross-schema FKs,
|
|
705
|
-
// and browser single-process mode doesn't need FK enforcement.
|
|
706
|
-
// covers CREATE TABLE inline FKs and ALTER TABLE ADD CONSTRAINT FKs.
|
|
707
|
-
if (/FOREIGN\s+KEY/i.test(text)) {
|
|
708
|
-
if (/CREATE\s+TABLE/i.test(text)) {
|
|
709
|
-
text = text.replace(
|
|
710
|
-
/,?\s*(?:CONSTRAINT\s+\w+\s+)?FOREIGN\s+KEY\s*\([^)]*\)\s*REFERENCES\s+[^,(]+(?:\s*\([^)]*\))?(?:\s+ON\s+(?:DELETE|UPDATE)\s+(?:CASCADE|SET\s+NULL|SET\s+DEFAULT|RESTRICT|NO\s+ACTION))*(?:\s+DEFERRABLE[^,)]*)?/gi,
|
|
711
|
-
''
|
|
712
|
-
)
|
|
713
|
-
}
|
|
714
|
-
if (/ALTER\s+TABLE/i.test(text) && /ADD\s+CONSTRAINT/i.test(text)) {
|
|
715
|
-
text = text.replace(
|
|
716
|
-
/ALTER\s+TABLE\s+[^\s]+\s+ADD\s+CONSTRAINT\s+[^\s]+\s*\n?\s*FOREIGN\s+KEY\s*\([^)]*\)\s*\n?\s*REFERENCES\s+[^;]+/gi,
|
|
717
|
-
'SELECT 1'
|
|
718
|
-
)
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const isMulti = hasMultipleStatements(text)
|
|
723
|
-
|
|
724
|
-
if (!isMulti) {
|
|
725
|
-
// use normal PGlite query — rawQuery breaks transaction state
|
|
726
|
-
const r = await (params.length > 0
|
|
727
|
-
? executor.query(text, params)
|
|
728
|
-
: executor.query(text))
|
|
729
|
-
const result = createResultArray(r as Results<any>, text)
|
|
730
|
-
if (isWriteCommand(text)) {
|
|
731
|
-
signalReplicationChange()
|
|
732
|
-
debouncedSignal() // also fire debounced in case immediate signal is consumed
|
|
733
|
-
}
|
|
734
|
-
return result
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// multi-statement: ALWAYS split and run individually
|
|
738
|
-
if (params.length === 0) {
|
|
739
|
-
const stmts = splitStatements(text)
|
|
740
|
-
let lastResult: Results<any> = { rows: [], fields: [], affectedRows: 0 } as any
|
|
741
|
-
for (const stmt of stmts) {
|
|
742
|
-
lastResult = (await executor.query(stmt)) as Results<any>
|
|
743
|
-
}
|
|
744
|
-
const result = createResultArray(lastResult, text)
|
|
745
|
-
if (isWriteCommand(text)) signalReplicationChange()
|
|
746
|
-
return result
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// multi-statement WITH params — split and run each statement,
|
|
750
|
-
// distributing $N params to the correct statement
|
|
751
|
-
const statements = splitStatements(text)
|
|
752
|
-
let lastResult: Results<any> = { rows: [], fields: [], affectedRows: 0 } as any
|
|
753
|
-
|
|
754
|
-
for (const stmt of statements) {
|
|
755
|
-
// find which $N params this statement references
|
|
756
|
-
const paramRefs = [...stmt.matchAll(/\$(\d+)/g)].map((m) => Number(m[1]))
|
|
757
|
-
|
|
758
|
-
if (paramRefs.length > 0) {
|
|
759
|
-
// remap $N to $1, $2, ... for this statement's params
|
|
760
|
-
const stmtParams = paramRefs.map((n) => params[n - 1])
|
|
761
|
-
let remapped = stmt
|
|
762
|
-
paramRefs.forEach((origN, i) => {
|
|
763
|
-
remapped = remapped.replace(new RegExp(`\\$${origN}\\b`), `$${i + 1}`)
|
|
764
|
-
})
|
|
765
|
-
lastResult = (await executor.query(remapped, stmtParams)) as Results<any>
|
|
766
|
-
} else {
|
|
767
|
-
// no params in this statement — can use query() directly
|
|
768
|
-
lastResult = (await executor.query(stmt)) as Results<any>
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
const result = createResultArray(lastResult, text)
|
|
773
|
-
if (isWriteCommand(text)) signalReplicationChange()
|
|
774
|
-
return result
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const WRITE_COMMANDS = new Set(['INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', 'ALTER'])
|
|
778
|
-
|
|
779
|
-
function isWriteCommand(sql: string): boolean {
|
|
780
|
-
return WRITE_COMMANDS.has(detectCommand(sql))
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// split SQL into individual statements, respecting string literals
|
|
784
|
-
function splitStatements(sql: string): string[] {
|
|
785
|
-
// strip string literals to find real semicolons
|
|
786
|
-
const literals: string[] = []
|
|
787
|
-
// dollar-quoted strings first ($$ ... $$ or $tag$ ... $tag$)
|
|
788
|
-
let stripped = sql.replace(/(\$[a-zA-Z_]*\$)([\s\S]*?)\1/g, (match) => {
|
|
789
|
-
literals.push(match)
|
|
790
|
-
return `__LIT${literals.length - 1}__`
|
|
791
|
-
})
|
|
792
|
-
stripped = stripped.replace(/'(?:[^']|'')*'/g, (match) => {
|
|
793
|
-
literals.push(match)
|
|
794
|
-
return `__LIT${literals.length - 1}__`
|
|
795
|
-
})
|
|
796
|
-
stripped = stripped.replace(/"(?:[^"]|"")*"/g, (match) => {
|
|
797
|
-
literals.push(match)
|
|
798
|
-
return `__LIT${literals.length - 1}__`
|
|
799
|
-
})
|
|
800
|
-
// strip -- comments
|
|
801
|
-
stripped = stripped.replace(/--[^\n]*/g, '')
|
|
802
|
-
// strip /* block comments */
|
|
803
|
-
stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
804
|
-
|
|
805
|
-
// split on semicolons
|
|
806
|
-
const parts = stripped
|
|
807
|
-
.split(';')
|
|
808
|
-
.map((s) => s.trim())
|
|
809
|
-
.filter((s) => s.length > 0)
|
|
810
|
-
|
|
811
|
-
// restore literals
|
|
812
|
-
return parts.map((part) => part.replace(/__LIT(\d+)__/g, (_, i) => literals[Number(i)]))
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// -- sql function factory for a given executor --
|
|
816
|
-
// used both for the top-level sql and for transaction sql
|
|
817
|
-
|
|
818
|
-
function createSqlFunction(
|
|
819
|
-
executor: {
|
|
820
|
-
query<T>(sql: string, params?: unknown[]): Promise<Results<T>>
|
|
821
|
-
exec(sql: string): Promise<Array<Results>>
|
|
822
|
-
},
|
|
823
|
-
rootPglite?: PGlite
|
|
824
|
-
) {
|
|
825
|
-
function sql(first: any, ...rest: any[]): any {
|
|
826
|
-
// tagged template: sql`SELECT ...`
|
|
827
|
-
if (first && Array.isArray(first.raw)) {
|
|
828
|
-
const { text, params } = buildQuery(first as TemplateStringsArray, rest)
|
|
829
|
-
const promise = executeQuery(executor, text, params, rootPglite)
|
|
830
|
-
return createPendingQuery(promise)
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// function call with string: sql('identifier') => Identifier
|
|
834
|
-
if (typeof first === 'string' && rest.length === 0) {
|
|
835
|
-
return new Identifier(first)
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// sql(object) — helper for dynamic INSERT/UPDATE
|
|
839
|
-
if (typeof first === 'object' && first !== null && !Array.isArray(first)) {
|
|
840
|
-
return {
|
|
841
|
-
_isHelper: true,
|
|
842
|
-
_data: first,
|
|
843
|
-
toInsert() {
|
|
844
|
-
const keys = Object.keys(first)
|
|
845
|
-
const columns = keys.map((k) => '"' + k.replace(/"/g, '""') + '"').join(', ')
|
|
846
|
-
const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ')
|
|
847
|
-
return { columns, values: placeholders, params: keys.map((k) => first[k]) }
|
|
848
|
-
},
|
|
849
|
-
toUpdate() {
|
|
850
|
-
const keys = Object.keys(first)
|
|
851
|
-
const set = keys
|
|
852
|
-
.map((k, i) => `"${k.replace(/"/g, '""')}" = $${i + 1}`)
|
|
853
|
-
.join(', ')
|
|
854
|
-
return { set, params: keys.map((k) => first[k]) }
|
|
855
|
-
},
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
// sql(array) — parameter expansion for IN clauses
|
|
860
|
-
// wrap in marker object so buildQuery knows to expand (not serialize as JSON)
|
|
861
|
-
if (Array.isArray(first)) {
|
|
862
|
-
return { _isArrayExpansion: true, _values: first }
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
throw new Error('postgres shim: unsupported sql() call')
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
return sql
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// -- COPY TO STDOUT support --
|
|
872
|
-
|
|
873
|
-
function createCopyPendingQuery(
|
|
874
|
-
copyQuery: string,
|
|
875
|
-
executor: { query: (sql: string, params?: unknown[]) => Promise<Results<any>> }
|
|
876
|
-
): any {
|
|
877
|
-
// extract the query from COPY (SELECT ...) TO STDOUT or COPY table TO STDOUT
|
|
878
|
-
let selectQuery: string
|
|
879
|
-
const parenMatch = copyQuery.match(/COPY\s*\(([\s\S]+)\)\s*TO\s+STDOUT/i)
|
|
880
|
-
if (parenMatch) {
|
|
881
|
-
selectQuery = parenMatch[1].trim()
|
|
882
|
-
} else {
|
|
883
|
-
const tableMatch = copyQuery.match(
|
|
884
|
-
/COPY\s+("(?:[^"]|"")*"(?:\."(?:[^"]|"")*")*|\S+)\s+TO\s+STDOUT/i
|
|
885
|
-
)
|
|
886
|
-
selectQuery = tableMatch ? `SELECT * FROM ${tableMatch[1]}` : 'SELECT 1 WHERE false'
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// returns a Node.js Readable stream (via PassThrough) compatible with pipeline()
|
|
890
|
-
const readablePromise = (async () => {
|
|
891
|
-
const result = await executor.query(selectQuery)
|
|
892
|
-
const rows = result.rows as any[]
|
|
893
|
-
const pt = new PassThrough()
|
|
894
|
-
|
|
895
|
-
// write all rows as TSV-encoded COPY output, using Buffer for stream compatibility
|
|
896
|
-
const encoder = typeof Buffer !== 'undefined' ? null : new TextEncoder()
|
|
897
|
-
for (const row of rows) {
|
|
898
|
-
const values = Object.values(row).map((v: any) => {
|
|
899
|
-
if (v === null || v === undefined) return '\\N'
|
|
900
|
-
if (typeof v === 'boolean') return v ? 't' : 'f'
|
|
901
|
-
if (typeof v === 'object')
|
|
902
|
-
return JSON.stringify(v)
|
|
903
|
-
.replace(/\\/g, '\\\\')
|
|
904
|
-
.replace(/\t/g, '\\t')
|
|
905
|
-
.replace(/\n/g, '\\n')
|
|
906
|
-
return String(v)
|
|
907
|
-
.replace(/\\/g, '\\\\')
|
|
908
|
-
.replace(/\t/g, '\\t')
|
|
909
|
-
.replace(/\n/g, '\\n')
|
|
910
|
-
})
|
|
911
|
-
const line = values.join('\t') + '\n'
|
|
912
|
-
const chunk = encoder ? encoder.encode(line) : Buffer.from(line)
|
|
913
|
-
pt.push(chunk)
|
|
914
|
-
}
|
|
915
|
-
// signal end of stream
|
|
916
|
-
pt.push(null)
|
|
917
|
-
return pt
|
|
918
|
-
})()
|
|
919
|
-
|
|
920
|
-
const pending = readablePromise as any
|
|
921
|
-
pending.execute = () => pending
|
|
922
|
-
pending.simple = () => pending
|
|
923
|
-
pending.cancel = () => {}
|
|
924
|
-
pending.readable = () => readablePromise
|
|
925
|
-
pending.writable = () => Promise.resolve(new PassThrough())
|
|
926
|
-
pending.describe = () => Promise.reject(new Error('not supported'))
|
|
927
|
-
pending.values = () => Promise.reject(new Error('not supported'))
|
|
928
|
-
pending.raw = () => Promise.reject(new Error('not supported'))
|
|
929
|
-
pending.forEach = () => Promise.reject(new Error('not supported'))
|
|
930
|
-
pending.cursor = () => {
|
|
931
|
-
throw new Error('not supported')
|
|
932
|
-
}
|
|
933
|
-
pending.stream = () => {
|
|
934
|
-
throw new Error('not supported')
|
|
935
|
-
}
|
|
936
|
-
return pending
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
function createReplicationPendingQuery(
|
|
940
|
-
replicationQuery: string,
|
|
941
|
-
db: ReplicationCapableDb
|
|
942
|
-
): any {
|
|
943
|
-
const mutex = getSharedMutex(db as object)
|
|
944
|
-
const writable = new PassThrough()
|
|
945
|
-
let started = false
|
|
946
|
-
let startPromise: Promise<void> | null = null
|
|
947
|
-
let destroyed = false
|
|
948
|
-
|
|
949
|
-
// use PassThrough — AND expose a direct callback for the pipe to use.
|
|
950
|
-
// stream-browserify's PassThrough + Duplexify stop flowing after idle,
|
|
951
|
-
// so we also push data via a callback that the patched pipe() can use.
|
|
952
|
-
const readable = new PassThrough()
|
|
953
|
-
// direct data callback — bypasses broken stream plumbing entirely.
|
|
954
|
-
// registered on globalThis so the patched pipe() can find it regardless
|
|
955
|
-
// of how many stream wrappers (Duplexify, etc.) sit between.
|
|
956
|
-
const dataListeners: Array<(chunk: Buffer) => void> = []
|
|
957
|
-
;(globalThis as any).__orez_repl_data_push = (fn: (chunk: Buffer) => void) => {
|
|
958
|
-
dataListeners.push(fn)
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
const start = async () => {
|
|
962
|
-
if (started) return
|
|
963
|
-
started = true
|
|
964
|
-
startPromise = handleStartReplication(
|
|
965
|
-
replicationQuery,
|
|
966
|
-
{
|
|
967
|
-
write(chunk: Uint8Array) {
|
|
968
|
-
if (destroyed) return
|
|
969
|
-
// skip CopyBothResponse, unwrap CopyData
|
|
970
|
-
if (chunk[0] === 0x57) return
|
|
971
|
-
const data =
|
|
972
|
-
chunk[0] === 0x64 && chunk.length >= 6
|
|
973
|
-
? Buffer.from(chunk.subarray(5))
|
|
974
|
-
: Buffer.from(chunk)
|
|
975
|
-
readable.write(data)
|
|
976
|
-
// also call pipe handlers directly — stream polyfills are broken in browser
|
|
977
|
-
const handlers = (globalThis as any).__orez_pipe_handlers as
|
|
978
|
-
| Array<(chunk: Buffer) => void>
|
|
979
|
-
| undefined
|
|
980
|
-
if (handlers && handlers.length > 0) {
|
|
981
|
-
;(globalThis as any).__orez_repl_bypass_count =
|
|
982
|
-
((globalThis as any).__orez_repl_bypass_count || 0) + 1
|
|
983
|
-
}
|
|
984
|
-
if (handlers) {
|
|
985
|
-
for (const fn of handlers) {
|
|
986
|
-
try {
|
|
987
|
-
fn(data)
|
|
988
|
-
} catch {}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
},
|
|
992
|
-
get closed() {
|
|
993
|
-
return destroyed || writable.destroyed || !!db.closed
|
|
994
|
-
},
|
|
995
|
-
},
|
|
996
|
-
db as PGlite,
|
|
997
|
-
mutex
|
|
998
|
-
).catch((err) => {
|
|
999
|
-
// don't destroy the readable on handler errors — the change-streamer
|
|
1000
|
-
// would reconnect with a new subscription, losing the reference the
|
|
1001
|
-
// producer holds. just log the error and let the handler restart.
|
|
1002
|
-
console.warn(
|
|
1003
|
-
'[orez:repl] handler error:',
|
|
1004
|
-
err instanceof Error ? err.message : String(err)
|
|
1005
|
-
)
|
|
1006
|
-
})
|
|
1007
|
-
return Promise.resolve()
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
const pending = Promise.resolve() as any
|
|
1011
|
-
pending.execute = () => pending
|
|
1012
|
-
pending.simple = () => pending
|
|
1013
|
-
pending.cancel = () => {
|
|
1014
|
-
destroyed = true
|
|
1015
|
-
readable.destroy()
|
|
1016
|
-
writable.destroy()
|
|
1017
|
-
}
|
|
1018
|
-
pending.readable = async () => {
|
|
1019
|
-
await start()
|
|
1020
|
-
return readable
|
|
1021
|
-
}
|
|
1022
|
-
pending.writable = async () => {
|
|
1023
|
-
await start()
|
|
1024
|
-
return writable
|
|
1025
|
-
}
|
|
1026
|
-
pending.describe = () => Promise.reject(new Error('not supported'))
|
|
1027
|
-
pending.values = () => Promise.reject(new Error('not supported'))
|
|
1028
|
-
pending.raw = () => Promise.reject(new Error('not supported'))
|
|
1029
|
-
pending.forEach = () => Promise.reject(new Error('not supported'))
|
|
1030
|
-
pending.cursor = () => {
|
|
1031
|
-
throw new Error('not supported')
|
|
1032
|
-
}
|
|
1033
|
-
pending.stream = () => {
|
|
1034
|
-
throw new Error('not supported')
|
|
1035
|
-
}
|
|
1036
|
-
return pending
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
// -- type parsers --
|
|
1040
|
-
// pre-populated parsers matching the postgres npm package's type registry.
|
|
1041
|
-
|
|
1042
|
-
function identity(x: string) {
|
|
1043
|
-
return x
|
|
1044
|
-
}
|
|
1045
|
-
function parseFloat_(x: string) {
|
|
1046
|
-
return parseFloat(x)
|
|
1047
|
-
}
|
|
1048
|
-
function parseInt_(x: string) {
|
|
1049
|
-
return parseInt(x, 10)
|
|
1050
|
-
}
|
|
1051
|
-
function parseBool(x: string) {
|
|
1052
|
-
return x === 't' || x === 'true'
|
|
1053
|
-
}
|
|
1054
|
-
function parseJSON(x: string) {
|
|
1055
|
-
try {
|
|
1056
|
-
return JSON.parse(x)
|
|
1057
|
-
} catch {
|
|
1058
|
-
return x
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function makeArrayParser(elementParser: (x: string) => unknown) {
|
|
1063
|
-
const fn = (x: string) => {
|
|
1064
|
-
if (!x || x === '{}') return []
|
|
1065
|
-
const inner = x.slice(1, -1)
|
|
1066
|
-
return inner.split(',').map((v) => {
|
|
1067
|
-
if (v === 'NULL') return null
|
|
1068
|
-
return elementParser(v.replace(/^"|"$/g, ''))
|
|
1069
|
-
})
|
|
1070
|
-
}
|
|
1071
|
-
;(fn as any).array = true
|
|
1072
|
-
return fn
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
function buildDefaultParsers(): Record<number, (value: string) => unknown> {
|
|
1076
|
-
const p: Record<number, any> = {}
|
|
1077
|
-
// scalar types
|
|
1078
|
-
p[16] = parseBool // bool
|
|
1079
|
-
p[17] = identity // bytea
|
|
1080
|
-
p[20] = identity // int8 (bigint as string)
|
|
1081
|
-
p[21] = parseInt_ // int2
|
|
1082
|
-
p[23] = parseInt_ // int4
|
|
1083
|
-
p[25] = identity // text
|
|
1084
|
-
p[26] = parseInt_ // oid
|
|
1085
|
-
p[114] = parseJSON // json
|
|
1086
|
-
p[700] = parseFloat_ // float4
|
|
1087
|
-
p[701] = parseFloat_ // float8
|
|
1088
|
-
p[1042] = identity // bpchar
|
|
1089
|
-
p[1043] = identity // varchar
|
|
1090
|
-
p[1082] = identity // date
|
|
1091
|
-
p[1083] = identity // time
|
|
1092
|
-
// timestamps → epoch ms (zero-cache expects number, not string)
|
|
1093
|
-
const parseTimestamp = (val: string) => {
|
|
1094
|
-
if (typeof val === 'number') return val
|
|
1095
|
-
const d = Date.parse(val)
|
|
1096
|
-
return isNaN(d) ? val : d
|
|
1097
|
-
}
|
|
1098
|
-
p[1114] = parseTimestamp // timestamp
|
|
1099
|
-
p[1184] = parseTimestamp // timestamptz
|
|
1100
|
-
p[1266] = identity // timetz
|
|
1101
|
-
p[1700] = identity // numeric
|
|
1102
|
-
p[2950] = identity // uuid
|
|
1103
|
-
p[3802] = parseJSON // jsonb
|
|
1104
|
-
// array types
|
|
1105
|
-
p[1000] = makeArrayParser(parseBool) // bool[]
|
|
1106
|
-
p[1005] = makeArrayParser(parseInt_) // int2[]
|
|
1107
|
-
p[1007] = makeArrayParser(parseInt_) // int4[]
|
|
1108
|
-
p[1009] = makeArrayParser(identity) // text[]
|
|
1109
|
-
p[1016] = makeArrayParser(identity) // int8[]
|
|
1110
|
-
p[1021] = makeArrayParser(parseFloat_) // float4[]
|
|
1111
|
-
p[1022] = makeArrayParser(parseFloat_) // float8[]
|
|
1112
|
-
p[1015] = makeArrayParser(identity) // varchar[]
|
|
1113
|
-
p[1182] = makeArrayParser(identity) // date[]
|
|
1114
|
-
p[1115] = makeArrayParser(identity) // timestamp[]
|
|
1115
|
-
p[1185] = makeArrayParser(identity) // timestamptz[]
|
|
1116
|
-
p[2951] = makeArrayParser(identity) // uuid[]
|
|
1117
|
-
p[199] = makeArrayParser(parseJSON) // json[]
|
|
1118
|
-
p[3807] = makeArrayParser(parseJSON) // jsonb[]
|
|
1119
|
-
return p
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
// -- main export --
|
|
1123
|
-
|
|
1124
|
-
export interface PostgresShimOptions {
|
|
1125
|
-
max?: number
|
|
1126
|
-
max_lifetime?: number
|
|
1127
|
-
idle_timeout?: number
|
|
1128
|
-
fetch_types?: boolean
|
|
1129
|
-
ssl?: unknown
|
|
1130
|
-
onnotice?: (notice: unknown) => void
|
|
1131
|
-
connection?: Record<string, unknown>
|
|
1132
|
-
types?: Record<string, unknown>
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
export function createPostgresShim(pglite: PGlite, opts?: PostgresShimOptions) {
|
|
1136
|
-
const sqlFn = createSqlFunction(pglite, pglite)
|
|
1137
|
-
|
|
1138
|
-
function sql(first: any, ...rest: any[]): any {
|
|
1139
|
-
return sqlFn(first, ...rest)
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
// sql.unsafe(queryString, params?) — raw SQL execution
|
|
1143
|
-
sql.unsafe = (queryString: string, params?: unknown[]) => {
|
|
1144
|
-
const upper = queryString.trimStart().toUpperCase()
|
|
1145
|
-
|
|
1146
|
-
// START_REPLICATION — expose an in-process duplex stream backed by
|
|
1147
|
-
// orez's replication handler instead of the TCP protocol adapter.
|
|
1148
|
-
if (upper.startsWith('START_REPLICATION')) {
|
|
1149
|
-
return createReplicationPendingQuery(queryString, pglite as ReplicationCapableDb)
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// COPY TO STDOUT — returns readable stream of rows
|
|
1153
|
-
if (upper.startsWith('COPY') && upper.includes('TO STDOUT')) {
|
|
1154
|
-
return createCopyPendingQuery(queryString, pglite)
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// strip FK constraints (see executeQuery for why)
|
|
1158
|
-
if (/FOREIGN\s+KEY/i.test(queryString)) {
|
|
1159
|
-
if (/CREATE\s+TABLE/i.test(queryString)) {
|
|
1160
|
-
queryString = queryString.replace(
|
|
1161
|
-
/,?\s*(?:CONSTRAINT\s+\w+\s+)?FOREIGN\s+KEY\s*\([^)]*\)\s*REFERENCES\s+[^,(]+(?:\s*\([^)]*\))?(?:\s+ON\s+(?:DELETE|UPDATE)\s+(?:CASCADE|SET\s+NULL|SET\s+DEFAULT|RESTRICT|NO\s+ACTION))*(?:\s+DEFERRABLE[^,)]*)?/gi,
|
|
1162
|
-
''
|
|
1163
|
-
)
|
|
1164
|
-
}
|
|
1165
|
-
if (/ALTER\s+TABLE/i.test(queryString) && /ADD\s+CONSTRAINT/i.test(queryString)) {
|
|
1166
|
-
queryString = queryString.replace(
|
|
1167
|
-
/ALTER\s+TABLE\s+[^\s]+\s+ADD\s+CONSTRAINT\s+[^\s]+\s*\n?\s*FOREIGN\s+KEY\s*\([^)]*\)\s*\n?\s*REFERENCES\s+[^;]+/gi,
|
|
1168
|
-
'SELECT 1'
|
|
1169
|
-
)
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
const serializedParams = (params ?? []).map(serializeParam)
|
|
1174
|
-
|
|
1175
|
-
// multi-statement with no params: split and run each individually
|
|
1176
|
-
// PGliteWorker's execProtocol rejects multi-statement, so always split upfront
|
|
1177
|
-
if (hasMultipleStatements(queryString) && serializedParams.length === 0) {
|
|
1178
|
-
const promise = (async () => {
|
|
1179
|
-
const intercepted = await interceptReplicationQuery(queryString, pglite)
|
|
1180
|
-
if (intercepted) return intercepted
|
|
1181
|
-
{
|
|
1182
|
-
const statements = splitStatements(queryString)
|
|
1183
|
-
const resultArrays = []
|
|
1184
|
-
for (const stmt of statements) {
|
|
1185
|
-
const r = await pglite.query(stmt)
|
|
1186
|
-
resultArrays.push(createResultArray(r as Results<any>, stmt))
|
|
1187
|
-
}
|
|
1188
|
-
const combined = resultArrays as any
|
|
1189
|
-
combined.count = resultArrays.length
|
|
1190
|
-
combined.command = 'SELECT'
|
|
1191
|
-
combined.state = { status: 'idle', pid: 0, secret: 0 }
|
|
1192
|
-
combined.statement = { name: '', string: queryString, types: [], columns: [] }
|
|
1193
|
-
combined.columns = []
|
|
1194
|
-
return combined
|
|
1195
|
-
}
|
|
1196
|
-
})()
|
|
1197
|
-
return createPendingQuery(promise)
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
const promise = executeQuery(pglite, queryString, serializedParams, pglite)
|
|
1201
|
-
return createPendingQuery(promise)
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// sql.begin(options?, callback) — transactions
|
|
1205
|
-
//
|
|
1206
|
-
// BROWSER FIX: don't use pglite.transaction() for the top-level callback.
|
|
1207
|
-
// zero-cache's transaction pool holds the callback open indefinitely
|
|
1208
|
-
// (await dequeue() loop), which permanently holds PGlite's Fe2 mutex
|
|
1209
|
-
// and deadlocks ALL external queries (push handler, pg-query, etc.).
|
|
1210
|
-
//
|
|
1211
|
-
// instead, route queries directly through PGlite — each query acquires
|
|
1212
|
-
// and releases Fe2 independently. this trades transaction atomicity for
|
|
1213
|
-
// Fe2 availability, which is acceptable in the browser dev preview
|
|
1214
|
-
// (PGlite is single-connection so operations are serialized anyway).
|
|
1215
|
-
//
|
|
1216
|
-
// nested begin/savepoint calls still use real pglite.transaction()
|
|
1217
|
-
// since those are short-lived (task-scoped, not pool-scoped).
|
|
1218
|
-
sql.begin = async (
|
|
1219
|
-
optionsOrCb: string | ((tx: any) => any),
|
|
1220
|
-
maybeCb?: (tx: any) => any
|
|
1221
|
-
) => {
|
|
1222
|
-
const cb = typeof optionsOrCb === 'function' ? optionsOrCb : maybeCb!
|
|
1223
|
-
|
|
1224
|
-
// create sql function backed by pglite directly (not a Transaction)
|
|
1225
|
-
// each query acquires/releases Fe2 independently
|
|
1226
|
-
const txSql = createSqlFunction(pglite, pglite)
|
|
1227
|
-
|
|
1228
|
-
function txSqlFn(first: any, ...rest: any[]): any {
|
|
1229
|
-
return txSql(first, ...rest)
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// unsafe: routes through pglite directly
|
|
1233
|
-
txSqlFn.unsafe = (queryString: string, params?: unknown[]) => {
|
|
1234
|
-
const upper = queryString.trimStart().toUpperCase()
|
|
1235
|
-
if (upper.startsWith('COPY') && upper.includes('TO STDOUT')) {
|
|
1236
|
-
return createCopyPendingQuery(queryString, pglite)
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
const serializedParams = (params ?? []).map(serializeParam)
|
|
1240
|
-
|
|
1241
|
-
if (hasMultipleStatements(queryString) && serializedParams.length === 0) {
|
|
1242
|
-
const promise = (async () => {
|
|
1243
|
-
const stmts = splitStatements(queryString)
|
|
1244
|
-
const resultArrays = []
|
|
1245
|
-
for (const stmt of stmts) {
|
|
1246
|
-
const r = await pglite.query(stmt)
|
|
1247
|
-
resultArrays.push(createResultArray(r as Results<any>, stmt))
|
|
1248
|
-
}
|
|
1249
|
-
const combined = resultArrays as any
|
|
1250
|
-
combined.count = resultArrays.length
|
|
1251
|
-
combined.command = 'SELECT'
|
|
1252
|
-
combined.state = { status: 'idle', pid: 0, secret: 0 }
|
|
1253
|
-
combined.statement = {
|
|
1254
|
-
name: '',
|
|
1255
|
-
string: queryString,
|
|
1256
|
-
types: [],
|
|
1257
|
-
columns: [],
|
|
1258
|
-
}
|
|
1259
|
-
combined.columns = []
|
|
1260
|
-
return combined
|
|
1261
|
-
})()
|
|
1262
|
-
return createPendingQuery(promise)
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
const promise = executeQuery(pglite, queryString, serializedParams, pglite)
|
|
1266
|
-
return createPendingQuery(promise)
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
// nested begin: use real pglite.transaction() for atomicity (short-lived)
|
|
1270
|
-
// nested begin: non-transactional (same as top level)
|
|
1271
|
-
// real pglite.transaction() holds Fe2 during async SQLite work in the syncer,
|
|
1272
|
-
// which deadlocks any concurrent PGlite queries. non-transactional avoids this.
|
|
1273
|
-
txSqlFn.begin = async (
|
|
1274
|
-
innerOptOrCb: string | ((tx: any) => any),
|
|
1275
|
-
innerMaybeCb?: (tx: any) => any
|
|
1276
|
-
) => {
|
|
1277
|
-
const innerCb = typeof innerOptOrCb === 'function' ? innerOptOrCb : innerMaybeCb!
|
|
1278
|
-
const innerTxSql = createSqlFunction(pglite, pglite)
|
|
1279
|
-
function innerTxSqlFn(first: any, ...rest: any[]): any {
|
|
1280
|
-
return innerTxSql(first, ...rest)
|
|
1281
|
-
}
|
|
1282
|
-
innerTxSqlFn.unsafe = txSqlFn.unsafe
|
|
1283
|
-
innerTxSqlFn.begin = txSqlFn.begin
|
|
1284
|
-
innerTxSqlFn.savepoint = txSqlFn.savepoint
|
|
1285
|
-
innerTxSqlFn.end = async () => {}
|
|
1286
|
-
innerTxSqlFn.options = sql.options
|
|
1287
|
-
innerTxSqlFn.PostgresError = PostgresError
|
|
1288
|
-
try {
|
|
1289
|
-
const result = await innerCb(innerTxSqlFn)
|
|
1290
|
-
return Array.isArray(result) ? await Promise.all(result) : result
|
|
1291
|
-
} finally {
|
|
1292
|
-
signalReplicationChange()
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
// savepoint at top level: DON'T use pglite.transaction() — same reasoning
|
|
1297
|
-
// as sql.begin(). the callback might be long-running (e.g. setupTriggers).
|
|
1298
|
-
// just run the callback with PGlite-backed sql (each query acquires/releases Fe2).
|
|
1299
|
-
let _savepointIdx = 0
|
|
1300
|
-
txSqlFn.savepoint = async (nameOrFn: any, maybeFn?: any) => {
|
|
1301
|
-
const fn = typeof nameOrFn === 'function' ? nameOrFn : maybeFn
|
|
1302
|
-
return fn(txSqlFn)
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
txSqlFn.end = async () => {}
|
|
1306
|
-
txSqlFn.options = sql.options
|
|
1307
|
-
txSqlFn.PostgresError = PostgresError
|
|
1308
|
-
|
|
1309
|
-
// use explicit BEGIN/COMMIT/ROLLBACK SQL instead of pglite.transaction().
|
|
1310
|
-
// each statement acquires/releases Fe2 independently, so the mutex is
|
|
1311
|
-
// NOT held during async gaps in the callback — avoiding the deadlock
|
|
1312
|
-
// that pglite.transaction() causes with zero-cache's long-running pool.
|
|
1313
|
-
await pglite.exec('BEGIN')
|
|
1314
|
-
try {
|
|
1315
|
-
const result = await cb(txSqlFn)
|
|
1316
|
-
await pglite.exec('COMMIT')
|
|
1317
|
-
signalReplicationChange() // signal immediately after commit, don't wait for finally
|
|
1318
|
-
return Array.isArray(result) ? await Promise.all(result) : result
|
|
1319
|
-
} catch (err) {
|
|
1320
|
-
await pglite.exec('ROLLBACK')
|
|
1321
|
-
throw err
|
|
1322
|
-
} finally {
|
|
1323
|
-
signalReplicationChange()
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
// sql.end() — no-op (PGlite lifecycle managed elsewhere)
|
|
1328
|
-
sql.end = async (_opts?: { timeout?: number }) => {}
|
|
1329
|
-
|
|
1330
|
-
// sql.close() — alias for end
|
|
1331
|
-
sql.close = sql.end
|
|
1332
|
-
|
|
1333
|
-
// sql.options — connection metadata
|
|
1334
|
-
sql.options = {
|
|
1335
|
-
host: ['localhost'],
|
|
1336
|
-
port: [5432],
|
|
1337
|
-
database: 'pglite',
|
|
1338
|
-
user: 'pglite',
|
|
1339
|
-
max: opts?.max ?? 1,
|
|
1340
|
-
parsers: buildDefaultParsers(),
|
|
1341
|
-
fetch_types: opts?.fetch_types ?? true,
|
|
1342
|
-
connection: opts?.connection ?? {},
|
|
1343
|
-
ssl: opts?.ssl ?? false,
|
|
1344
|
-
types: opts?.types ?? {},
|
|
1345
|
-
transform: {
|
|
1346
|
-
undefined: undefined,
|
|
1347
|
-
column: { from: undefined, to: undefined },
|
|
1348
|
-
value: { from: undefined, to: undefined },
|
|
1349
|
-
row: { from: undefined, to: undefined },
|
|
1350
|
-
},
|
|
1351
|
-
serializers: {} as Record<number, (value: unknown) => unknown>,
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
// sql.PostgresError — error class
|
|
1355
|
-
sql.PostgresError = PostgresError
|
|
1356
|
-
|
|
1357
|
-
// sql.CLOSE / sql.END — sentinel objects
|
|
1358
|
-
sql.CLOSE = {} as Record<string, never>
|
|
1359
|
-
sql.END = sql.CLOSE
|
|
1360
|
-
|
|
1361
|
-
// sql.parameters — server parameters
|
|
1362
|
-
sql.parameters = {
|
|
1363
|
-
application_name: 'pglite-shim',
|
|
1364
|
-
server_version: '17.0',
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
// sql.types / sql.typed — type helpers
|
|
1368
|
-
sql.typed = (value: unknown, oid: number) => ({ value, type: oid })
|
|
1369
|
-
sql.types = sql.typed
|
|
1370
|
-
|
|
1371
|
-
// sql.json — json parameter helper
|
|
1372
|
-
sql.json = (value: unknown) => JSON.stringify(value)
|
|
1373
|
-
|
|
1374
|
-
// sql.array — array parameter helper
|
|
1375
|
-
sql.array = (value: unknown[], type?: number) => ({ value, type, array: true })
|
|
1376
|
-
|
|
1377
|
-
// sql.listen — not supported
|
|
1378
|
-
sql.listen = () => {
|
|
1379
|
-
throw new Error('listen() not supported in worker mode')
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// sql.notify — not supported
|
|
1383
|
-
sql.notify = () => {
|
|
1384
|
-
throw new Error('notify() not supported in worker mode')
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
// sql.subscribe — not supported
|
|
1388
|
-
sql.subscribe = () => {
|
|
1389
|
-
throw new Error('subscribe() not supported in worker mode')
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
// sql.reserve — not supported
|
|
1393
|
-
sql.reserve = () => {
|
|
1394
|
-
throw new Error('reserve() not supported in worker mode')
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// sql.file — not supported
|
|
1398
|
-
sql.file = () => {
|
|
1399
|
-
throw new Error('file() not supported in worker mode')
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
// sql.largeObject — not supported
|
|
1403
|
-
sql.largeObject = () => {
|
|
1404
|
-
throw new Error('largeObject() not supported in worker mode')
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
return sql
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
// -- default export --
|
|
1411
|
-
// matches the `postgres` package's default export shape:
|
|
1412
|
-
// import postgres from 'postgres'
|
|
1413
|
-
// const sql = postgres(url, options)
|
|
1414
|
-
//
|
|
1415
|
-
// when used as a bundler alias, zero-cache calls postgres(connectionURI, options).
|
|
1416
|
-
// we intercept by reading the PGlite instance from globalThis.__orez_pglite.
|
|
1417
|
-
|
|
1418
|
-
function postgres(
|
|
1419
|
-
_urlOrOpts?: string | PostgresShimOptions,
|
|
1420
|
-
opts?: PostgresShimOptions
|
|
1421
|
-
): ReturnType<typeof createPostgresShim> {
|
|
1422
|
-
// multi-instance routing: if __orez_pglite_instances is set, route by URL
|
|
1423
|
-
const instances = (globalThis as any).__orez_pglite_instances as
|
|
1424
|
-
| { postgres: PGlite; cvr: PGlite; cdb: PGlite }
|
|
1425
|
-
| undefined
|
|
1426
|
-
let pglite: PGlite | undefined
|
|
1427
|
-
if (instances && typeof _urlOrOpts === 'string') {
|
|
1428
|
-
if (_urlOrOpts.includes('/zero_cvr')) pglite = instances.cvr
|
|
1429
|
-
else if (_urlOrOpts.includes('/zero_cdb')) pglite = instances.cdb
|
|
1430
|
-
else pglite = instances.postgres
|
|
1431
|
-
} else {
|
|
1432
|
-
pglite = (globalThis as any).__orez_pglite as PGlite | undefined
|
|
1433
|
-
}
|
|
1434
|
-
if (!pglite) {
|
|
1435
|
-
throw new Error(
|
|
1436
|
-
'postgres shim: no PGlite instance found. ' +
|
|
1437
|
-
'set globalThis.__orez_pglite or __orez_pglite_instances.'
|
|
1438
|
-
)
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
const resolvedOpts = typeof _urlOrOpts === 'object' ? _urlOrOpts : opts
|
|
1442
|
-
return createPostgresShim(pglite, resolvedOpts)
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
// attach PostgresError and BigInt to the default export (matches postgres package)
|
|
1446
|
-
postgres.PostgresError = PostgresError
|
|
1447
|
-
postgres.BigInt = {
|
|
1448
|
-
to: 20,
|
|
1449
|
-
from: [20],
|
|
1450
|
-
parse: (x: string) => globalThis.BigInt(x),
|
|
1451
|
-
serialize: (x: bigint) => x.toString(),
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
export default postgres
|