postgres-memory-server 0.1.0 → 0.2.1

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/dist/cli.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import process from "process";
4
+ import process3 from "process";
5
5
 
6
6
  // src/PostgresMemoryServer.ts
7
- import { promises as fs } from "fs";
8
- import path from "path";
9
- import { PostgreSqlContainer } from "@testcontainers/postgresql";
7
+ import { promises as fs2, rmSync } from "fs";
8
+ import os2 from "os";
9
+ import path2 from "path";
10
+ import process2 from "process";
11
+ import EmbeddedPostgres from "embedded-postgres";
10
12
  import { Client } from "pg";
11
13
 
12
14
  // src/errors.ts
@@ -28,20 +30,439 @@ var ServerStoppedError = class extends PostgresMemoryServerError {
28
30
  super("The PostgresMemoryServer has already been stopped.");
29
31
  }
30
32
  };
33
+ var ExtensionInstallError = class extends PostgresMemoryServerError {
34
+ constructor(extensionName, cause) {
35
+ super(
36
+ `Failed to install the "${extensionName}" extension. ${cause?.message ?? ""}`.trim(),
37
+ cause ? { cause } : void 0
38
+ );
39
+ }
40
+ };
41
+
42
+ // src/native.ts
43
+ import { promises as fs, readFileSync, existsSync } from "fs";
44
+ import { createRequire } from "module";
45
+ import { execFile as execFileCb } from "child_process";
46
+ import { promisify } from "util";
47
+ import net from "net";
48
+ import os from "os";
49
+ import path from "path";
50
+ var execFile = promisify(execFileCb);
51
+ async function getFreePort() {
52
+ return new Promise((resolve, reject) => {
53
+ const server = net.createServer();
54
+ server.listen(0, () => {
55
+ const { port } = server.address();
56
+ server.close(() => resolve(port));
57
+ });
58
+ server.on("error", reject);
59
+ });
60
+ }
61
+ function getPgMajorVersion() {
62
+ const req = createRequire(import.meta.url);
63
+ const mainEntry = req.resolve("embedded-postgres");
64
+ let dir = path.dirname(mainEntry);
65
+ while (dir !== path.dirname(dir)) {
66
+ const candidate = path.join(dir, "package.json");
67
+ try {
68
+ const content = readFileSync(candidate, "utf8");
69
+ const pkg = JSON.parse(content);
70
+ if (pkg.name === "embedded-postgres" && pkg.version) {
71
+ const major = pkg.version.split(".")[0];
72
+ if (major) return major;
73
+ }
74
+ } catch {
75
+ }
76
+ dir = path.dirname(dir);
77
+ }
78
+ throw new Error(
79
+ "Could not determine embedded-postgres version. Ensure embedded-postgres is installed."
80
+ );
81
+ }
82
+ function getNativeDir() {
83
+ const platform = os.platform();
84
+ const arch = os.arch();
85
+ const platformPkgNames = {
86
+ darwin: {
87
+ arm64: "@embedded-postgres/darwin-arm64",
88
+ x64: "@embedded-postgres/darwin-x64"
89
+ },
90
+ linux: {
91
+ x64: "@embedded-postgres/linux-x64",
92
+ arm64: "@embedded-postgres/linux-arm64"
93
+ },
94
+ win32: {
95
+ x64: "@embedded-postgres/windows-x64"
96
+ }
97
+ };
98
+ const pkgName = platformPkgNames[platform]?.[arch];
99
+ if (!pkgName) {
100
+ throw new Error(`Unsupported platform: ${platform}-${arch}`);
101
+ }
102
+ const req = createRequire(import.meta.url);
103
+ const mainEntry = req.resolve(pkgName);
104
+ let dir = path.dirname(mainEntry);
105
+ while (dir !== path.dirname(dir)) {
106
+ const nativeDir = path.join(dir, "native");
107
+ if (existsSync(nativeDir)) {
108
+ return nativeDir;
109
+ }
110
+ dir = path.dirname(dir);
111
+ }
112
+ throw new Error(
113
+ `Could not find native directory for ${pkgName}. Ensure embedded-postgres is installed correctly.`
114
+ );
115
+ }
116
+ function getCacheDir() {
117
+ const xdgCache = process.env.XDG_CACHE_HOME;
118
+ const base = xdgCache || path.join(os.homedir(), ".cache");
119
+ return path.join(base, "postgres-memory-server");
120
+ }
121
+ async function installParadeDBExtension(nativeDir, paradedbVersion, pgMajorVersion) {
122
+ const libDir = path.join(nativeDir, "lib", "postgresql");
123
+ const extDir = path.join(nativeDir, "share", "postgresql", "extension");
124
+ const soName = os.platform() === "darwin" && parseInt(pgMajorVersion, 10) >= 16 ? "pg_search.dylib" : "pg_search.so";
125
+ try {
126
+ await fs.access(path.join(libDir, soName));
127
+ await fs.access(path.join(extDir, "pg_search.control"));
128
+ return;
129
+ } catch {
130
+ }
131
+ const cacheDir = getCacheDir();
132
+ const platform = os.platform();
133
+ const arch = os.arch();
134
+ const cacheKey = `paradedb-${paradedbVersion}-pg${pgMajorVersion}-${platform}-${arch}`;
135
+ const cachedDir = path.join(cacheDir, cacheKey);
136
+ let cached = false;
137
+ try {
138
+ await fs.access(path.join(cachedDir, "lib", soName));
139
+ cached = true;
140
+ } catch {
141
+ }
142
+ if (!cached) {
143
+ const url = buildDownloadUrl(
144
+ paradedbVersion,
145
+ pgMajorVersion,
146
+ platform,
147
+ arch
148
+ );
149
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "paradedb-"));
150
+ try {
151
+ const filename = decodeURIComponent(url.split("/").pop());
152
+ const archivePath = path.join(tmpDir, filename);
153
+ await downloadFile(url, archivePath);
154
+ const extractedDir = path.join(tmpDir, "extracted");
155
+ await fs.mkdir(extractedDir, { recursive: true });
156
+ if (platform === "darwin") {
157
+ await extractPkg(archivePath, extractedDir);
158
+ } else {
159
+ await extractDeb(archivePath, extractedDir);
160
+ }
161
+ const cacheLibDir2 = path.join(cachedDir, "lib");
162
+ const cacheExtDir2 = path.join(cachedDir, "extension");
163
+ await fs.mkdir(cacheLibDir2, { recursive: true });
164
+ await fs.mkdir(cacheExtDir2, { recursive: true });
165
+ const soFiles = await findFiles(
166
+ extractedDir,
167
+ /pg_search\.(so|dylib)$/
168
+ );
169
+ for (const soFile of soFiles) {
170
+ await copyFileWithPermissions(soFile, path.join(cacheLibDir2, path.basename(soFile)));
171
+ }
172
+ const extFiles = await findFiles(
173
+ extractedDir,
174
+ /pg_search[^/]*(\.control|\.sql)$/
175
+ );
176
+ for (const extFile of extFiles) {
177
+ await copyFileWithPermissions(
178
+ extFile,
179
+ path.join(cacheExtDir2, path.basename(extFile))
180
+ );
181
+ }
182
+ } finally {
183
+ await fs.rm(tmpDir, { recursive: true, force: true });
184
+ }
185
+ }
186
+ await fs.mkdir(libDir, { recursive: true });
187
+ await fs.mkdir(extDir, { recursive: true });
188
+ const cacheLibDir = path.join(cachedDir, "lib");
189
+ const cacheExtDir = path.join(cachedDir, "extension");
190
+ for (const file of await fs.readdir(cacheLibDir)) {
191
+ await copyFileWithPermissions(path.join(cacheLibDir, file), path.join(libDir, file));
192
+ }
193
+ for (const file of await fs.readdir(cacheExtDir)) {
194
+ await copyFileWithPermissions(path.join(cacheExtDir, file), path.join(extDir, file));
195
+ }
196
+ }
197
+ function buildDownloadUrl(version, pgMajorVersion, platform, arch) {
198
+ const base = `https://github.com/paradedb/paradedb/releases/download/v${version}`;
199
+ if (platform === "darwin") {
200
+ if (arch !== "arm64") {
201
+ throw new Error(
202
+ "ParadeDB only provides macOS binaries for arm64 (Apple Silicon). Intel Macs are not supported."
203
+ );
204
+ }
205
+ const macosName = getMacOSCodename();
206
+ return `${base}/pg_search%40${pgMajorVersion}--${version}.arm64_${macosName}.pkg`;
207
+ }
208
+ if (platform === "linux") {
209
+ const debArch = arch === "arm64" ? "arm64" : "amd64";
210
+ return `${base}/postgresql-${pgMajorVersion}-pg-search_${version}-1PARADEDB-bookworm_${debArch}.deb`;
211
+ }
212
+ throw new Error(
213
+ `ParadeDB does not provide prebuilt binaries for ${platform}. Use the Docker-based preset instead.`
214
+ );
215
+ }
216
+ function getMacOSCodename() {
217
+ const release = os.release();
218
+ const majorVersion = parseInt(release.split(".")[0] ?? "0", 10);
219
+ if (majorVersion >= 24) return "sequoia";
220
+ if (majorVersion >= 23) return "sonoma";
221
+ throw new Error(
222
+ `ParadeDB requires macOS 14 (Sonoma) or later. Detected Darwin ${release}.`
223
+ );
224
+ }
225
+ async function downloadFile(url, destPath) {
226
+ const response = await fetch(url, { redirect: "follow" });
227
+ if (!response.ok) {
228
+ throw new Error(
229
+ `Failed to download ParadeDB extension from ${url}: ${response.status} ${response.statusText}`
230
+ );
231
+ }
232
+ const buffer = Buffer.from(await response.arrayBuffer());
233
+ await fs.writeFile(destPath, buffer);
234
+ }
235
+ async function extractDeb(debPath, extractDir) {
236
+ await execFile("ar", ["x", debPath], { cwd: extractDir });
237
+ const files = await fs.readdir(extractDir);
238
+ const dataTar = files.find((f) => f.startsWith("data.tar"));
239
+ if (!dataTar) {
240
+ throw new Error(
241
+ "No data.tar.* found in .deb archive. The ParadeDB package format may have changed."
242
+ );
243
+ }
244
+ const dataDir = path.join(extractDir, "data");
245
+ await fs.mkdir(dataDir, { recursive: true });
246
+ await execFile("tar", [
247
+ "xf",
248
+ path.join(extractDir, dataTar),
249
+ "-C",
250
+ dataDir
251
+ ]);
252
+ }
253
+ async function extractPkg(pkgPath, extractDir) {
254
+ const pkgDir = path.join(extractDir, "pkg");
255
+ await execFile("pkgutil", ["--expand-full", pkgPath, pkgDir]);
256
+ }
257
+ async function copyFileWithPermissions(src, dest) {
258
+ const content = await fs.readFile(src);
259
+ await fs.writeFile(dest, content, { mode: 493 });
260
+ }
261
+ async function findFiles(dir, pattern) {
262
+ const results = [];
263
+ async function walk(currentDir) {
264
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
265
+ for (const entry of entries) {
266
+ const fullPath = path.join(currentDir, entry.name);
267
+ if (entry.isDirectory()) {
268
+ await walk(fullPath);
269
+ } else if (pattern.test(entry.name)) {
270
+ results.push(fullPath);
271
+ }
272
+ }
273
+ }
274
+ await walk(dir);
275
+ return results;
276
+ }
277
+ async function installPgVectorExtension(nativeDir, pgMajorVersion) {
278
+ const libDir = path.join(nativeDir, "lib", "postgresql");
279
+ const extDir = path.join(nativeDir, "share", "postgresql", "extension");
280
+ const soName = os.platform() === "darwin" ? "vector.dylib" : "vector.so";
281
+ try {
282
+ await fs.access(path.join(libDir, soName));
283
+ await fs.access(path.join(extDir, "vector.control"));
284
+ return;
285
+ } catch {
286
+ }
287
+ const platform = os.platform();
288
+ const arch = os.arch();
289
+ const cacheDir = getCacheDir();
290
+ const cacheKey = `pgvector-pg${pgMajorVersion}-${platform}-${arch}`;
291
+ const cachedDir = path.join(cacheDir, cacheKey);
292
+ let cached = false;
293
+ try {
294
+ await fs.access(path.join(cachedDir, "lib", soName));
295
+ cached = true;
296
+ } catch {
297
+ }
298
+ if (!cached) {
299
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "pgvector-"));
300
+ try {
301
+ const formulaRes = await fetch(
302
+ "https://formulae.brew.sh/api/formula/pgvector.json"
303
+ );
304
+ if (!formulaRes.ok) {
305
+ throw new Error(
306
+ `Failed to fetch pgvector formula: ${formulaRes.status}`
307
+ );
308
+ }
309
+ const formula = await formulaRes.json();
310
+ const bottleTag = getHomebrewBottleTag(platform, arch);
311
+ const fileInfo = formula.bottle.stable.files[bottleTag];
312
+ if (!fileInfo) {
313
+ throw new Error(
314
+ `No pgvector Homebrew bottle for ${bottleTag}. Available: ${Object.keys(formula.bottle.stable.files).join(", ")}`
315
+ );
316
+ }
317
+ const tokenRes = await fetch(
318
+ "https://ghcr.io/token?scope=repository:homebrew/core/pgvector:pull"
319
+ );
320
+ if (!tokenRes.ok) {
321
+ throw new Error(`Failed to get GHCR token: ${tokenRes.status}`);
322
+ }
323
+ const { token } = await tokenRes.json();
324
+ const blobUrl = `https://ghcr.io/v2/homebrew/core/pgvector/blobs/sha256:${fileInfo.sha256}`;
325
+ const blobRes = await fetch(blobUrl, {
326
+ headers: { Authorization: `Bearer ${token}` },
327
+ redirect: "follow"
328
+ });
329
+ if (!blobRes.ok) {
330
+ throw new Error(
331
+ `Failed to download pgvector bottle: ${blobRes.status}`
332
+ );
333
+ }
334
+ const bottlePath = path.join(tmpDir, "pgvector.tar.gz");
335
+ const buffer = Buffer.from(await blobRes.arrayBuffer());
336
+ await fs.writeFile(bottlePath, buffer);
337
+ const extractDir = path.join(tmpDir, "extracted");
338
+ await fs.mkdir(extractDir, { recursive: true });
339
+ await execFile("tar", ["xzf", bottlePath, "-C", extractDir]);
340
+ const cacheLibDir2 = path.join(cachedDir, "lib");
341
+ const cacheExtDir2 = path.join(cachedDir, "extension");
342
+ await fs.mkdir(cacheLibDir2, { recursive: true });
343
+ await fs.mkdir(cacheExtDir2, { recursive: true });
344
+ const pgSubdir = `postgresql@${pgMajorVersion}`;
345
+ let soFiles = await findFiles(
346
+ extractDir,
347
+ new RegExp(`${pgSubdir}.*vector\\.(so|dylib)$`)
348
+ );
349
+ if (soFiles.length === 0) {
350
+ soFiles = await findFiles(extractDir, /vector\.(so|dylib)$/);
351
+ }
352
+ for (const f of soFiles) {
353
+ await copyFileWithPermissions(f, path.join(cacheLibDir2, path.basename(f)));
354
+ }
355
+ let extFiles = await findFiles(
356
+ extractDir,
357
+ new RegExp(`${pgSubdir}.*vector[^/]*(\\.control|\\.sql)$`)
358
+ );
359
+ if (extFiles.length === 0) {
360
+ extFiles = await findFiles(extractDir, /vector[^/]*(\.control|\.sql)$/);
361
+ }
362
+ for (const f of extFiles) {
363
+ await copyFileWithPermissions(f, path.join(cacheExtDir2, path.basename(f)));
364
+ }
365
+ } finally {
366
+ await fs.rm(tmpDir, { recursive: true, force: true });
367
+ }
368
+ }
369
+ await fs.mkdir(libDir, { recursive: true });
370
+ await fs.mkdir(extDir, { recursive: true });
371
+ const cacheLibDir = path.join(cachedDir, "lib");
372
+ const cacheExtDir = path.join(cachedDir, "extension");
373
+ for (const file of await fs.readdir(cacheLibDir)) {
374
+ await copyFileWithPermissions(path.join(cacheLibDir, file), path.join(libDir, file));
375
+ }
376
+ for (const file of await fs.readdir(cacheExtDir)) {
377
+ await copyFileWithPermissions(path.join(cacheExtDir, file), path.join(extDir, file));
378
+ }
379
+ }
380
+ function getHomebrewBottleTag(platform, arch) {
381
+ if (platform === "darwin") {
382
+ const release = os.release();
383
+ const major = parseInt(release.split(".")[0] ?? "0", 10);
384
+ const prefix = arch === "arm64" ? "arm64_" : "";
385
+ if (major >= 25) return `${prefix}tahoe`;
386
+ if (major >= 24) return `${prefix}sequoia`;
387
+ if (major >= 23) return `${prefix}sonoma`;
388
+ return `${prefix}ventura`;
389
+ }
390
+ if (platform === "linux") {
391
+ return arch === "arm64" ? "aarch64_linux" : "x86_64_linux";
392
+ }
393
+ throw new Error(`No Homebrew bottles available for ${platform}-${arch}`);
394
+ }
395
+ var ORPHAN_MIN_AGE_MS = 6e4;
396
+ async function sweepOrphanedDataDirs(minAgeMs = ORPHAN_MIN_AGE_MS) {
397
+ const tmpDir = os.tmpdir();
398
+ let entries;
399
+ try {
400
+ entries = await fs.readdir(tmpDir);
401
+ } catch {
402
+ return;
403
+ }
404
+ const cutoff = Date.now() - minAgeMs;
405
+ await Promise.all(
406
+ entries.filter((name) => name.startsWith("postgres-memory-server-")).map(async (name) => {
407
+ const fullPath = path.join(tmpDir, name);
408
+ let stat;
409
+ try {
410
+ stat = await fs.stat(fullPath);
411
+ if (!stat.isDirectory()) return;
412
+ } catch {
413
+ return;
414
+ }
415
+ const pidFile = path.join(fullPath, "postmaster.pid");
416
+ let pid = null;
417
+ let pidFileExists = false;
418
+ try {
419
+ const content = await fs.readFile(pidFile, "utf8");
420
+ pidFileExists = true;
421
+ const firstLine = content.split("\n")[0]?.trim();
422
+ const parsed = firstLine ? parseInt(firstLine, 10) : NaN;
423
+ if (!Number.isNaN(parsed) && parsed > 0) {
424
+ pid = parsed;
425
+ }
426
+ } catch {
427
+ }
428
+ if (pid !== null) {
429
+ try {
430
+ process.kill(pid, 0);
431
+ return;
432
+ } catch (err) {
433
+ const code = err.code;
434
+ if (code === "EPERM") {
435
+ return;
436
+ }
437
+ }
438
+ }
439
+ if (!pidFileExists && stat.mtimeMs > cutoff) {
440
+ return;
441
+ }
442
+ await fs.rm(fullPath, { recursive: true, force: true }).catch(() => {
443
+ });
444
+ })
445
+ );
446
+ }
447
+ function parseParadeDBVersion(version) {
448
+ const match = version.match(/^(\d+\.\d+\.\d+)(?:-pg(\d+))?$/);
449
+ if (!match || !match[1]) {
450
+ return { extVersion: version };
451
+ }
452
+ return {
453
+ extVersion: match[1],
454
+ pgVersion: match[2]
455
+ };
456
+ }
31
457
 
