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/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // src/PostgresMemoryServer.ts
2
- import { promises as fs } from "fs";
3
- import path from "path";
4
- import { PostgreSqlContainer } from "@testcontainers/postgresql";
2
+ import { promises as fs2 } from "fs";
3
+ import os2 from "os";
4
+ import path2 from "path";
5
+ import EmbeddedPostgres from "embedded-postgres";
5
6
  import { Client } from "pg";
6
7
 
7
8
  // src/errors.ts
@@ -23,20 +24,386 @@ var ServerStoppedError = class extends PostgresMemoryServerError {
23
24
  super("The PostgresMemoryServer has already been stopped.");
24
25
  }
25
26
  };
27
+ var ExtensionInstallError = class extends PostgresMemoryServerError {
28
+ constructor(extensionName, cause) {
29
+ super(
30
+ `Failed to install the "${extensionName}" extension. ${cause?.message ?? ""}`.trim(),
31
+ cause ? { cause } : void 0
32
+ );
33
+ }
34
+ };
35
+
36
+ // src/native.ts
37
+ import { promises as fs, readFileSync, existsSync } from "fs";
38
+ import { createRequire } from "module";
39
+ import { execFile as execFileCb } from "child_process";
40
+ import { promisify } from "util";
41
+ import net from "net";
42
+ import os from "os";
43
+ import path from "path";
44
+ var execFile = promisify(execFileCb);
45
+ async function getFreePort() {
46
+ return new Promise((resolve, reject) => {
47
+ const server = net.createServer();
48
+ server.listen(0, () => {
49
+ const { port } = server.address();
50
+ server.close(() => resolve(port));
51
+ });
52
+ server.on("error", reject);
53
+ });
54
+ }
55
+ function getPgMajorVersion() {
56
+ const req = createRequire(import.meta.url);
57
+ const mainEntry = req.resolve("embedded-postgres");
58
+ let dir = path.dirname(mainEntry);
59
+ while (dir !== path.dirname(dir)) {
60
+ const candidate = path.join(dir, "package.json");
61
+ try {
62
+ const content = readFileSync(candidate, "utf8");
63
+ const pkg = JSON.parse(content);
64
+ if (pkg.name === "embedded-postgres" && pkg.version) {
65
+ return pkg.version.split(".")[0];
66
+ }
67
+ } catch {
68
+ }
69
+ dir = path.dirname(dir);
70
+ }
71
+ throw new Error(
72
+ "Could not determine embedded-postgres version. Ensure embedded-postgres is installed."
73
+ );
74
+ }
75
+ function getNativeDir() {
76
+ const platform = os.platform();
77
+ const arch = os.arch();
78
+ const platformPkgNames = {
79
+ darwin: {
80
+ arm64: "@embedded-postgres/darwin-arm64",
81
+ x64: "@embedded-postgres/darwin-x64"
82
+ },
83
+ linux: {
84
+ x64: "@embedded-postgres/linux-x64",
85
+ arm64: "@embedded-postgres/linux-arm64"
86
+ },
87
+ win32: {
88
+ x64: "@embedded-postgres/windows-x64"
89
+ }
90
+ };
91
+ const pkgName = platformPkgNames[platform]?.[arch];
92
+ if (!pkgName) {
93
+ throw new Error(`Unsupported platform: ${platform}-${arch}`);
94
+ }
95
+ const req = createRequire(import.meta.url);
96
+ const mainEntry = req.resolve(pkgName);
97
+ let dir = path.dirname(mainEntry);
98
+ while (dir !== path.dirname(dir)) {
99
+ const nativeDir = path.join(dir, "native");
100
+ if (existsSync(nativeDir)) {
101
+ return nativeDir;
102
+ }
103
+ dir = path.dirname(dir);
104
+ }
105
+ throw new Error(
106
+ `Could not find native directory for ${pkgName}. Ensure embedded-postgres is installed correctly.`
107
+ );
108
+ }
109
+ function getCacheDir() {
110
+ const xdgCache = process.env.XDG_CACHE_HOME;
111
+ const base = xdgCache || path.join(os.homedir(), ".cache");
112
+ return path.join(base, "postgres-memory-server");
113
+ }
114
+ async function installParadeDBExtension(nativeDir, paradedbVersion, pgMajorVersion) {
115
+ const libDir = path.join(nativeDir, "lib", "postgresql");
116
+ const extDir = path.join(nativeDir, "share", "postgresql", "extension");
117
+ const soName = os.platform() === "darwin" && parseInt(pgMajorVersion, 10) >= 16 ? "pg_search.dylib" : "pg_search.so";
118
+ try {
119
+ await fs.access(path.join(libDir, soName));
120
+ await fs.access(path.join(extDir, "pg_search.control"));
121
+ return;
122
+ } catch {
123
+ }
124
+ const cacheDir = getCacheDir();
125
+ const platform = os.platform();
126
+ const arch = os.arch();
127
+ const cacheKey = `paradedb-${paradedbVersion}-pg${pgMajorVersion}-${platform}-${arch}`;
128
+ const cachedDir = path.join(cacheDir, cacheKey);
129
+ let cached = false;
130
+ try {
131
+ await fs.access(path.join(cachedDir, "lib", soName));
132
+ cached = true;
133
+ } catch {
134
+ }
135
+ if (!cached) {
136
+ const url = buildDownloadUrl(
137
+ paradedbVersion,
138
+ pgMajorVersion,
139
+ platform,
140
+ arch
141
+ );
142
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "paradedb-"));
143
+ try {
144
+ const filename = decodeURIComponent(url.split("/").pop());
145
+ const archivePath = path.join(tmpDir, filename);
146
+ await downloadFile(url, archivePath);
147
+ const extractedDir = path.join(tmpDir, "extracted");
148
+ await fs.mkdir(extractedDir, { recursive: true });
149
+ if (platform === "darwin") {
150
+ await extractPkg(archivePath, extractedDir);
151
+ } else {
152
+ await extractDeb(archivePath, extractedDir);
153
+ }
154
+ const cacheLibDir2 = path.join(cachedDir, "lib");
155
+ const cacheExtDir2 = path.join(cachedDir, "extension");
156
+ await fs.mkdir(cacheLibDir2, { recursive: true });
157
+ await fs.mkdir(cacheExtDir2, { recursive: true });
158
+ const soFiles = await findFiles(
159
+ extractedDir,
160
+ /pg_search\.(so|dylib)$/
161
+ );
162
+ for (const soFile of soFiles) {
163
+ await copyFileWithPermissions(soFile, path.join(cacheLibDir2, path.basename(soFile)));
164
+ }
165
+ const extFiles = await findFiles(
166
+ extractedDir,
167
+ /pg_search[^/]*(\.control|\.sql)$/
168
+ );
169
+ for (const extFile of extFiles) {
170
+ await copyFileWithPermissions(
171
+ extFile,
172
+ path.join(cacheExtDir2, path.basename(extFile))
173
+ );
174
+ }
175
+ } finally {
176
+ await fs.rm(tmpDir, { recursive: true, force: true });
177
+ }
178
+ }
179
+ await fs.mkdir(libDir, { recursive: true });
180
+ await fs.mkdir(extDir, { recursive: true });
181
+ const cacheLibDir = path.join(cachedDir, "lib");
182
+ const cacheExtDir = path.join(cachedDir, "extension");
183
+ for (const file of await fs.readdir(cacheLibDir)) {
184
+ await copyFileWithPermissions(path.join(cacheLibDir, file), path.join(libDir, file));
185
+ }
186
+ for (const file of await fs.readdir(cacheExtDir)) {
187
+ await copyFileWithPermissions(path.join(cacheExtDir, file), path.join(extDir, file));
188
+ }
189
+ }
190
+ function buildDownloadUrl(version, pgMajorVersion, platform, arch) {
191
+ const base = `https://github.com/paradedb/paradedb/releases/download/v${version}`;
192
+ if (platform === "darwin") {
193
+ if (arch !== "arm64") {
194
+ throw new Error(
195
+ "ParadeDB only provides macOS binaries for arm64 (Apple Silicon). Intel Macs are not supported."
196
+ );
197
+ }
198
+ const macosName = getMacOSCodename();
199
+ return `${base}/pg_search%40${pgMajorVersion}--${version}.arm64_${macosName}.pkg`;
200
+ }
201
+ if (platform === "linux") {
202
+ const debArch = arch === "arm64" ? "arm64" : "amd64";
203
+ return `${base}/postgresql-${pgMajorVersion}-pg-search_${version}-1PARADEDB-bookworm_${debArch}.deb`;
204
+ }
205
+ throw new Error(
206
+ `ParadeDB does not provide prebuilt binaries for ${platform}. Use the Docker-based preset instead.`
207
+ );
208
+ }
209
+ function getMacOSCodename() {
210
+ const release = os.release();
211
+ const majorVersion = parseInt(release.split(".")[0], 10);
212
+ if (majorVersion >= 24) return "sequoia";
213
+ if (majorVersion >= 23) return "sonoma";
214
+ throw new Error(
215
+ `ParadeDB requires macOS 14 (Sonoma) or later. Detected Darwin ${release}.`
216
+ );
217
+ }
218
+ async function downloadFile(url, destPath) {
219
+ const response = await fetch(url, { redirect: "follow" });
220
+ if (!response.ok) {
221
+ throw new Error(
222
+ `Failed to download ParadeDB extension from ${url}: ${response.status} ${response.statusText}`
223
+ );
224
+ }
225
+ const buffer = Buffer.from(await response.arrayBuffer());
226
+ await fs.writeFile(destPath, buffer);
227
+ }
228
+ async function extractDeb(debPath, extractDir) {
229
+ await execFile("ar", ["x", debPath], { cwd: extractDir });
230
+ const files = await fs.readdir(extractDir);
231
+ const dataTar = files.find((f) => f.startsWith("data.tar"));
232
+ if (!dataTar) {
233
+ throw new Error(
234
+ "No data.tar.* found in .deb archive. The ParadeDB package format may have changed."
235
+ );
236
+ }
237
+ const dataDir = path.join(extractDir, "data");
238
+ await fs.mkdir(dataDir, { recursive: true });
239
+ await execFile("tar", [
240
+ "xf",
241
+ path.join(extractDir, dataTar),
242
+ "-C",
243
+ dataDir
244
+ ]);
245
+ }
246
+ async function extractPkg(pkgPath, extractDir) {
247
+ const pkgDir = path.join(extractDir, "pkg");
248
+ await execFile("pkgutil", ["--expand-full", pkgPath, pkgDir]);
249
+ }
250
+ async function copyFileWithPermissions(src, dest) {
251
+ const content = await fs.readFile(src);
252
+ await fs.writeFile(dest, content, { mode: 493 });
253
+ }
254
+ async function findFiles(dir, pattern) {
255
+ const results = [];
256
+ async function walk(currentDir) {
257
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
258
+ for (const entry of entries) {
259
+ const fullPath = path.join(currentDir, entry.name);
260
+ if (entry.isDirectory()) {
261
+ await walk(fullPath);
262
+ } else if (pattern.test(entry.name)) {
263
+ results.push(fullPath);
264
+ }
265
+ }
266
+ }
267
+ await walk(dir);
268
+ return results;
269
+ }
270
+ async function installPgVectorExtension(nativeDir, pgMajorVersion) {
271
+ const libDir = path.join(nativeDir, "lib", "postgresql");
272
+ const extDir = path.join(nativeDir, "share", "postgresql", "extension");
273
+ const soName = os.platform() === "darwin" ? "vector.dylib" : "vector.so";
274
+ try {
275
+ await fs.access(path.join(libDir, soName));
276
+ await fs.access(path.join(extDir, "vector.control"));
277
+ return;
278
+ } catch {
279
+ }
280
+ const platform = os.platform();
281
+ const arch = os.arch();
282
+ const cacheDir = getCacheDir();
283
+ const cacheKey = `pgvector-pg${pgMajorVersion}-${platform}-${arch}`;
284
+ const cachedDir = path.join(cacheDir, cacheKey);
285
+ let cached = false;
286
+ try {
287
+ await fs.access(path.join(cachedDir, "lib", soName));
288
+ cached = true;
289
+ } catch {
290
+ }
291
+ if (!cached) {
292
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "pgvector-"));
293
+ try {
294
+ const formulaRes = await fetch(
295
+ "https://formulae.brew.sh/api/formula/pgvector.json"
296
+ );
297
+ if (!formulaRes.ok) {
298
+ throw new Error(
299
+ `Failed to fetch pgvector formula: ${formulaRes.status}`
300
+ );
301
+ }
302
+ const formula = await formulaRes.json();
303
+ const bottleTag = getHomebrewBottleTag(platform, arch);
304
+ const fileInfo = formula.bottle.stable.files[bottleTag];
305
+ if (!fileInfo) {
306
+ throw new Error(
307
+ `No pgvector Homebrew bottle for ${bottleTag}. Available: ${Object.keys(formula.bottle.stable.files).join(", ")}`
308
+ );
309
+ }
310
+ const tokenRes = await fetch(
311
+ "https://ghcr.io/token?scope=repository:homebrew/core/pgvector:pull"
312
+ );
313
+ if (!tokenRes.ok) {
314
+ throw new Error(`Failed to get GHCR token: ${tokenRes.status}`);
315
+ }
316
+ const { token } = await tokenRes.json();
317
+ const blobUrl = `https://ghcr.io/v2/homebrew/core/pgvector/blobs/sha256:${fileInfo.sha256}`;
318
+ const blobRes = await fetch(blobUrl, {
319
+ headers: { Authorization: `Bearer ${token}` },
320
+ redirect: "follow"
321
+ });
322
+ if (!blobRes.ok) {
323
+ throw new Error(
324
+ `Failed to download pgvector bottle: ${blobRes.status}`
325
+ );
326
+ }
327
+ const bottlePath = path.join(tmpDir, "pgvector.tar.gz");
328
+ const buffer = Buffer.from(await blobRes.arrayBuffer());
329
+ await fs.writeFile(bottlePath, buffer);
330
+ const extractDir = path.join(tmpDir, "extracted");
331
+ await fs.mkdir(extractDir, { recursive: true });
332
+ await execFile("tar", ["xzf", bottlePath, "-C", extractDir]);
333
+ const cacheLibDir2 = path.join(cachedDir, "lib");
334
+ const cacheExtDir2 = path.join(cachedDir, "extension");
335
+ await fs.mkdir(cacheLibDir2, { recursive: true });
336
+ await fs.mkdir(cacheExtDir2, { recursive: true });
337
+ const pgSubdir = `postgresql@${pgMajorVersion}`;
338
+ let soFiles = await findFiles(
339
+ extractDir,
340
+ new RegExp(`${pgSubdir}.*vector\\.(so|dylib)$`)
341
+ );
342
+ if (soFiles.length === 0) {
343
+ soFiles = await findFiles(extractDir, /vector\.(so|dylib)$/);
344
+ }
345
+ for (const f of soFiles) {
346
+ await copyFileWithPermissions(f, path.join(cacheLibDir2, path.basename(f)));
347
+ }
348
+ let extFiles = await findFiles(
349
+ extractDir,
350
+ new RegExp(`${pgSubdir}.*vector[^/]*(\\.control|\\.sql)$`)
351
+ );
352
+ if (extFiles.length === 0) {
353
+ extFiles = await findFiles(extractDir, /vector[^/]*(\.control|\.sql)$/);
354
+ }
355
+ for (const f of extFiles) {
356
+ await copyFileWithPermissions(f, path.join(cacheExtDir2, path.basename(f)));
357
+ }
358
+ } finally {
359
+ await fs.rm(tmpDir, { recursive: true, force: true });
360
+ }
361
+ }
362
+ await fs.mkdir(libDir, { recursive: true });
363
+ await fs.mkdir(extDir, { recursive: true });
364
+ const cacheLibDir = path.join(cachedDir, "lib");
365
+ const cacheExtDir = path.join(cachedDir, "extension");
366
+ for (const file of await fs.readdir(cacheLibDir)) {
367
+ await copyFileWithPermissions(path.join(cacheLibDir, file), path.join(libDir, file));
368
+ }
369
+ for (const file of await fs.readdir(cacheExtDir)) {
370
+ await copyFileWithPermissions(path.join(cacheExtDir, file), path.join(extDir, file));
371
+ }
372
+ }
373
+ function getHomebrewBottleTag(platform, arch) {
374
+ if (platform === "darwin") {
375
+ const release = os.release();
376
+ const major = parseInt(release.split(".")[0], 10);
377
+ const prefix = arch === "arm64" ? "arm64_" : "";
378
+ if (major >= 25) return `${prefix}tahoe`;
379
+ if (major >= 24) return `${prefix}sequoia`;
380
+ if (major >= 23) return `${prefix}sonoma`;
381
+ return `${prefix}ventura`;
382
+ }
383
+ if (platform === "linux") {
384
+ return arch === "arm64" ? "aarch64_linux" : "x86_64_linux";
385
+ }
386
+ throw new Error(`No Homebrew bottles available for ${platform}-${arch}`);
387
+ }
388
+ function parseParadeDBVersion(version) {
389
+ const match = version.match(/^(\d+\.\d+\.\d+)(?:-pg(\d+))?$/);
390
+ if (!match) {
391
+ return { extVersion: version };
392
+ }
393
+ return {
394
+ extVersion: match[1],
395
+ pgVersion: match[2]
396
+ };
397
+ }
26
398
 
