pg-here 0.1.0
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 +263 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +70 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# pg-here
|
|
2
|
+
|
|
3
|
+
Per-project Postgres instances with instant snapshot & restore to support yolo development methods.
|
|
4
|
+
|
|
5
|
+
Repository: https://github.com/mayfer/pg-here
|
|
6
|
+
|
|
7
|
+
## Project-Local PostgreSQL Setup
|
|
8
|
+
|
|
9
|
+
Each project runs its own isolated PostgreSQL instance. Downloads correct binary automatically for your CPU architecture (x86_64 / arm64). Database lives inside the project folder.
|
|
10
|
+
|
|
11
|
+
### Quick Start
|
|
12
|
+
|
|
13
|
+
Start PostgreSQL:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun run db:up
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Output:
|
|
20
|
+
```
|
|
21
|
+
postgresql://postgres:postgres@localhost:55432/postgres
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Connect from your app using that connection string, then Ctrl+C to stop.
|
|
25
|
+
|
|
26
|
+
### Programmatic usage
|
|
27
|
+
|
|
28
|
+
Use `pg-here` directly from your server startup code and auto-create the app database if missing.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { startPgHere } from "pg-here";
|
|
32
|
+
|
|
33
|
+
const pgHere = await startPgHere({
|
|
34
|
+
projectDir: process.cwd(),
|
|
35
|
+
port: 55432,
|
|
36
|
+
database: "my_app",
|
|
37
|
+
createDatabaseIfMissing: true,
|
|
38
|
+
postgresVersion: "18.0.0",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log(pgHere.databaseConnectionString);
|
|
42
|
+
|
|
43
|
+
// On shutdown:
|
|
44
|
+
await pgHere.stop();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`databaseConnectionString` points to your target DB (`my_app` above).
|
|
48
|
+
If the DB does not exist yet, `createDatabaseIfMissing: true` creates it on startup.
|
|
49
|
+
Set `postgresVersion` if you want to pin/select a specific PostgreSQL version.
|
|
50
|
+
|
|
51
|
+
### CLI Options
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Custom credentials
|
|
55
|
+
bun run db:up --username myuser --password mypass
|
|
56
|
+
|
|
57
|
+
# Short flags
|
|
58
|
+
bun run db:up -u myuser -p mypass
|
|
59
|
+
|
|
60
|
+
# Custom port
|
|
61
|
+
bun run db:up --port 55433
|
|
62
|
+
|
|
63
|
+
# All together
|
|
64
|
+
bun run db:up -u myuser -p mypass --port 55433
|
|
65
|
+
|
|
66
|
+
# Pin postgres version
|
|
67
|
+
bun run db:up --pg-version 18.0.0
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Defaults**: username=`postgres`, password=`postgres`, port=`55432`, pg-version=`PG_VERSION` or pg-embedded default
|
|
71
|
+
|
|
72
|
+
### Project Structure
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
project/
|
|
76
|
+
pg_local/
|
|
77
|
+
data/ # PostgreSQL data cluster (persists between runs)
|
|
78
|
+
bin/ # Downloaded PostgreSQL binaries
|
|
79
|
+
scripts/
|
|
80
|
+
pg-dev.mjs # Runner script
|
|
81
|
+
package.json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### How It Works
|
|
85
|
+
|
|
86
|
+
- PostgreSQL only runs when you execute `bun run db:up`
|
|
87
|
+
- Correct architecture binary downloads automatically on first run
|
|
88
|
+
- Data persists in `pg_local/data/` across restarts
|
|
89
|
+
- Process stops completely on exit (Ctrl+C)
|
|
90
|
+
- One instance per project, no system PostgreSQL dependency
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Use this from another project (recommended)
|
|
95
|
+
|
|
96
|
+
Keep this repo in one place, and point it at any other project directory when you want a dedicated Postgres instance there.
|
|
97
|
+
|
|
98
|
+
Example: your app lives at `/path/to/my-app`, but this repo lives elsewhere.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
# start postgres for that project (one-time init happens automatically)
|
|
102
|
+
bun run snapshot snapshot /path/to/my-app/.pg-here
|
|
103
|
+
|
|
104
|
+
# list snapshots for that project
|
|
105
|
+
bun run snapshot list /path/to/my-app/.pg-here
|
|
106
|
+
|
|
107
|
+
# revert that project to a snapshot
|
|
108
|
+
bun run revert /path/to/my-app/.pg-here snap_YYYYMMDD_HHMMSS
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Tips:
|
|
112
|
+
- Pick a per-project folder (e.g. `.pg-here`) and reuse it.
|
|
113
|
+
- The project directory just needs to be on the same APFS volume for clones to be fast.
|
|
114
|
+
- You can also pass the directory with `--project/-p` instead of positional.
|
|
115
|
+
|
|
116
|
+
To install dependencies:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
bun install
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
To run:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
bun run index.ts
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
|
129
|
+
|
|
130
|
+
## APFS clone snapshots (macOS) — insanely simple
|
|
131
|
+
|
|
132
|
+
### The 30‑second version
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
# take a snapshot for the default project directory
|
|
136
|
+
bun run snapshot snapshot
|
|
137
|
+
|
|
138
|
+
# list snapshots
|
|
139
|
+
bun run snapshot list
|
|
140
|
+
|
|
141
|
+
# revert to a snapshot (copy from list output)
|
|
142
|
+
bun run snapshot revert snap_YYYYMMDD_HHMMSS
|
|
143
|
+
|
|
144
|
+
# or use the alias
|
|
145
|
+
bun run revert snap_YYYYMMDD_HHMMSS
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
By default, snapshots live under `./pg_projects/default`. You can pass a project directory explicitly if you want.
|
|
149
|
+
|
|
150
|
+
### What this does
|
|
151
|
+
|
|
152
|
+
- Uses APFS copy‑on‑write clones (`cp -cR` / `ditto --clone`)
|
|
153
|
+
- Stops Postgres during snapshot/revert (cold stop/start)
|
|
154
|
+
- Keeps snapshots immutable and restores into new instances
|
|
155
|
+
|
|
156
|
+
### Why it's great
|
|
157
|
+
|
|
158
|
+
- Near‑instant snapshot and revert via copy‑on‑write
|
|
159
|
+
- Space grows only with changed blocks
|
|
160
|
+
- No volume‑wide rollback
|
|
161
|
+
- No WAL/PITR complexity
|
|
162
|
+
- macOS‑native, zero extra services
|
|
163
|
+
- Deterministic failure modes
|
|
164
|
+
|
|
165
|
+
### Operational constraints
|
|
166
|
+
|
|
167
|
+
- PostgreSQL must be stopped for snapshot/revert
|
|
168
|
+
- Source and destination must be on the same APFS volume
|
|
169
|
+
- One cluster per project
|
|
170
|
+
|
|
171
|
+
### Under the hood layout
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
~/pg/proj/
|
|
175
|
+
current -> instances/inst_active
|
|
176
|
+
instances/
|
|
177
|
+
snaps/
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Full commands (helper script)
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
# snapshot current cluster
|
|
184
|
+
bun run snapshot snapshot /path/to/project
|
|
185
|
+
|
|
186
|
+
# list snapshots
|
|
187
|
+
bun run snapshot list /path/to/project
|
|
188
|
+
|
|
189
|
+
# revert to a snapshot
|
|
190
|
+
bun run snapshot revert /path/to/project snap_YYYYMMDD_HHMMSS
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Flags (optional):
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
--project/-p project directory (same as positional projectDir)
|
|
197
|
+
--snap/-s snapshot name (for revert)
|
|
198
|
+
--pg-ctl path to pg_ctl (overrides PG_CTL)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
If `pg_ctl` isn't on your `PATH`, set `PG_CTL`. By default the script looks in `./pg_local/bin/*/bin/pg_ctl`.
|
|
202
|
+
|
|
203
|
+
### One‑shot test (does everything end‑to‑end)
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
bun run snapshot:test
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
This:
|
|
210
|
+
1) Starts a temporary cluster
|
|
211
|
+
2) Writes sample data
|
|
212
|
+
3) Snapshots
|
|
213
|
+
4) Mutates data
|
|
214
|
+
5) Restores
|
|
215
|
+
6) Verifies the original data returns
|
|
216
|
+
|
|
217
|
+
Optional flags:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
--project/-p project directory (default: ./pg_projects/apfs_test_TIMESTAMP)
|
|
221
|
+
--port postgres port (default: 55433 or PGPORT_SNAPSHOT_TEST)
|
|
222
|
+
--pg-version postgres version (default: PG_VERSION or pg-embedded default)
|
|
223
|
+
--keep keep the project directory after the test
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Bun test integration
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
bun test
|
|
230
|
+
bun run test:apfs
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Set `SKIP_APFS_TEST=1` to skip the APFS snapshot test.
|
|
234
|
+
|
|
235
|
+
## APFS clone speed benchmark
|
|
236
|
+
|
|
237
|
+
Compares clone time between a small and large dataset by seeding a table and cloning the data directory.
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
bun run bench:apfs
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Optional flags:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
--project/-p project directory (default: ./pg_projects/bench)
|
|
247
|
+
--port postgres port (default: 55434 or PGPORT_BENCH)
|
|
248
|
+
--small-rows rows for small dataset (default: 50_000)
|
|
249
|
+
--large-rows rows for large dataset (default: 2_000_000)
|
|
250
|
+
--row-bytes payload bytes per row (default: 256)
|
|
251
|
+
--pg-version postgres version (default: PG_VERSION or pg-embedded default)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Example:
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
bun run bench:apfs --small-rows 100000 --large-rows 5000000 --row-bytes 512
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### When to choose something else
|
|
261
|
+
|
|
262
|
+
- Need online "rewind to 5 minutes ago" repeatedly → base backup + WAL/PITR
|
|
263
|
+
- Dataset ≥ ~50 GB with heavy churn → dedicated APFS volume + volume snapshots, or move DB off the laptop
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { PostgresInstance } from "pg-embedded";
|
|
2
|
+
export interface PgHereOptions {
|
|
3
|
+
projectDir?: string;
|
|
4
|
+
dataDir?: string;
|
|
5
|
+
installationDir?: string;
|
|
6
|
+
postgresVersion?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
port?: number;
|
|
9
|
+
username?: string;
|
|
10
|
+
password?: string;
|
|
11
|
+
persistent?: boolean;
|
|
12
|
+
database?: string;
|
|
13
|
+
createDatabaseIfMissing?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface PgHereHandle {
|
|
16
|
+
instance: PostgresInstance;
|
|
17
|
+
connectionString: string;
|
|
18
|
+
databaseConnectionString: string;
|
|
19
|
+
database: string;
|
|
20
|
+
stop: () => Promise<void>;
|
|
21
|
+
ensureDatabase: (databaseName?: string) => Promise<boolean>;
|
|
22
|
+
}
|
|
23
|
+
export declare function createPgHereInstance(options?: PgHereOptions): PostgresInstance;
|
|
24
|
+
export declare function ensurePgHereDatabase(instance: PostgresInstance, databaseName: string): Promise<boolean>;
|
|
25
|
+
export declare function stopPgHere(instance: PostgresInstance): Promise<void>;
|
|
26
|
+
export declare function startPgHere(options?: PgHereOptions): Promise<PgHereHandle>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { PostgresInstance } from "pg-embedded";
|
|
3
|
+
const DEFAULT_USERNAME = "postgres";
|
|
4
|
+
const DEFAULT_PASSWORD = "postgres";
|
|
5
|
+
const DEFAULT_PORT = 55432;
|
|
6
|
+
const DEFAULT_DATABASE = "postgres";
|
|
7
|
+
export function createPgHereInstance(options = {}) {
|
|
8
|
+
const root = resolve(options.projectDir ?? process.cwd());
|
|
9
|
+
const dataDir = resolve(options.dataDir ?? join(root, "pg_local", "data"));
|
|
10
|
+
const installationDir = resolve(options.installationDir ?? join(root, "pg_local", "bin"));
|
|
11
|
+
const postgresVersion = options.postgresVersion ?? options.version;
|
|
12
|
+
return new PostgresInstance({
|
|
13
|
+
version: postgresVersion,
|
|
14
|
+
dataDir,
|
|
15
|
+
installationDir,
|
|
16
|
+
port: options.port ?? DEFAULT_PORT,
|
|
17
|
+
username: options.username ?? DEFAULT_USERNAME,
|
|
18
|
+
password: options.password ?? DEFAULT_PASSWORD,
|
|
19
|
+
persistent: options.persistent ?? true,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function ensurePgHereDatabase(instance, databaseName) {
|
|
23
|
+
if (!databaseName || databaseName === DEFAULT_DATABASE) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const exists = await instance.databaseExists(databaseName);
|
|
27
|
+
if (exists) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
await instance.createDatabase(databaseName);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
export async function stopPgHere(instance) {
|
|
34
|
+
try {
|
|
35
|
+
await instance.stop();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// no-op
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
await instance.cleanup();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// no-op
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function startPgHere(options = {}) {
|
|
48
|
+
const instance = createPgHereInstance(options);
|
|
49
|
+
await instance.start();
|
|
50
|
+
const database = options.database ?? DEFAULT_DATABASE;
|
|
51
|
+
const shouldCreate = options.createDatabaseIfMissing ?? database !== DEFAULT_DATABASE;
|
|
52
|
+
if (shouldCreate) {
|
|
53
|
+
await ensurePgHereDatabase(instance, database);
|
|
54
|
+
}
|
|
55
|
+
const connectionString = instance.connectionInfo.connectionString;
|
|
56
|
+
const databaseConnectionString = setConnectionDatabase(connectionString, database);
|
|
57
|
+
return {
|
|
58
|
+
instance,
|
|
59
|
+
connectionString,
|
|
60
|
+
databaseConnectionString,
|
|
61
|
+
database,
|
|
62
|
+
stop: async () => stopPgHere(instance),
|
|
63
|
+
ensureDatabase: async (databaseName = database) => ensurePgHereDatabase(instance, databaseName),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function setConnectionDatabase(connectionString, database) {
|
|
67
|
+
const connectionUrl = new URL(connectionString);
|
|
68
|
+
connectionUrl.pathname = `/${database}`;
|
|
69
|
+
return connectionUrl.toString();
|
|
70
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pg-here",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Per-project embedded PostgreSQL with programmatic startup and auto DB creation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"private": false,
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/mayfer/pg-here.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/mayfer/pg-here",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/mayfer/pg-here/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"postgres",
|
|
29
|
+
"postgresql",
|
|
30
|
+
"embedded",
|
|
31
|
+
"local-development"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.build.json",
|
|
41
|
+
"db:up": "bun run scripts/pg-dev.mjs",
|
|
42
|
+
"snapshot": "bun run scripts/apfs-snapshot.mjs",
|
|
43
|
+
"snapshot:test": "bun run scripts/apfs-snapshot-test.mjs",
|
|
44
|
+
"revert": "bun run scripts/apfs-snapshot.mjs revert",
|
|
45
|
+
"bench:apfs": "bun run scripts/apfs-clone-bench.mjs",
|
|
46
|
+
"test": "bun test",
|
|
47
|
+
"test:apfs": "bun test tests/apfs-snapshot.test.ts",
|
|
48
|
+
"prepublishOnly": "npm run build"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/bun": "latest",
|
|
52
|
+
"typescript": "^5.9.2"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"pg": "^8.16.3",
|
|
56
|
+
"pg-embedded": "^0.2.3",
|
|
57
|
+
"yargs": "^18.0.0"
|
|
58
|
+
}
|
|
59
|
+
}
|