postgres-memory-server 0.1.0 → 0.2.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/dist/cli.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import process from "process";
4
+ import process2 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 } from "fs";
8
+ import os2 from "os";
9
+ import path2 from "path";
10
+ import EmbeddedPostgres from "embedded-postgres";
10
11
  import { Client } from "pg";
11
12
 
12
13
  // src/errors.ts
@@ -28,20 +29,386 @@ var ServerStoppedError = class extends PostgresMemoryServerError {
28
29
  super("The PostgresMemoryServer has already been stopped.");
29
30
  }
30
31
  };
32
+ var ExtensionInstallError = class extends PostgresMemoryServerError {
33
+ constructor(extensionName, cause) {
34
+ super(
35
+ `Failed to install the "${extensionName}" extension. ${cause?.message ?? ""}`.trim(),
36
+ cause ? { cause } : void 0
37
+ );
38
+ }
39
+ };
40
+
41
+ // src/native.ts
42
+ import { promises as fs, readFileSync, existsSync } from "fs";
43
+ import { createRequire } from "module";
44
+ import { execFile as execFileCb } from "child_process";
45
+ import { promisify } from "util";
46
+ import net from "net";
47
+ import os from "os";
48
+ import path from "path";
49
+ var execFile = promisify(execFileCb);
50
+ async function getFreePort() {
51
+ return new Promise((resolve, reject) => {
52
+ const server = net.createServer();
53
+ server.listen(0, () => {
54
+ const { port } = server.address();
55
+ server.close(() => resolve(port));
56
+ });
57
+ server.on("error", reject);
58
+ });
59
+ }
60
+ function getPgMajorVersion() {
61
+ const req = createRequire(import.meta.url);
62
+ const mainEntry = req.resolve("embedded-postgres");
63
+ let dir = path.dirname(mainEntry);
64
+ while (dir !== path.dirname(dir)) {
65
+ const candidate = path.join(dir, "package.json");
66
+ try {
67
+ const content = readFileSync(candidate, "utf8");
68
+ const pkg = JSON.parse(content);
69
+ if (pkg.name === "embedded-postgres" && pkg.version) {
70
+ return pkg.version.split(".")[0];
71
+ }
72
+ } catch {
73
+ }
74
+ dir = path.dirname(dir);
75
+ }
76
+ throw new Error(
77
+ "Could not determine embedded-postgres version. Ensure embedded-postgres is installed."
78
+ );
79
+ }
80
+ function getNativeDir() {
81
+ const platform = os.platform();
82
+ const arch = os.arch();
83
+ const platformPkgNames = {
84
+ darwin: {
85
+ arm64: "@embedded-postgres/darwin-arm64",
86
+ x64: "@embedded-postgres/darwin-x64"
87
+ },
88
+ linux: {
89
+ x64: "@embedded-postgres/linux-x64",
90
+ arm64: "@embedded-postgres/linux-arm64"
91
+ },
92
+ win32: {
93
+ x64: "@embedded-postgres/windows-x64"
94
+ }
95
+ };
96
+ const pkgName = platformPkgNames[platform]?.[arch];
97
+ if (!pkgName) {
98
+ throw new Error(`Unsupported platform: ${platform}-${arch}`);
99
+ }
100
+ const req = createRequire(import.meta.url);
101
+ const mainEntry = req.resolve(pkgName);
102
+ let dir = path.dirname(mainEntry);
103
+ while (dir !== path.dirname(dir)) {
104
+ const nativeDir = path.join(dir, "native");
105
+ if (existsSync(nativeDir)) {
106
+ return nativeDir;
107
+ }
108
+ dir = path.dirname(dir);
109
+ }
110
+ throw new Error(
111
+ `Could not find native directory for ${pkgName}. Ensure embedded-postgres is installed correctly.`
112
+ );
113
+ }
114
+ function getCacheDir() {
115
+ const xdgCache = process.env.XDG_CACHE_HOME;
116
+ const base = xdgCache || path.join(os.homedir(), ".cache");
117
+ return path.join(base, "postgres-memory-server");
118
+ }
119
+ async function installParadeDBExtension(nativeDir, paradedbVersion, pgMajorVersion) {
120
+ const libDir = path.join(nativeDir, "lib", "postgresql");
121
+ const extDir = path.join(nativeDir, "share", "postgresql", "extension");
122
+ const soName = os.platform() === "darwin" && parseInt(pgMajorVersion, 10) >= 16 ? "pg_search.dylib" : "pg_search.so";
123
+ try {
124
+ await fs.access(path.join(libDir, soName));
125
+ await fs.access(path.join(extDir, "pg_search.control"));
126
+ return;
127
+ } catch {
128
+ }
129
+ const cacheDir = getCacheDir();
130
+ const platform = os.platform();
131
+ const arch = os.arch();
132
+ const cacheKey = `paradedb-${paradedbVersion}-pg${pgMajorVersion}-${platform}-${arch}`;
133
+ const cachedDir = path.join(cacheDir, cacheKey);
134
+ let cached = false;
135
+ try {
136
+ await fs.access(path.join(cachedDir, "lib", soName));
137
+ cached = true;
138
+ } catch {
139
+ }
140
+ if (!cached) {
141
+ const url = buildDownloadUrl(
142
+ paradedbVersion,
143
+ pgMajorVersion,
144
+ platform,
145
+ arch
146
+ );
147
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "paradedb-"));
148
+ try {
149
+ const filename = decodeURIComponent(url.split("/").pop());
150
+ const archivePath = path.join(tmpDir, filename);
151
+ await downloadFile(url, archivePath);
152
+ const extractedDir = path.join(tmpDir, "extracted");
153
+ await fs.mkdir(extractedDir, { recursive: true });
154
+ if (platform === "darwin") {
155
+ await extractPkg(archivePath, extractedDir);
156
+ } else {
157
+ await extractDeb(archivePath, extractedDir);
158
+ }
159
+ const cacheLibDir2 = path.join(cachedDir, "lib");
160
+ const cacheExtDir2 = path.join(cachedDir, "extension");
161
+ await fs.mkdir(cacheLibDir2, { recursive: true });
162
+ await fs.mkdir(cacheExtDir2, { recursive: true });
163
+ const soFiles = await findFiles(
164
+ extractedDir,
165
+ /pg_search\.(so|dylib)$/
166
+ );
167
+ for (const soFile of soFiles) {
168
+ await copyFileWithPermissions(soFile, path.join(cacheLibDir2, path.basename(soFile)));
169
+ }
170
+ const extFiles = await findFiles(
171
+ extractedDir,
172
+ /pg_search[^/]*(\.control|\.sql)$/
173
+ );
174
+ for (const extFile of extFiles) {
175
+ await copyFileWithPermissions(
176
+ extFile,
177
+ path.join(cacheExtDir2, path.basename(extFile))
178
+ );
179
+ }
180
+ } finally {
181
+ await fs.rm(tmpDir, { recursive: true, force: true });
182
+ }
183
+ }
184
+ await fs.mkdir(libDir, { recursive: true });
185
+ await fs.mkdir(extDir, { recursive: true });
186
+ const cacheLibDir = path.join(cachedDir, "lib");
187
+ const cacheExtDir = path.join(cachedDir, "extension");
188
+ for (const file of await fs.readdir(cacheLibDir)) {
189
+ await copyFileWithPermissions(path.join(cacheLibDir, file), path.join(libDir, file));
190
+ }
191
+ for (const file of await fs.readdir(cacheExtDir)) {
192
+ await copyFileWithPermissions(path.join(cacheExtDir, file), path.join(extDir, file));
193
+ }
194
+ }
195
+ function buildDownloadUrl(version, pgMajorVersion, platform, arch) {
196
+ const base = `https://github.com/paradedb/paradedb/releases/download/v${version}`;
197
+ if (platform === "darwin") {
198
+ if (arch !== "arm64") {
199
+ throw new Error(
200
+ "ParadeDB only provides macOS binaries for arm64 (Apple Silicon). Intel Macs are not supported."
201
+ );
202
+ }
203
+ const macosName = getMacOSCodename();
204
+ return `${base}/pg_search%40${pgMajorVersion}--${version}.arm64_${macosName}.pkg`;
205
+ }
206
+ if (platform === "linux") {
207
+ const debArch = arch === "arm64" ? "arm64" : "amd64";
208
+ return `${base}/postgresql-${pgMajorVersion}-pg-search_${version}-1PARADEDB-bookworm_${debArch}.deb`;
209
+ }
210
+ throw new Error(
211
+ `ParadeDB does not provide prebuilt binaries for ${platform}. Use the Docker-based preset instead.`
212
+ );
213
+ }
214
+ function getMacOSCodename() {
215
+ const release = os.release();
216
+ const majorVersion = parseInt(release.split(".")[0], 10);
217
+ if (majorVersion >= 24) return "sequoia";
218
+ if (majorVersion >= 23) return "sonoma";
219
+ throw new Error(
220
+ `ParadeDB requires macOS 14 (Sonoma) or later. Detected Darwin ${release}.`
221
+ );
222
+ }
223
+ async function downloadFile(url, destPath) {
224
+ const response = await fetch(url, { redirect: "follow" });
225
+ if (!response.ok) {
226
+ throw new Error(
227
+ `Failed to download ParadeDB extension from ${url}: ${response.status} ${response.statusText}`
228
+ );
229
+ }
230
+ const buffer = Buffer.from(await response.arrayBuffer());
231
+ await fs.writeFile(destPath, buffer);
232
+ }
233
+ async function extractDeb(debPath, extractDir) {
234
+ await execFile("ar", ["x", debPath], { cwd: extractDir });
235
+ const files = await fs.readdir(extractDir);
236
+ const dataTar = files.find((f) => f.startsWith("data.tar"));
237
+ if (!dataTar) {
238
+ throw new Error(
239
+ "No data.tar.* found in .deb archive. The ParadeDB package format may have changed."
240
+ );
241
+ }
242
+ const dataDir = path.join(extractDir, "data");
243
+ await fs.mkdir(dataDir, { recursive: true });
244
+ await execFile("tar", [
245
+ "xf",
246
+ path.join(extractDir, dataTar),
247
+ "-C",
248
+ dataDir
249
+ ]);
250
+ }
251
+ async function extractPkg(pkgPath, extractDir) {
252
+ const pkgDir = path.join(extractDir, "pkg");
253
+ await execFile("pkgutil", ["--expand-full", pkgPath, pkgDir]);
254
+ }
255
+ async function copyFileWithPermissions(src, dest) {
256
+ const content = await fs.readFile(src);
257
+ await fs.writeFile(dest, content, { mode: 493 });
258
+ }
259
+ async function findFiles(dir, pattern) {
260
+ const results = [];
261
+ async function walk(currentDir) {
262
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
263
+ for (const entry of entries) {
264
+ const fullPath = path.join(currentDir, entry.name);
265
+ if (entry.isDirectory()) {
266
+ await walk(fullPath);
267
+ } else if (pattern.test(entry.name)) {
268
+ results.push(fullPath);
269
+ }
270
+ }
271
+ }
272
+ await walk(dir);
273
+ return results;
274
+ }
275
+ async function installPgVectorExtension(nativeDir, pgMajorVersion) {
276
+ const libDir = path.join(nativeDir, "lib", "postgresql");
277
+ const extDir = path.join(nativeDir, "share", "postgresql", "extension");
278
+ const soName = os.platform() === "darwin" ? "vector.dylib" : "vector.so";
279
+ try {
280
+ await fs.access(path.join(libDir, soName));
281
+ await fs.access(path.join(extDir, "vector.control"));
282
+ return;
283
+ } catch {
284
+ }
285
+ const platform = os.platform();
286
+ const arch = os.arch();
287
+ const cacheDir = getCacheDir();
288
+ const cacheKey = `pgvector-pg${pgMajorVersion}-${platform}-${arch}`;
289
+ const cachedDir = path.join(cacheDir, cacheKey);
290
+ let cached = false;
291
+ try {
292
+ await fs.access(path.join(cachedDir, "lib", soName));
293
+ cached = true;
294
+ } catch {
295
+ }
296
+ if (!cached) {
297
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "pgvector-"));
298
+ try {
299
+ const formulaRes = await fetch(
300
+ "https://formulae.brew.sh/api/formula/pgvector.json"
301
+ );
302
+ if (!formulaRes.ok) {
303
+ throw new Error(
304
+ `Failed to fetch pgvector formula: ${formulaRes.status}`
305
+ );
306
+ }
307
+ const formula = await formulaRes.json();
308
+ const bottleTag = getHomebrewBottleTag(platform, arch);
309
+ const fileInfo = formula.bottle.stable.files[bottleTag];
310
+ if (!fileInfo) {
311
+ throw new Error(
312
+ `No pgvector Homebrew bottle for ${bottleTag}. Available: ${Object.keys(formula.bottle.stable.files).join(", ")}`
313
+ );
314
+ }
315
+ const tokenRes = await fetch(
316
+ "https://ghcr.io/token?scope=repository:homebrew/core/pgvector:pull"
317
+ );
318
+ if (!tokenRes.ok) {
319
+ throw new Error(`Failed to get GHCR token: ${tokenRes.status}`);
320
+ }
321
+ const { token } = await tokenRes.json();
322
+ const blobUrl = `https://ghcr.io/v2/homebrew/core/pgvector/blobs/sha256:${fileInfo.sha256}`;
323
+ const blobRes = await fetch(blobUrl, {
324
+ headers: { Authorization: `Bearer ${token}` },
325
+ redirect: "follow"
326
+ });
327
+ if (!blobRes.ok) {
328
+ throw new Error(
329
+ `Failed to download pgvector bottle: ${blobRes.status}`
330
+ );
331
+ }
332
+ const bottlePath = path.join(tmpDir, "pgvector.tar.gz");
333
+ const buffer = Buffer.from(await blobRes.arrayBuffer());
334
+ await fs.writeFile(bottlePath, buffer);
335
+ const extractDir = path.join(tmpDir, "extracted");
336
+ await fs.mkdir(extractDir, { recursive: true });
337
+ await execFile("tar", ["xzf", bottlePath, "-C", extractDir]);
338
+ const cacheLibDir2 = path.join(cachedDir, "lib");
339
+ const cacheExtDir2 = path.join(cachedDir, "extension");
340
+ await fs.mkdir(cacheLibDir2, { recursive: true });
341
+ await fs.mkdir(cacheExtDir2, { recursive: true });
342
+ const pgSubdir = `postgresql@${pgMajorVersion}`;
343
+ let soFiles = await findFiles(
344
+ extractDir,
345
+ new RegExp(`${pgSubdir}.*vector\\.(so|dylib)$`)
346
+ );
347
+ if (soFiles.length === 0) {
348
+ soFiles = await findFiles(extractDir, /vector\.(so|dylib)$/);
349
+ }
350
+ for (const f of soFiles) {
351
+ await copyFileWithPermissions(f, path.join(cacheLibDir2, path.basename(f)));
352
+ }
353
+ let extFiles = await findFiles(
354
+ extractDir,
355
+ new RegExp(`${pgSubdir}.*vector[^/]*(\\.control|\\.sql)$`)
356
+ );
357
+ if (extFiles.length === 0) {
358
+ extFiles = await findFiles(extractDir, /vector[^/]*(\.control|\.sql)$/);
359
+ }
360
+ for (const f of extFiles) {
361
+ await copyFileWithPermissions(f, path.join(cacheExtDir2, path.basename(f)));
362
+ }
363
+ } finally {
364
+ await fs.rm(tmpDir, { recursive: true, force: true });
365
+ }
366
+ }
367
+ await fs.mkdir(libDir, { recursive: true });
368
+ await fs.mkdir(extDir, { recursive: true });
369
+ const cacheLibDir = path.join(cachedDir, "lib");
370
+ const cacheExtDir = path.join(cachedDir, "extension");
371
+ for (const file of await fs.readdir(cacheLibDir)) {
372
+ await copyFileWithPermissions(path.join(cacheLibDir, file), path.join(libDir, file));
373
+ }
374
+ for (const file of await fs.readdir(cacheExtDir)) {
375
+ await copyFileWithPermissions(path.join(cacheExtDir, file), path.join(extDir, file));
376
+ }
377
+ }
378
+ function getHomebrewBottleTag(platform, arch) {
379
+ if (platform === "darwin") {
380
+ const release = os.release();
381
+ const major = parseInt(release.split(".")[0], 10);
382
+ const prefix = arch === "arm64" ? "arm64_" : "";
383
+ if (major >= 25) return `${prefix}tahoe`;
384
+ if (major >= 24) return `${prefix}sequoia`;
385
+ if (major >= 23) return `${prefix}sonoma`;
386
+ return `${prefix}ventura`;
387
+ }
388
+ if (platform === "linux") {
389
+ return arch === "arm64" ? "aarch64_linux" : "x86_64_linux";
390
+ }
391
+ throw new Error(`No Homebrew bottles available for ${platform}-${arch}`);
392
+ }
393
+ function parseParadeDBVersion(version) {
394
+ const match = version.match(/^(\d+\.\d+\.\d+)(?:-pg(\d+))?$/);
395
+ if (!match) {
396
+ return { extVersion: version };
397
+ }
398
+ return {
399
+ extVersion: match[1],
400
+ pgVersion: match[2]
401
+ };
402
+ }
31
403
 