27
399
  // src/presets.ts
400
+ var DEFAULT_PARADEDB_EXT_VERSION = "0.22.5";
401
+ var DEFAULT_POSTGRES_VERSION = getPgMajorVersion();
402
+ var DEFAULT_PARADEDB_VERSION = `${DEFAULT_PARADEDB_EXT_VERSION}-pg${DEFAULT_POSTGRES_VERSION}`;
403
+ var DEFAULT_POSTGRES_IMAGE = `postgres:${DEFAULT_POSTGRES_VERSION}`;
404
+ var DEFAULT_PARADEDB_IMAGE = `paradedb:${DEFAULT_PARADEDB_VERSION}`;
28
405
  var POSTGRES_IMAGE_REPOSITORY = "postgres";
29
- var PARADEDB_IMAGE_REPOSITORY = "paradedb/paradedb";
30
- var DEFAULT_POSTGRES_VERSION = "17";
31
- var DEFAULT_PARADEDB_VERSION = "0.22.3-pg17";
32
- var DEFAULT_POSTGRES_IMAGE = getImageForVersion(
33
- "postgres",
34
- DEFAULT_POSTGRES_VERSION
35
- );
36
- var DEFAULT_PARADEDB_IMAGE = getImageForVersion(
37
- "paradedb",
38
- DEFAULT_PARADEDB_VERSION
39
- );
406
+ var PARADEDB_IMAGE_REPOSITORY = "paradedb";
40
407
  var DEFAULT_DATABASE = "testdb";