32
458
  // src/presets.ts
459
+ var DEFAULT_PARADEDB_EXT_VERSION = "0.22.5";
460
+ var DEFAULT_POSTGRES_VERSION = getPgMajorVersion();
461
+ var DEFAULT_PARADEDB_VERSION = `${DEFAULT_PARADEDB_EXT_VERSION}-pg${DEFAULT_POSTGRES_VERSION}`;
462
+ var DEFAULT_POSTGRES_IMAGE = `postgres:${DEFAULT_POSTGRES_VERSION}`;
463
+ var DEFAULT_PARADEDB_IMAGE = `paradedb:${DEFAULT_PARADEDB_VERSION}`;
33
464
  var POSTGRES_IMAGE_REPOSITORY = "postgres";
34
- var PARADEDB_IMAGE_REPOSITORY = "paradedb/paradedb";
35
- var DEFAULT_POSTGRES_VERSION = "17";
36
- var DEFAULT_PARADEDB_VERSION = "0.22.3-pg17";
37
- var DEFAULT_POSTGRES_IMAGE = getImageForVersion(
38
- "postgres",
39
- DEFAULT_POSTGRES_VERSION
40
- );
41
- var DEFAULT_PARADEDB_IMAGE = getImageForVersion(
42
- "paradedb",
43
- DEFAULT_PARADEDB_VERSION
44
- );
465
+ var PARADEDB_IMAGE_REPOSITORY = "paradedb";
45
466
  var DEFAULT_DATABASE = "testdb";
