orez 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -225
- package/dist/admin/log-store.d.ts.map +1 -1
- package/dist/admin/log-store.js +17 -6
- package/dist/admin/log-store.js.map +1 -1
- package/dist/admin/server.d.ts +1 -0
- package/dist/admin/server.d.ts.map +1 -1
- package/dist/admin/server.js +10 -0
- package/dist/admin/server.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +89 -45
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +104 -17
- package/dist/index.js.map +1 -1
- package/dist/integration/test-permissions.d.ts +5 -0
- package/dist/integration/test-permissions.d.ts.map +1 -0
- package/dist/integration/test-permissions.js +89 -0
- package/dist/integration/test-permissions.js.map +1 -0
- package/dist/pg-proxy.js +2 -2
- package/dist/pg-proxy.js.map +1 -1
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +15 -13
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +27 -2
- package/dist/replication/handler.js.map +1 -1
- package/dist/sqlite-mode/index.d.ts +1 -0
- package/dist/sqlite-mode/index.d.ts.map +1 -1
- package/dist/sqlite-mode/index.js +1 -0
- package/dist/sqlite-mode/index.js.map +1 -1
- package/dist/sqlite-mode/native-binary.d.ts +11 -0
- package/dist/sqlite-mode/native-binary.d.ts.map +1 -0
- package/dist/sqlite-mode/native-binary.js +67 -0
- package/dist/sqlite-mode/native-binary.js.map +1 -0
- package/package.json +8 -2
- package/src/admin/log-store.ts +19 -9
- package/src/admin/server.ts +12 -0
- package/src/cli.ts +92 -43
- package/src/index.ts +117 -18
- package/src/integration/integration.test.ts +86 -15
- package/src/integration/native-binary.guard.test.ts +13 -0
- package/src/integration/native-startup.test.ts +44 -0
- package/src/integration/restore-live-stress.test.ts +437 -0
- package/src/integration/restore-reset.test.ts +135 -16
- package/src/integration/test-permissions.ts +111 -0
- package/src/pg-proxy.ts +2 -2
- package/src/replication/change-tracker.test.ts +1 -1
- package/src/replication/change-tracker.ts +16 -13
- package/src/replication/handler.test.ts +2 -2
- package/src/replication/handler.ts +30 -2
- package/src/sqlite-mode/index.ts +1 -0
- package/src/sqlite-mode/native-binary.ts +89 -0
package/README.md
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
# oreZ
|
|
2
2
|
|
|
3
|
-
[
|
|
3
|
+
[](https://www.npmjs.com/package/orez)
|
|
4
|
+
[](https://github.com/natew/orez/blob/main/LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Run [Zero](https://zero.rocicorp.dev) locally with zero native dependencies. No Postgres install, no SQLite compilation, no Docker.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
bunx orez
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
oreZ makes Zero work on [PGlite](https://pglite.dev) (Postgres in WASM) and [bedrock-sqlite](https://www.npmjs.com/package/bedrock-sqlite) (SQLite in WASM), bundled together so local development is as simple as `bun install && bunx orez`.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- **Bun** 1.0+ or **Node.js** 20+
|
|
17
|
+
- **Zero** 0.18+ (tested with 0.18.x)
|
|
18
|
+
|
|
19
|
+
## Limitations
|
|
20
|
+
|
|
21
|
+
This is a **development tool only**. Not suitable for production.
|
|
22
|
+
|
|
23
|
+
- **Single-session per database** — queries are serialized through a mutex. Fine for development, would bottleneck under load.
|
|
24
|
+
- **Trigger overhead** — every write fires change-tracking triggers.
|
|
25
|
+
- **Local filesystem** — no replication, no HA. Use `orez pg_dump` for backups.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
6
28
|
|
|
7
29
|
```
|
|
8
30
|
bunx orez
|
|
@@ -10,13 +32,13 @@ bunx orez
|
|
|
10
32
|
|
|
11
33
|
**What oreZ handles automatically:**
|
|
12
34
|
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
35
|
+
- **Zero native deps** — both Postgres and SQLite run as WASM. Nothing to compile, nothing platform-specific.
|
|
36
|
+
- **Memory management** — auto-sizes Node heap (~50% RAM, min 4GB), purges consumed WAL, batches restores with CHECKPOINTs
|
|
37
|
+
- **Real-time replication** — changes sync instantly via `pg_notify` triggers, with adaptive polling fallback (20ms catching up, 500ms idle)
|
|
38
|
+
- **Auto-recovery** — finds available ports if configured ones are busy, provides reset/restart controls
|
|
39
|
+
- **PGlite compatibility** — rewrites unsupported queries, fakes wire protocol responses, filters unsupported column types
|
|
40
|
+
- **Admin dashboard** — live logs, HTTP request inspector, restart/reset controls, env viewer
|
|
41
|
+
- **Production restores** — `pg_dump`/`pg_restore` with COPY→INSERT conversion, auto-coordinates with zero-cache
|
|
20
42
|
- **Extensions** — pgvector and pg_trgm enabled by default
|
|
21
43
|
|
|
22
44
|
## CLI
|
|
@@ -37,32 +59,31 @@ bunx orez
|
|
|
37
59
|
--log-level=warn error, warn, info, debug
|
|
38
60
|
--s3 also start a local s3-compatible server
|
|
39
61
|
--s3-port=9200 s3 server port
|
|
40
|
-
--disable-wasm-sqlite use native @rocicorp/zero-sqlite3 instead of wasm
|
|
41
|
-
--on-db-ready=CMD command to run after db+proxy
|
|
42
|
-
--on-healthy=CMD command to run once all services
|
|
43
|
-
--disable-admin disable admin dashboard
|
|
44
|
-
--admin-port=6477 admin dashboard port
|
|
62
|
+
--disable-wasm-sqlite use native @rocicorp/zero-sqlite3 instead of wasm
|
|
63
|
+
--on-db-ready=CMD command to run after db+proxy ready, before zero-cache
|
|
64
|
+
--on-healthy=CMD command to run once all services healthy
|
|
65
|
+
--disable-admin disable admin dashboard
|
|
66
|
+
--admin-port=6477 admin dashboard port (default: 6477)
|
|
45
67
|
```
|
|
46
68
|
|
|
47
69
|
Ports auto-increment if already in use.
|
|
48
70
|
|
|
49
71
|
## Admin Dashboard
|
|
50
72
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
bunx orez --disable-admin
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Open `http://localhost:6477` for a real-time dashboard with:
|
|
73
|
+
Enabled by default at `http://localhost:6477`.
|
|
58
74
|
|
|
59
75
|
- **Logs** — live-streaming logs from zero-cache, filterable by source and level
|
|
76
|
+
- **HTTP** — request/response inspector for zero-cache traffic
|
|
60
77
|
- **Env** — environment variables passed to zero-cache
|
|
61
|
-
- **Actions** — restart zero-cache, reset (wipe replica + resync),
|
|
78
|
+
- **Actions** — restart zero-cache, reset (wipe replica + resync), full reset (wipe CVR/CDB too)
|
|
62
79
|
|
|
63
|
-
|
|
80
|
+
Logs are also written to separate files in your data directory: `zero.log`, `proxy.log`, `pglite.log`, etc.
|
|
64
81
|
|
|
65
|
-
|
|
82
|
+
```
|
|
83
|
+
bunx orez --disable-admin # disable dashboard
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Programmatic API
|
|
66
87
|
|
|
67
88
|
```
|
|
68
89
|
bun install orez
|
|
@@ -76,6 +97,7 @@ const { config, stop, db, instances } = await startZeroLite({
|
|
|
76
97
|
zeroPort: 5849,
|
|
77
98
|
migrationsDir: 'src/database/migrations',
|
|
78
99
|
seedFile: 'src/database/seed.sql',
|
|
100
|
+
adminPort: 6477, // set to 0 to disable
|
|
79
101
|
})
|
|
80
102
|
|
|
81
103
|
// your app connects to zero-cache at localhost:5849
|
|
@@ -84,11 +106,10 @@ const { config, stop, db, instances } = await startZeroLite({
|
|
|
84
106
|
// db is the postgres PGlite instance (for direct queries)
|
|
85
107
|
// instances has all three: { postgres, cvr, cdb }
|
|
86
108
|
|
|
87
|
-
// when done
|
|
88
109
|
await stop()
|
|
89
110
|
```
|
|
90
111
|
|
|
91
|
-
All options are optional with sensible defaults.
|
|
112
|
+
All options are optional with sensible defaults.
|
|
92
113
|
|
|
93
114
|
### Lifecycle hooks
|
|
94
115
|
|
|
@@ -97,9 +118,9 @@ All options are optional with sensible defaults. Ports auto-find if in use.
|
|
|
97
118
|
| on-db-ready | `--on-db-ready=CMD` | `onDbReady: 'CMD'` or `onDbReady: fn` | after db + proxy ready, before zero |
|
|
98
119
|
| on-healthy | `--on-healthy=CMD` | `onHealthy: 'CMD'` or `onHealthy: fn` | after all services ready |
|
|
99
120
|
|
|
100
|
-
|
|
121
|
+
Shell commands receive env vars: `DATABASE_URL`, `OREZ_PG_PORT`, `OREZ_ZERO_PORT`. Change tracking triggers are re-installed after `onDbReady`.
|
|
101
122
|
|
|
102
|
-
## Vite
|
|
123
|
+
## Vite Plugin
|
|
103
124
|
|
|
104
125
|
```typescript
|
|
105
126
|
import { orezPlugin } from 'orez/vite'
|
|
@@ -110,7 +131,6 @@ export default {
|
|
|
110
131
|
pgPort: 6434,
|
|
111
132
|
zeroPort: 5849,
|
|
112
133
|
migrationsDir: 'src/database/migrations',
|
|
113
|
-
// lifecycle hooks (optional)
|
|
114
134
|
onDbReady: () => console.log('db ready'),
|
|
115
135
|
onHealthy: () => console.log('all services healthy'),
|
|
116
136
|
}),
|
|
@@ -118,280 +138,220 @@ export default {
|
|
|
118
138
|
}
|
|
119
139
|
```
|
|
120
140
|
|
|
121
|
-
Starts oreZ when vite dev
|
|
122
|
-
|
|
123
|
-
## How it works
|
|
124
|
-
|
|
125
|
-
oreZ starts three things:
|
|
141
|
+
Starts oreZ when vite dev starts, stops on close. Supports all `startZeroLite` options plus `s3` and `s3Port`.
|
|
126
142
|
|
|
127
|
-
|
|
128
|
-
2. A TCP proxy that speaks the PostgreSQL wire protocol, routing connections to the correct PGlite instance and handling logical replication
|
|
129
|
-
3. A zero-cache child process that connects to the proxy thinking it's a real Postgres server
|
|
130
|
-
|
|
131
|
-
### Multi-instance architecture
|
|
132
|
-
|
|
133
|
-
zero-cache expects three separate databases: `postgres` (app data), `zero_cvr` (client view records), and `zero_cdb` (change-streamer state). In real PostgreSQL these are independent databases with separate connection pools and transaction contexts.
|
|
143
|
+
## Backup & Restore
|
|
134
144
|
|
|
135
|
-
|
|
145
|
+
Dump and restore your local database — no native Postgres install needed.
|
|
136
146
|
|
|
137
|
-
|
|
147
|
+
```bash
|
|
148
|
+
bunx orez pg_dump > backup.sql
|
|
149
|
+
bunx orez pg_dump --output backup.sql
|
|
150
|
+
bunx orez pg_restore backup.sql
|
|
151
|
+
bunx orez pg_restore backup.sql --clean # drop public schema first
|
|
152
|
+
```
|
|
138
153
|
|
|
139
|
-
|
|
140
|
-
| ------------------- | --------------- | ----------------- |
|
|
141
|
-
| `postgres` | postgres | `pgdata-postgres` |
|
|
142
|
-
| `zero_cvr` | cvr | `pgdata-cvr` |
|
|
143
|
-
| `zero_cdb` | cdb | `pgdata-cdb` |
|
|
154
|
+
### Restoring into a running instance
|
|
144
155
|
|
|
145
|
-
|
|
156
|
+
When oreZ is running, restore through the wire protocol:
|
|
146
157
|
|
|
147
|
-
|
|
158
|
+
```bash
|
|
159
|
+
bunx orez pg_restore backup.sql --pg-port 6434
|
|
160
|
+
```
|
|
148
161
|
|
|
149
|
-
|
|
162
|
+
This automatically:
|
|
150
163
|
|
|
151
|
-
|
|
164
|
+
1. Stops zero-cache before restore (via admin API)
|
|
165
|
+
2. Clears replication state and shard schemas
|
|
166
|
+
3. Restores the dump
|
|
167
|
+
4. Adds all public tables to the publication
|
|
168
|
+
5. Restarts zero-cache
|
|
152
169
|
|
|
153
|
-
|
|
170
|
+
The `--direct` flag forces direct PGlite access, skipping wire protocol.
|
|
154
171
|
|
|
155
|
-
|
|
172
|
+
### What restore handles
|
|
156
173
|
|
|
157
|
-
|
|
174
|
+
- **COPY → INSERT** — PGlite doesn't support COPY protocol; converted to batched multi-row INSERTs
|
|
175
|
+
- **Unsupported extensions** — `pg_stat_statements`, `pg_buffercache`, `pg_cron` etc. silently skipped
|
|
176
|
+
- **Idempotent DDL** — `CREATE SCHEMA` → `IF NOT EXISTS`, `CREATE FUNCTION` → `OR REPLACE`
|
|
177
|
+
- **Oversized rows** — rows >16MB skipped with warning (WASM limit)
|
|
178
|
+
- **Transaction batching** — 200 statements per transaction, CHECKPOINT every 3 batches
|
|
179
|
+
- **Dollar-quoting** — correctly parses `$$` and `$tag$` in function bodies
|
|
158
180
|
|
|
159
|
-
|
|
181
|
+
Standard Postgres tools (`pg_dump`, `pg_restore`, `psql`) also work against the running proxy.
|
|
160
182
|
|
|
161
|
-
|
|
183
|
+
## Environment Variables
|
|
162
184
|
|
|
163
|
-
|
|
185
|
+
All `ZERO_*` env vars are forwarded to zero-cache. oreZ provides defaults:
|
|
164
186
|
|
|
165
|
-
|
|
187
|
+
| Variable | Default | Overridable |
|
|
188
|
+
| --------------------------- | ------------------ | ----------- |
|
|
189
|
+
| `NODE_ENV` | `development` | yes |
|
|
190
|
+
| `ZERO_LOG_LEVEL` | from `--log-level` | yes |
|
|
191
|
+
| `ZERO_NUM_SYNC_WORKERS` | `1` | yes |
|
|
192
|
+
| `ZERO_ENABLE_QUERY_PLANNER` | `false` | yes |
|
|
193
|
+
| `ZERO_UPSTREAM_DB` | _(managed)_ | no |
|
|
194
|
+
| `ZERO_CVR_DB` | _(managed)_ | no |
|
|
195
|
+
| `ZERO_CHANGE_DB` | _(managed)_ | no |
|
|
196
|
+
| `ZERO_REPLICA_FILE` | _(managed)_ | no |
|
|
197
|
+
| `ZERO_PORT` | _(managed)_ | no |
|
|
166
198
|
|
|
167
|
-
|
|
199
|
+
Common vars you might set:
|
|
168
200
|
|
|
169
|
-
|
|
201
|
+
```bash
|
|
202
|
+
ZERO_MUTATE_URL=http://localhost:3000/api/zero/push
|
|
203
|
+
ZERO_QUERY_URL=http://localhost:3000/api/zero/pull
|
|
204
|
+
```
|
|
170
205
|
|
|
171
|
-
|
|
172
|
-
| --------------------------- | ------------------- | ----------- |
|
|
173
|
-
| `NODE_ENV` | `development` | yes |
|
|
174
|
-
| `ZERO_LOG_LEVEL` | from `--log-level` | yes |
|
|
175
|
-
| `ZERO_NUM_SYNC_WORKERS` | `1` | yes |
|
|
176
|
-
| `ZERO_ENABLE_QUERY_PLANNER` | `false` | yes |
|
|
177
|
-
| `ZERO_UPSTREAM_DB` | _(managed by oreZ)_ | no |
|
|
178
|
-
| `ZERO_CVR_DB` | _(managed by oreZ)_ | no |
|
|
179
|
-
| `ZERO_CHANGE_DB` | _(managed by oreZ)_ | no |
|
|
180
|
-
| `ZERO_REPLICA_FILE` | _(managed by oreZ)_ | no |
|
|
181
|
-
| `ZERO_PORT` | _(managed by oreZ)_ | no |
|
|
206
|
+
## Local S3
|
|
182
207
|
|
|
183
|
-
|
|
208
|
+
Since Zero apps often need file uploads and MinIO requires Docker:
|
|
184
209
|
|
|
185
|
-
|
|
210
|
+
```bash
|
|
211
|
+
bunx orez --s3 # with orez
|
|
212
|
+
bunx orez s3 # standalone
|
|
213
|
+
```
|
|
186
214
|
|
|
187
|
-
|
|
215
|
+
```typescript
|
|
216
|
+
import { startS3Local } from 'orez/s3'
|
|
188
217
|
|
|
189
|
-
|
|
190
|
-
ZERO_MUTATE_URL=http://localhost:3000/api/zero/push
|
|
191
|
-
ZERO_QUERY_URL=http://localhost:3000/api/zero/pull
|
|
218
|
+
const server = await startS3Local({ port: 9200, dataDir: '.orez' })
|
|
192
219
|
```
|
|
193
220
|
|
|
194
|
-
|
|
221
|
+
Handles GET, PUT, DELETE, HEAD with CORS. Files stored on disk. No multipart, no ACLs, no versioning.
|
|
195
222
|
|
|
196
|
-
|
|
223
|
+
---
|
|
197
224
|
|
|
198
|
-
|
|
199
|
-
- `CREATE_REPLICATION_SLOT` persists slot info in a local table and returns a valid LSN
|
|
200
|
-
- `START_REPLICATION` enters streaming mode, encoding changes as pgoutput binary messages
|
|
201
|
-
- `version()` returns a standard PostgreSQL 16.4 version string (PGlite's Emscripten string breaks `pg_restore` and other tools)
|
|
202
|
-
- `current_setting('wal_level')` always returns `logical`
|
|
203
|
-
- `pg_replication_slots` queries are redirected to a local tracking table
|
|
204
|
-
- `SET TRANSACTION SNAPSHOT` is silently accepted (PGlite doesn't support imported snapshots)
|
|
205
|
-
- `ALTER ROLE ... REPLICATION` returns success
|
|
206
|
-
- `READ ONLY` is stripped from transaction starts (PGlite is single-session)
|
|
207
|
-
- `ISOLATION LEVEL` is stripped from all queries (meaningless with a single-session database)
|
|
208
|
-
- `SET TRANSACTION` / `SET SESSION` return synthetic success without hitting PGlite
|
|
225
|
+
# How It Works
|
|
209
226
|
|
|
210
|
-
|
|
227
|
+
## Architecture
|
|
211
228
|
|
|
212
|
-
|
|
229
|
+
oreZ runs three components:
|
|
213
230
|
|
|
214
|
-
|
|
231
|
+
1. **Three PGlite instances** — PostgreSQL 17 in WASM, one per database zero-cache expects (postgres, zero_cvr, zero_cdb)
|
|
232
|
+
2. **TCP proxy** — speaks PostgreSQL wire protocol, routes to correct PGlite, handles logical replication
|
|
233
|
+
3. **zero-cache** — child process connecting to proxy, thinks it's real Postgres
|
|
215
234
|
|
|
216
|
-
###
|
|
235
|
+
### Why three instances?
|
|
217
236
|
|
|
218
|
-
|
|
237
|
+
zero-cache expects three databases with independent transaction contexts. PGlite is single-session — all connections share one session. Without isolation, CVR transactions get corrupted by postgres queries (`ConcurrentModificationException`).
|
|
219
238
|
|
|
220
|
-
|
|
239
|
+
| Connection database | PGlite instance | Data directory |
|
|
240
|
+
| ------------------- | --------------- | ----------------- |
|
|
241
|
+
| `postgres` | postgres | `pgdata-postgres` |
|
|
242
|
+
| `zero_cvr` | cvr | `pgdata-cvr` |
|
|
243
|
+
| `zero_cdb` | cdb | `pgdata-cdb` |
|
|
221
244
|
|
|
222
|
-
|
|
245
|
+
### Replication
|
|
223
246
|
|
|
224
|
-
|
|
247
|
+
PGlite doesn't support logical replication, so oreZ fakes it:
|
|
225
248
|
|
|
226
|
-
|
|
249
|
+
1. Triggers capture every mutation into `_orez._zero_changes`
|
|
250
|
+
2. Changes are encoded as pgoutput binary protocol
|
|
251
|
+
3. Streamed to zero-cache through the replication connection
|
|
227
252
|
|
|
228
|
-
|
|
253
|
+
Change notifications use `pg_notify` for real-time sync. Polling (20ms/500ms adaptive) is fallback only.
|
|
229
254
|
|
|
230
|
-
|
|
255
|
+
### SQLite WASM
|
|
231
256
|
|
|
232
|
-
|
|
257
|
+
zero-cache needs SQLite via `@rocicorp/zero-sqlite3` (native C addon). oreZ intercepts this at runtime using Node's ESM loader hooks, redirecting to [bedrock-sqlite](https://www.npmjs.com/package/bedrock-sqlite) — SQLite's bedrock branch compiled to WASM with BEGIN CONCURRENT and WAL2.
|
|
233
258
|
|
|
234
|
-
|
|
259
|
+
The shim also polyfills the better-sqlite3 API surface zero-cache expects.
|
|
235
260
|
|
|
236
|
-
|
|
261
|
+
### Native SQLite mode
|
|
237
262
|
|
|
238
|
-
|
|
263
|
+
For `--disable-wasm-sqlite`, bootstrap the native addon first:
|
|
239
264
|
|
|
240
|
-
|
|
265
|
+
```bash
|
|
266
|
+
bun run native:bootstrap
|
|
267
|
+
```
|
|
241
268
|
|
|
242
|
-
|
|
269
|
+
## Internal Schema
|
|
243
270
|
|
|
244
|
-
|
|
271
|
+
oreZ stores replication state in the `_orez` schema (survives `pg_restore --clean`):
|
|
245
272
|
|
|
246
|
-
|
|
273
|
+
- `_orez._zero_changes` — change log for replication
|
|
274
|
+
- `_orez._zero_replication_slots` — slot tracking
|
|
275
|
+
- `_orez._zero_watermark` — LSN sequence
|
|
247
276
|
|
|
248
|
-
|
|
277
|
+
## Wire Protocol Compatibility
|
|
249
278
|
|
|
250
|
-
|
|
279
|
+
The proxy intercepts and rewrites to make PGlite look like real Postgres:
|
|
251
280
|
|
|
252
|
-
|
|
281
|
+
| Query/Command | What oreZ does |
|
|
282
|
+
| ------------------------------- | --------------------------------------------------- |
|
|
283
|
+
| `version()` | Returns `PostgreSQL 17.4 on x86_64-pc-linux-gnu...` |
|
|
284
|
+
| `current_setting('wal_level')` | Returns `logical` |
|
|
285
|
+
| `IDENTIFY_SYSTEM` | Returns fake system ID and timeline |
|
|
286
|
+
| `CREATE_REPLICATION_SLOT` | Persists to local table, returns valid LSN |
|
|
287
|
+
| `START_REPLICATION` | Streams changes as pgoutput binary |
|
|
288
|
+
| `pg_replication_slots` | Redirects to local tracking table |
|
|
289
|
+
| `READ ONLY` / `ISOLATION LEVEL` | Stripped (single-session) |
|
|
253
290
|
|
|
254
|
-
|
|
291
|
+
## Workarounds
|
|
255
292
|
|
|
256
|
-
|
|
293
|
+
Things that don't "just work" when replacing Postgres with PGlite and native SQLite with WASM:
|
|
257
294
|
|
|
258
|
-
###
|
|
295
|
+
### Session state bleed
|
|
259
296
|
|
|
260
|
-
|
|
297
|
+
PGlite is single-session — if `pg_restore` sets `search_path = ''`, every subsequent connection inherits it. On disconnect, oreZ resets `search_path`, `statement_timeout`, `lock_timeout`, and rolls back open transactions.
|
|
261
298
|
|
|
262
|
-
###
|
|
299
|
+
### Query planner disabled
|
|
263
300
|
|
|
264
|
-
|
|
301
|
+
`ZERO_ENABLE_QUERY_PLANNER=false` because it relies on SQLite scan statistics that cause infinite loops in WASM.
|
|
265
302
|
|
|
266
|
-
###
|
|
303
|
+
### Unsupported column types
|
|
267
304
|
|
|
268
|
-
|
|
305
|
+
Columns with `tsvector`, `tsquery`, `USER-DEFINED` types are filtered from replication messages.
|
|
269
306
|
|
|
270
|
-
###
|
|
307
|
+
### Publication-aware tracking
|
|
271
308
|
|
|
272
|
-
`
|
|
309
|
+
If `ZERO_APP_PUBLICATIONS` is set, only tables in that publication get change-tracking triggers.
|
|
273
310
|
|
|
274
|
-
###
|
|
311
|
+
### Broken trigger cleanup
|
|
275
312
|
|
|
276
|
-
|
|
313
|
+
After restore, triggers whose backing functions don't exist are dropped (happens with filtered pg_dump).
|
|
277
314
|
|
|
278
315
|
## Tests
|
|
279
316
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
```
|
|
317
|
+
```bash
|
|
283
318
|
bun run test # orez tests
|
|
319
|
+
bun run test:integration:native # native sqlite integration
|
|
284
320
|
cd sqlite-wasm && bunx vitest run # bedrock-sqlite tests
|
|
285
321
|
```
|
|
286
322
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
The bedrock-sqlite tests cover Database/Statement API, transactions, WAL/WAL2 modes, BEGIN CONCURRENT, FTS5, JSON functions, custom functions, aggregates, bigint handling, and file persistence.
|
|
290
|
-
|
|
291
|
-
## Limitations
|
|
292
|
-
|
|
293
|
-
This is a development tool. It is not suitable for production use.
|
|
294
|
-
|
|
295
|
-
- PGlite is single-session per instance. All queries to the same database are serialized through a mutex. Cross-database queries are independent (each database has its own PGlite instance and mutex). Fine for development but would bottleneck under real load.
|
|
296
|
-
- Triggers add overhead to every write. Again, fine for development.
|
|
297
|
-
- PGlite stores data on the local filesystem. No replication, no high availability. Use `orez pg_dump` / `orez pg_restore` for backups.
|
|
298
|
-
|
|
299
|
-
## Project structure
|
|
323
|
+
## Project Structure
|
|
300
324
|
|
|
301
325
|
```
|
|
302
326
|
src/
|
|
303
|
-
cli-entry.ts
|
|
327
|
+
cli-entry.ts auto heap sizing wrapper
|
|
304
328
|
cli.ts cli with citty
|
|
305
|
-
index.ts main entry, orchestrates startup
|
|
329
|
+
index.ts main entry, orchestrates startup
|
|
306
330
|
config.ts configuration with defaults
|
|
307
|
-
log.ts colored log prefixes
|
|
308
|
-
mutex.ts
|
|
331
|
+
log.ts colored log prefixes, log files
|
|
332
|
+
mutex.ts serializing pglite access
|
|
309
333
|
port.ts auto port finding
|
|
310
|
-
pg-proxy.ts
|
|
311
|
-
pglite-manager.ts multi-instance pglite
|
|
312
|
-
s3-local.ts local s3
|
|
313
|
-
vite-plugin.ts vite
|
|
334
|
+
pg-proxy.ts postgresql wire protocol proxy
|
|
335
|
+
pglite-manager.ts multi-instance pglite, migrations
|
|
336
|
+
s3-local.ts local s3 server (orez/s3)
|
|
337
|
+
vite-plugin.ts vite plugin (orez/vite)
|
|
338
|
+
admin/
|
|
339
|
+
server.ts admin dashboard backend
|
|
340
|
+
ui.ts admin dashboard frontend
|
|
341
|
+
log-store.ts log aggregation
|
|
342
|
+
http-proxy.ts http request logging
|
|
314
343
|
replication/
|
|
315
|
-
handler.ts replication
|
|
316
|
-
pgoutput-encoder.ts binary pgoutput
|
|
317
|
-
change-tracker.ts trigger installation,
|
|
344
|
+
handler.ts replication state machine, adaptive polling
|
|
345
|
+
pgoutput-encoder.ts binary pgoutput encoder
|
|
346
|
+
change-tracker.ts trigger installation, change purging
|
|
318
347
|
integration/
|
|
319
|
-
|
|
320
|
-
restore.test.ts pg_dump/restore integration test
|
|
348
|
+
*.test.ts end-to-end tests
|
|
321
349
|
sqlite-wasm/
|
|
322
|
-
Makefile emscripten build
|
|
323
|
-
|
|
324
|
-
native/
|
|
325
|
-
api.js better-sqlite3 compatible database/statement API
|
|
326
|
-
vfs.c custom VFS with SHM support for WAL/WAL2
|
|
327
|
-
vfs.js javascript VFS bridge
|
|
328
|
-
test/
|
|
329
|
-
database.test.ts wasm sqlite engine tests
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
## Backup & Restore
|
|
333
|
-
|
|
334
|
-
Dump and restore your local PGlite database using WASM-compiled `pg_dump` — no native Postgres install needed.
|
|
335
|
-
|
|
336
|
-
```
|
|
337
|
-
bunx orez pg_dump > backup.sql
|
|
338
|
-
bunx orez pg_dump --output backup.sql
|
|
339
|
-
bunx orez pg_restore backup.sql
|
|
340
|
-
bunx orez pg_restore backup.sql --clean
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
```
|
|
344
|
-
pg_dump options:
|
|
345
|
-
--data-dir=.orez data directory
|
|
346
|
-
-o, --output output file path (default: stdout)
|
|
347
|
-
|
|
348
|
-
pg_restore options:
|
|
349
|
-
--data-dir=.orez data directory
|
|
350
|
-
--clean drop and recreate public schema before restoring
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
`pg_restore` also supports connecting to a running oreZ instance via wire protocol — just pass `--pg-port`:
|
|
354
|
-
|
|
355
|
-
```
|
|
356
|
-
bunx orez pg_restore backup.sql --pg-port 6434
|
|
357
|
-
bunx orez pg_restore backup.sql --pg-port 6434 --pg-user user --pg-password password
|
|
358
|
-
bunx orez pg_restore backup.sql --direct # force direct PGlite access, skip wire protocol
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
Restore streams the dump file line-by-line so it can handle large dumps without loading everything into memory. SQL is parsed using [pgsql-parser](https://www.npmjs.com/package/pgsql-parser) (the real PostgreSQL C parser compiled to WASM) for accurate statement classification and rewriting.
|
|
362
|
-
|
|
363
|
-
### What restore handles automatically
|
|
364
|
-
|
|
365
|
-
- **COPY FROM stdin → INSERT**: PGlite WASM doesn't support the COPY protocol, so COPY blocks are converted to batched multi-row INSERTs (50 rows per statement, flushed at 1MB)
|
|
366
|
-
- **Unsupported extensions**: `pg_stat_statements`, `pg_buffercache`, `pg_cron`, etc. — CREATE, DROP, and COMMENT ON EXTENSION statements are skipped
|
|
367
|
-
- **Idempotent DDL**: `CREATE SCHEMA` → `IF NOT EXISTS`, `CREATE FUNCTION/VIEW` → `OR REPLACE`
|
|
368
|
-
- **Oversized rows**: Rows larger than 16MB are skipped with a warning (PGlite WASM crashes around 24MB per value)
|
|
369
|
-
- **Missing table references**: DDL errors from filtered dumps (e.g. ALTER TABLE on excluded tables) log a warning and continue
|
|
370
|
-
- **Transaction batching**: Data statements are grouped 200 per transaction with CHECKPOINT every 3 batches to manage WASM memory
|
|
371
|
-
- **PostgreSQL 18+ artifacts**: `SET transaction_timeout` silently skipped
|
|
372
|
-
- **psql meta-commands**: `\restrict` and similar silently skipped
|
|
373
|
-
|
|
374
|
-
This means you can take a `pg_dump` from a production Postgres database and restore it directly into oreZ — incompatible statements are handled automatically.
|
|
375
|
-
|
|
376
|
-
When oreZ is not running, `pg_restore` opens PGlite directly. When oreZ is running, pass `--pg-port` to restore through the wire protocol. Standard Postgres tools (`pg_dump`, `pg_restore`, `psql`) also work against the running proxy since oreZ presents a standard PostgreSQL 16.4 version string over the wire.
|
|
377
|
-
|
|
378
|
-
## Extra: orez/s3
|
|
379
|
-
|
|
380
|
-
Since we use this stack often with a file uploading service like MinIO which also requires docker, I threw in a tiny s3-compatible endpoint too:
|
|
381
|
-
|
|
382
|
-
`bunx orez --s3` or standalone `bunx orez s3`.
|
|
383
|
-
|
|
384
|
-
```typescript
|
|
385
|
-
import { startS3Local } from 'orez/s3'
|
|
386
|
-
|
|
387
|
-
const server = await startS3Local({
|
|
388
|
-
port: 9200,
|
|
389
|
-
dataDir: '.orez',
|
|
390
|
-
})
|
|
350
|
+
Makefile emscripten build
|
|
351
|
+
native/api.js better-sqlite3 compatible API
|
|
352
|
+
native/vfs.c custom VFS with SHM for WAL2
|
|
391
353
|
```
|
|
392
354
|
|
|
393
|
-
Handles GET, PUT, DELETE, HEAD with CORS. Files stored on disk. No multipart, no ACLs, no versioning.
|
|
394
|
-
|
|
395
355
|
## License
|
|
396
356
|
|
|
397
357
|
MIT
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-store.d.ts","sourceRoot":"","sources":["../../src/admin/log-store.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACtD,KAAK,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QACjF,OAAO,EAAE,QAAQ,EAAE,CAAA;QACnB,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,MAAM,IAAI,QAAQ,EAAE,CAAA;IACpB,KAAK,IAAI,IAAI,CAAA;CACd;AAOD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,UAAO,GAAG,QAAQ,
|
|
1
|
+
{"version":3,"file":"log-store.d.ts","sourceRoot":"","sources":["../../src/admin/log-store.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACtD,KAAK,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QACjF,OAAO,EAAE,QAAQ,EAAE,CAAA;QACnB,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,MAAM,IAAI,QAAQ,EAAE,CAAA;IACpB,KAAK,IAAI,IAAI,CAAA;CACd;AAOD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,UAAO,GAAG,QAAQ,CA4G5E"}
|
package/dist/admin/log-store.js
CHANGED
|
@@ -8,20 +8,26 @@ export function createLogStore(dataDir, writeToDisk = true) {
|
|
|
8
8
|
const entries = [];
|
|
9
9
|
let nextId = 1;
|
|
10
10
|
const logsDir = join(dataDir, 'logs');
|
|
11
|
-
const logFile = join(logsDir, 'orez.log');
|
|
12
|
-
const backupFile = join(logsDir, 'orez.log.1');
|
|
13
11
|
if (writeToDisk) {
|
|
14
12
|
mkdirSync(logsDir, { recursive: true });
|
|
15
13
|
}
|
|
16
|
-
|
|
14
|
+
// track file sizes to rotate per-source
|
|
15
|
+
const fileSizes = {};
|
|
16
|
+
function getLogFile(source) {
|
|
17
|
+
return join(logsDir, `${source}.log`);
|
|
18
|
+
}
|
|
19
|
+
function rotateIfNeeded(source) {
|
|
17
20
|
if (!writeToDisk)
|
|
18
21
|
return;
|
|
19
22
|
try {
|
|
23
|
+
const logFile = getLogFile(source);
|
|
20
24
|
if (!existsSync(logFile))
|
|
21
25
|
return;
|
|
22
26
|
const stat = statSync(logFile);
|
|
27
|
+
fileSizes[source] = stat.size;
|
|
23
28
|
if (stat.size > MAX_FILE_SIZE) {
|
|
24
|
-
renameSync(logFile,
|
|
29
|
+
renameSync(logFile, logFile + '.1');
|
|
30
|
+
fileSizes[source] = 0;
|
|
25
31
|
}
|
|
26
32
|
}
|
|
27
33
|
catch { }
|
|
@@ -41,8 +47,13 @@ export function createLogStore(dataDir, writeToDisk = true) {
|
|
|
41
47
|
if (writeToDisk) {
|
|
42
48
|
try {
|
|
43
49
|
const ts = new Date(entry.ts).toISOString();
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
const logFile = getLogFile(source);
|
|
51
|
+
appendFileSync(logFile, `[${ts}] [${level}] ${entry.msg}\n`);
|
|
52
|
+
// check rotation every ~100 writes to this source
|
|
53
|
+
fileSizes[source] = (fileSizes[source] || 0) + entry.msg.length + 50;
|
|
54
|
+
if (fileSizes[source] > MAX_FILE_SIZE) {
|
|
55
|
+
rotateIfNeeded(source);
|
|
56
|
+
}
|
|
46
57
|
}
|
|
47
58
|
catch { }
|
|
48
59
|
}
|