orez 0.1.6 → 0.1.8
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 +186 -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 +96 -46
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +158 -23
- package/dist/index.js.map +1 -1
- package/dist/integration/test-permissions.d.ts +7 -0
- package/dist/integration/test-permissions.d.ts.map +1 -0
- package/dist/integration/test-permissions.js +117 -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/dist/sqlite-mode/package-resolve.d.ts +6 -0
- package/dist/sqlite-mode/package-resolve.d.ts.map +1 -0
- package/dist/sqlite-mode/package-resolve.js +20 -0
- package/dist/sqlite-mode/package-resolve.js.map +1 -0
- package/dist/sqlite-mode/resolve-mode.d.ts +12 -7
- package/dist/sqlite-mode/resolve-mode.d.ts.map +1 -1
- package/dist/sqlite-mode/resolve-mode.js +27 -23
- package/dist/sqlite-mode/resolve-mode.js.map +1 -1
- package/package.json +8 -2
- package/src/admin/log-store.ts +19 -9
- package/src/admin/server.ts +12 -0
- package/src/cli.ts +99 -44
- package/src/config.ts +2 -0
- package/src/index.ts +186 -24
- package/src/integration/integration.test.ts +93 -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 +433 -0
- package/src/integration/restore-reset.test.ts +136 -20
- package/src/integration/test-permissions.ts +147 -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/src/sqlite-mode/package-resolve.ts +17 -0
- package/src/sqlite-mode/resolve-mode.ts +31 -21
- package/src/sqlite-mode/sqlite-mode.test.ts +11 -5
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,32 @@ 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
|
-
--
|
|
41
|
-
--
|
|
42
|
-
--on-
|
|
43
|
-
--
|
|
44
|
-
--
|
|
62
|
+
--force-wasm-sqlite force wasm sqlite even if native is available
|
|
63
|
+
--disable-wasm-sqlite force native sqlite (fail if not available)
|
|
64
|
+
--on-db-ready=CMD command to run after db+proxy ready, before zero-cache
|
|
65
|
+
--on-healthy=CMD command to run once all services healthy
|
|
66
|
+
--disable-admin disable admin dashboard
|
|
67
|
+
--admin-port=6477 admin dashboard port (default: 6477)
|
|
45
68
|
```
|
|
46
69
|
|
|
47
70
|
Ports auto-increment if already in use.
|
|
48
71
|
|
|
49
72
|
## Admin Dashboard
|
|
50
73
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
bunx orez --disable-admin
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Open `http://localhost:6477` for a real-time dashboard with:
|
|
74
|
+
Enabled by default at `http://localhost:6477`.
|
|
58
75
|
|
|
59
76
|
- **Logs** — live-streaming logs from zero-cache, filterable by source and level
|
|
77
|
+
- **HTTP** — request/response inspector for zero-cache traffic
|
|
60
78
|
- **Env** — environment variables passed to zero-cache
|
|
61
|
-
- **Actions** — restart zero-cache, reset (wipe replica + resync),
|
|
79
|
+
- **Actions** — restart zero-cache, reset (wipe replica + resync), full reset (wipe CVR/CDB too)
|
|
62
80
|
|
|
63
|
-
|
|
81
|
+
Logs are also written to separate files in your data directory: `zero.log`, `proxy.log`, `pglite.log`, etc.
|
|
64
82
|
|
|
65
|
-
|
|
83
|
+
```
|
|
84
|
+
bunx orez --disable-admin # disable dashboard
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Programmatic API
|
|
66
88
|
|
|
67
89
|
```
|
|
68
90
|
bun install orez
|
|
@@ -76,6 +98,7 @@ const { config, stop, db, instances } = await startZeroLite({
|
|
|
76
98
|
zeroPort: 5849,
|
|
77
99
|
migrationsDir: 'src/database/migrations',
|
|
78
100
|
seedFile: 'src/database/seed.sql',
|
|
101
|
+
adminPort: 6477, // set to 0 to disable
|
|
79
102
|
})
|
|
80
103
|
|
|
81
104
|
// your app connects to zero-cache at localhost:5849
|
|
@@ -84,11 +107,10 @@ const { config, stop, db, instances } = await startZeroLite({
|
|
|
84
107
|
// db is the postgres PGlite instance (for direct queries)
|
|
85
108
|
// instances has all three: { postgres, cvr, cdb }
|
|
86
109
|
|
|
87
|
-
// when done
|
|
88
110
|
await stop()
|
|
89
111
|
```
|
|
90
112
|
|
|
91
|
-
All options are optional with sensible defaults.
|
|
113
|
+
All options are optional with sensible defaults.
|
|
92
114
|
|
|
93
115
|
### Lifecycle hooks
|
|
94
116
|
|
|
@@ -97,9 +119,9 @@ All options are optional with sensible defaults. Ports auto-find if in use.
|
|
|
97
119
|
| on-db-ready | `--on-db-ready=CMD` | `onDbReady: 'CMD'` or `onDbReady: fn` | after db + proxy ready, before zero |
|
|
98
120
|
| on-healthy | `--on-healthy=CMD` | `onHealthy: 'CMD'` or `onHealthy: fn` | after all services ready |
|
|
99
121
|
|
|
100
|
-
|
|
122
|
+
Shell commands receive env vars: `DATABASE_URL`, `OREZ_PG_PORT`, `OREZ_ZERO_PORT`. Change tracking triggers are re-installed after `onDbReady`.
|
|
101
123
|
|
|
102
|
-
## Vite
|
|
124
|
+
## Vite Plugin
|
|
103
125
|
|
|
104
126
|
```typescript
|
|
105
127
|
import { orezPlugin } from 'orez/vite'
|
|
@@ -110,7 +132,6 @@ export default {
|
|
|
110
132
|
pgPort: 6434,
|
|
111
133
|
zeroPort: 5849,
|
|
112
134
|
migrationsDir: 'src/database/migrations',
|
|
113
|
-
// lifecycle hooks (optional)
|
|
114
135
|
onDbReady: () => console.log('db ready'),
|
|
115
136
|
onHealthy: () => console.log('all services healthy'),
|
|
116
137
|
}),
|
|
@@ -118,280 +139,220 @@ export default {
|
|
|
118
139
|
}
|
|
119
140
|
```
|
|
120
141
|
|
|
121
|
-
Starts oreZ when vite dev
|
|
122
|
-
|
|
123
|
-
## How it works
|
|
124
|
-
|
|
125
|
-
oreZ starts three things:
|
|
142
|
+
Starts oreZ when vite dev starts, stops on close. Supports all `startZeroLite` options plus `s3` and `s3Port`.
|
|
126
143
|
|
|
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.
|
|
144
|
+
## Backup & Restore
|
|
134
145
|
|
|
135
|
-
|
|
146
|
+
Dump and restore your local database — no native Postgres install needed.
|
|
136
147
|
|
|
137
|
-
|
|
148
|
+
```bash
|
|
149
|
+
bunx orez pg_dump > backup.sql
|
|
150
|
+
bunx orez pg_dump --output backup.sql
|
|
151
|
+
bunx orez pg_restore backup.sql
|
|
152
|
+
bunx orez pg_restore backup.sql --clean # drop public schema first
|
|
153
|
+
```
|
|
138
154
|
|
|
139
|
-
|
|
140
|
-
| ------------------- | --------------- | ----------------- |
|
|
141
|
-
| `postgres` | postgres | `pgdata-postgres` |
|
|
142
|
-
| `zero_cvr` | cvr | `pgdata-cvr` |
|
|
143
|
-
| `zero_cdb` | cdb | `pgdata-cdb` |
|
|
155
|
+
### Restoring into a running instance
|
|
144
156
|
|
|
145
|
-
|
|
157
|
+
When oreZ is running, restore through the wire protocol:
|
|
146
158
|
|
|
147
|
-
|
|
159
|
+
```bash
|
|
160
|
+
bunx orez pg_restore backup.sql --pg-port 6434
|
|
161
|
+
```
|
|
148
162
|
|
|
149
|
-
|
|
163
|
+
This automatically:
|
|
150
164
|
|
|
151
|
-
|
|
165
|
+
1. Stops zero-cache before restore (via admin API)
|
|
166
|
+
2. Clears replication state and shard schemas
|
|
167
|
+
3. Restores the dump
|
|
168
|
+
4. Adds all public tables to the publication
|
|
169
|
+
5. Restarts zero-cache
|
|
152
170
|
|
|
153
|
-
|
|
171
|
+
The `--direct` flag forces direct PGlite access, skipping wire protocol.
|
|
154
172
|
|
|
155
|
-
|
|
173
|
+
### What restore handles
|
|
156
174
|
|
|
157
|
-
|
|
175
|
+
- **COPY → INSERT** — PGlite doesn't support COPY protocol; converted to batched multi-row INSERTs
|
|
176
|
+
- **Unsupported extensions** — `pg_stat_statements`, `pg_buffercache`, `pg_cron` etc. silently skipped
|
|
177
|
+
- **Idempotent DDL** — `CREATE SCHEMA` → `IF NOT EXISTS`, `CREATE FUNCTION` → `OR REPLACE`
|
|
178
|
+
- **Oversized rows** — rows >16MB skipped with warning (WASM limit)
|
|
179
|
+
- **Transaction batching** — 200 statements per transaction, CHECKPOINT every 3 batches
|
|
180
|
+
- **Dollar-quoting** — correctly parses `$$` and `$tag$` in function bodies
|
|
158
181
|
|
|
159
|
-
|
|
182
|
+
Standard Postgres tools (`pg_dump`, `pg_restore`, `psql`) also work against the running proxy.
|
|
160
183
|
|
|
161
|
-
|
|
184
|
+
## Environment Variables
|
|
162
185
|
|
|
163
|
-
|
|
186
|
+
All `ZERO_*` env vars are forwarded to zero-cache. oreZ provides defaults:
|
|
164
187
|
|
|
165
|
-
|
|
188
|
+
| Variable | Default | Overridable |
|
|
189
|
+
| --------------------------- | ------------------ | ----------- |
|
|
190
|
+
| `NODE_ENV` | `development` | yes |
|
|
191
|
+
| `ZERO_LOG_LEVEL` | from `--log-level` | yes |
|
|
192
|
+
| `ZERO_NUM_SYNC_WORKERS` | `1` | yes |
|
|
193
|
+
| `ZERO_ENABLE_QUERY_PLANNER` | `false` | yes |
|
|
194
|
+
| `ZERO_UPSTREAM_DB` | _(managed)_ | no |
|
|
195
|
+
| `ZERO_CVR_DB` | _(managed)_ | no |
|
|
196
|
+
| `ZERO_CHANGE_DB` | _(managed)_ | no |
|
|
197
|
+
| `ZERO_REPLICA_FILE` | _(managed)_ | no |
|
|
198
|
+
| `ZERO_PORT` | _(managed)_ | no |
|
|
166
199
|
|
|
167
|
-
|
|
200
|
+
Common vars you might set:
|
|
168
201
|
|
|
169
|
-
|
|
202
|
+
```bash
|
|
203
|
+
ZERO_MUTATE_URL=http://localhost:3000/api/zero/push
|
|
204
|
+
ZERO_QUERY_URL=http://localhost:3000/api/zero/pull
|
|
205
|
+
```
|
|
170
206
|
|
|
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 |
|
|
207
|
+
## Local S3
|
|
182
208
|
|
|
183
|
-
|
|
209
|
+
Since Zero apps often need file uploads and MinIO requires Docker:
|
|
184
210
|
|
|
185
|
-
|
|
211
|
+
```bash
|
|
212
|
+
bunx orez --s3 # with orez
|
|
213
|
+
bunx orez s3 # standalone
|
|
214
|
+
```
|
|
186
215
|
|
|
187
|
-
|
|
216
|
+
```typescript
|
|
217
|
+
import { startS3Local } from 'orez/s3'
|
|
188
218
|
|
|
189
|
-
|
|
190
|
-
ZERO_MUTATE_URL=http://localhost:3000/api/zero/push
|
|
191
|
-
ZERO_QUERY_URL=http://localhost:3000/api/zero/pull
|
|
219
|
+
const server = await startS3Local({ port: 9200, dataDir: '.orez' })
|
|
192
220
|
```
|
|
193
221
|
|
|
194
|
-
|
|
222
|
+
Handles GET, PUT, DELETE, HEAD with CORS. Files stored on disk. No multipart, no ACLs, no versioning.
|
|
195
223
|
|
|
196
|
-
|
|
224
|
+
---
|
|
197
225
|
|
|
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
|
|
226
|
+
# How It Works
|
|
209
227
|
|
|
210
|
-
|
|
228
|
+
## Architecture
|
|
211
229
|
|
|
212
|
-
|
|
230
|
+
oreZ runs three components:
|
|
213
231
|
|
|
214
|
-
|
|
232
|
+
1. **Three PGlite instances** — PostgreSQL 17 in WASM, one per database zero-cache expects (postgres, zero_cvr, zero_cdb)
|
|
233
|
+
2. **TCP proxy** — speaks PostgreSQL wire protocol, routes to correct PGlite, handles logical replication
|
|
234
|
+
3. **zero-cache** — child process connecting to proxy, thinks it's real Postgres
|
|
215
235
|
|
|
216
|
-
###
|
|
236
|
+
### Why three instances?
|
|
217
237
|
|
|
218
|
-
|
|
238
|
+
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
239
|
|
|
220
|
-
|
|
240
|
+
| Connection database | PGlite instance | Data directory |
|
|
241
|
+
| ------------------- | --------------- | ----------------- |
|
|
242
|
+
| `postgres` | postgres | `pgdata-postgres` |
|
|
243
|
+
| `zero_cvr` | cvr | `pgdata-cvr` |
|
|
244
|
+
| `zero_cdb` | cdb | `pgdata-cdb` |
|
|
221
245
|
|
|
222
|
-
|
|
246
|
+
### Replication
|
|
223
247
|
|
|
224
|
-
|
|
248
|
+
PGlite doesn't support logical replication, so oreZ fakes it:
|
|
225
249
|
|
|
226
|
-
|
|
250
|
+
1. Triggers capture every mutation into `_orez._zero_changes`
|
|
251
|
+
2. Changes are encoded as pgoutput binary protocol
|
|
252
|
+
3. Streamed to zero-cache through the replication connection
|
|
227
253
|
|
|
228
|
-
|
|
254
|
+
Change notifications use `pg_notify` for real-time sync. Polling (20ms/500ms adaptive) is fallback only.
|
|
229
255
|
|
|
230
|
-
|
|
256
|
+
### SQLite WASM
|
|
231
257
|
|
|
232
|
-
|
|
258
|
+
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
259
|
|
|
234
|
-
|
|
260
|
+
The shim also polyfills the better-sqlite3 API surface zero-cache expects.
|
|
235
261
|
|
|
236
|
-
|
|
262
|
+
### Native SQLite mode
|
|
237
263
|
|
|
238
|
-
|
|
264
|
+
For `--disable-wasm-sqlite`, bootstrap the native addon first:
|
|
239
265
|
|
|
240
|
-
|
|
266
|
+
```bash
|
|
267
|
+
bun run native:bootstrap
|
|
268
|
+
```
|
|
241
269
|
|
|
242
|
-
|
|
270
|
+
## Internal Schema
|
|
243
271
|
|
|
244
|
-
|
|
272
|
+
oreZ stores replication state in the `_orez` schema (survives `pg_restore --clean`):
|
|
245
273
|
|
|
246
|
-
|
|
274
|
+
- `_orez._zero_changes` — change log for replication
|
|
275
|
+
- `_orez._zero_replication_slots` — slot tracking
|
|
276
|
+
- `_orez._zero_watermark` — LSN sequence
|
|
247
277
|
|
|
248
|
-
|
|
278
|
+
## Wire Protocol Compatibility
|
|
249
279
|
|
|
250
|
-
|
|
280
|
+
The proxy intercepts and rewrites to make PGlite look like real Postgres:
|
|
251
281
|
|
|
252
|
-
|
|
282
|
+
| Query/Command | What oreZ does |
|
|
283
|
+
| ------------------------------- | --------------------------------------------------- |
|
|
284
|
+
| `version()` | Returns `PostgreSQL 17.4 on x86_64-pc-linux-gnu...` |
|
|
285
|
+
| `current_setting('wal_level')` | Returns `logical` |
|
|
286
|
+
| `IDENTIFY_SYSTEM` | Returns fake system ID and timeline |
|
|
287
|
+
| `CREATE_REPLICATION_SLOT` | Persists to local table, returns valid LSN |
|
|
288
|
+
| `START_REPLICATION` | Streams changes as pgoutput binary |
|
|
289
|
+
| `pg_replication_slots` | Redirects to local tracking table |
|
|
290
|
+
| `READ ONLY` / `ISOLATION LEVEL` | Stripped (single-session) |
|
|
253
291
|
|
|
254
|
-
|
|
292
|
+
## Workarounds
|
|
255
293
|
|
|
256
|
-
|
|
294
|
+
Things that don't "just work" when replacing Postgres with PGlite and native SQLite with WASM:
|
|
257
295
|
|
|
258
|
-
###
|
|
296
|
+
### Session state bleed
|
|
259
297
|
|
|
260
|
-
|
|
298
|
+
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
299
|
|
|
262
|
-
###
|
|
300
|
+
### Query planner disabled
|
|
263
301
|
|
|
264
|
-
|
|
302
|
+
`ZERO_ENABLE_QUERY_PLANNER=false` because it relies on SQLite scan statistics that cause infinite loops in WASM.
|
|
265
303
|
|
|
266
|
-
###
|
|
304
|
+
### Unsupported column types
|
|
267
305
|
|
|
268
|
-
|
|
306
|
+
Columns with `tsvector`, `tsquery`, `USER-DEFINED` types are filtered from replication messages.
|
|
269
307
|
|
|
270
|
-
###
|
|
308
|
+
### Publication-aware tracking
|
|
271
309
|
|
|
272
|
-
`
|
|
310
|
+
If `ZERO_APP_PUBLICATIONS` is set, only tables in that publication get change-tracking triggers.
|
|
273
311
|
|
|
274
|
-
###
|
|
312
|
+
### Broken trigger cleanup
|
|
275
313
|
|
|
276
|
-
|
|
314
|
+
After restore, triggers whose backing functions don't exist are dropped (happens with filtered pg_dump).
|
|
277
315
|
|
|
278
316
|
## Tests
|
|
279
317
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
```
|
|
318
|
+
```bash
|
|
283
319
|
bun run test # orez tests
|
|
320
|
+
bun run test:integration:native # native sqlite integration
|
|
284
321
|
cd sqlite-wasm && bunx vitest run # bedrock-sqlite tests
|
|
285
322
|
```
|
|
286
323
|
|
|
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
|
|
324
|
+
## Project Structure
|
|
300
325
|
|
|
301
326
|
```
|
|
302
327
|
src/
|
|
303
|
-
cli-entry.ts
|
|
328
|
+
cli-entry.ts auto heap sizing wrapper
|
|
304
329
|
cli.ts cli with citty
|
|
305
|
-
index.ts main entry, orchestrates startup
|
|
330
|
+
index.ts main entry, orchestrates startup
|
|
306
331
|
config.ts configuration with defaults
|
|
307
|
-
log.ts colored log prefixes
|
|
308
|
-
mutex.ts
|
|
332
|
+
log.ts colored log prefixes, log files
|
|
333
|
+
mutex.ts serializing pglite access
|
|
309
334
|
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
|
|
335
|
+
pg-proxy.ts postgresql wire protocol proxy
|
|
336
|
+
pglite-manager.ts multi-instance pglite, migrations
|
|
337
|
+
s3-local.ts local s3 server (orez/s3)
|
|
338
|
+
vite-plugin.ts vite plugin (orez/vite)
|
|
339
|
+
admin/
|
|
340
|
+
server.ts admin dashboard backend
|
|
341
|
+
ui.ts admin dashboard frontend
|
|
342
|
+
log-store.ts log aggregation
|
|
343
|
+
http-proxy.ts http request logging
|
|
314
344
|
replication/
|
|
315
|
-
handler.ts replication
|
|
316
|
-
pgoutput-encoder.ts binary pgoutput
|
|
317
|
-
change-tracker.ts trigger installation,
|
|
345
|
+
handler.ts replication state machine, adaptive polling
|
|
346
|
+
pgoutput-encoder.ts binary pgoutput encoder
|
|
347
|
+
change-tracker.ts trigger installation, change purging
|
|
318
348
|
integration/
|
|
319
|
-
|
|
320
|
-
restore.test.ts pg_dump/restore integration test
|
|
349
|
+
*.test.ts end-to-end tests
|
|
321
350
|
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
|
-
})
|
|
351
|
+
Makefile emscripten build
|
|
352
|
+
native/api.js better-sqlite3 compatible API
|
|
353
|
+
native/vfs.c custom VFS with SHM for WAL2
|
|
391
354
|
```
|
|
392
355
|
|
|
393
|
-
Handles GET, PUT, DELETE, HEAD with CORS. Files stored on disk. No multipart, no ACLs, no versioning.
|
|
394
|
-
|
|
395
356
|
## License
|
|
396
357
|
|
|
397
358
|
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
|
}
|