46
467
  var DEFAULT_USERNAME = "testuser";
47
468
  var DEFAULT_PASSWORD = "testpassword";
@@ -49,7 +470,7 @@ var PARADEDB_DEFAULT_EXTENSIONS = ["pg_search", "vector"];
49
470
  function normalizeOptions(options = {}) {
50
471
  const preset = options.preset ?? "postgres";
51
472
  const version = options.version;
52
- const image = options.image ?? getImage(preset, version);
473
+ const image = getImageLabel(preset, version);
53
474
  const database = options.database ?? DEFAULT_DATABASE;
54
475
  const username = options.username ?? DEFAULT_USERNAME;
55
476
  const password = options.password ?? DEFAULT_PASSWORD;
@@ -73,8 +494,11 @@ function getImageForVersion(preset, version) {
73
494
  function getDefaultImage(preset) {
74
495
  return preset === "paradedb" ? DEFAULT_PARADEDB_IMAGE : DEFAULT_POSTGRES_IMAGE;
75
496
  }
76
- function getImage(preset, version) {
77
- return version ? getImageForVersion(preset, version) : getDefaultImage(preset);
497
+ function getImageLabel(preset, version) {
498
+ if (version) {
499
+ return getImageForVersion(preset, version);
500
+ }
501
+ return getDefaultImage(preset);
78
502
  }
79
503
  function getDefaultExtensions(preset) {
80
504
  return preset === "paradedb" ? [...PARADEDB_DEFAULT_EXTENSIONS] : [];
@@ -85,6 +509,21 @@ function buildInitStatements(options) {
85
509
  );
86
510
  return [...extensionStatements, ...options.initSql];
87
511
  }
512
+ function resolveParadeDBVersion(version) {
513
+ if (!version) {
514
+ return DEFAULT_PARADEDB_EXT_VERSION;
515
+ }
516
+ const parsed = parseParadeDBVersion(version);
517
+ if (parsed.pgVersion) {
518
+ const installedPg = DEFAULT_POSTGRES_VERSION;
519
+ if (parsed.pgVersion !== installedPg) {
520
+ throw new Error(
521
+ `ParadeDB version "${version}" targets PostgreSQL ${parsed.pgVersion}, but embedded-postgres provides PostgreSQL ${installedPg}. Install embedded-postgres@${parsed.pgVersion}.x to match, or use version "${parsed.extVersion}" without the -pg suffix.`
522
+ );
523
+ }
524
+ }
525
+ return parsed.extVersion;
526
+ }
88
527
  function quoteIdentifier(name) {
89
528
  if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
90
529
  return name;
@@ -93,23 +532,117 @@ function quoteIdentifier(name) {
93
532
  }
94
533
 
95
534
  // src/PostgresMemoryServer.ts
535
+ var liveInstances = /* @__PURE__ */ new Set();
536
+ var exitHandlersRegistered = false;
537
+ var orphanSweepDone = false;
538
+ function registerExitHandlers() {
539
+ if (exitHandlersRegistered) return;
540
+ exitHandlersRegistered = true;
541
+ const cleanup = () => {
542
+ for (const instance of liveInstances) {
543
+ try {
544
+ instance._cleanupSync();
545
+ } catch {
546
+ }
547
+ }
548
+ };
549
+ process2.once("exit", cleanup);
550
+ const signalCleanup = (signal) => {
551
+ cleanup();
552
+ process2.removeListener(signal, signalCleanup);
553
+ process2.kill(process2.pid, signal);
554
+ };
555
+ process2.on("SIGINT", signalCleanup);
556
+ process2.on("SIGTERM", signalCleanup);
557
+ process2.on("SIGHUP", signalCleanup);
558
+ }
96
559
  var PostgresMemoryServer = class _PostgresMemoryServer {
97
- constructor(container, options) {
98
- this.container = container;
560
+ constructor(pg, port, dataDir, options) {
561
+ this.pg = pg;
562
+ this.port = port;
563
+ this.dataDir = dataDir;
99
564
  this.options = options;
100
565
  this.snapshotSupported = options.database !== "postgres";
101
566
  }
102
567
  stopped = false;
103
568
  snapshotSupported;
569
+ hasSnapshot = false;
104
570
  static async create(options = {}) {
571
+ if (!orphanSweepDone) {
572
+ orphanSweepDone = true;
573
+ await sweepOrphanedDataDirs().catch(() => {
574
+ });
575
+ }
105
576
  const normalized = normalizeOptions(options);
106
- const container = await new PostgreSqlContainer(normalized.image).withDatabase(normalized.database).withUsername(normalized.username).withPassword(normalized.password).start();
107
- const server = new _PostgresMemoryServer(container, normalized);
108
- const initStatements = buildInitStatements(normalized);
109
- if (initStatements.length > 0) {
110
- await server.runSql(initStatements);
577
+ const port = await getFreePort();
578
+ const dataDir = await fs2.mkdtemp(
579
+ path2.join(os2.tmpdir(), "postgres-memory-server-")
580
+ );
581
+ let pg;
582
+ try {
583
+ const postgresFlags = [];
584
+ if (normalized.preset === "paradedb") {
585
+ const nativeDir = getNativeDir();
586
+ const extVersion = resolveParadeDBVersion(normalized.version);
587
+ const pgMajor = DEFAULT_POSTGRES_VERSION;
588
+ try {
589
+ await installParadeDBExtension(nativeDir, extVersion, pgMajor);
590
+ } catch (error) {
591
+ throw new ExtensionInstallError(
592
+ "pg_search",
593
+ error instanceof Error ? error : new Error(String(error))
594
+ );
595
+ }
596
+ if (normalized.extensions.includes("vector")) {
597
+ try {
598
+ await installPgVectorExtension(nativeDir, pgMajor);
599
+ } catch (error) {
600
+ throw new ExtensionInstallError(
601
+ "vector",
602
+ error instanceof Error ? error : new Error(String(error))
603
+ );
604
+ }
605
+ }
606
+ if (normalized.extensions.includes("pg_search") || normalized.extensions.length === 0) {
607
+ postgresFlags.push("-c", "shared_preload_libraries=pg_search");
608
+ }
609
+ }
610
+ pg = new EmbeddedPostgres({
611
+ databaseDir: dataDir,
612
+ port,
613
+ user: normalized.username,
614
+ password: normalized.password,
615
+ persistent: false,
616
+ postgresFlags,
617
+ onLog: () => {
618
+ },
619
+ onError: () => {
620
+ }
621
+ });
622
+ await pg.initialise();
623
+ await pg.start();
624
+ if (normalized.database !== "postgres") {
625
+ await pg.createDatabase(normalized.database);
626
+ }
627
+ const server = new _PostgresMemoryServer(pg, port, dataDir, normalized);
628
+ liveInstances.add(server);
629
+ registerExitHandlers();
630
+ const initStatements = buildInitStatements(normalized);
631
+ if (initStatements.length > 0) {
632
+ await server.runSql(initStatements);
633
+ }
634
+ return server;
635
+ } catch (error) {
636
+ if (pg) {
637
+ try {
638
+ await pg.stop();
639
+ } catch {
640
+ }
641
+ }
642
+ await fs2.rm(dataDir, { recursive: true, force: true }).catch(() => {
643
+ });
644
+ throw error;
111
645
  }
112
- return server;
113
646
  }
114
647
  static createPostgres(options = {}) {
115
648
  return _PostgresMemoryServer.create({ ...options, preset: "postgres" });
@@ -119,15 +652,15 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
119
652
  }
120
653
  getUri() {
121
654
  this.ensureRunning();
122
- return this.container.getConnectionUri();
655
+ return `postgres://${this.options.username}:${this.options.password}@localhost:${this.port}/${this.options.database}`;
123
656
  }
124
657
  getHost() {
125
658
  this.ensureRunning();
126
- return this.container.getHost();
659
+ return "localhost";
127
660
  }
128
661
  getPort() {
129
662
  this.ensureRunning();
130
- return this.container.getPort();
663
+ return this.port;
131
664
  }
132
665
  getDatabase() {
133
666
  return this.options.database;
@@ -141,6 +674,14 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
141
674
  getImage() {
142
675
  return this.options.image;
143
676
  }
677
+ /**
678
+ * Returns the absolute path to the temporary PostgreSQL data directory
679
+ * for this instance. Useful for debugging or backing up state. The
680
+ * directory is automatically removed by `stop()`.
681
+ */
682
+ getDataDir() {
683
+ return this.dataDir;
684
+ }
144
685
  getConnectionOptions() {
145
686
  return {
146
687
  host: this.getHost(),
@@ -182,35 +723,115 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
182
723
  });
183
724
  }
184
725
  async runSqlFile(filePath) {
185
- const sql = await fs.readFile(filePath, "utf8");
726
+ const sql = await fs2.readFile(filePath, "utf8");
186
727
  await this.runSql(sql);
187
728
  }
188
729
  async runMigrationsDir(dirPath) {
189
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
730
+ const entries = await fs2.readdir(dirPath, { withFileTypes: true });
190
731
  const files = entries.filter(
191
732
  (entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".sql")
192
733
  ).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
193
734
  for (const file of files) {
194
- await this.runSqlFile(path.join(dirPath, file));
735
+ await this.runSqlFile(path2.join(dirPath, file));
195
736
  }
196
737
  return files;
197
738
  }
739
+ /**
740
+ * Create a snapshot of the current database state.
741
+ * Uses PostgreSQL template databases for fast, native snapshots.
742
+ */
198
743
  async snapshot() {
199
744
  this.ensureRunning();
200
745
  this.ensureSnapshotSupported();
201
- await this.container.snapshot();
746
+ const snapshotDb = `${this.options.database}_snapshot`;
747
+ await this.withAdminClient(async (client) => {
748
+ await client.query(
749
+ `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid != pg_backend_pid()`,
750
+ [this.options.database]
751
+ );
752
+ if (this.hasSnapshot) {
753
+ await client.query(`DROP DATABASE IF EXISTS "${snapshotDb}"`);
754
+ }
755
+ await client.query(
756
+ `CREATE DATABASE "${snapshotDb}" TEMPLATE "${this.options.database}"`
757
+ );
758
+ });
759
+ this.hasSnapshot = true;
202
760
  }
761
+ /**
762
+ * Restore the database to the last snapshot.
763
+ * Drops and recreates the database from the snapshot template.
764
+ */
203
765
  async restore() {
204
766
  this.ensureRunning();
205
767
  this.ensureSnapshotSupported();
206
- await this.container.restoreSnapshot();
768
+ if (!this.hasSnapshot) {
769
+ throw new Error(
770
+ "No snapshot exists. Call snapshot() before calling restore()."
771
+ );
772
+ }
773
+ const snapshotDb = `${this.options.database}_snapshot`;
774
+ await this.withAdminClient(async (client) => {
775
+ await client.query(
776
+ `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid != pg_backend_pid()`,
777
+ [this.options.database]
778
+ );
779
+ await client.query(`DROP DATABASE "${this.options.database}"`);
780
+ await client.query(
781
+ `CREATE DATABASE "${this.options.database}" TEMPLATE "${snapshotDb}"`
782
+ );
783
+ });
207
784
  }
208
785
  async stop() {
209
786
  if (this.stopped) {
210
787
  return;
211
788
  }
212
789
  this.stopped = true;
213
- await this.container.stop();
790
+ liveInstances.delete(this);
791
+ try {
792
+ await this.pg.stop();
793
+ } catch {
794
+ }
795
+ await fs2.rm(this.dataDir, { recursive: true, force: true }).catch(() => {
796
+ });
797
+ }
798
+ /**
799
+ * Synchronous cleanup for use in process exit handlers. Cannot await,
800
+ * so we just remove the data directory and let the OS reap the postgres
801
+ * child process. embedded-postgres registers its own exit hook to kill
802
+ * the process; this method is a backup for the data directory only.
803
+ *
804
+ * @internal
805
+ */
806
+ _cleanupSync() {
807
+ if (this.stopped) {
808
+ return;
809
+ }
810
+ this.stopped = true;
811
+ liveInstances.delete(this);
812
+ try {
813
+ rmSync(this.dataDir, { recursive: true, force: true });
814
+ } catch {
815
+ }
816
+ }
817
+ /**
818
+ * Connect to the "postgres" system database for admin operations
819
+ * (snapshot, restore, etc.).
820
+ */
821
+ async withAdminClient(callback) {
822
+ const client = new Client({
823
+ host: "localhost",
824
+ port: this.port,
825
+ database: "postgres",
826
+ user: this.options.username,
827
+ password: this.options.password
828
+ });
829
+ await client.connect();
830
+ try {
831
+ return await callback(client);
832
+ } finally {
833
+ await client.end();
834
+ }
214
835
  }
215
836
  ensureRunning() {
216
837
  if (this.stopped) {
@@ -226,7 +847,7 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
226
847
 
227
848
  // src/cli.ts
228
849
  async function main() {
229
- const { options, initFiles, json } = parseArgs(process.argv.slice(2));
850
+ const { options, initFiles, json } = parseArgs(process3.argv.slice(2));
230
851
  const server = await PostgresMemoryServer.create(options);
231
852
  try {
232
853
  for (const file of initFiles) {
@@ -242,45 +863,46 @@ async function main() {
242
863
  image: server.getImage()
243
864
  };
244
865
  if (json) {
245
- process.stdout.write(`${JSON.stringify(payload, null, 2)}
866
+ process3.stdout.write(`${JSON.stringify(payload, null, 2)}
246
867
  `);
247
868
  } else {
248
- process.stdout.write(`POSTGRES_MEMORY_SERVER_URI=${payload.uri}
869
+ process3.stdout.write(`POSTGRES_MEMORY_SERVER_URI=${payload.uri}
249
870
  `);
250
- process.stdout.write(`POSTGRES_MEMORY_SERVER_HOST=${payload.host}
871
+ process3.stdout.write(`POSTGRES_MEMORY_SERVER_HOST=${payload.host}
251
872
  `);
252
- process.stdout.write(`POSTGRES_MEMORY_SERVER_PORT=${payload.port}
873
+ process3.stdout.write(`POSTGRES_MEMORY_SERVER_PORT=${payload.port}
253
874
  `);
254
- process.stdout.write(
875
+ process3.stdout.write(
255
876
  `POSTGRES_MEMORY_SERVER_DATABASE=${payload.database}
256
877
  `
257
878
  );
258
- process.stdout.write(
879
+ process3.stdout.write(
259
880
  `POSTGRES_MEMORY_SERVER_USERNAME=${payload.username}
260
881
  `
261
882
  );
262
- process.stdout.write(
883
+ process3.stdout.write(
263
884
  `POSTGRES_MEMORY_SERVER_PASSWORD=${payload.password}
264
885
  `
265
886
  );
266
- process.stdout.write(`POSTGRES_MEMORY_SERVER_IMAGE=${payload.image}
887
+ process3.stdout.write(`POSTGRES_MEMORY_SERVER_IMAGE=${payload.image}
267
888
  `);
268
- process.stdout.write("\nPress Ctrl+C to stop the container.\n");
889
+ process3.stdout.write("\nPress Ctrl+C to stop the server.\n");
269
890
  }
270
891
  const stop = async () => {
271
892
  await server.stop();
272
- process.exit(0);
893
+ process3.exit(0);
273
894
  };
274
- process.on("SIGINT", () => {
895
+ process3.on("SIGINT", () => {
275
896
  void stop();
276
897
  });
277
- process.on("SIGTERM", () => {
898
+ process3.on("SIGTERM", () => {
278
899
  void stop();
279
900
  });
280
901
  await new Promise(() => {
281
902
  });
282
903
  } catch (error) {
283
- await server.stop();
904
+ await server.stop().catch(() => {
905
+ });
284
906
  throw error;
285
907
  }
286
908
  }
@@ -302,7 +924,7 @@ function parseArgs(argv) {
302
924
  break;
303
925
  }
304
926
  case "--image": {
305
- options.image = readValue(argv, ++index, arg);
927
+ readValue(argv, ++index, arg);
306
928
  break;
307
929
  }
308
930
  case "--version": {
@@ -335,7 +957,7 @@ function parseArgs(argv) {
335
957
  }
336
958
  case "--help": {
337
959
  printHelp();
338
- process.exit(0);
960
+ process3.exit(0);
339
961
  }
340
962
  default: {
341
963
  throw new Error(`Unknown argument: ${arg}`);
@@ -355,36 +977,36 @@ function readValue(argv, index, flag) {
355
977
  return value;
356
978
  }
357
979
  function printHelp() {
358
- process.stdout.write(`postgres-memory-server
980
+ process3.stdout.write(`postgres-memory-server
359
981
 
360
982
  `);
361
- process.stdout.write(`Options:
983
+ process3.stdout.write(`Options:
362
984
  `);
363
- process.stdout.write(` --preset postgres|paradedb
985
+ process3.stdout.write(` --preset postgres|paradedb
364
986
  `);
365
- process.stdout.write(` --version <tag>
987
+ process3.stdout.write(` --version <tag>
366
988
  `);
367
- process.stdout.write(` --image <image>
989
+ process3.stdout.write(` --image <image> (deprecated, ignored)
368
990
  `);
369
- process.stdout.write(` --database <name>
991
+ process3.stdout.write(` --database <name>
370
992
  `);
371
- process.stdout.write(` --username <name>
993
+ process3.stdout.write(` --username <name>
372
994
  `);
373
- process.stdout.write(` --password <password>
995
+ process3.stdout.write(` --password <password>
374
996
  `);
375
- process.stdout.write(` --extension <name> repeatable
997
+ process3.stdout.write(` --extension <name> repeatable
376
998
  `);
377
- process.stdout.write(` --init-file <path> repeatable
999
+ process3.stdout.write(` --init-file <path> repeatable
378
1000
  `);
379
- process.stdout.write(` --json
1001
+ process3.stdout.write(` --json
380
1002
  `);
381
- process.stdout.write(` --help
1003
+ process3.stdout.write(` --help
382
1004
  `);
383
1005
  }
384
1006
  void main().catch((error) => {
385
1007
  const message = error instanceof Error ? error.stack ?? error.message : String(error);
386
- process.stderr.write(`${message}
1008
+ process3.stderr.write(`${message}
387
1009
  `);
388
- process.exit(1);
1010
+ process3.exit(1);
389
1011
  });
390
1012
  //# sourceMappingURL=cli.js.map