pg-here 0.1.8 → 0.1.9

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 CHANGED
@@ -1,366 +1,92 @@
1
1
  # pg-here
2
2
 
3
- Per-project Postgres instances with instant snapshot & restore to support yolo development methods.
3
+ Run a local PostgreSQL instance in your project folder with one command.
4
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
- ### One-command start from npm (no local install)
27
-
28
- If you have Bun installed, you can run the published package directly:
5
+ ## 30-second start
29
6
 
30
7
  ```bash
31
8
  bunx pg-here
32
9
  ```
33
10
 
34
- This is the single command you want from any directory.
35
-
36
- If it still resolves an older release, the package `latest` dist-tag may be behind:
37
-
38
- To force that version and bypass a stale cache:
39
-
40
- ```bash
41
- bunx pg-here@0.1.8
42
- ```
43
-
44
- If maintainers want plain `bunx pg-here` to work for everyone, run once:
45
-
46
- ```bash
47
- npm dist-tag add pg-here@0.1.8 latest
48
- ```
49
-
50
- After that, this should work anywhere:
11
+ Default output:
51
12
 
52
- ```bash
53
- bunx pg-here
13
+ ```text
14
+ Launching PostgreSQL 18.0.0 into new pg_local/
15
+ psql postgresql://postgres:postgres@localhost:55432/postgres
54
16
  ```
55
17
 
56
- This starts a local PostgreSQL instance in your current project directory and prints the connection string, then keeps the process alive until you stop it.
57
-
58
- `bunx pg-here` uses these defaults if you pass nothing:
59
- - `username=postgres`
60
- - `password=postgres`
61
- - `database=postgres`
62
- - `port=55432`
63
-
64
- All args are optional.
18
+ If a data folder already exists:
65
19
 
66
- Pass CLI flags just like the local script when you want to override defaults:
67
-
68
- ```bash
69
- bunx pg-here --username postgres --password postgres --database my_app --port 55432
20
+ ```text
21
+ Reusing existing pg_local/data/ with PostgreSQL 18.0.0
22
+ psql postgresql://postgres:postgres@localhost:55432/postgres
70
23
  ```
71
24
 
72
- #### Linux note
73
-
74
- If `bunx pg-here` fails with `error while loading shared libraries`:
75
- - install missing system packages on the host (not in npm), then retry
76
- - restart the command
77
-
78
- Most commonly:
25
+ If the cached folder version differs:
79
26
 
80
- ```bash
81
- # Ubuntu/Debian
82
- sudo apt-get update && sudo apt-get install -y libxml2
83
-
84
- # Fedora/RHEL
85
- sudo dnf install -y libxml2
86
-
87
- # Alpine
88
- sudo apk add libxml2
27
+ ```text
28
+ Reusing existing pg_local/data/ (pg_local/bin has 18.0.0, running PostgreSQL is 18.0)
29
+ psql postgresql://postgres:postgres@localhost:55432/postgres
89
30
  ```
90
31
 
91
- If that machine is intentionally minimal (or containerized), use an image with PostgreSQL runtime dependencies.
92
-
93
- If apt says `Package 'libxml2' has no installation candidate`, your apt sources are likely missing standard repos.
32
+ The process stays alive until you stop it.
33
+ Ctrl+C → exits and stops Postgres.
94
34
 
95
- ```bash
96
- cat /etc/os-release
97
- apt-cache search '^libxml2$'
98
- ```
35
+ ## Defaults (all args optional)
99
36
 
100
- and if that returns nothing, fix apt sources for your distro/arch first, or use a base image that includes PostgreSQL runtime packages.
37
+ `bunx pg-here`
101
38
 
102
- Important ABI note:
39
+ - `username = postgres`
40
+ - `password = postgres`
41
+ - `database = postgres`
42
+ - `port = 55432`
43
+ - `pg-version` = auto
103
44
 
104
- - If your host exposes `libxml2.so.16` but not `libxml2.so.2`, older releases may fail to start before applying fallback.
105
- - If a failure mentions `/pg_local/bin/bin/postgres`, clear and redownload:
45
+ ## Custom run
106
46
 
107
47
  ```bash