32
404
  // src/presets.ts
405
+ var DEFAULT_PARADEDB_EXT_VERSION = "0.22.5";
406
+ var DEFAULT_POSTGRES_VERSION = getPgMajorVersion();
407
+ var DEFAULT_PARADEDB_VERSION = `${DEFAULT_PARADEDB_EXT_VERSION}-pg${DEFAULT_POSTGRES_VERSION}`;
408
+ var DEFAULT_POSTGRES_IMAGE = `postgres:${DEFAULT_POSTGRES_VERSION}`;
409
+ var DEFAULT_PARADEDB_IMAGE = `paradedb:${DEFAULT_PARADEDB_VERSION}`;
33
410
  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
- );
411
+ var PARADEDB_IMAGE_REPOSITORY = "paradedb";
45
412
  var DEFAULT_DATABASE = "testdb";
46
413
  var DEFAULT_USERNAME = "testuser";
47
414
  var DEFAULT_PASSWORD = "testpassword";
@@ -49,7 +416,7 @@ var PARADEDB_DEFAULT_EXTENSIONS = ["pg_search", "vector"];
49
416
  function normalizeOptions(options = {}) {
50
417
  const preset = options.preset ?? "postgres";
51
418
  const version = options.version;
52
- const image = options.image ?? getImage(preset, version);
419
+ const image = getImageLabel(preset, version);
53
420
  const database = options.database ?? DEFAULT_DATABASE;
54
421
  const username = options.username ?? DEFAULT_USERNAME;
55
422
  const password = options.password ?? DEFAULT_PASSWORD;
@@ -73,8 +440,11 @@ function getImageForVersion(preset, version) {
73
440
  function getDefaultImage(preset) {
74
441
  return preset === "paradedb" ? DEFAULT_PARADEDB_IMAGE : DEFAULT_POSTGRES_IMAGE;
75
442
  }
76
- function getImage(preset, version) {
77
- return version ? getImageForVersion(preset, version) : getDefaultImage(preset);
443
+ function getImageLabel(preset, version) {
444
+ if (version) {
445
+ return getImageForVersion(preset, version);
446
+ }
447
+ return getDefaultImage(preset);
78
448
  }
79
449
  function getDefaultExtensions(preset) {
80
450
  return preset === "paradedb" ? [...PARADEDB_DEFAULT_EXTENSIONS] : [];
@@ -85,6 +455,21 @@ function buildInitStatements(options) {
85
455
  );
86
456
  return [...extensionStatements, ...options.initSql];
87
457
  }
458
+ function resolveParadeDBVersion(version) {
459
+ if (!version) {
460
+ return DEFAULT_PARADEDB_EXT_VERSION;
461
+ }
462
+ const parsed = parseParadeDBVersion(version);
463
+ if (parsed.pgVersion) {
464
+ const installedPg = DEFAULT_POSTGRES_VERSION;
465
+ if (parsed.pgVersion !== installedPg) {
466
+ throw new Error(
467
+ `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.`
468
+ );
469
+ }
470
+ }
471
+ return parsed.extVersion;
472
+ }
88
473
  function quoteIdentifier(name) {
89
474
  if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
90
475
  return name;
@@ -94,17 +479,70 @@ function quoteIdentifier(name) {
94
479
 
95
480
  // src/PostgresMemoryServer.ts
96
481
  var PostgresMemoryServer = class _PostgresMemoryServer {
97
- constructor(container, options) {
98
- this.container = container;
482
+ constructor(pg, port, dataDir, options) {
483
+ this.pg = pg;
484
+ this.port = port;
485
+ this.dataDir = dataDir;
99
486
  this.options = options;
100
487
  this.snapshotSupported = options.database !== "postgres";
101
488
  }
102
489
  stopped = false;
103
490
  snapshotSupported;
491
+ hasSnapshot = false;
104
492
  static async create(options = {}) {
105
493
  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);
494
+ const port = await getFreePort();
495
+ const dataDir = await fs2.mkdtemp(
496
+ path2.join(os2.tmpdir(), "postgres-memory-server-")
497
+ );
498
+ const postgresFlags = [];
499
+ if (normalized.preset === "paradedb") {
500
+ const nativeDir = getNativeDir();
501
+ const extVersion = resolveParadeDBVersion(normalized.version);
502
+ const pgMajor = DEFAULT_POSTGRES_VERSION;
503
+ try {
504
+ await installParadeDBExtension(nativeDir, extVersion, pgMajor);
505
+ } catch (error) {
506
+ throw new ExtensionInstallError(
507
+ "pg_search",
508
+ error instanceof Error ? error : new Error(String(error))
509
+ );
510
+ }
511
+ if (normalized.extensions.includes("vector")) {
512
+ try {
513
+ await installPgVectorExtension(nativeDir, pgMajor);
514
+ } catch (error) {
515
+ throw new ExtensionInstallError(
516
+ "vector",
517
+ error instanceof Error ? error : new Error(String(error))
518
+ );
519
+ }
520
+ }
521
+ if (normalized.extensions.includes("pg_search") || normalized.extensions.length === 0) {
522
+ postgresFlags.push(
523
+ "-c",
524
+ "shared_preload_libraries=pg_search"
525
+ );
526
+ }
527
+ }
528
+ const pg = new EmbeddedPostgres({
529
+ databaseDir: dataDir,
530
+ port,
531
+ user: normalized.username,
532
+ password: normalized.password,
533
+ persistent: false,
534
+ postgresFlags,
535
+ onLog: () => {
536
+ },
537
+ onError: () => {
538
+ }
539
+ });
540
+ await pg.initialise();
541
+ await pg.start();
542
+ if (normalized.database !== "postgres") {
543
+ await pg.createDatabase(normalized.database);
544
+ }
545
+ const server = new _PostgresMemoryServer(pg, port, dataDir, normalized);
108
546
  const initStatements = buildInitStatements(normalized);
109
547
  if (initStatements.length > 0) {
110
548
  await server.runSql(initStatements);
@@ -119,15 +557,15 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
119
557
  }
120
558
  getUri() {
121
559
  this.ensureRunning();
122
- return this.container.getConnectionUri();
560
+ return `postgres://${this.options.username}:${this.options.password}@localhost:${this.port}/${this.options.database}`;
123
561
  }
124
562
  getHost() {
125
563
  this.ensureRunning();
126
- return this.container.getHost();
564
+ return "localhost";
127
565
  }
128
566
  getPort() {
129
567
  this.ensureRunning();
130
- return this.container.getPort();
568
+ return this.port;
131
569
  }
132
570
  getDatabase() {
133
571
  return this.options.database;
@@ -182,35 +620,90 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
182
620
  });
183
621
  }
184
622
  async runSqlFile(filePath) {
185
- const sql = await fs.readFile(filePath, "utf8");
623
+ const sql = await fs2.readFile(filePath, "utf8");
186
624
  await this.runSql(sql);
187
625
  }
188
626
  async runMigrationsDir(dirPath) {
189
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
627
+ const entries = await fs2.readdir(dirPath, { withFileTypes: true });
190
628
  const files = entries.filter(
191
629
  (entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".sql")
192
630
  ).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
193
631
  for (const file of files) {
194
- await this.runSqlFile(path.join(dirPath, file));
632
+ await this.runSqlFile(path2.join(dirPath, file));
195
633
  }
196
634
  return files;
197
635
  }
636
+ /**
637
+ * Create a snapshot of the current database state.
638
+ * Uses PostgreSQL template databases for fast, native snapshots.
639
+ */
198
640
  async snapshot() {
199
641
  this.ensureRunning();
200
642
  this.ensureSnapshotSupported();
201
- await this.container.snapshot();
643
+ const snapshotDb = `${this.options.database}_snapshot`;
644
+ await this.withAdminClient(async (client) => {
645
+ await client.query(
646
+ `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid != pg_backend_pid()`,
647
+ [this.options.database]
648
+ );
649
+ if (this.hasSnapshot) {
650
+ await client.query(`DROP DATABASE IF EXISTS "${snapshotDb}"`);
651
+ }
652
+ await client.query(
653
+ `CREATE DATABASE "${snapshotDb}" TEMPLATE "${this.options.database}"`
654
+ );
655
+ });
656
+ this.hasSnapshot = true;
202
657
  }
658
+ /**
659
+ * Restore the database to the last snapshot.
660
+ * Drops and recreates the database from the snapshot template.
661
+ */
203
662
  async restore() {
204
663
  this.ensureRunning();
205
664
  this.ensureSnapshotSupported();
206
- await this.container.restoreSnapshot();
665
+ if (!this.hasSnapshot) {
666
+ throw new Error(
667
+ "No snapshot exists. Call snapshot() before calling restore()."
668
+ );
669
+ }
670
+ const snapshotDb = `${this.options.database}_snapshot`;
671
+ await this.withAdminClient(async (client) => {
672
+ await client.query(
673
+ `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid != pg_backend_pid()`,
674
+ [this.options.database]
675
+ );
676
+ await client.query(`DROP DATABASE "${this.options.database}"`);
677
+ await client.query(
678
+ `CREATE DATABASE "${this.options.database}" TEMPLATE "${snapshotDb}"`
679
+ );
680
+ });
207
681
  }
208
682
  async stop() {
209
683
  if (this.stopped) {
210
684
  return;
211
685
  }
212
686
  this.stopped = true;
213
- await this.container.stop();
687
+ await this.pg.stop();
688
+ }
689
+ /**
690
+ * Connect to the "postgres" system database for admin operations
691
+ * (snapshot, restore, etc.).
692
+ */
693
+ async withAdminClient(callback) {
694
+ const client = new Client({
695
+ host: "localhost",
696
+ port: this.port,
697
+ database: "postgres",
698
+ user: this.options.username,
699
+ password: this.options.password
700
+ });
701
+ await client.connect();
702
+ try {
703
+ return await callback(client);
704
+ } finally {
705
+ await client.end();
706
+ }
214
707
  }
215
708
  ensureRunning() {
216
709
  if (this.stopped) {
@@ -226,7 +719,7 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
226
719
 
227
720
  // src/cli.ts
228
721
  async function main() {
229
- const { options, initFiles, json } = parseArgs(process.argv.slice(2));
722
+ const { options, initFiles, json } = parseArgs(process2.argv.slice(2));
230
723
  const server = await PostgresMemoryServer.create(options);
231
724
  try {
232
725
  for (const file of initFiles) {
@@ -242,45 +735,46 @@ async function main() {
242
735
  image: server.getImage()
243
736
  };
244
737
  if (json) {
245
- process.stdout.write(`${JSON.stringify(payload, null, 2)}
738
+ process2.stdout.write(`${JSON.stringify(payload, null, 2)}
246
739
  `);
247
740
  } else {
248
- process.stdout.write(`POSTGRES_MEMORY_SERVER_URI=${payload.uri}
741
+ process2.stdout.write(`POSTGRES_MEMORY_SERVER_URI=${payload.uri}
249
742
  `);
250
- process.stdout.write(`POSTGRES_MEMORY_SERVER_HOST=${payload.host}
743
+ process2.stdout.write(`POSTGRES_MEMORY_SERVER_HOST=${payload.host}
251
744
  `);
252
- process.stdout.write(`POSTGRES_MEMORY_SERVER_PORT=${payload.port}
745
+ process2.stdout.write(`POSTGRES_MEMORY_SERVER_PORT=${payload.port}
253
746
  `);
254
- process.stdout.write(
747
+ process2.stdout.write(
255
748
  `POSTGRES_MEMORY_SERVER_DATABASE=${payload.database}
256
749
  `
257
750
  );
258
- process.stdout.write(
751
+ process2.stdout.write(
259
752
  `POSTGRES_MEMORY_SERVER_USERNAME=${payload.username}
260
753
  `
261
754
  );
262
- process.stdout.write(
755
+ process2.stdout.write(
263
756
  `POSTGRES_MEMORY_SERVER_PASSWORD=${payload.password}
264
757
  `
265
758
  );
266
- process.stdout.write(`POSTGRES_MEMORY_SERVER_IMAGE=${payload.image}
759
+ process2.stdout.write(`POSTGRES_MEMORY_SERVER_IMAGE=${payload.image}
267
760
  `);
268
- process.stdout.write("\nPress Ctrl+C to stop the container.\n");
761
+ process2.stdout.write("\nPress Ctrl+C to stop the server.\n");
269
762
  }
270
763
  const stop = async () => {
271
764
  await server.stop();
272
- process.exit(0);
765
+ process2.exit(0);
273
766
  };
274
- process.on("SIGINT", () => {
767
+ process2.on("SIGINT", () => {
275
768
  void stop();
276
769
  });
277
- process.on("SIGTERM", () => {
770
+ process2.on("SIGTERM", () => {
278
771
  void stop();
279
772
  });
280
773
  await new Promise(() => {
281
774
  });
282
775
  } catch (error) {
283
- await server.stop();
776
+ await server.stop().catch(() => {
777
+ });
284
778
  throw error;
285
779
  }
286
780
  }
@@ -302,7 +796,7 @@ function parseArgs(argv) {
302
796
  break;
303
797
  }
304
798
  case "--image": {
305
- options.image = readValue(argv, ++index, arg);
799
+ readValue(argv, ++index, arg);
306
800
  break;
307
801
  }
308
802
  case "--version": {
@@ -335,7 +829,7 @@ function parseArgs(argv) {
335
829
  }
336
830
  case "--help": {
337
831
  printHelp();
338
- process.exit(0);
832
+ process2.exit(0);
339
833
  }
340
834
  default: {
341
835
  throw new Error(`Unknown argument: ${arg}`);
@@ -355,36 +849,36 @@ function readValue(argv, index, flag) {
355
849
  return value;
356
850
  }
357
851
  function printHelp() {
358
- process.stdout.write(`postgres-memory-server
852
+ process2.stdout.write(`postgres-memory-server
359
853
 
360
854
  `);
361
- process.stdout.write(`Options:
855
+ process2.stdout.write(`Options:
362
856
  `);
363
- process.stdout.write(` --preset postgres|paradedb
857
+ process2.stdout.write(` --preset postgres|paradedb
364
858
  `);
365
- process.stdout.write(` --version <tag>
859
+ process2.stdout.write(` --version <tag>
366
860
  `);
367
- process.stdout.write(` --image <image>
861
+ process2.stdout.write(` --image <image> (deprecated, ignored)
368
862
  `);
369
- process.stdout.write(` --database <name>
863
+ process2.stdout.write(` --database <name>
370
864
  `);
371
- process.stdout.write(` --username <name>
865
+ process2.stdout.write(` --username <name>
372
866
  `);
373
- process.stdout.write(` --password <password>
867
+ process2.stdout.write(` --password <password>
374
868
  `);
375
- process.stdout.write(` --extension <name> repeatable
869
+ process2.stdout.write(` --extension <name> repeatable
376
870
  `);
377
- process.stdout.write(` --init-file <path> repeatable
871
+ process2.stdout.write(` --init-file <path> repeatable
378
872
  `);
379
- process.stdout.write(` --json
873
+ process2.stdout.write(` --json
380
874
  `);
381
- process.stdout.write(` --help
875
+ process2.stdout.write(` --help
382
876
  `);
383
877
  }
384
878
  void main().catch((error) => {
385
879
  const message = error instanceof Error ? error.stack ?? error.message : String(error);
386
- process.stderr.write(`${message}
880
+ process2.stderr.write(`${message}
387
881
  `);
388
- process.exit(1);
882
+ process2.exit(1);
389
883
  });
390
884
  //# sourceMappingURL=cli.js.map