41
408
  var DEFAULT_USERNAME = "testuser";
42
409
  var DEFAULT_PASSWORD = "testpassword";
@@ -44,7 +411,7 @@ var PARADEDB_DEFAULT_EXTENSIONS = ["pg_search", "vector"];
44
411
  function normalizeOptions(options = {}) {
45
412
  const preset = options.preset ?? "postgres";
46
413
  const version = options.version;
47
- const image = options.image ?? getImage(preset, version);
414
+ const image = getImageLabel(preset, version);
48
415
  const database = options.database ?? DEFAULT_DATABASE;
49
416
  const username = options.username ?? DEFAULT_USERNAME;
50
417
  const password = options.password ?? DEFAULT_PASSWORD;
@@ -68,8 +435,11 @@ function getImageForVersion(preset, version) {
68
435
  function getDefaultImage(preset) {
69
436
  return preset === "paradedb" ? DEFAULT_PARADEDB_IMAGE : DEFAULT_POSTGRES_IMAGE;
70
437
  }
71
- function getImage(preset, version) {
72
- return version ? getImageForVersion(preset, version) : getDefaultImage(preset);
438
+ function getImageLabel(preset, version) {
439
+ if (version) {
440
+ return getImageForVersion(preset, version);
441
+ }
442
+ return getDefaultImage(preset);
73
443
  }
74
444
  function getDefaultExtensions(preset) {
75
445
  return preset === "paradedb" ? [...PARADEDB_DEFAULT_EXTENSIONS] : [];
@@ -80,6 +450,21 @@ function buildInitStatements(options) {
80
450
  );
81
451
  return [...extensionStatements, ...options.initSql];
82
452
  }
453
+ function resolveParadeDBVersion(version) {
454
+ if (!version) {
455
+ return DEFAULT_PARADEDB_EXT_VERSION;
456
+ }
457
+ const parsed = parseParadeDBVersion(version);
458
+ if (parsed.pgVersion) {
459
+ const installedPg = DEFAULT_POSTGRES_VERSION;
460
+ if (parsed.pgVersion !== installedPg) {
461
+ throw new Error(
462
+ `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.`
463
+ );
464
+ }
465
+ }
466
+ return parsed.extVersion;
467
+ }
83
468
  function quoteIdentifier(name) {
84
469
  if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
85
470
  return name;
@@ -89,17 +474,70 @@ function quoteIdentifier(name) {
89
474
 
90
475
  // src/PostgresMemoryServer.ts
91
476
  var PostgresMemoryServer = class _PostgresMemoryServer {
92
- constructor(container, options) {
93
- this.container = container;
477
+ constructor(pg, port, dataDir, options) {
478
+ this.pg = pg;
479
+ this.port = port;
480
+ this.dataDir = dataDir;
94
481
  this.options = options;
95
482
  this.snapshotSupported = options.database !== "postgres";
96
483
  }
97
484
  stopped = false;
98
485
  snapshotSupported;
486
+ hasSnapshot = false;
99
487
  static async create(options = {}) {
100
488
  const normalized = normalizeOptions(options);
101
- const container = await new PostgreSqlContainer(normalized.image).withDatabase(normalized.database).withUsername(normalized.username).withPassword(normalized.password).start();
102
- const server = new _PostgresMemoryServer(container, normalized);
489
+ const port = await getFreePort();
490
+ const dataDir = await fs2.mkdtemp(
491
+ path2.join(os2.tmpdir(), "postgres-memory-server-")
492
+ );
493
+ const postgresFlags = [];
494
+ if (normalized.preset === "paradedb") {
495
+ const nativeDir = getNativeDir();
496
+ const extVersion = resolveParadeDBVersion(normalized.version);
497
+ const pgMajor = DEFAULT_POSTGRES_VERSION;
498
+ try {
499
+ await installParadeDBExtension(nativeDir, extVersion, pgMajor);
500
+ } catch (error) {
501
+ throw new ExtensionInstallError(
502
+ "pg_search",
503
+ error instanceof Error ? error : new Error(String(error))
504
+ );
505
+ }
506
+ if (normalized.extensions.includes("vector")) {
507
+ try {
508
+ await installPgVectorExtension(nativeDir, pgMajor);
509
+ } catch (error) {
510
+ throw new ExtensionInstallError(
511
+ "vector",
512
+ error instanceof Error ? error : new Error(String(error))
513
+ );
514
+ }
515
+ }
516
+ if (normalized.extensions.includes("pg_search") || normalized.extensions.length === 0) {
517
+ postgresFlags.push(
518
+ "-c",
519
+ "shared_preload_libraries=pg_search"
520
+ );
521
+ }
522
+ }
523
+ const pg = new EmbeddedPostgres({
524
+ databaseDir: dataDir,
525
+ port,
526
+ user: normalized.username,
527
+ password: normalized.password,
528
+ persistent: false,
529
+ postgresFlags,
530
+ onLog: () => {
531
+ },
532
+ onError: () => {
533
+ }
534
+ });
535
+ await pg.initialise();
536
+ await pg.start();
537
+ if (normalized.database !== "postgres") {
538
+ await pg.createDatabase(normalized.database);
539
+ }
540
+ const server = new _PostgresMemoryServer(pg, port, dataDir, normalized);
103
541
  const initStatements = buildInitStatements(normalized);
104
542
  if (initStatements.length > 0) {
105
543
  await server.runSql(initStatements);
@@ -114,15 +552,15 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
114
552
  }
115
553
  getUri() {
116
554
  this.ensureRunning();
117
- return this.container.getConnectionUri();
555
+ return `postgres://${this.options.username}:${this.options.password}@localhost:${this.port}/${this.options.database}`;
118
556
  }
119
557
  getHost() {
120
558
  this.ensureRunning();
121
- return this.container.getHost();
559
+ return "localhost";
122
560
  }
123
561
  getPort() {
124
562
  this.ensureRunning();
125
- return this.container.getPort();
563
+ return this.port;
126
564
  }
127
565
  getDatabase() {
128
566
  return this.options.database;
@@ -177,35 +615,90 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
177
615
  });
178
616
  }
179
617
  async runSqlFile(filePath) {
180
- const sql = await fs.readFile(filePath, "utf8");
618
+ const sql = await fs2.readFile(filePath, "utf8");
181
619
  await this.runSql(sql);
182
620
  }
183
621
  async runMigrationsDir(dirPath) {
184
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
622
+ const entries = await fs2.readdir(dirPath, { withFileTypes: true });
185
623
  const files = entries.filter(
186
624
  (entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".sql")
187
625
  ).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
188
626
  for (const file of files) {
189
- await this.runSqlFile(path.join(dirPath, file));
627
+ await this.runSqlFile(path2.join(dirPath, file));
190
628
  }
191
629
  return files;
192
630
  }
631
+ /**
632
+ * Create a snapshot of the current database state.
633
+ * Uses PostgreSQL template databases for fast, native snapshots.
634
+ */
193
635
  async snapshot() {
194
636
  this.ensureRunning();
195
637
  this.ensureSnapshotSupported();
196
- await this.container.snapshot();
638
+ const snapshotDb = `${this.options.database}_snapshot`;
639
+ await this.withAdminClient(async (client) => {
640
+ await client.query(
641
+ `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid != pg_backend_pid()`,
642
+ [this.options.database]
643
+ );
644
+ if (this.hasSnapshot) {
645
+ await client.query(`DROP DATABASE IF EXISTS "${snapshotDb}"`);
646
+ }
647
+ await client.query(
648
+ `CREATE DATABASE "${snapshotDb}" TEMPLATE "${this.options.database}"`
649
+ );
650
+ });
651
+ this.hasSnapshot = true;
197
652
  }
653
+ /**
654
+ * Restore the database to the last snapshot.
655
+ * Drops and recreates the database from the snapshot template.
656
+ */
198
657
  async restore() {
199
658
  this.ensureRunning();
200
659
  this.ensureSnapshotSupported();
201
- await this.container.restoreSnapshot();
660
+ if (!this.hasSnapshot) {
661
+ throw new Error(
662
+ "No snapshot exists. Call snapshot() before calling restore()."
663
+ );
664
+ }
665
+ const snapshotDb = `${this.options.database}_snapshot`;
666
+ await this.withAdminClient(async (client) => {
667
+ await client.query(
668
+ `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid != pg_backend_pid()`,
669
+ [this.options.database]
670
+ );
671
+ await client.query(`DROP DATABASE "${this.options.database}"`);
672
+ await client.query(
673
+ `CREATE DATABASE "${this.options.database}" TEMPLATE "${snapshotDb}"`
674
+ );
675
+ });
202
676
  }
203
677
  async stop() {
204
678
  if (this.stopped) {
205
679
  return;
206
680
  }
207
681
  this.stopped = true;
208
- await this.container.stop();
682
+ await this.pg.stop();
683
+ }
684
+ /**
685
+ * Connect to the "postgres" system database for admin operations
686
+ * (snapshot, restore, etc.).
687
+ */
688
+ async withAdminClient(callback) {
689
+ const client = new Client({
690
+ host: "localhost",
691
+ port: this.port,
692
+ database: "postgres",
693
+ user: this.options.username,
694
+ password: this.options.password
695
+ });
696
+ await client.connect();
697
+ try {
698
+ return await callback(client);
699
+ } finally {
700
+ await client.end();
701
+ }
209
702
  }
210
703
  ensureRunning() {
211
704
  if (this.stopped) {
@@ -220,21 +713,21 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
220
713
  };
221
714
 
222
715
  // src/jest.ts
223
- import { promises as fs2 } from "fs";
716
+ import { promises as fs3 } from "fs";
224
717
  import { spawn } from "child_process";
225
718
  import { createHash } from "crypto";
226
- import path2 from "path";
719
+ import path3 from "path";
227
720
  import { tmpdir } from "os";
228
- import process from "process";
721
+ import process2 from "process";
229
722
  import { fileURLToPath, pathToFileURL } from "url";
230
723
  var CHILD_OPTIONS_ENV_VAR = "POSTGRES_MEMORY_SERVER_CHILD_OPTIONS_B64";
231
724
  var CHILD_SETUP_TIMEOUT_MS = 12e4;
232
725
  var CHILD_SHUTDOWN_TIMEOUT_MS = 3e4;
233
726
  var POLL_INTERVAL_MS = 100;
234
727
  var DEFAULT_JEST_ENV_VAR_NAME = "DATABASE_URL";
235
- var DEFAULT_JEST_STATE_FILE = path2.join(
728
+ var DEFAULT_JEST_STATE_FILE = path3.join(
236
729
  tmpdir(),
237
- `postgres-memory-server-jest-${createHash("sha256").update(process.cwd()).digest("hex").slice(0, 12)}.json`
730
+ `postgres-memory-server-jest-${createHash("sha256").update(process2.cwd()).digest("hex").slice(0, 12)}.json`
238
731
  );
239
732
  function getChildScript(childModuleUrl) {
240
733
  return `
@@ -285,7 +778,7 @@ function createJestGlobalSetup(options = {}) {
285
778
  ...serverOptions
286
779
  } = options;
287
780
  const resolvedStateFilePath = resolveStateFilePath(stateFilePath);
288
- await fs2.mkdir(path2.dirname(resolvedStateFilePath), { recursive: true });
781
+ await fs3.mkdir(path3.dirname(resolvedStateFilePath), { recursive: true });
289
782
  const existingState = await readStateFile(resolvedStateFilePath);
290
783
  if (existingState) {
291
784
  await stopChildProcess(existingState.pid);
@@ -297,7 +790,7 @@ function createJestGlobalSetup(options = {}) {
297
790
  envVarName,
298
791
  payload
299
792
  };
300
- await fs2.writeFile(
793
+ await fs3.writeFile(
301
794
  resolvedStateFilePath,
302
795
  JSON.stringify(state, null, 2),
303
796
  "utf8"
@@ -312,15 +805,15 @@ function createJestGlobalTeardown(options = {}) {
312
805
  return;
313
806
  }
314
807
  await stopChildProcess(state.pid);
315
- await fs2.rm(resolvedStateFilePath, { force: true });
808
+ await fs3.rm(resolvedStateFilePath, { force: true });
316
809
  };
317
810
  }
318
811
  function resolveStateFilePath(stateFilePath) {
319
- return stateFilePath ? path2.resolve(stateFilePath) : DEFAULT_JEST_STATE_FILE;
812
+ return stateFilePath ? path3.resolve(stateFilePath) : DEFAULT_JEST_STATE_FILE;
320
813
  }
321
814
  async function readStateFile(filePath) {
322
815
  try {
323
- const content = await fs2.readFile(filePath, "utf8");
816
+ const content = await fs3.readFile(filePath, "utf8");
324
817
  return JSON.parse(content);
325
818
  } catch (error) {
326
819
  if (isMissingFileError(error)) {
@@ -330,24 +823,24 @@ async function readStateFile(filePath) {
330
823
  }
331
824
  }
332
825
  function applyConnectionEnvironment(envVarName, payload) {
333
- process.env[envVarName] = payload.uri;
334
- process.env.POSTGRES_MEMORY_SERVER_URI = payload.uri;
335
- process.env.POSTGRES_MEMORY_SERVER_HOST = payload.host;
336
- process.env.POSTGRES_MEMORY_SERVER_PORT = String(payload.port);
337
- process.env.POSTGRES_MEMORY_SERVER_DATABASE = payload.database;
338
- process.env.POSTGRES_MEMORY_SERVER_USERNAME = payload.username;
339
- process.env.POSTGRES_MEMORY_SERVER_PASSWORD = payload.password;
340
- process.env.POSTGRES_MEMORY_SERVER_IMAGE = payload.image;
826
+ process2.env[envVarName] = payload.uri;
827
+ process2.env.POSTGRES_MEMORY_SERVER_URI = payload.uri;
828
+ process2.env.POSTGRES_MEMORY_SERVER_HOST = payload.host;
829
+ process2.env.POSTGRES_MEMORY_SERVER_PORT = String(payload.port);
830
+ process2.env.POSTGRES_MEMORY_SERVER_DATABASE = payload.database;
831
+ process2.env.POSTGRES_MEMORY_SERVER_USERNAME = payload.username;
832
+ process2.env.POSTGRES_MEMORY_SERVER_PASSWORD = payload.password;
833
+ process2.env.POSTGRES_MEMORY_SERVER_IMAGE = payload.image;
341
834
  }
342
835
  async function startChildProcess(options) {
343
836
  const childModuleUrl = await resolveChildModuleUrl();
344
837
  return new Promise((resolve, reject) => {
345
838
  const child = spawn(
346
- process.execPath,
839
+ process2.execPath,
347
840
  ["--input-type=module", "--eval", getChildScript(childModuleUrl)],
348
841
  {
349
842
  env: {
350
- ...process.env,
843
+ ...process2.env,
351
844
  [CHILD_OPTIONS_ENV_VAR]: Buffer.from(
352
845
  JSON.stringify(options),
353
846
  "utf8"
@@ -427,10 +920,10 @@ async function startChildProcess(options) {
427
920
  }
428
921
  async function resolveChildModuleUrl() {
429
922
  const currentFilePath = fileURLToPath(import.meta.url);
430
- const currentDirectoryPath = path2.dirname(currentFilePath);
431
- const distEntryPath = path2.resolve(currentDirectoryPath, "../dist/index.js");
923
+ const currentDirectoryPath = path3.dirname(currentFilePath);
924
+ const distEntryPath = path3.resolve(currentDirectoryPath, "../dist/index.js");
432
925
  try {
433
- await fs2.access(distEntryPath);
926
+ await fs3.access(distEntryPath);
434
927
  } catch (error) {
435
928
  if (isMissingFileError(error)) {
436
929
  throw new Error(
@@ -443,7 +936,7 @@ async function resolveChildModuleUrl() {
443
936
  }
444
937
  async function stopChildProcess(pid) {
445
938
  try {
446
- process.kill(pid, "SIGTERM");
939
+ process2.kill(pid, "SIGTERM");
447
940
  } catch (error) {
448
941
  if (isMissingProcessError(error)) {
449
942
  return;
@@ -454,7 +947,7 @@ async function stopChildProcess(pid) {
454
947
  while (Date.now() < deadline) {
455
948
  await sleep(POLL_INTERVAL_MS);
456
949
  try {
457
- process.kill(pid, 0);
950
+ process2.kill(pid, 0);
458
951
  } catch (error) {
459
952
  if (isMissingProcessError(error)) {
460
953
  return;
@@ -483,10 +976,12 @@ function isNodeErrorWithCode(error, code) {
483
976
  export {
484
977
  DEFAULT_JEST_ENV_VAR_NAME,
485
978
  DEFAULT_JEST_STATE_FILE,
979
+ DEFAULT_PARADEDB_EXT_VERSION,
486
980
  DEFAULT_PARADEDB_IMAGE,
487
981
  DEFAULT_PARADEDB_VERSION,
488
982
  DEFAULT_POSTGRES_IMAGE,
489
983
  DEFAULT_POSTGRES_VERSION,
984
+ ExtensionInstallError,
490
985
  PARADEDB_IMAGE_REPOSITORY,
491
986
  POSTGRES_IMAGE_REPOSITORY,
492
987
  PostgresMemoryServer,