108
- rm -rf pg_local
109
- bunx pg-here@0.1.8
48
+ bunx pg-here --username me --password secret --database my_app --port 55433 --pg-version 17.0.0
110
49
  ```
111
50
 
112
- For this release, `0.1.8` also attempts a one-time compatibility workaround when it detects
113
- `libxml2.so.16`-only hosts:
114
- - it creates a local runtime symlink in `./pg_local/runtime-libs/libxml2.so.2` pointing to the discovered `libxml2.so.16`
115
- - sets `LD_LIBRARY_PATH` for the retry so `postgres`/`initdb` can launch
116
-
117
- If your system is locked down and this still fails, create a global compatibility symlink (where permitted):
51
+ You can also run locally in this repo:
118
52
 
119
53
  ```bash
120
- sudo ln -sfn /usr/lib/x86_64-linux-gnu/libxml2.so.16 /usr/local/lib/libxml2.so.2
121
- sudo ldconfig
54
+ bun run db:up
122
55
  ```
123
56
 
124
- ### Programmatic usage
57
+ Same CLI flags are supported.
125
58
 
126
- Use `pg-here` directly from your server startup code and auto-create the app database if missing.
59
+ ## Programmatic
127
60
 
128
61
  ```ts
129
62
  import { startPgHere } from "pg-here";
130
63
 
