orez 0.2.26 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/cf-do/worker.d.ts.map +1 -1
  2. package/dist/cf-do/worker.js +9 -1
  3. package/dist/cf-do/worker.js.map +1 -1
  4. package/dist/pg-proxy-do-backend.d.ts +2 -0
  5. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  6. package/dist/pg-proxy-do-backend.js +49 -7
  7. package/dist/pg-proxy-do-backend.js.map +1 -1
  8. package/dist/pg-sqlite-compiler/catalog/seed.d.ts +67 -0
  9. package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +1 -0
  10. package/dist/pg-sqlite-compiler/catalog/seed.js +436 -0
  11. package/dist/pg-sqlite-compiler/catalog/seed.js.map +1 -0
  12. package/dist/pg-sqlite-compiler/index.d.ts +12 -0
  13. package/dist/pg-sqlite-compiler/index.d.ts.map +1 -0
  14. package/dist/pg-sqlite-compiler/index.js +59 -0
  15. package/dist/pg-sqlite-compiler/index.js.map +1 -0
  16. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +48 -0
  17. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +1 -0
  18. package/dist/pg-sqlite-compiler/passes/ast-utils.js +93 -0
  19. package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +1 -0
  20. package/dist/pg-sqlite-compiler/passes/catalog.d.ts +34 -0
  21. package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +1 -0
  22. package/dist/pg-sqlite-compiler/passes/catalog.js +30 -0
  23. package/dist/pg-sqlite-compiler/passes/catalog.js.map +1 -0
  24. package/dist/pg-sqlite-compiler/passes/datetime.d.ts +21 -0
  25. package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +1 -0
  26. package/dist/pg-sqlite-compiler/passes/datetime.js +53 -0
  27. package/dist/pg-sqlite-compiler/passes/datetime.js.map +1 -0
  28. package/dist/pg-sqlite-compiler/passes/index.d.ts +21 -0
  29. package/dist/pg-sqlite-compiler/passes/index.d.ts.map +1 -0
  30. package/dist/pg-sqlite-compiler/passes/index.js +39 -0
  31. package/dist/pg-sqlite-compiler/passes/index.js.map +1 -0
  32. package/dist/pg-sqlite-compiler/passes/types.d.ts +41 -0
  33. package/dist/pg-sqlite-compiler/passes/types.d.ts.map +1 -0
  34. package/dist/pg-sqlite-compiler/passes/types.js +103 -0
  35. package/dist/pg-sqlite-compiler/passes/types.js.map +1 -0
  36. package/dist/pg-sqlite-compiler/test/oracle.d.ts +34 -0
  37. package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +1 -0
  38. package/dist/pg-sqlite-compiler/test/oracle.js +204 -0
  39. package/dist/pg-sqlite-compiler/test/oracle.js.map +1 -0
  40. package/dist/pg-sqlite-compiler/types.d.ts +55 -0
  41. package/dist/pg-sqlite-compiler/types.d.ts.map +1 -0
  42. package/dist/pg-sqlite-compiler/types.js +2 -0
  43. package/dist/pg-sqlite-compiler/types.js.map +1 -0
  44. package/package.json +8 -4
  45. package/src/admin/admin-data.test.ts +0 -348
  46. package/src/admin/http-proxy.ts +0 -252
  47. package/src/admin/log-store.ts +0 -192
  48. package/src/admin/server.ts +0 -471
  49. package/src/admin/ui.ts +0 -1322
  50. package/src/bench/proxy-throughput.bench.ts +0 -343
  51. package/src/bench/serial-mutations.bench.ts +0 -270
  52. package/src/browser.ts +0 -203
  53. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  54. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  55. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  56. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  57. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0f0f3bdf0abda097eb6f1246db4657d9fc622081362d894d82c1a1ce067b05b6.sqlite +0 -0
  58. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/1ddd3a4a48a11b51658444f5458a1fb175194b1d5b6a5bda20ef3fe3205b900c.sqlite +0 -0
  59. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/204a39120310d37e972c5914cfd71ad55c151bdb9e8ed289a5f8c5b052dd60e4.sqlite +0 -0
  60. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/3835f242df9728adba3d127a238793fd054ed3e51df3f60749ee744c469bf2a2.sqlite +0 -0
  61. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/4aa9c80eb716cf55b8995ccf7afab0b36c683e6da07d7c37a3f9c570136036df.sqlite +0 -0
  62. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/533e2fd1d6ea46e7a9a0017916ef341802d438d72583462755f2c1f8225e9bf2.sqlite +0 -0
  63. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/5ffa1aced1225ecaeac6366f7586aa3de92761cdff8711d81fbd81f248076abd.sqlite +0 -0
  64. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/686c3a9f0d7e59ed2ab607efd4b76d779c97cafeb3818380033bf7c7eb86c819.sqlite +0 -0
  65. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/6e8214e8dcfadd0deb52d64e5e9ca85c6b329ace11193909845995396914c473.sqlite +0 -0
  66. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/78d9ec9ff873d3fe3507ff53c2a6f6dfc408b4268eb0db3f2a146c0678965366.sqlite +0 -0
  67. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/7eff9f0ed7e27ad0d3f9d923de0682fab1928591172c1ba336c5f79a134a5d85.sqlite +0 -0
  68. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/836cda5b995b25867d722ed4f4c2292167e80351a3c6038db626648eb247dd8b.sqlite +0 -0
  69. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/91ef63b112209ab30172763acd8a0935106c248f7f1bcae5545ce37a9f201551.sqlite +0 -0
  70. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/a66ea4293a5f5938bc6d116edfa2522bb85bc37aea3541fbc09c3b613b9b32c0.sqlite +0 -0
  71. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/ceb2ab26b80590840b65651deb6e948d3bf81565c6751f3a58752cf4bf4aecae.sqlite +0 -0
  72. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  73. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  74. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  75. package/src/cf-do/ARCHITECTURE.md +0 -83
  76. package/src/cf-do/watermark.test.ts +0 -103
  77. package/src/cf-do/watermark.ts +0 -118
  78. package/src/cf-do/worker.ts +0 -1033
  79. package/src/cf-do/wrangler.toml +0 -11
  80. package/src/cf-pglite/README.md +0 -19
  81. package/src/change-tracking.ts +0 -25
  82. package/src/child-process.test.ts +0 -147
  83. package/src/child-process.ts +0 -90
  84. package/src/cli-entry.ts +0 -72
  85. package/src/cli.test.ts +0 -38
  86. package/src/cli.ts +0 -1214
  87. package/src/config.ts +0 -150
  88. package/src/do-sql-tracking.test.ts +0 -19
  89. package/src/do-sql-tracking.ts +0 -19
  90. package/src/index.ts +0 -1215
  91. package/src/integration/integration.test.ts +0 -517
  92. package/src/integration/native-binary.guard.test.ts +0 -13
  93. package/src/integration/native-startup.test.ts +0 -44
  94. package/src/integration/replication-latency.test.ts +0 -428
  95. package/src/integration/restore-live-stress.test.ts +0 -433
  96. package/src/integration/restore-reset.test.ts +0 -400
  97. package/src/integration/restore.test.ts +0 -274
  98. package/src/integration/test-permissions.ts +0 -147
  99. package/src/load-config.ts +0 -46
  100. package/src/log.ts +0 -96
  101. package/src/mutex.ts +0 -47
  102. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  103. package/src/pg-proxy-browser.ts +0 -2022
  104. package/src/pg-proxy-do-backend.test.ts +0 -3890
  105. package/src/pg-proxy-do-backend.ts +0 -7157
  106. package/src/pg-proxy.ts +0 -1087
  107. package/src/pglite-ipc.test.ts +0 -116
  108. package/src/pglite-ipc.ts +0 -266
  109. package/src/pglite-manager.ts +0 -557
  110. package/src/pglite-web-proxy.test.ts +0 -57
  111. package/src/pglite-web-proxy.ts +0 -221
  112. package/src/pglite-web-worker.ts +0 -152
  113. package/src/pglite-worker-thread.ts +0 -253
  114. package/src/port.ts +0 -25
  115. package/src/process-title.ts +0 -9
  116. package/src/recovery.ts +0 -155
  117. package/src/replication/change-tracker.test.ts +0 -357
  118. package/src/replication/change-tracker.ts +0 -279
  119. package/src/replication/handler.test.ts +0 -511
  120. package/src/replication/handler.ts +0 -1190
  121. package/src/replication/pgoutput-encoder.test.ts +0 -697
  122. package/src/replication/pgoutput-encoder.ts +0 -373
  123. package/src/replication/tcp-replication.test.ts +0 -876
  124. package/src/replication/zero-compat.test.ts +0 -1150
  125. package/src/restore-stress.test.ts +0 -188
  126. package/src/s3-local.ts +0 -203
  127. package/src/shim/hooks.mjs +0 -120
  128. package/src/shim/register.mjs +0 -4
  129. package/src/sqlite-mode/apply-mode.ts +0 -224
  130. package/src/sqlite-mode/index.ts +0 -15
  131. package/src/sqlite-mode/native-binary.ts +0 -89
  132. package/src/sqlite-mode/package-resolve.ts +0 -17
  133. package/src/sqlite-mode/resolve-mode.ts +0 -80
  134. package/src/sqlite-mode/shim-template.ts +0 -159
  135. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  136. package/src/sqlite-mode/types.ts +0 -30
  137. package/src/vite-plugin.ts +0 -67
  138. package/src/wasm-sqlite.test.ts +0 -537
  139. package/src/worker/browser-admin.ts +0 -52
  140. package/src/worker/browser-build-config.test.ts +0 -71
  141. package/src/worker/browser-build-config.ts +0 -109
  142. package/src/worker/browser-embed-admin.test.ts +0 -75
  143. package/src/worker/browser-embed.ts +0 -345
  144. package/src/worker/cf-patches.ts +0 -384
  145. package/src/worker/embed-integration.test.ts +0 -321
  146. package/src/worker/index.ts +0 -138
  147. package/src/worker/shims/fastify.test.ts +0 -255
  148. package/src/worker/shims/fastify.ts +0 -306
  149. package/src/worker/shims/http-service.test.ts +0 -355
  150. package/src/worker/shims/http-service.ts +0 -293
  151. package/src/worker/shims/node-stub.ts +0 -290
  152. package/src/worker/shims/oxfmt.ts +0 -3
  153. package/src/worker/shims/postgres-browser.ts +0 -59
  154. package/src/worker/shims/postgres-socket.test.ts +0 -576
  155. package/src/worker/shims/postgres-socket.ts +0 -310
  156. package/src/worker/shims/postgres.test.ts +0 -364
  157. package/src/worker/shims/postgres.ts +0 -1454
  158. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  159. package/src/worker/shims/sqlite-browser.ts +0 -175
  160. package/src/worker/shims/sqlite.test.ts +0 -786
  161. package/src/worker/shims/sqlite.ts +0 -978
  162. package/src/worker/shims/stream-browser.ts +0 -15
  163. package/src/worker/shims/ws-browser.test.ts +0 -205
  164. package/src/worker/shims/ws-browser.ts +0 -248
  165. package/src/worker/shims/ws.test.ts +0 -288
  166. package/src/worker/shims/ws.ts +0 -467
  167. package/src/worker/shims/zero-process-env.ts +0 -11
  168. package/src/worker/types.ts +0 -75
  169. package/src/worker/worker-integration.test.ts +0 -223
  170. package/src/worker/worker.test.ts +0 -136
  171. package/src/worker/zero-cache-embed-cf.ts +0 -463
  172. package/src/worker/zero-cache-embed.ts +0 -277
