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 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
@@ -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
+ }