131
- const pgHere = await startPgHere({
64
+ const pg = await startPgHere({
132
65
  projectDir: process.cwd(),
133
- port: 55432,
134
66
  database: "my_app",
135
67
  createDatabaseIfMissing: true,
136
- postgresVersion: "18.0.0",
137
68
  });
138
69
 
139
- console.log(pgHere.databaseConnectionString);
140
-
141
- // On shutdown:
142
- await pgHere.stop();
70
+ console.log(pg.databaseConnectionString); // psql-ready URL
71
+ await pg.stop();
143
72
  ```
144
73
 
145
- `databaseConnectionString` points to your target DB (`my_app` above).
146
- If the DB does not exist yet, `createDatabaseIfMissing: true` creates it on startup.
147
- Set `postgresVersion` if you want to pin/select a specific PostgreSQL version.
148
- By default, `startPgHere()` installs SIGINT/SIGTERM shutdown hooks that stop Postgres when
149
- your process exits, and `stop()` preserves data (no cluster cleanup/delete).
150
- Use `await pgHere.cleanup()` only when you explicitly want full resource cleanup.
151
- `pg_stat_statements` is enabled automatically (`shared_preload_libraries` + extension creation).
152
- Set `enablePgStatStatements: false` to opt out.
74
+ ## Linux runtime error (quick fix)
153
75
 
154
- ### CLI Options
76
+ If startup fails with missing `libxml2` libraries, install runtime packages and retry:
155
77
 
156
78
  ```bash
157
- # Custom credentials
158
- bun run db:up --username postgres --password postgres
159
-
160
- # Short flags
161
- bun run db:up -u postgres -p postgres
162
-
163
- # Custom port
164
- bun run db:up --port 55433
165
-
166
- # All together
167
- bun run db:up -u postgres -p postgres --database postgres --port 55433
168
-
169
- # Pin postgres version
170
- bun run db:up --pg-version 18.0.0
171
- ```
172
-
173
- **Defaults**: username=`postgres`, password=`postgres`, port=`55432`, pg-version=`PG_VERSION` or pg-embedded default
174
-
175
- ### Project Structure
176
-
177
- ```
178
- project/
179
- pg_local/
180
- data/ # PostgreSQL data cluster (persists between runs)
181
- bin/ # Downloaded PostgreSQL binaries
182
- scripts/
183
- pg-dev.mjs # Runner script
184
- package.json
185
- ```
186
-
187
- ### How It Works
188
-
189
- - PostgreSQL only runs when you execute `bun run db:up`
190
- - Correct architecture binary downloads automatically on first run
191
- - Data persists in `pg_local/data/` across restarts
192
- - Process stops completely on exit (Ctrl+C)
193
- - One instance per project, no system PostgreSQL dependency
194
-
195
- ---
196
-
197
- ## Use this from another project (recommended)
198
-
199
- Keep this repo in one place, and point it at any other project directory when you want a dedicated Postgres instance there.
200
-
201
- Example: your app lives at `/path/to/my-app`, but this repo lives elsewhere.
202
-
203
- ```
204
- # start postgres for that project (one-time init happens automatically)
205
- bun run snapshot snapshot /path/to/my-app/.pg-here
206
-
207
- # list snapshots for that project
208
- bun run snapshot list /path/to/my-app/.pg-here
209
-
210
- # revert that project to a snapshot
211
- bun run revert /path/to/my-app/.pg-here snap_YYYYMMDD_HHMMSS
79
+ sudo apt-get update && sudo apt-get install -y libxml2 libxml2-utils
80
+ sudo dnf install -y libxml2
81
+ sudo apk add libxml2
212
82
  ```
213
83
 
214
- Tips:
215
- - Pick a per-project folder (e.g. `.pg-here`) and reuse it.
216
- - The project directory just needs to be on the same APFS volume for clones to be fast.
217
- - You can also pass the directory with `--project/-p` instead of positional.
218
-
219
- To install dependencies:
84
+ This release already retries startup with a project-local `libxml2` compatibility fallback when needed.
220
85
 
221
- ```bash
222
- bun install
223
- ```
86
+ ## Version pin / stale cache
224
87
 
225
- To run:
88
+ If your environment keeps resolving an older release, force a specific version:
226
89
 
227
90
  ```bash
228
- bun run index.ts
229
- ```
230
-
231
- This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
232
-
233
- ## APFS clone snapshots (macOS) — insanely simple
234
-
235
- ### The 30‑second version
236
-
237
- ```
238
- # take a snapshot for the default project directory
239
- bun run snapshot snapshot
240
-
241
- # list snapshots
242
- bun run snapshot list
243
-
244
- # revert to a snapshot (copy from list output)
245
- bun run snapshot revert snap_YYYYMMDD_HHMMSS
246
-
247
- # or use the alias
248
- bun run revert snap_YYYYMMDD_HHMMSS
249
- ```
250
-
251
- By default, snapshots live under `./pg_projects/default`. You can pass a project directory explicitly if you want.
252
-
253
- ### What this does
254
-
255
- - Uses APFS copy‑on‑write clones (`cp -cR` / `ditto --clone`)
256
- - Stops Postgres during snapshot/revert (cold stop/start)
257
- - Keeps snapshots immutable and restores into new instances
258
-
259
- ### Why it's great
260
-
261
- - Near‑instant snapshot and revert via copy‑on‑write
262
- - Space grows only with changed blocks
263
- - No volume‑wide rollback
264
- - No WAL/PITR complexity
265
- - macOS‑native, zero extra services
266
- - Deterministic failure modes
267
-
268
- ### Operational constraints
269
-
270
- - PostgreSQL must be stopped for snapshot/revert
271
- - Source and destination must be on the same APFS volume
272
- - One cluster per project
273
-
274
- ### Under the hood layout
275
-
276
- ```
277
- ~/pg/proj/
278
- current -> instances/inst_active
279
- instances/
280
- snaps/
281
- ```
282
-
283
- ### Full commands (helper script)
284
-
285
- ```
286
- # snapshot current cluster
287
- bun run snapshot snapshot /path/to/project
288
-
289
- # list snapshots
290
- bun run snapshot list /path/to/project
291
-
292
- # revert to a snapshot
293
- bun run snapshot revert /path/to/project snap_YYYYMMDD_HHMMSS
294
- ```
295
-
296
- Flags (optional):
297
-
298
- ```
299
- --project/-p project directory (same as positional projectDir)
300
- --snap/-s snapshot name (for revert)
301
- --pg-ctl path to pg_ctl (overrides PG_CTL)
302
- ```
303
-
304
- If `pg_ctl` isn't on your `PATH`, set `PG_CTL`. By default the script looks in `./pg_local/bin/*/bin/pg_ctl`.
305
-
306
- ### One‑shot test (does everything end‑to‑end)
307
-
308
- ```
309
- bun run snapshot:test
310
- ```
311
-
312
- This:
313
- 1) Starts a temporary cluster
314
- 2) Writes sample data
315
- 3) Snapshots
316
- 4) Mutates data
317
- 5) Restores
318
- 6) Verifies the original data returns
319
-
320
- Optional flags:
321
-
322
- ```
323
- --project/-p project directory (default: ./pg_projects/apfs_test_TIMESTAMP)
324
- --port postgres port (default: 55433 or PGPORT_SNAPSHOT_TEST)
325
- --pg-version postgres version (default: PG_VERSION or pg-embedded default)
326
- --keep keep the project directory after the test
327
- ```
328
-
329
- ### Bun test integration
330
-
331
- ```
332
- bun test
333
- bun run test:apfs
91
+ bunx pg-here@0.1.9
334
92
  ```
335
-
336
- Set `SKIP_APFS_TEST=1` to skip the APFS snapshot test.
337
-
338
- ## APFS clone speed benchmark
339
-
340
- Compares clone time between a small and large dataset by seeding a table and cloning the data directory.
341
-
342
- ```
343
- bun run bench:apfs
344
- ```
345
-
346
- Optional flags:
347
-
348
- ```
349
- --project/-p project directory (default: ./pg_projects/bench)
350
- --port postgres port (default: 55434 or PGPORT_BENCH)
351
- --small-rows rows for small dataset (default: 50_000)
352
- --large-rows rows for large dataset (default: 2_000_000)
353
- --row-bytes payload bytes per row (default: 256)
354
- --pg-version postgres version (default: PG_VERSION or pg-embedded default)
355
- ```
356
-
357
- Example:
358
-
359
- ```
360
- bun run bench:apfs --small-rows 100000 --large-rows 5000000 --row-bytes 512
361
- ```
362
-
363
- ### When to choose something else
364
-
365
- - Need online "rewind to 5 minutes ago" repeatedly → base backup + WAL/PITR
366
- - Dataset ≥ ~50 GB with heavy churn → dedicated APFS volume + volume snapshots, or move DB off the laptop
package/bin/pg-here.mjs CHANGED
@@ -5,6 +5,8 @@ import { hideBin } from "yargs/helpers";
5
5
  import { startPgHere } from "../dist/index.js";
6
6
  import {
7
7
  maybePrintLinuxRuntimeHelp,
8
+ getPreStartPgHereState,
9
+ printPgHereStartupInfo,
8
10
  startPgHereWithLibxml2Compat,
9
11
  } from "../scripts/cli-error-help.mjs";
10
12
 
@@ -36,6 +38,7 @@ const argv = await yargs(hideBin(process.argv))
36
38
  .parse();
37
39
 
38
40
  let pg;
41
+ const preStartState = getPreStartPgHereState(process.cwd());
39
42
  const startInstance = () =>
40
43
  startPgHere({
41
44
  projectDir: process.cwd(),
@@ -55,5 +58,11 @@ try {
55
58
  throw error;
56
59
  }
57
60
 
58
- console.log(pg.databaseConnectionString);
61
+ printPgHereStartupInfo({
62
+ connectionString: pg.databaseConnectionString,
63
+ instance: pg.instance,
64
+ preStartState,
65
+ requestedVersion: argv["pg-version"],
66
+ });
67
+
59
68
  setInterval(() => {}, 1 << 30);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pg-here",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Per-project embedded PostgreSQL with programmatic startup and auto DB creation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,5 +1,15 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { accessSync, existsSync, lstatSync, mkdirSync, readlinkSync, rmSync, symlinkSync, F_OK } from "node:fs";
2
+ import {
3
+ accessSync,
4
+ existsSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ readlinkSync,
8
+ readdirSync,
9
+ rmSync,
10
+ symlinkSync,
11
+ F_OK,
12
+ } from "node:fs";
3
13
  import { join } from "node:path";
4
14
 
5
15
  const LIBXML2_SONAME = "libxml2.so.2";
@@ -16,6 +26,12 @@ const LIB_PATHS = [
16
26
  "/usr/local/lib",
17
27
  ];
18
28
 
29
+ const PG_LOCAL_DIR = "pg_local";
30
+ const PG_LOCAL_DATA_DIR = "data";
31
+ const PG_LOCAL_BIN_DIR = "bin";
32
+ const PG_VERSION_FILE = "PG_VERSION";
33
+ const VERSION_DIR_RE = /^\d+\.\d+(?:\.\d+)?$/;
34
+
19
35
  export function maybePrintLinuxRuntimeHelp(error) {
20
36
  const message = String(error?.message ?? error);
21
37
  const missingLibsFromError = extractMissingLibrariesFromMessage(message);
@@ -54,7 +70,7 @@ export function maybePrintLinuxRuntimeHelp(error) {
54
70
  if (binPath.includes("/bin/bin/postgres")) {
55
71
  console.error(" Your local pg_local cache looks partially provisioned (bin/bin/postgres path).");
56
72
  console.error(" Try removing it and retrying:");
57
- console.error(" rm -rf pg_local && bunx pg-here@0.1.8");
73
+ console.error(" rm -rf pg_local && bunx pg-here@0.1.9");
58
74
  }
59
75
  console.error(
60
76
  `If your distro requires different package names, install packages that provide: ${missingLibs.join(", ")}`
@@ -94,6 +110,91 @@ export function hasLibxml2CompatibilityNeed(error) {
94
110
  return missing.includes(LIBXML2_SONAME) || message.includes("libxml2.so.2");
95
111
  }
96
112
 
113
+ export function getPreStartPgHereState(projectDir) {
114
+ const normalizedProjectDir = typeof projectDir === "string" && projectDir ? projectDir : process.cwd();
115
+ const dataDir = join(normalizedProjectDir, PG_LOCAL_DIR, PG_LOCAL_DATA_DIR);
116
+ const binDir = join(normalizedProjectDir, PG_LOCAL_DIR, PG_LOCAL_BIN_DIR);
117
+ const hasData = existsSync(dataDir);
118
+ const hasPgVersionFile = existsSync(join(dataDir, PG_VERSION_FILE));
119
+ const installedVersions = getInstalledPostgresVersions(binDir);
120
+
121
+ return {
122
+ dataDir,
123
+ hasData,
124
+ hasPgVersionFile,
125
+ installedVersions,
126
+ installedVersion: installedVersions[0] ?? "",
127
+ };
128
+ }
129
+
130
+ export function printPgHereStartupInfo({
131
+ connectionString,
132
+ instance,
133
+ preStartState,
134
+ requestedVersion,
135
+ }) {
136
+ const { hasData, installedVersion } = preStartState ?? {};
137
+ const startedVersion = getPostgresInstanceVersion(instance);
138
+
139
+ const displayVersion =
140
+ startedVersion || requestedVersion || "default";
141
+ const dataPath = `${PG_LOCAL_DIR}/${PG_LOCAL_DATA_DIR}/`;
142
+ const firstLine = hasData
143
+ ? getExistingDataStatusLine({ installedVersion, startedVersion, requestedVersion, dataPath })
144
+ : `Launching PostgreSQL ${displayVersion} into new ${PG_LOCAL_DIR}/`;
145
+
146
+ console.log(firstLine);
147
+ if (typeof connectionString === "string" && connectionString.length > 0) {
148
+ console.log(`psql ${connectionString}`);
149
+ }
150
+ }
151
+
152
+ function getExistingDataStatusLine({
153
+ installedVersion,
154
+ startedVersion,
155
+ requestedVersion,
156
+ dataPath,
157
+ }) {
158
+ const runVersion = startedVersion || requestedVersion || "default";
159
+ if (installedVersion && startedVersion && installedVersion !== startedVersion) {
160
+ return `Reusing existing ${dataPath} (pg_local/bin has ${installedVersion}, running PostgreSQL is ${startedVersion})`;
161
+ }
162
+
163
+ if (installedVersion && startedVersion && installedVersion === startedVersion) {
164
+ return `Reusing existing ${dataPath} with PostgreSQL ${runVersion}`;
165
+ }
166
+
167
+ return `Reusing existing ${dataPath} with PostgreSQL ${runVersion}`;
168
+ }
169
+
170
+ function getPostgresInstanceVersion(instance) {
171
+ try {
172
+ if (typeof instance?.getPostgreSqlVersion === "function") {
173
+ return instance.getPostgreSqlVersion();
174
+ }
175
+ } catch {
176
+ return "";
177
+ }
178
+
179
+ return "";
180
+ }
181
+
182
+ function compareSemVerDesc(left, right) {
183
+ const leftParts = left.split(".").map((segment) => Number.parseInt(segment, 10) || 0);
184
+ const rightParts = right.split(".").map((segment) => Number.parseInt(segment, 10) || 0);
185
+ const maxLength = Math.max(leftParts.length, rightParts.length);
186
+ for (let i = 0; i < maxLength; i += 1) {
187
+ const leftValue = leftParts[i] ?? 0;
188
+ const rightValue = rightParts[i] ?? 0;
189
+ if (leftValue === rightValue) {
190
+ continue;
191
+ }
192
+ return rightValue - leftValue;
193
+ }
194
+
195
+ return 0;
196
+ }
197
+
97
198
  export async function startPgHereWithLibxml2Compat(start, workingDir) {
98
199
  try {
99
200
  return await start();
@@ -111,6 +212,25 @@ export async function startPgHereWithLibxml2Compat(start, workingDir) {
111
212
  }
112
213
  }
113
214
 
215
+ function getInstalledPostgresVersions(baseDir) {
216
+ if (!existsSync(baseDir)) {
217
+ return [];
218
+ }
219
+
220
+ let entries = [];
221
+ try {
222
+ entries = readdirSync(baseDir, { withFileTypes: true });
223
+ } catch {
224
+ return [];
225
+ }
226
+
227
+ return entries
228
+ .filter((entry) => entry.isDirectory() && VERSION_DIR_RE.test(entry.name))
229
+ .map((entry) => entry.name)
230
+ .sort(compareSemVerDesc);
231
+ }
232
+
233
+
114
234
  export function ensureLibxml2Compatibility(workingDir) {
115
235
  if (process.platform !== "linux") {
116
236
  return false;
@@ -3,6 +3,8 @@ import { hideBin } from "yargs/helpers";
3
3
  import { startPgHere } from "../index.ts";
4
4
  import {
5
5
  maybePrintLinuxRuntimeHelp,
6
+ getPreStartPgHereState,
7
+ printPgHereStartupInfo,
6
8
  startPgHereWithLibxml2Compat,
7
9
  } from "./cli-error-help.mjs";
8
10
 
@@ -34,6 +36,7 @@ const argv = await yargs(hideBin(process.argv))
34
36
  .parse();
35
37
 
36
38
  let pg;
39
+ const preStartState = getPreStartPgHereState(process.cwd());
37
40
  const startInstance = () =>
38
41
  startPgHere({
39
42
  projectDir: process.cwd(),
@@ -53,8 +56,12 @@ try {
53
56
  throw error;
54
57
  }
55
58
 
56
- // print connection string for tooling
57
- console.log(pg.databaseConnectionString);
59
+ printPgHereStartupInfo({
60
+ connectionString: pg.databaseConnectionString,
61
+ instance: pg.instance,
62
+ preStartState,
63
+ requestedVersion: argv["pg-version"],
64
+ });
58
65
 
59
66
  // keep this process alive; Ctrl-C stops postgres
60
67
  setInterval(() => {}, 1 << 30);