package/src/cli.ts DELETED
@@ -1,1214 +0,0 @@
1
- #!/usr/bin/env node
2
- import { spawn } from 'node:child_process'
3
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
4
- import { resolve } from 'node:path'
5
-
6
- import { isPidRunning } from './child-process.js'
7
- import { orezTitle } from './process-title.js'
8
-
9
- process.title = orezTitle()
10
-
11
- import { defineCommand } from 'citty'
12
- import { deparseSync, loadModule, parseSync } from 'pgsql-parser'
13
-
14
- import { startZeroLite } from './index.js'
15
- import { loadConfigFile, resolveOrezConfig } from './load-config.js'
16
- import { log, url } from './log.js'
17
-
18
- // detect admin port from running orez instance
19
- async function detectAdminPort(dataDir: string): Promise<number | null> {
20
- const pidFile = resolve(dataDir, 'orez.pid')
21
- const adminFile = resolve(dataDir, 'orez.admin')
22
-
23
- if (!existsSync(pidFile)) return null
24
-
25
- let pid: number | null = null
26
- try {
27
- const value = Number.parseInt(readFileSync(pidFile, 'utf-8').trim(), 10)
28
- if (Number.isInteger(value) && value > 0) {
29
- pid = value
30
- }
31
- } catch {}
32
-
33
- if (!isPidRunning(pid)) {
34
- try {
35
- unlinkSync(pidFile)
36
- } catch {}
37
- try {
38
- unlinkSync(adminFile)
39
- } catch {}
40
- return null
41
- }
42
-
43
- // check if admin port file exists
44
- if (existsSync(adminFile)) {
45
- try {
46
- const port = parseInt(readFileSync(adminFile, 'utf-8').trim(), 10)
47
- if (port > 0) return port
48
- } catch {}
49
- }
50
-
51
- // fallback: try common admin ports
52
- for (const port of [6477, 6478, 6479]) {
53
- try {
54
- const res = await fetch(`http://127.0.0.1:${port}/health`, {
55
- signal: AbortSignal.timeout(500),
56
- })
57
- if (res.ok) return port
58
- } catch {}
59
- }
60
-
61
- return null
62
- }
63
-
64
- const s3Command = defineCommand({
65
- meta: {
66
- name: 's3',
67
- description: 'start a local s3-compatible server',
68
- },
69
- args: {
70
- port: {
71
- type: 'string',
72
- description: 'port to listen on',
73
- default: '9200',
74
- },
75
- 'data-dir': {
76
- type: 'string',
77
- description: 'data directory for stored files',
78
- default: '.orez',
79
- },
80
- },
81
- async run({ args }) {
82
- const { startS3Local } = await import('./s3-local.js')
83
- const server = await startS3Local({
84
- port: Number(args.port),
85
- dataDir: args['data-dir'],
86
- })
87
-
88
- process.on('SIGINT', () => {
89
- server.close()
90
- process.exit(0)
91
- })
92
- process.on('SIGTERM', () => {
93
- server.close()
94
- process.exit(0)
95
- })
96
- },
97
- })
98
-
99
- const pgDumpCommand = defineCommand({
100
- meta: {
101
- name: 'pg_dump',
102
- description: 'dump the pglite postgres database to a SQL file',
103
- },
104
- args: {
105
- 'data-dir': {
106
- type: 'string',
107
- description: 'data directory',
108
- default: '.orez',
109
- },
110
- output: {
111
- type: 'string',
112
- description: 'output file path (default: stdout)',
113
- alias: 'o',
114
- },
115
- },
116
- async run({ args }) {
117
- const { PGlite } = await import('@electric-sql/pglite')
118
- const { vector } = await import('@electric-sql/pglite/vector')
119
- const { pg_trgm } = await import('@electric-sql/pglite/contrib/pg_trgm')
120
- const { pgcrypto } = await import('@electric-sql/pglite/contrib/pgcrypto')
121
- const { pgDump } = await import('@electric-sql/pglite-tools/pg_dump')
122
-
123
- const dataPath = resolve(args['data-dir'], 'pgdata-postgres')
124
- if (!existsSync(dataPath)) {
125
- console.error(`error: no database found at ${dataPath}`)
126
- process.exit(1)
127
- }
128
-
129
- let db: InstanceType<typeof PGlite> | undefined
130
- try {
131
- db = new PGlite({
132
- dataDir: dataPath,
133
- extensions: { vector, pg_trgm, pgcrypto },
134
- })
135
- await db.waitReady
136
-
137
- const file = await pgDump({ pg: db })
138
- const sql = await file.text()
139
-
140
- if (args.output) {
141
- writeFileSync(args.output, sql)
142
- log.orez(`dump written to ${args.output}`)
143
- } else {
144
- process.stdout.write(sql)
145
- }
146
- } catch (err: any) {
147
- if (err?.message?.includes('lock')) {
148
- console.error(
149
- 'error: database is locked — stop orez first before running pg_dump'
150
- )
151
- } else {
152
- console.error(`error: ${err?.message ?? err}`)
153
- }
154
- process.exit(1)
155
- } finally {
156
- await db?.close()
157
- }
158
- },
159
- })
160
-
161
- // extensions that don't exist in pglite — skip during restore
162
- const UNSUPPORTED_EXTENSIONS = new Set([
163
- 'pg_stat_statements',
164
- 'pg_buffercache',
165
- 'pg_freespacemap',
166
- 'pg_prewarm',
167
- 'pg_stat_kcache',
168
- 'pg_wait_sampling',
169
- 'auto_explain',
170
- 'pg_cron',
171
- ])
172
-
173
- // check if a statement should be skipped during restore
174
- function shouldSkipStatement(stmt: string): boolean {
175
- const trimmed = stmt.trimStart()
176
- // skip psql meta-commands like \restrict (can't be parsed)
177
- if (trimmed.startsWith('\\')) return true
178
-
179
- let parsed
180
- try {
181
- parsed = parseSync(trimmed)
182
- } catch {
183
- return false // if parser can't handle it, let pglite try
184
- }
185
-
186
- for (const entry of parsed.stmts) {
187
- const nodeType = Object.keys(entry.stmt)[0]
188
- const node = entry.stmt[nodeType]
189
-
190
- // skip SET transaction_timeout (pg 18+ artifact)
191
- if (nodeType === 'VariableSetStmt' && node.name === 'transaction_timeout') return true
192
-
193
- // skip CREATE EXTENSION for unsupported extensions
194
- if (nodeType === 'CreateExtensionStmt' && UNSUPPORTED_EXTENSIONS.has(node.extname))
195
- return true
196
-
197
- // skip DROP EXTENSION for unsupported extensions
198
- if (nodeType === 'DropStmt' && node.removeType === 'OBJECT_EXTENSION') {
199
- const extName = node.objects?.[0]?.String?.sval
200
- if (extName && UNSUPPORTED_EXTENSIONS.has(extName)) return true
201
- }
202
-
203
- // skip COMMENT ON EXTENSION for unsupported extensions
204
- if (nodeType === 'CommentStmt' && node.objtype === 'OBJECT_EXTENSION') {
205
- const extName = node.object?.String?.sval
206
- if (extName && UNSUPPORTED_EXTENSIONS.has(extName)) return true
207
- }
208
-
209
- // skip CREATE/ALTER/DROP PUBLICATION — pglite doesn't support wal_level=logical
210
- // internally, so CREATE PUBLICATION errors and can roll back the transaction.
211
- // orez handles replication via its own change tracker, not publications.
212
- if (nodeType === 'CreatePublicationStmt' || nodeType === 'AlterPublicationStmt')
213
- return true
214
- if (nodeType === 'DropStmt' && node.removeType === 'OBJECT_PUBLICATION') return true
215
- }
216
-
217
- return false
218
- }
219
-
220
- // how many data statements to batch into a single transaction
221
- const BATCH_SIZE = 200
222
- // run CHECKPOINT every N batches to flush WAL and reclaim wasm memory
223
- const CHECKPOINT_INTERVAL = 3
224
-
225
- // true for statements that are data manipulation (INSERT/UPDATE/DELETE)
226
- // these get batched into transactions. DDL runs outside batches.
227
- // note: COPY FROM stdin is handled separately by the copy-data converter
228
- function isDataStatement(stmt: string): boolean {
229
- try {
230
- const parsed = parseSync(stmt)
231
- if (parsed.stmts.length === 0) return false
232
- const nodeType = Object.keys(parsed.stmts[0].stmt)[0]
233
- return (
234
- nodeType === 'InsertStmt' || nodeType === 'UpdateStmt' || nodeType === 'DeleteStmt'
235
- )
236
- } catch {
237
- return false
238
- }
239
- }
240
-
241
- // detect COPY ... FROM stdin and extract table + columns from AST
242
- function parseCopyFromStdin(stmt: string): { table: string; columns: string[] } | null {
243
- try {
244
- const parsed = parseSync(stmt)
245
- if (parsed.stmts.length === 0) return null
246
- const node = parsed.stmts[0].stmt.CopyStmt
247
- if (!node || !node.is_from) return null
248
- const schema = node.relation.schemaname
249
- const table = schema
250
- ? `"${schema}"."${node.relation.relname}"`
251
- : `"${node.relation.relname}"`
252
- const columns = node.attlist ? node.attlist.map((a: any) => `"${a.String.sval}"`) : []
253
- return { table, columns }
254
- } catch {
255
- return null
256
- }
257
- }
258
-
259
- // convert a COPY text-format value to a SQL literal
260
- // handles: \N → NULL, \\ → \, \t \n \r escapes, and single-quote escaping
261
- function copyValueToLiteral(val: string): string {
262
- if (val === '\\N') return 'NULL'
263
- let result = ''
264
- for (let i = 0; i < val.length; i++) {
265
- if (val[i] === '\\' && i + 1 < val.length) {
266
- const next = val[i + 1]
267
- if (next === '\\') {
268
- result += '\\'
269
- i++
270
- } else if (next === 'n') {
271
- result += '\n'
272
- i++
273
- } else if (next === 'r') {
274
- result += '\r'
275
- i++
276
- } else if (next === 't') {
277
- result += '\t'
278
- i++
279
- } else {
280
- result += val[i]
281
- }
282
- } else {
283
- result += val[i]
284
- }
285
- }
286
- return "'" + result.replace(/'/g, "''") + "'"
287
- }
288
-
289
- // stream a sql dump file statement-by-statement with transaction batching
290
- export async function execDumpFile(
291
- db: { exec: (sql: string) => Promise<unknown> },
292
- filePath: string
293
- ): Promise<{ executed: number; skipped: number }> {
294
- const { createReadStream } = await import('node:fs')
295
- const { createInterface } = await import('node:readline')
296
-
297
- const rl = createInterface({
298
- input: createReadStream(filePath, 'utf-8'),
299
- crlfDelay: Infinity,
300
- })
301
-
302
- let buf = ''
303
- let executed = 0
304
- let skipped = 0
305
- let batchCount = 0
306
- let batchesSinceCheckpoint = 0
307
- let inBatch = false
308
- let dollarTag: string | null = null // tracks $tag$ quoting
309
-
310
- // copy-data mode: when we hit COPY ... FROM stdin, we read data lines until \.
311
- let copyTarget: { table: string; columns: string[] } | null = null
312
- // accumulate COPY rows for multi-row INSERT (reduces statement count ~50x)
313
- const COPY_ROWS_PER_INSERT = 50
314
- // flush early if accumulated SQL exceeds this (prevents WASM OOM on huge rows)
315
- const COPY_BATCH_MAX_BYTES = 1_000_000
316
- // skip individual rows larger than this — PGlite WASM crashes around 24MB
317
- const MAX_ROW_BYTES = 16_000_000
318
- let copyRows: string[] = []
319
- let copyRowsBytes = 0
320
-
321
- async function flushCopyRows() {
322
- if (copyRows.length === 0 || !copyTarget) return
323
- const colList =
324
- copyTarget.columns.length > 0 ? ` (${copyTarget.columns.join(', ')})` : ''
325
- const insert = `INSERT INTO ${copyTarget.table}${colList} VALUES ${copyRows.join(', ')}`
326
- try {
327
- await db.exec(insert)
328
- executed += copyRows.length
329
- batchCount += copyRows.length
330
- } catch (err: any) {
331
- log.orez(`warning: ${err?.message?.split('\n')[0] ?? err}`)
332
- skipped += copyRows.length
333
- // transaction is aborted, rollback and start fresh
334
- if (inBatch) {
335
- try {
336
- await db.exec('ROLLBACK')
337
- } catch {}
338
- inBatch = false
339
- batchCount = 0
340
- }
341
- }
342
- copyRows = []
343
- copyRowsBytes = 0
344
- }
345
-
346
- async function flushBatch() {
347
- if (inBatch) {
348
- await db.exec('COMMIT')
349
- inBatch = false
350
- batchesSinceCheckpoint++
351
- if (batchesSinceCheckpoint >= CHECKPOINT_INTERVAL) {
352
- await db.exec('CHECKPOINT')
353
- batchesSinceCheckpoint = 0
354
- }
355
- }
356
- batchCount = 0
357
- }
358
-
359
- for await (const line of rl) {
360
- // in copy-data mode: read tab-delimited rows until \.
361
- if (copyTarget) {
362
- if (line === '\\.') {
363
- if (copyRows.length > 0) {
364
- if (!inBatch) {
365
- await db.exec('BEGIN')
366
- inBatch = true
367
- }
368
- await flushCopyRows()
369
- }
370
- copyTarget = null
371
- continue
372
- }
373
- const values = line.split('\t').map(copyValueToLiteral)
374
- const row = `(${values.join(', ')})`
375
-
376
- // skip rows that exceed WASM memory limits (~24MB crashes PGlite)
377
- if (row.length > MAX_ROW_BYTES) {
378
- log.orez(
379
- `skipping oversized row (${(row.length / 1_000_000).toFixed(1)}MB) in ${copyTarget.table}`
380
- )
381
- skipped++
382
- continue
383
- }
384
-
385
- // flush accumulated rows before adding if this would exceed size limit
386
- if (copyRows.length > 0 && copyRowsBytes + row.length > COPY_BATCH_MAX_BYTES) {
387
- if (!inBatch) {
388
- await db.exec('BEGIN')
389
- inBatch = true
390
- }
391
- await flushCopyRows()
392
- if (batchCount >= BATCH_SIZE) {
393
- await flushBatch()
394
- }
395
- }
396
-
397
- copyRows.push(row)
398
- copyRowsBytes += row.length
399
- if (
400
- copyRows.length >= COPY_ROWS_PER_INSERT ||
401
- copyRowsBytes >= COPY_BATCH_MAX_BYTES
402
- ) {
403
- if (!inBatch) {
404
- await db.exec('BEGIN')
405
- inBatch = true
406
- }
407
- await flushCopyRows()
408
- if (batchCount >= BATCH_SIZE) {
409
- await flushBatch()
410
- }
411
- }
412
- continue
413
- }
414
-
415
- // skip empty lines and sql comments (only outside dollar-quoted blocks)
416
- if (!dollarTag && (line === '' || line.startsWith('--'))) continue
417
-
418
- buf += (buf ? '\n' : '') + line
419
-
420
- // track dollar-quoting: $$ or $tag$
421
- const dollarMatches = line.matchAll(/(\$[a-zA-Z_]*\$)/g)
422
- for (const m of dollarMatches) {
423
- if (dollarTag === null) {
424
- dollarTag = m[1]
425
- } else if (m[1] === dollarTag) {
426
- dollarTag = null
427
- }
428
- }
429
-
430
- // can't end a statement while inside a dollar-quoted block
431
- if (dollarTag) continue
432
-
433
- // statements end with ; at end of line (pg_dump always formats this way)
434
- if (!line.trimEnd().endsWith(';')) continue
435
-
436
- const stmt = buf
437
- buf = ''
438
-
439
- if (shouldSkipStatement(stmt)) {
440
- skipped++
441
- continue
442
- }
443
-
444
- // check for COPY ... FROM stdin → convert to INSERTs
445
- const copyInfo = parseCopyFromStdin(stmt)
446
- if (copyInfo) {
447
- copyTarget = copyInfo
448
- continue
449
- }
450
-
451
- // rewrite statements to be idempotent so restores don't crash on "already exists"
452
- let rewritten = stmt
453
- try {
454
- const parsed = parseSync(stmt)
455
- if (parsed.stmts.length > 0) {
456
- const nodeType = Object.keys(parsed.stmts[0].stmt)[0]
457
- const node = parsed.stmts[0].stmt[nodeType]
458
- let modified = false
459
-
460
- // CREATE SCHEMA → CREATE SCHEMA IF NOT EXISTS
461
- if (nodeType === 'CreateSchemaStmt' && !node.if_not_exists) {
462
- node.if_not_exists = true
463
- modified = true
464
- }
465
- // CREATE FUNCTION/PROCEDURE → CREATE OR REPLACE
466
- if (nodeType === 'CreateFunctionStmt' && !node.replace) {
467
- node.replace = true
468
- modified = true
469
- }
470
- // CREATE VIEW → CREATE OR REPLACE VIEW
471
- if (nodeType === 'ViewStmt' && !node.replace) {
472
- node.replace = true
473
- modified = true
474
- }
475
-
476
- if (modified) rewritten = deparseSync(parsed)
477
- }
478
- } catch {
479
- // if parse/deparse fails, use original
480
- }
481
-
482
- if (isDataStatement(rewritten)) {
483
- // batch data statements into transactions
484
- if (!inBatch) {
485
- await db.exec('BEGIN')
486
- inBatch = true
487
- }
488
- try {
489
- await db.exec(rewritten)
490
- executed++
491
- batchCount++
492
- if (batchCount >= BATCH_SIZE) {
493
- await flushBatch()
494
- }
495
- } catch (err: any) {
496
- // non-fatal data errors (duplicate keys from internal tables, etc.)
497
- log.orez(`warning: ${err?.message?.split('\n')[0] ?? err}`)
498
- skipped++
499
- // transaction is aborted, rollback and start fresh
500
- try {
501
- await db.exec('ROLLBACK')
502
- } catch {}
503
- inBatch = false
504
- batchCount = 0
505
- }
506
- } else {
507
- // DDL runs outside batches
508
- await flushBatch()
509
- try {
510
- await db.exec(rewritten)
511
- executed++
512
- } catch (err: any) {
513
- // non-fatal DDL errors (missing tables from filtered dumps, etc.)
514
- log.orez(`warning: ${err?.message?.split('\n')[0] ?? err}`)
515
- skipped++
516
- }
517
- }
518
- }
519
-
520
- // flush remaining batch + buffer
521
- await flushBatch()
522
- if (buf.trim()) {
523
- if (!shouldSkipStatement(buf)) {
524
- await db.exec(buf)
525
- executed++
526
- } else {
527
- skipped++
528
- }
529
- }
530
-
531
- return { executed, skipped }
532
- }
533
-
534
- // after restore, drop triggers whose backing functions no longer exist.
535
- // this happens when a filtered dump includes triggers on public-schema tables
536
- // that reference functions from excluded schemas.
537
- async function cleanupBrokenTriggers(db: { exec: (q: string) => Promise<unknown> }) {
538
- try {
539
- const result = (await db.exec(`
540
- SELECT tgname, relname, nspname, proname, pronamespace
541
- FROM pg_trigger t
542
- JOIN pg_class c ON c.oid = t.tgrelid
543
- JOIN pg_namespace n ON n.oid = c.relnamespace
544
- LEFT JOIN pg_proc p ON p.oid = t.tgfoid
545
- WHERE NOT t.tgisinternal
546
- AND n.nspname = 'public'
547
- AND (p.oid IS NULL OR p.pronamespace != n.oid)
548
- `)) as any
549
-
550
- const rows = result?.rows || result?.[0]?.rows || []
551
- for (const row of rows) {
552
- const trigger = row.tgname
553
- const table = row.relname
554
- try {
555
- await db.exec(`DROP TRIGGER IF EXISTS "${trigger}" ON "public"."${table}"`)
556
- log.orez(`dropped broken trigger "${trigger}" on "${table}"`)
557
- } catch {}
558
- }
559
- } catch {
560
- // best-effort cleanup
561
- }
562
- }
563
-
564
- // try restoring via wire protocol (postgres running on given port)
565
- // returns true if connected and restored, false if connection unavailable
566
- async function tryWireRestore(opts: {
567
- port: number
568
- user: string
569
- password: string
570
- clean: boolean
571
- sqlFile: string
572
- dataDir: string
573
- }): Promise<boolean> {
574
- const postgres = (await import('postgres')).default
575
- const sql = postgres({
576
- host: '127.0.0.1',
577
- port: opts.port,
578
- user: opts.user,
579
- password: opts.password,
580
- database: 'postgres',
581
- connect_timeout: 3,
582
- max: 1, // single connection so BEGIN/COMMIT work correctly
583
- onnotice: () => {}, // suppress pglite transaction warnings
584
- })
585
-
586
- try {
587
- await sql`SELECT 1`
588
- } catch {
589
- await sql.end({ timeout: 0 }).catch(() => {})
590
- return false
591
- }
592
-
593
- // connected — restore errors should propagate, not fall back
594
- log.orez(`connected via wire protocol on port ${opts.port}`)
595
-
596
- // automatically stop zero-cache before restore to prevent conflicts
597
- const adminPort = await detectAdminPort(opts.dataDir)
598
- if (adminPort) {
599
- log.orez('stopping zero-cache for restore...')
600
- try {
601
- await fetch(`http://127.0.0.1:${adminPort}/api/actions/stop-zero`, {
602
- method: 'POST',
603
- signal: AbortSignal.timeout(10_000),
604
- })
605
- // give zero-cache time to stop
606
- await new Promise((r) => setTimeout(r, 1000))
607
- } catch {
608
- log.orez('warning: could not stop zero-cache (may not be running)')
609
- }
610
- }
611
-
612
- try {
613
- const pubName = process.env.ZERO_APP_PUBLICATIONS?.trim()
614
- let pubTablesBeforeRestore: string[] = []
615
- if (pubName) {
616
- try {
617
- const existing = await sql<{ tablename: string }[]>`
618
- SELECT tablename
619
- FROM pg_publication_tables
620
- WHERE pubname = ${pubName}
621
- AND schemaname = 'public'
622
- `
623
- pubTablesBeforeRestore = existing.map((r) => r.tablename)
624
- } catch {
625
- // publication might not exist yet
626
- }
627
- }
628
-
629
- if (opts.clean) {
630
- log.orez('dropping and recreating public schema')
631
- await sql.unsafe('DROP SCHEMA public CASCADE')
632
- await sql.unsafe('CREATE SCHEMA public')
633
- }
634
-
635
- const db = { exec: (query: string) => sql.unsafe(query) as Promise<unknown> }
636
- const { executed, skipped } = await execDumpFile(db, opts.sqlFile)
637
- await cleanupBrokenTriggers(db)
638
- await db.exec('SET search_path TO public')
639
- log.orez(
640
- `restored ${opts.sqlFile} via wire protocol (${executed} statements, ${skipped} skipped)`
641
- )
642
-
643
- // clear zero replication state (in _orez schema)
644
- await sql.unsafe('TRUNCATE _orez._zero_changes').catch(() => {})
645
- await sql.unsafe('TRUNCATE _orez._zero_replication_slots').catch(() => {})
646
- log.orez('cleared zero replication state')
647
-
648
- // drop zero cdb cdc schemas so zero-cache can recreate them fresh
649
- const cdbSql = postgres({
650
- host: '127.0.0.1',
651
- port: opts.port,
652
- user: opts.user,
653
- password: opts.password,
654
- database: 'zero_cdb',
655
- connect_timeout: 3,
656
- max: 1,
657
- onnotice: () => {},
658
- })
659
- try {
660
- const cdcSchemas = await cdbSql<{ nspname: string }[]>`
661
- SELECT DISTINCT nspname FROM pg_namespace WHERE nspname LIKE '%/cdc'
662
- `
663
- for (const { nspname } of cdcSchemas) {
664
- await cdbSql.unsafe(`DROP SCHEMA IF EXISTS "${nspname}" CASCADE`).catch(() => {})
665
- }
666
- if (cdcSchemas.length > 0) {
667
- log.orez(`dropped ${cdcSchemas.length} cdc schema(s) from zero_cdb`)
668
- }
669
- } catch {
670
- // zero_cdb might not exist yet
671
- } finally {
672
- await cdbSql.end({ timeout: 1 }).catch(() => {})
673
- }
674
-
675
- if (pubName) {
676
- const quoted = '"' + pubName.replace(/"/g, '""') + '"'
677
- await sql.unsafe(`CREATE PUBLICATION ${quoted}`).catch(() => {})
678
-
679
- // Rebuild publication membership after restore so replication resumes
680
- // without requiring an app restart or migration rerun.
681
- const existingPublicTables = await sql<{ tablename: string }[]>`
682
- SELECT tablename
683
- FROM pg_tables
684
- WHERE schemaname = 'public'
685
- AND tablename NOT LIKE '_zero_%'
686
- `
687
- const existingSet = new Set(existingPublicTables.map((r) => r.tablename))
688
-
689
- // Prefer pre-restore publication membership; if unavailable, fall back to
690
- // ALL public tables (prod dumps don't have _0_version columns yet).
691
- const desired = new Set<string>(
692
- pubTablesBeforeRestore.filter((t) => existingSet.has(t))
693
- )
694
- if (desired.size === 0) {
695
- // Add all public tables except internal ones
696
- for (const { tablename } of existingPublicTables) {
697
- if (!tablename.startsWith('_')) {
698
- desired.add(tablename)
699
- }
700
- }
701
- }
702
-
703
- if (desired.size > 0) {
704
- const inPub = await sql<{ tablename: string }[]>`
705
- SELECT tablename
706
- FROM pg_publication_tables
707
- WHERE pubname = ${pubName}
708
- AND schemaname = 'public'
709
- `
710
- const inPubSet = new Set(inPub.map((r) => r.tablename))
711
- const toAdd = [...desired].filter((t) => !inPubSet.has(t))
712
- if (toAdd.length > 0) {
713
- const tableList = toAdd
714
- .map((t) => `"public"."${t.replace(/"/g, '""')}"`)
715
- .join(', ')
716
- await sql.unsafe(`ALTER PUBLICATION ${quoted} ADD TABLE ${tableList}`)
717
- log.orez(`added ${toAdd.length} table(s) to publication "${pubName}"`)
718
- }
719
- }
720
-
721
- const countRows = await sql<{ count: string }[]>`
722
- SELECT count(*)::text AS count
723
- FROM pg_publication_tables
724
- WHERE pubname = ${pubName}
725
- AND schemaname = 'public'
726
- `
727
- const count = Number(countRows[0]?.count || '0')
728
- log.orez(`publication "${pubName}" has ${count} table(s) after restore`)
729
- }
730
-
731
- // drop zero shard schemas to prevent conflicts when zero restarts
732
- const shardSchemas = await sql<{ nspname: string }[]>`
733
- SELECT nspname FROM pg_namespace
734
- WHERE nspname LIKE 'chat_%'
735
- OR nspname LIKE 'zero_%'
736
- OR nspname LIKE 'startchat_%'
737
- `
738
- for (const { nspname } of shardSchemas) {
739
- await sql.unsafe(`DROP SCHEMA IF EXISTS "${nspname}" CASCADE`).catch(() => {})
740
- }
741
- if (shardSchemas.length > 0) {
742
- log.orez(`dropped ${shardSchemas.length} shard schema(s)`)
743
- }
744
-
745
- log.orez('restore complete')
746
- } finally {
747
- await sql.end({ timeout: 1 })
748
- }
749
-
750
- // restart zero-cache so it recreates shard schemas fresh
751
- if (adminPort) {
752
- log.orez('restarting zero-cache...')
753
- try {
754
- await fetch(`http://127.0.0.1:${adminPort}/api/actions/restart-zero`, {
755
- method: 'POST',
756
- signal: AbortSignal.timeout(10_000),
757
- })
758
- log.orez('zero-cache restarting')
759
- } catch {
760
- log.orez('warning: could not restart zero-cache')
761
- }
762
- }
763
-
764
- return true
765
- }
766
-
767
- // restore by opening PGlite directly (requires no other process holding the lock)
768
- async function directRestore(opts: {
769
- dataDir: string
770
- clean: boolean
771
- sqlFile: string
772
- }): Promise<void> {
773
- const { PGlite } = await import('@electric-sql/pglite')
774
- const { vector } = await import('@electric-sql/pglite/vector')
775
- const { pg_trgm } = await import('@electric-sql/pglite/contrib/pg_trgm')
776
- const { pgcrypto } = await import('@electric-sql/pglite/contrib/pgcrypto')
777
-
778
- const dataPath = resolve(opts.dataDir, 'pgdata-postgres')
779
-
780
- let db: InstanceType<typeof PGlite> | undefined
781
- try {
782
- db = new PGlite({
783
- dataDir: dataPath,
784
- extensions: { vector, pg_trgm, pgcrypto },
785
- relaxedDurability: true,
786
- })
787
- await db.waitReady
788
-
789
- if (opts.clean) {
790
- log.orez('dropping and recreating public schema')
791
- await db.exec('DROP SCHEMA public CASCADE')
792
- await db.exec('CREATE SCHEMA public')
793
- }
794
-
795
- const { executed, skipped } = await execDumpFile(db, opts.sqlFile)
796
- await cleanupBrokenTriggers(db)
797
- await db.exec('SET search_path TO public')
798
- log.orez(
799
- `restored ${opts.sqlFile} into ${dataPath} (${executed} statements, ${skipped} skipped)`
800
- )
801
- } catch (err: any) {
802
- if (err?.message?.includes('lock')) {
803
- console.error(
804
- 'error: database is locked — stop orez first before running pg_restore'
805
- )
806
- } else {
807
- console.error(`error: ${err?.message ?? err}`)
808
- }
809
- process.exit(1)
810
- } finally {
811
- await db?.close()
812
- }
813
- }
814
-
815
- const pgRestoreCommand = defineCommand({
816
- meta: {
817
- name: 'pg_restore',
818
- description: 'restore a SQL dump into the pglite postgres database',
819
- },
820
- args: {
821
- file: {
822
- type: 'positional',
823
- description: 'SQL file to restore',
824
- required: true,
825
- },
826
- 'data-dir': {
827
- type: 'string',
828
- description: 'data directory',
829
- default: '.orez',
830
- },
831
- clean: {
832
- type: 'boolean',
833
- description: 'drop and recreate public schema before restoring',
834
- default: false,
835
- },
836
- 'pg-port': {
837
- type: 'string',
838
- description: 'postgresql port for wire protocol connection',
839
- default: '6434',
840
- },
841
- 'pg-user': {
842
- type: 'string',
843
- description: 'postgresql user',
844
- default: 'user',
845
- },
846
- 'pg-password': {
847
- type: 'string',
848
- description: 'postgresql password',
849
- default: 'password',
850
- },
851
- direct: {
852
- type: 'boolean',
853
- description: 'force direct PGlite access, skip wire protocol auto-detection',
854
- default: false,
855
- },
856
- },
857
- async run({ args }) {
858
- await loadModule() // initialize pgsql-parser WASM
859
-
860
- const sqlFile = args.file
861
- if (!existsSync(sqlFile)) {
862
- console.error(`error: file not found: ${sqlFile}`)
863
- process.exit(1)
864
- }
865
-
866
- // try wire protocol first (unless --direct)
867
- if (!args.direct) {
868
- try {
869
- const restored = await tryWireRestore({
870
- port: Number(args['pg-port']),
871
- user: args['pg-user'],
872
- password: args['pg-password'],
873
- clean: args.clean,
874
- sqlFile,
875
- dataDir: args['data-dir'],
876
- })
877
- if (restored) {
878
- // ensure clean exit - don't let any lingering handles keep process alive
879
- process.exit(0)
880
- }
881
- log.orez('wire protocol unavailable, falling back to direct PGlite')
882
- } catch (err: any) {
883
- // connected but restore failed — report error, don't fall back
884
- console.error(`error: ${err?.message ?? err}`)
885
- process.exit(1)
886
- }
887
- }
888
-
889
- await directRestore({
890
- dataDir: args['data-dir'],
891
- clean: args.clean,
892
- sqlFile,
893
- })
894
- },
895
- })
896
-
897
- export const main = defineCommand({
898
- meta: {
899
- name: 'orez',
900
- description: 'pglite-powered zero-sync development backend',
901
- },
902
- args: {
903
- 'pg-port': {
904
- type: 'string',
905
- description: 'postgresql proxy port',
906
- default: '6434',
907
- },
908
- 'zero-port': {
909
- type: 'string',
910
- description: 'zero-cache port',
911
- default: '5849',
912
- },
913
- 'data-dir': {
914
- type: 'string',
915
- description: 'data directory',
916
- default: '.orez',
917
- },
918
- migrations: {
919
- type: 'string',
920
- description: 'migrations directory',
921
- default: '',
922
- },
923
- seed: {
924
- type: 'string',
925
- description: 'seed file path',
926
- default: '',
927
- },
928
- 'pg-user': {
929
- type: 'string',
930
- description: 'postgresql user',
931
- default: 'user',
932
- },
933
- 'pg-password': {
934
- type: 'string',
935
- description: 'postgresql password',
936
- default: 'password',
937
- },
938
- 'skip-zero-cache': {
939
- type: 'boolean',
940
- description: 'run pglite + proxy only, skip zero-cache',
941
- default: false,
942
- },
943
- 'log-level': {
944
- type: 'string',
945
- description: 'log level: error, warn, info, debug (default: warn)',
946
- },
947
- s3: {
948
- type: 'boolean',
949
- description: 'also start a local s3-compatible server',
950
- default: false,
951
- },
952
- 's3-port': {
953
- type: 'string',
954
- description: 's3 server port',
955
- default: '9200',
956
- },
957
- 'disable-wasm-sqlite': {
958
- type: 'boolean',
959
- description: 'force native @rocicorp/zero-sqlite3 (fails if not available)',
960
- default: false,
961
- },
962
- 'force-wasm-sqlite': {
963
- type: 'boolean',
964
- description: 'force wasm bedrock-sqlite even if native is available',
965
- default: false,
966
- },
967
- 'no-worker-threads': {
968
- type: 'boolean',
969
- description: 'run pglite in-process instead of worker threads',
970
- default: false,
971
- },
972
- 'single-db': {
973
- type: 'boolean',
974
- description:
975
- 'use a single pglite instance for all databases (lighter for constrained environments)',
976
- default: false,
977
- },
978
- 'read-replicas': {
979
- type: 'string',
980
- description:
981
- 'number of pglite read replicas for postgres (0 to disable, default: auto)',
982
- default: '',
983
- },
984
- 'on-db-ready': {
985
- type: 'string',
986
- description: 'command to run after db+proxy are ready, before zero-cache starts',
987
- default: '',
988
- },
989
- 'on-healthy': {
990
- type: 'string',
991
- description: 'command to run once all services are healthy',
992
- default: '',
993
- },
994
- 'disable-admin': {
995
- type: 'boolean',
996
- description: 'disable admin dashboard',
997
- default: false,
998
- },
999
- 'admin-port': {
1000
- type: 'string',
1001
- description: 'admin dashboard port',
1002
- default: '6477',
1003
- },
1004
- 'checkpoint-interval': {
1005
- type: 'string',
1006
- description: 'WAL checkpoint interval in seconds (0 to disable, default: 300)',
1007
- default: '',
1008
- },
1009
- 'max-log-size': {
1010
- type: 'string',
1011
- description: 'max log file size in MB before rotation (default: 2)',
1012
- default: '',
1013
- },
1014
- 'disable-disk-logs': {
1015
- type: 'boolean',
1016
- description: 'disable writing logs to disk',
1017
- default: false,
1018
- },
1019
- },
1020
- subCommands: {
1021
- s3: s3Command,
1022
- pg_dump: pgDumpCommand,
1023
- pg_restore: pgRestoreCommand,
1024
- },
1025
- async run({ args }) {
1026
- // load orez.config.ts/js/mjs if present
1027
- const fileConfig = await loadConfigFile()
1028
-
1029
- // build cli overrides — only include values the user actually passed
1030
- // (citty fills defaults for all args, so we detect "user passed" by
1031
- // comparing against the declared defaults above)
1032
- const cliDefaults: Record<string, unknown> = {
1033
- 'pg-port': '6434',
1034
- 'zero-port': '5849',
1035
- 'data-dir': '.orez',
1036
- migrations: '',
1037
- seed: '',
1038
- 'pg-user': 'user',
1039
- 'pg-password': 'password',
1040
- 'skip-zero-cache': false,
1041
- 'log-level': undefined,
1042
- s3: false,
1043
- 's3-port': '9200',
1044
- 'disable-wasm-sqlite': false,
1045
- 'force-wasm-sqlite': false,
1046
- 'no-worker-threads': false,
1047
- 'single-db': false,
1048
- 'read-replicas': '',
1049
- 'on-db-ready': '',
1050
- 'on-healthy': '',
1051
- 'disable-admin': false,
1052
- 'admin-port': '6477',
1053
- 'checkpoint-interval': '',
1054
- 'max-log-size': '',
1055
- 'disable-disk-logs': false,
1056
- }
1057
- const wasSet = (key: string) => {
1058
- const val = (args as Record<string, unknown>)[key]
1059
- const def = cliDefaults[key]
1060
- return val !== def
1061
- }
1062
-
1063
- const cliOverrides = resolveOrezConfig(fileConfig, {
1064
- ...(wasSet('pg-port') && { pgPort: Number(args['pg-port']) }),
1065
- ...(wasSet('zero-port') && { zeroPort: Number(args['zero-port']) }),
1066
- ...(wasSet('admin-port') && { adminPort: Number(args['admin-port']) }),
1067
- ...(wasSet('data-dir') && { dataDir: args['data-dir'] }),
1068
- ...(wasSet('migrations') && { migrations: args.migrations }),
1069
- ...(wasSet('seed') && { seed: args.seed }),
1070
- ...(wasSet('pg-user') && { pgUser: args['pg-user'] }),
1071
- ...(wasSet('pg-password') && { pgPassword: args['pg-password'] }),
1072
- ...(wasSet('skip-zero-cache') && { skipZeroCache: args['skip-zero-cache'] }),
1073
- ...(wasSet('log-level') && {
1074
- logLevel: args['log-level'] as 'error' | 'warn' | 'info' | 'debug',
1075
- }),
1076
- ...(wasSet('s3') && { s3: args.s3 }),
1077
- ...(wasSet('s3-port') && { s3Port: Number(args['s3-port']) }),
1078
- ...(wasSet('disable-wasm-sqlite') && {
1079
- disableWasmSqlite: args['disable-wasm-sqlite'],
1080
- }),
1081
- ...(wasSet('force-wasm-sqlite') && { forceWasmSqlite: args['force-wasm-sqlite'] }),
1082
- ...(wasSet('no-worker-threads') && { noWorkerThreads: args['no-worker-threads'] }),
1083
- ...(wasSet('single-db') && { singleDb: args['single-db'] }),
1084
- ...(wasSet('read-replicas') && { readReplicas: Number(args['read-replicas']) }),
1085
- ...(wasSet('on-db-ready') && { onDbReady: args['on-db-ready'] }),
1086
- ...(wasSet('on-healthy') && { onHealthy: args['on-healthy'] }),
1087
- ...(wasSet('disable-admin') && { disableAdmin: args['disable-admin'] }),
1088
- ...(wasSet('checkpoint-interval') && {
1089
- checkpointIntervalMs: Number(args['checkpoint-interval']) * 1000,
1090
- }),
1091
- ...(wasSet('max-log-size') && {
1092
- maxLogFileSize: Number(args['max-log-size']) * 1024 * 1024,
1093
- }),
1094
- ...(wasSet('disable-disk-logs') && { disableDiskLogs: args['disable-disk-logs'] }),
1095
- })
1096
-
1097
- // resolve aliases and compute final values
1098
- const resolvedMigrations = cliOverrides.migrations ?? cliOverrides.migrationsDir ?? ''
1099
- const resolvedSeed = cliOverrides.seed ?? cliOverrides.seedFile ?? ''
1100
- const resolvedUseWorkerThreads =
1101
- cliOverrides.noWorkerThreads != null
1102
- ? !cliOverrides.noWorkerThreads
1103
- : (cliOverrides.useWorkerThreads ?? true)
1104
- const resolvedDisableAdmin = cliOverrides.disableAdmin ?? false
1105
- const resolvedAdminPort = resolvedDisableAdmin ? 0 : (cliOverrides.adminPort ?? 6477)
1106
-
1107
- const {
1108
- config,
1109
- stop,
1110
- instances,
1111
- zeroEnv,
1112
- logStore,
1113
- httpLog,
1114
- restartZero,
1115
- stopZero,
1116
- resetZero,
1117
- resetZeroFull,
1118
- } = await startZeroLite({
1119
- pgPort: cliOverrides.pgPort,
1120
- zeroPort: cliOverrides.zeroPort,
1121
- adminPort: resolvedAdminPort,
1122
- dataDir: cliOverrides.dataDir,
1123
- migrationsDir: resolvedMigrations,
1124
- seedFile: resolvedSeed,
1125
- pgUser: cliOverrides.pgUser,
1126
- pgPassword: cliOverrides.pgPassword,
1127
- skipZeroCache: cliOverrides.skipZeroCache,
1128
- disableWasmSqlite: cliOverrides.disableWasmSqlite,
1129
- forceWasmSqlite: cliOverrides.forceWasmSqlite,
1130
- useWorkerThreads: resolvedUseWorkerThreads,
1131
- singleDb: cliOverrides.singleDb,
1132
- readReplicas: cliOverrides.readReplicas,
1133
- logLevel: cliOverrides.logLevel,
1134
- onDbReady: cliOverrides.onDbReady || undefined,
1135
- onHealthy: cliOverrides.onHealthy || undefined,
1136
- pgliteOptions: cliOverrides.pgliteOptions,
1137
- zeroPublications: cliOverrides.zeroPublications,
1138
- zeroMutateUrl: cliOverrides.zeroMutateUrl,
1139
- zeroQueryUrl: cliOverrides.zeroQueryUrl,
1140
- checkpointIntervalMs: cliOverrides.checkpointIntervalMs,
1141
- maxLogFileSize: cliOverrides.maxLogFileSize,
1142
- disableDiskLogs: cliOverrides.disableDiskLogs,
1143
- })
1144
-
1145
- const s3Enabled = cliOverrides.s3 ?? false
1146
- let s3Server: import('node:http').Server | null = null
1147
- if (s3Enabled) {
1148
- const { startS3Local } = await import('./s3-local.js')
1149
- s3Server = await startS3Local({
1150
- port: cliOverrides.s3Port ?? 9200,
1151
- dataDir: config.dataDir,
1152
- })
1153
- }
1154
-
1155
- let adminServer: import('node:http').Server | null = null
1156
- if (!resolvedDisableAdmin && logStore && zeroEnv) {
1157
- const { startAdminServer } = await import('./admin/server.js')
1158
- adminServer = await startAdminServer({
1159
- port: config.adminPort,
1160
- logStore,
1161
- httpLog,
1162
- config,
1163
- zeroEnv,
1164
- actions: { restartZero, stopZero, resetZero, resetZeroFull },
1165
- startTime: Date.now(),
1166
- db: instances,
1167
- })
1168
- log.orez(`admin: ${url(`http://localhost:${config.adminPort}`)}`)
1169
- }
1170
-
1171
- log.pg(
1172
- `ready ${url(`postgresql://${config.pgUser}:${config.pgPassword}@127.0.0.1:${config.pgPort}/postgres`)}`
1173
- )
1174
-
1175
- let stopping = false
1176
- const shutdown = async (reason: string, exitCode = 0) => {
1177
- if (stopping) return
1178
- stopping = true
1179
- log.debug.orez(`shutdown requested: ${reason}`)
1180
- adminServer?.close()
1181
- s3Server?.close()
1182
- await stop()
1183
- process.exit(exitCode)
1184
- }
1185
-
1186
- process.on('SIGINT', () => shutdown('SIGINT'))
1187
- process.on('SIGTERM', () => shutdown('SIGTERM'))
1188
-
1189
- // handle crashes - try to clean up so next startup isn't corrupted
1190
- // EPIPE/ECONNRESET are transient socket errors (e.g. client disconnect) — not crashes
1191
- const isSocketError = (err: unknown): boolean => {
1192
- const code = (err as NodeJS.ErrnoException)?.code
1193
- if (code === 'EPIPE' || code === 'ECONNRESET') return true
1194
- const msg = err instanceof Error ? err.message : String(err)
1195
- return msg.includes('ended by the other party')
1196
- }
1197
- process.on('uncaughtException', async (err) => {
1198
- if (isSocketError(err)) return
1199
- log.orez(`uncaught exception: ${err.message}`)
1200
- await shutdown('uncaughtException', 1)
1201
- })
1202
- process.on('unhandledRejection', async (reason) => {
1203
- if (isSocketError(reason)) return
1204
- log.orez(`unhandled rejection: ${reason}`)
1205
- await shutdown('unhandledRejection', 1)
1206
- })
1207
- },
1208
- })
1209
-
1210
- // note: runMain is invoked from cli-entry.ts, not here. attempts to detect
1211
- // "is this the entry" via import.meta.main / process.argv[1] are fragile
1212
- // because bin symlinks land argv[1] at ".../.bin/orez" and nested imports
1213
- // flip import.meta.main. keeping the call in cli-entry (a dedicated entry)
1214
- // is the only reliable signal.