postgres-memory-server 0.2.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 +206 -78
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +15 -0
- package/dist/index.js +191 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import process3 from "process";
|
|
5
5
|
|
|
6
6
|
// src/PostgresMemoryServer.ts
|
|
7
|
-
import { promises as fs2 } from "fs";
|
|
7
|
+
import { promises as fs2, rmSync } from "fs";
|
|
8
8
|
import os2 from "os";
|
|
9
9
|
import path2 from "path";
|
|
10
|
+
import process2 from "process";
|
|
10
11
|
import EmbeddedPostgres from "embedded-postgres";
|
|
11
12
|
import { Client } from "pg";
|
|
12
13
|
|
|
@@ -67,7 +68,8 @@ function getPgMajorVersion() {
|
|
|
67
68
|
const content = readFileSync(candidate, "utf8");
|
|
68
69
|
const pkg = JSON.parse(content);
|
|
69
70
|
if (pkg.name === "embedded-postgres" && pkg.version) {
|
|
70
|
-
|
|
71
|
+
const major = pkg.version.split(".")[0];
|
|
72
|
+
if (major) return major;
|
|
71
73
|
}
|
|
72
74
|
} catch {
|
|
73
75
|
}
|
|
@@ -213,7 +215,7 @@ function buildDownloadUrl(version, pgMajorVersion, platform, arch) {
|
|
|
213
215
|
}
|
|
214
216
|
function getMacOSCodename() {
|
|
215
217
|
const release = os.release();
|
|
216
|
-
const majorVersion = parseInt(release.split(".")[0], 10);
|
|
218
|
+
const majorVersion = parseInt(release.split(".")[0] ?? "0", 10);
|
|
217
219
|
if (majorVersion >= 24) return "sequoia";
|
|
218
220
|
if (majorVersion >= 23) return "sonoma";
|
|
219
221
|
throw new Error(
|
|
@@ -378,7 +380,7 @@ async function installPgVectorExtension(nativeDir, pgMajorVersion) {
|
|
|
378
380
|
function getHomebrewBottleTag(platform, arch) {
|
|
379
381
|
if (platform === "darwin") {
|
|
380
382
|
const release = os.release();
|
|
381
|
-
const major = parseInt(release.split(".")[0], 10);
|
|
383
|
+
const major = parseInt(release.split(".")[0] ?? "0", 10);
|
|
382
384
|
const prefix = arch === "arm64" ? "arm64_" : "";
|
|
383
385
|
if (major >= 25) return `${prefix}tahoe`;
|
|
384
386
|
if (major >= 24) return `${prefix}sequoia`;
|
|
@@ -390,9 +392,61 @@ function getHomebrewBottleTag(platform, arch) {
|
|
|
390
392
|
}
|
|
391
393
|
throw new Error(`No Homebrew bottles available for ${platform}-${arch}`);
|
|
392
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
|
+
}
|
|
393
447
|
function parseParadeDBVersion(version) {
|
|
394
448
|
const match = version.match(/^(\d+\.\d+\.\d+)(?:-pg(\d+))?$/);
|
|
395
|
-
if (!match) {
|
|
449
|
+
if (!match || !match[1]) {
|
|
396
450
|
return { extVersion: version };
|
|
397
451
|
}
|
|
398
452
|
return {
|
|
@@ -478,6 +532,30 @@ function quoteIdentifier(name) {
|
|
|
478
532
|
}
|
|
479
533
|
|
|
480
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
|
+
}
|
|
481
559
|
var PostgresMemoryServer = class _PostgresMemoryServer {
|
|
482
560
|
constructor(pg, port, dataDir, options) {
|
|
483
561
|
this.pg = pg;
|
|
@@ -490,64 +568,81 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
|
|
|
490
568
|
snapshotSupported;
|
|
491
569
|
hasSnapshot = false;
|
|
492
570
|
static async create(options = {}) {
|
|
571
|
+
if (!orphanSweepDone) {
|
|
572
|
+
orphanSweepDone = true;
|
|
573
|
+
await sweepOrphanedDataDirs().catch(() => {
|
|
574
|
+
});
|
|
575
|
+
}
|
|
493
576
|
const normalized = normalizeOptions(options);
|
|
494
577
|
const port = await getFreePort();
|
|
495
578
|
const dataDir = await fs2.mkdtemp(
|
|
496
579
|
path2.join(os2.tmpdir(), "postgres-memory-server-")
|
|
497
580
|
);
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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")) {
|
|
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;
|
|
512
588
|
try {
|
|
513
|
-
await
|
|
589
|
+
await installParadeDBExtension(nativeDir, extVersion, pgMajor);
|
|
514
590
|
} catch (error) {
|
|
515
591
|
throw new ExtensionInstallError(
|
|
516
|
-
"
|
|
592
|
+
"pg_search",
|
|
517
593
|
error instanceof Error ? error : new Error(String(error))
|
|
518
594
|
);
|
|
519
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
|
+
}
|
|
520
609
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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);
|
|
526
626
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
persistent: false,
|
|
534
|
-
postgresFlags,
|
|
535
|
-
onLog: () => {
|
|
536
|
-
},
|
|
537
|
-
onError: () => {
|
|
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);
|
|
538
633
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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;
|
|
549
645
|
}
|
|
550
|
-
return server;
|
|
551
646
|
}
|
|
552
647
|
static createPostgres(options = {}) {
|
|
553
648
|
return _PostgresMemoryServer.create({ ...options, preset: "postgres" });
|
|
@@ -579,6 +674,14 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
|
|
|
579
674
|
getImage() {
|
|
580
675
|
return this.options.image;
|
|
581
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
|
+
}
|
|
582
685
|
getConnectionOptions() {
|
|
583
686
|
return {
|
|
584
687
|
host: this.getHost(),
|
|
@@ -684,7 +787,32 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
|
|
|
684
787
|
return;
|
|
685
788
|
}
|
|
686
789
|
this.stopped = true;
|
|
687
|
-
|
|
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
|
+
}
|
|
688
816
|
}
|
|
689
817
|
/**
|
|
690
818
|
* Connect to the "postgres" system database for admin operations
|
|
@@ -719,7 +847,7 @@ var PostgresMemoryServer = class _PostgresMemoryServer {
|
|
|
719
847
|
|
|
720
848
|
// src/cli.ts
|
|
721
849
|
async function main() {
|
|
722
|
-
const { options, initFiles, json } = parseArgs(
|
|
850
|
+
const { options, initFiles, json } = parseArgs(process3.argv.slice(2));
|
|
723
851
|
const server = await PostgresMemoryServer.create(options);
|
|
724
852
|
try {
|
|
725
853
|
for (const file of initFiles) {
|
|
@@ -735,39 +863,39 @@ async function main() {
|
|
|
735
863
|
image: server.getImage()
|
|
736
864
|
};
|
|
737
865
|
if (json) {
|
|
738
|
-
|
|
866
|
+
process3.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
739
867
|
`);
|
|
740
868
|
} else {
|
|
741
|
-
|
|
869
|
+
process3.stdout.write(`POSTGRES_MEMORY_SERVER_URI=${payload.uri}
|
|
742
870
|
`);
|
|
743
|
-
|
|
871
|
+
process3.stdout.write(`POSTGRES_MEMORY_SERVER_HOST=${payload.host}
|
|
744
872
|
`);
|
|
745
|
-
|
|
873
|
+
process3.stdout.write(`POSTGRES_MEMORY_SERVER_PORT=${payload.port}
|
|
746
874
|
`);
|
|
747
|
-
|
|
875
|
+
process3.stdout.write(
|
|
748
876
|
`POSTGRES_MEMORY_SERVER_DATABASE=${payload.database}
|
|
749
877
|
`
|
|
750
878
|
);
|
|
751
|
-
|
|
879
|
+
process3.stdout.write(
|
|
752
880
|
`POSTGRES_MEMORY_SERVER_USERNAME=${payload.username}
|
|
753
881
|
`
|
|
754
882
|
);
|
|
755
|
-
|
|
883
|
+
process3.stdout.write(
|
|
756
884
|
`POSTGRES_MEMORY_SERVER_PASSWORD=${payload.password}
|
|
757
885
|
`
|
|
758
886
|
);
|
|
759
|
-
|
|
887
|
+
process3.stdout.write(`POSTGRES_MEMORY_SERVER_IMAGE=${payload.image}
|
|
760
888
|
`);
|
|
761
|
-
|
|
889
|
+
process3.stdout.write("\nPress Ctrl+C to stop the server.\n");
|
|
762
890
|
}
|
|
763
891
|
const stop = async () => {
|
|
764
892
|
await server.stop();
|
|
765
|
-
|
|
893
|
+
process3.exit(0);
|
|
766
894
|
};
|
|
767
|
-
|
|
895
|
+
process3.on("SIGINT", () => {
|
|
768
896
|
void stop();
|
|
769
897
|
});
|
|
770
|
-
|
|
898
|
+
process3.on("SIGTERM", () => {
|
|
771
899
|
void stop();
|
|
772
900
|
});
|
|
773
901
|
await new Promise(() => {
|
|
@@ -829,7 +957,7 @@ function parseArgs(argv) {
|
|
|
829
957
|
}
|
|
830
958
|
case "--help": {
|
|
831
959
|
printHelp();
|
|
832
|
-
|
|
960
|
+
process3.exit(0);
|
|
833
961
|
}
|
|
834
962
|
default: {
|
|
835
963
|
throw new Error(`Unknown argument: ${arg}`);
|
|
@@ -849,36 +977,36 @@ function readValue(argv, index, flag) {
|
|
|
849
977
|
return value;
|
|
850
978
|
}
|
|
851
979
|
function printHelp() {
|
|
852
|
-
|
|
980
|
+
process3.stdout.write(`postgres-memory-server
|
|
853
981
|
|
|
854
982
|
`);
|
|
855
|
-
|
|
983
|
+
process3.stdout.write(`Options:
|
|
856
984
|
`);
|
|
857
|
-
|
|
985
|
+
process3.stdout.write(` --preset postgres|paradedb
|
|
858
986
|
`);
|
|
859
|
-
|
|
987
|
+
process3.stdout.write(` --version <tag>
|
|
860
988
|
`);
|
|
861
|
-
|
|
989
|
+
process3.stdout.write(` --image <image> (deprecated, ignored)
|
|
862
990
|
`);
|
|
863
|
-
|
|
991
|
+
process3.stdout.write(` --database <name>
|
|
864
992
|
`);
|
|
865
|
-
|
|
993
|
+
process3.stdout.write(` --username <name>
|
|
866
994
|
`);
|
|
867
|
-
|
|
995
|
+
process3.stdout.write(` --password <password>
|
|
868
996
|
`);
|
|
869
|
-
|
|
997
|
+
process3.stdout.write(` --extension <name> repeatable
|
|
870
998
|
`);
|
|
871
|
-
|
|
999
|
+
process3.stdout.write(` --init-file <path> repeatable
|
|
872
1000
|
`);
|
|
873
|
-
|
|
1001
|
+
process3.stdout.write(` --json
|
|
874
1002
|
`);
|
|
875
|
-
|
|
1003
|
+
process3.stdout.write(` --help
|
|
876
1004
|
`);
|
|
877
1005
|
}
|
|
878
1006
|
void main().catch((error) => {
|
|
879
1007
|
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
880
|
-
|
|
1008
|
+
process3.stderr.write(`${message}
|
|
881
1009
|
`);
|
|
882
|
-
|
|
1010
|
+
process3.exit(1);
|
|
883
1011
|
});
|
|
884
1012
|
//# sourceMappingURL=cli.js.map
|