pgserve 1.1.8 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/postgres.js +253 -65
- package/tests/pg-version-regex.test.js +82 -1
package/package.json
CHANGED
package/src/postgres.js
CHANGED
|
@@ -385,6 +385,27 @@ function buildCommand(cmd, libDir) {
|
|
|
385
385
|
return cmd;
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Compare a persisted pgvector install metadata record against the currently
|
|
390
|
+
* detected PG major and postgres binary path. Returns `true` only when the
|
|
391
|
+
* metadata is a plain object, has the expected shape, and matches the
|
|
392
|
+
* runtime environment. Used by the pgvector auto-heal path to decide whether
|
|
393
|
+
* an already-present `vector.so` is safe to reuse or must be replaced.
|
|
394
|
+
*
|
|
395
|
+
* Exported for unit tests — keep this pure (no I/O, no `this`).
|
|
396
|
+
*
|
|
397
|
+
* @param {unknown} meta - Value parsed from `vector.meta.json`, or null.
|
|
398
|
+
* @param {{pgMajor: string, postgresPath: string}} runtime - Current env.
|
|
399
|
+
* @returns {boolean}
|
|
400
|
+
*/
|
|
401
|
+
export function pgvectorMetaMatches(meta, runtime) {
|
|
402
|
+
if (!meta || typeof meta !== 'object') return false;
|
|
403
|
+
if (typeof meta.pgMajor !== 'string' || meta.pgMajor !== runtime.pgMajor) return false;
|
|
404
|
+
// postgresPath is optional in older metadata — only compare when present.
|
|
405
|
+
if (meta.postgresPath && meta.postgresPath !== runtime.postgresPath) return false;
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
388
409
|
export class PostgresManager {
|
|
389
410
|
constructor(options = {}) {
|
|
390
411
|
this.dataDir = options.dataDir || null; // null = memory mode (temp dir)
|
|
@@ -953,62 +974,172 @@ export class PostgresManager {
|
|
|
953
974
|
return;
|
|
954
975
|
}
|
|
955
976
|
|
|
977
|
+
const paths = this._pgvectorPaths();
|
|
978
|
+
|
|
979
|
+
let pgMajor;
|
|
980
|
+
try {
|
|
981
|
+
pgMajor = await this._detectPgMajor();
|
|
982
|
+
} catch (error) {
|
|
983
|
+
this.logger.warn({ err: error.message }, 'Failed to detect PG major version for pgvector install (non-fatal)');
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Proactive staleness check: if vector.so and vector.control both exist,
|
|
988
|
+
// trust them ONLY if the sidecar metadata file matches the current PG
|
|
989
|
+
// major and the current postgres binary path. Any mismatch — including a
|
|
990
|
+
// missing metadata file from a pre-auto-heal install — triggers a clean
|
|
991
|
+
// reinstall. This is what heals existing deployments that were shipped
|
|
992
|
+
// with the regex bug: on first run after upgrading pgserve, the stale
|
|
993
|
+
// PG17 .so will be detected (no metadata → mismatch) and replaced.
|
|
994
|
+
const filesPresent = fs.existsSync(paths.vectorSo) && fs.existsSync(paths.vectorControl);
|
|
995
|
+
if (filesPresent) {
|
|
996
|
+
const meta = this._readPgvectorMeta(paths.vectorMeta);
|
|
997
|
+
if (pgvectorMetaMatches(meta, { pgMajor, postgresPath: this.binaries.postgres })) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
this.logger.warn(
|
|
1001
|
+
{
|
|
1002
|
+
detectedPgMajor: pgMajor,
|
|
1003
|
+
metaPgMajor: meta?.pgMajor ?? null,
|
|
1004
|
+
metaPresent: meta !== null,
|
|
1005
|
+
vectorMeta: paths.vectorMeta,
|
|
1006
|
+
},
|
|
1007
|
+
'pgvector install metadata missing or mismatched — auto-healing stale install'
|
|
1008
|
+
);
|
|
1009
|
+
this._removePgvectorFiles(paths);
|
|
1010
|
+
} else {
|
|
1011
|
+
this.logger.info('pgvector extension files not found — downloading prebuilt binary...');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
try {
|
|
1015
|
+
await this._installPgvectorFromDeb({ pgMajor, ...paths });
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
this.logger.warn({ err: error.message }, 'Failed to install pgvector extension files (non-fatal)');
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Compute the canonical pgvector file paths for this PG install.
|
|
1023
|
+
* Extracted so proactive install, reactive heal, and cleanup all agree.
|
|
1024
|
+
*/
|
|
1025
|
+
_pgvectorPaths() {
|
|
956
1026
|
const libDir = this.binaries.libDir;
|
|
957
1027
|
const binDir = this.binaries.binDir;
|
|
958
1028
|
const extDir = path.join(path.dirname(binDir), 'share', 'postgresql', 'extension');
|
|
959
|
-
|
|
960
|
-
|
|
1029
|
+
return {
|
|
1030
|
+
libDir,
|
|
1031
|
+
extDir,
|
|
1032
|
+
vectorSo: path.join(libDir, 'vector.so'),
|
|
1033
|
+
vectorControl: path.join(extDir, 'vector.control'),
|
|
1034
|
+
vectorMeta: path.join(libDir, 'vector.meta.json'),
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
961
1037
|
|
|
962
|
-
|
|
963
|
-
|
|
1038
|
+
/**
|
|
1039
|
+
* Parse `postgres --version` output and return the major version string.
|
|
1040
|
+
* Throws on unparseable output so callers can fail loudly instead of
|
|
1041
|
+
* silently downloading the wrong pgvector .deb.
|
|
1042
|
+
*/
|
|
1043
|
+
async _detectPgMajor() {
|
|
1044
|
+
// `postgres --version` output is `postgres (PostgreSQL) 18.2`, so the
|
|
1045
|
+
// regex must tolerate the `)` that separates the product name from the
|
|
1046
|
+
// version number. The previous pattern `/PostgreSQL (\d+)/` expected a
|
|
1047
|
+
// digit immediately after `PostgreSQL ` and silently fell back to '17'
|
|
1048
|
+
// on PG 14+, causing the wrong pgvector .deb to be downloaded and a
|
|
1049
|
+
// later "incompatible library version mismatch" at CREATE EXTENSION time.
|
|
1050
|
+
const { execSync } = await import('node:child_process');
|
|
1051
|
+
const pgVersion = execSync(`${this.binaries.postgres} --version`, { encoding: 'utf-8' }).trim();
|
|
1052
|
+
const majorMatch = pgVersion.match(/PostgreSQL\)?\s+(\d+)/);
|
|
1053
|
+
if (!majorMatch) {
|
|
1054
|
+
throw new Error(`Could not detect PostgreSQL major version from: ${JSON.stringify(pgVersion)}`);
|
|
1055
|
+
}
|
|
1056
|
+
this.logger.debug({ pgMajor: majorMatch[1], pgVersion }, 'Detected PostgreSQL major version');
|
|
1057
|
+
return majorMatch[1];
|
|
1058
|
+
}
|
|
964
1059
|
|
|
965
|
-
|
|
1060
|
+
/**
|
|
1061
|
+
* Read and parse the pgvector install metadata sidecar, if present.
|
|
1062
|
+
* Returns null on missing file or any parse error (caller treats both as
|
|
1063
|
+
* "unknown, needs reinstall").
|
|
1064
|
+
*/
|
|
1065
|
+
_readPgvectorMeta(metaPath) {
|
|
1066
|
+
try {
|
|
1067
|
+
if (!fs.existsSync(metaPath)) return null;
|
|
1068
|
+
return JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
1069
|
+
} catch {
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
966
1073
|
|
|
1074
|
+
/**
|
|
1075
|
+
* Write the pgvector install metadata sidecar. Best-effort — failure to
|
|
1076
|
+
* write metadata should not crash the install; it just means the next
|
|
1077
|
+
* startup will trigger a re-heal (idempotent).
|
|
1078
|
+
*/
|
|
1079
|
+
_writePgvectorMeta(metaPath, data) {
|
|
967
1080
|
try {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
// Detect architecture — fail explicitly on unsupported platforms
|
|
987
|
-
const nodeArch = os.arch();
|
|
988
|
-
let arch;
|
|
989
|
-
if (nodeArch === 'x64') arch = 'amd64';
|
|
990
|
-
else if (nodeArch === 'arm64') arch = 'arm64';
|
|
991
|
-
else {
|
|
992
|
-
this.logger.warn({ arch: nodeArch }, 'Unsupported architecture for pgvector auto-install. Supported: x64, arm64');
|
|
993
|
-
return;
|
|
1081
|
+
fs.writeFileSync(metaPath, JSON.stringify(data, null, 2));
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
this.logger.warn({ err: error.message, metaPath }, 'Failed to write pgvector metadata sidecar');
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Remove all pgvector files (vector.so, vector.meta.json, and any
|
|
1089
|
+
* vector*.sql / vector.control files in the extension dir) so that a
|
|
1090
|
+
* subsequent install starts from a clean slate. Used by the auto-heal
|
|
1091
|
+
* paths — never call this while PG is mid-transaction.
|
|
1092
|
+
*/
|
|
1093
|
+
_removePgvectorFiles(paths) {
|
|
1094
|
+
const { libDir, extDir, vectorSo, vectorMeta } = paths;
|
|
1095
|
+
const toRemove = [vectorSo, vectorMeta];
|
|
1096
|
+
if (fs.existsSync(extDir)) {
|
|
1097
|
+
for (const f of fs.readdirSync(extDir)) {
|
|
1098
|
+
if (f.startsWith('vector')) toRemove.push(path.join(extDir, f));
|
|
994
1099
|
}
|
|
1100
|
+
}
|
|
1101
|
+
for (const p of toRemove) {
|
|
1102
|
+
try { fs.rmSync(p, { force: true }); } catch { /* ignore */ }
|
|
1103
|
+
}
|
|
1104
|
+
this.logger.info({ libDir, extDir }, 'Removed stale pgvector files');
|
|
1105
|
+
}
|
|
995
1106
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1107
|
+
/**
|
|
1108
|
+
* Download + extract + install pgvector from apt.postgresql.org for the
|
|
1109
|
+
* given PG major. Writes a metadata sidecar on success so future starts
|
|
1110
|
+
* can detect staleness without re-downloading.
|
|
1111
|
+
*/
|
|
1112
|
+
async _installPgvectorFromDeb({ pgMajor, extDir, vectorSo, vectorControl, vectorMeta }) {
|
|
1113
|
+
const { execSync } = await import('node:child_process');
|
|
1114
|
+
|
|
1115
|
+
// Detect architecture — fail explicitly on unsupported platforms
|
|
1116
|
+
const nodeArch = os.arch();
|
|
1117
|
+
let arch;
|
|
1118
|
+
if (nodeArch === 'x64') arch = 'amd64';
|
|
1119
|
+
else if (nodeArch === 'arm64') arch = 'arm64';
|
|
1120
|
+
else {
|
|
1121
|
+
this.logger.warn({ arch: nodeArch }, 'Unsupported architecture for pgvector auto-install. Supported: x64, arm64');
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Download prebuilt pgvector .deb from apt.postgresql.org (HTTPS)
|
|
1126
|
+
// Version 0.8.1-2 — update when new releases ship
|
|
1127
|
+
const pgvectorVersion = '0.8.1-2';
|
|
1128
|
+
const debUrl = `https://apt.postgresql.org/pub/repos/apt/pool/main/p/pgvector/postgresql-${pgMajor}-pgvector_${pgvectorVersion}.pgdg%2B1_${arch}.deb`;
|
|
1129
|
+
this.logger.info({ url: debUrl, pgMajor }, 'Downloading pgvector...');
|
|
1000
1130
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1131
|
+
const res = await fetch(debUrl);
|
|
1132
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
1003
1133
|
|
|
1004
|
-
|
|
1134
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
1005
1135
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1136
|
+
// Extract .deb (it's an ar archive containing data.tar.xz)
|
|
1137
|
+
const tmpDir = path.join(os.tmpdir(), `pgserve-pgvector-${process.pid}-${Date.now()}`);
|
|
1138
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1139
|
+
const debPath = path.join(tmpDir, 'pgvector.deb');
|
|
1140
|
+
fs.writeFileSync(debPath, buffer);
|
|
1011
1141
|
|
|
1142
|
+
try {
|
|
1012
1143
|
// Use dpkg-deb or ar to extract
|
|
1013
1144
|
try {
|
|
1014
1145
|
execSync(`dpkg-deb -x ${debPath} ${tmpDir}/extracted`, { stdio: 'pipe' });
|
|
@@ -1018,12 +1149,13 @@ export class PostgresManager {
|
|
|
1018
1149
|
execSync(`cd ${tmpDir} && ar x pgvector.deb && tar xf data.tar.* -C ${tmpDir}/extracted 2>/dev/null || tar xf data.tar.xz -C ${tmpDir}/extracted`, { stdio: 'pipe' });
|
|
1019
1150
|
}
|
|
1020
1151
|
|
|
1021
|
-
// Copy .so file
|
|
1152
|
+
// Copy .so file — fail loudly if missing so we don't silently ship broken
|
|
1022
1153
|
const soSrc = path.join(tmpDir, 'extracted', 'usr', 'lib', 'postgresql', pgMajor, 'lib', 'vector.so');
|
|
1023
|
-
if (fs.existsSync(soSrc)) {
|
|
1024
|
-
|
|
1025
|
-
this.logger.info({ path: vectorSo }, 'Installed vector.so');
|
|
1154
|
+
if (!fs.existsSync(soSrc)) {
|
|
1155
|
+
throw new Error(`Extracted .deb missing expected vector.so at ${soSrc}`);
|
|
1026
1156
|
}
|
|
1157
|
+
fs.copyFileSync(soSrc, vectorSo);
|
|
1158
|
+
this.logger.info({ path: vectorSo }, 'Installed vector.so');
|
|
1027
1159
|
|
|
1028
1160
|
// Copy extension SQL + control files
|
|
1029
1161
|
const extSrc = path.join(tmpDir, 'extracted', 'usr', 'share', 'postgresql', pgMajor, 'extension');
|
|
@@ -1049,29 +1181,55 @@ export class PostgresManager {
|
|
|
1049
1181
|
this.logger.info('Patched vector.control with absolute module path');
|
|
1050
1182
|
}
|
|
1051
1183
|
|
|
1052
|
-
//
|
|
1184
|
+
// Write metadata sidecar so future starts can detect staleness
|
|
1185
|
+
this._writePgvectorMeta(vectorMeta, {
|
|
1186
|
+
pgMajor,
|
|
1187
|
+
pgvectorVersion,
|
|
1188
|
+
sourceUrl: debUrl,
|
|
1189
|
+
postgresPath: this.binaries.postgres,
|
|
1190
|
+
installedAt: new Date().toISOString(),
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
this.logger.info({ pgMajor, pgvectorVersion }, 'pgvector extension installed successfully');
|
|
1194
|
+
} finally {
|
|
1195
|
+
// Always clean up tmpdir, even on failure
|
|
1053
1196
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1054
|
-
this.logger.info('pgvector extension installed successfully');
|
|
1055
|
-
} catch (error) {
|
|
1056
|
-
this.logger.warn({ err: error.message }, 'Failed to install pgvector extension files (non-fatal)');
|
|
1057
1197
|
}
|
|
1058
1198
|
}
|
|
1059
1199
|
|
|
1200
|
+
/**
|
|
1201
|
+
* Tear down an existing pgvector install and reinstall from scratch.
|
|
1202
|
+
* Called reactively when CREATE EXTENSION surfaces an ABI mismatch —
|
|
1203
|
+
* this is the last-resort heal for deployments that somehow bypassed
|
|
1204
|
+
* the proactive staleness check (e.g. metadata file got corrupted, or
|
|
1205
|
+
* the files were placed by an older pgserve that didn't write metadata).
|
|
1206
|
+
*/
|
|
1207
|
+
async _healStalePgvector() {
|
|
1208
|
+
if (!this.binaries?.libDir || os.platform() !== 'linux') return;
|
|
1209
|
+
const paths = this._pgvectorPaths();
|
|
1210
|
+
this._removePgvectorFiles(paths);
|
|
1211
|
+
// _doEnsurePgvectorFiles is serialized via _pgvectorInstallPromise;
|
|
1212
|
+
// this call goes through the mutex wrapper to stay race-safe.
|
|
1213
|
+
await this.ensurePgvectorFiles();
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1060
1216
|
/**
|
|
1061
1217
|
* Enable pgvector extension on a database
|
|
1062
|
-
* Creates a temporary connection to the specific database to run CREATE EXTENSION
|
|
1218
|
+
* Creates a temporary connection to the specific database to run CREATE EXTENSION.
|
|
1219
|
+
* If the CREATE hits an ABI mismatch (stale vector.so from an older pgserve
|
|
1220
|
+
* install that shipped the wrong PG major), auto-heal the install and retry
|
|
1221
|
+
* once. This is the reactive safety net for deployments that already have a
|
|
1222
|
+
* broken vector.so on disk when this version of pgserve first starts.
|
|
1063
1223
|
* @param {string} dbName - Database name to enable pgvector on
|
|
1064
1224
|
*/
|
|
1065
1225
|
async enablePgvectorExtension(dbName) {
|
|
1066
|
-
// Ensure extension files are installed first
|
|
1226
|
+
// Ensure extension files are installed first (proactive path)
|
|
1067
1227
|
await this.ensurePgvectorFiles();
|
|
1068
1228
|
|
|
1069
1229
|
const { SQL } = await import('bun');
|
|
1070
|
-
let dbPool = null;
|
|
1071
1230
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
dbPool = new SQL({
|
|
1231
|
+
const tryCreateExtension = async () => {
|
|
1232
|
+
const dbPool = new SQL({
|
|
1075
1233
|
hostname: '127.0.0.1',
|
|
1076
1234
|
port: this.port,
|
|
1077
1235
|
database: dbName,
|
|
@@ -1081,17 +1239,47 @@ export class PostgresManager {
|
|
|
1081
1239
|
idleTimeout: 5,
|
|
1082
1240
|
connectionTimeout: 5,
|
|
1083
1241
|
});
|
|
1242
|
+
try {
|
|
1243
|
+
await dbPool.unsafe('CREATE EXTENSION IF NOT EXISTS vector');
|
|
1244
|
+
} finally {
|
|
1245
|
+
await dbPool.close().catch(() => {});
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1084
1248
|
|
|
1085
|
-
|
|
1086
|
-
await
|
|
1249
|
+
try {
|
|
1250
|
+
await tryCreateExtension();
|
|
1087
1251
|
this.logger.info({ dbName }, 'pgvector extension enabled');
|
|
1252
|
+
return;
|
|
1088
1253
|
} catch (error) {
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
//
|
|
1093
|
-
|
|
1094
|
-
|
|
1254
|
+
const msg = error?.message || '';
|
|
1255
|
+
// Postgres surfaces stale .so as "incompatible library version" or
|
|
1256
|
+
// "version mismatch" depending on the nature of the ABI break.
|
|
1257
|
+
// PG_MODULE_MAGIC mismatches show the same symptoms.
|
|
1258
|
+
const abiMismatch = /version mismatch|incompatible library version|PG_MODULE_MAGIC/i.test(msg);
|
|
1259
|
+
if (!abiMismatch) {
|
|
1260
|
+
this.logger.warn({ dbName, err: msg }, 'Failed to enable pgvector extension (non-fatal)');
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
this.logger.warn(
|
|
1265
|
+
{ dbName, err: msg },
|
|
1266
|
+
'pgvector ABI mismatch detected — auto-healing stale install and retrying'
|
|
1267
|
+
);
|
|
1268
|
+
try {
|
|
1269
|
+
await this._healStalePgvector();
|
|
1270
|
+
} catch (healError) {
|
|
1271
|
+
this.logger.error({ dbName, err: healError.message }, 'pgvector auto-heal failed during reinstall');
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
try {
|
|
1276
|
+
await tryCreateExtension();
|
|
1277
|
+
this.logger.info({ dbName }, 'pgvector auto-heal successful — extension enabled');
|
|
1278
|
+
} catch (retryError) {
|
|
1279
|
+
this.logger.error(
|
|
1280
|
+
{ dbName, err: retryError.message },
|
|
1281
|
+
'pgvector still failing after auto-heal — manual intervention required'
|
|
1282
|
+
);
|
|
1095
1283
|
}
|
|
1096
1284
|
}
|
|
1097
1285
|
}
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { test, expect, describe } from 'bun:test';
|
|
16
|
+
import { pgvectorMetaMatches } from '../src/postgres.js';
|
|
16
17
|
|
|
17
|
-
// Keep this in sync with `
|
|
18
|
+
// Keep this in sync with `_detectPgMajor()` in src/postgres.js
|
|
18
19
|
const PG_VERSION_REGEX = /PostgreSQL\)?\s+(\d+)/;
|
|
19
20
|
|
|
20
21
|
function detectMajor(versionString) {
|
|
@@ -46,3 +47,83 @@ describe('PG major version detection for pgvector auto-install', () => {
|
|
|
46
47
|
expect(detectMajor('mysql 8.0')).toBeNull();
|
|
47
48
|
});
|
|
48
49
|
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Staleness detection tests for the pgvector auto-heal path.
|
|
53
|
+
*
|
|
54
|
+
* `pgvectorMetaMatches` decides whether an already-present vector.so on
|
|
55
|
+
* disk can be trusted (return true → reuse) or must be torn down and
|
|
56
|
+
* reinstalled (return false → heal). Getting this wrong in either
|
|
57
|
+
* direction is a production bug:
|
|
58
|
+
*
|
|
59
|
+
* - False positive (matches when it shouldn't) → stale PG17 .so stays on
|
|
60
|
+
* disk, CREATE EXTENSION dies with "incompatible library version" on
|
|
61
|
+
* PG18, brain-ingest blows up mid-run.
|
|
62
|
+
* - False negative (doesn't match when it should) → pgserve re-downloads
|
|
63
|
+
* pgvector on every start, wasting bandwidth and triggering
|
|
64
|
+
* apt.postgresql.org rate limits.
|
|
65
|
+
*
|
|
66
|
+
* These tests pin the exact matching semantics so the auto-heal doesn't
|
|
67
|
+
* silently regress.
|
|
68
|
+
*/
|
|
69
|
+
describe('pgvectorMetaMatches — pgvector install staleness detection', () => {
|
|
70
|
+
const RUNTIME = {
|
|
71
|
+
pgMajor: '18',
|
|
72
|
+
postgresPath: '/home/user/.pgserve/bin/linux-x64/bin/postgres',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
test('matches when metadata pgMajor and postgresPath agree with runtime', () => {
|
|
76
|
+
const meta = {
|
|
77
|
+
pgMajor: '18',
|
|
78
|
+
pgvectorVersion: '0.8.1-2',
|
|
79
|
+
postgresPath: '/home/user/.pgserve/bin/linux-x64/bin/postgres',
|
|
80
|
+
installedAt: '2026-04-10T18:00:00.000Z',
|
|
81
|
+
};
|
|
82
|
+
expect(pgvectorMetaMatches(meta, RUNTIME)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('matches when postgresPath is absent (older metadata format)', () => {
|
|
86
|
+
const meta = { pgMajor: '18', pgvectorVersion: '0.8.1-2' };
|
|
87
|
+
expect(pgvectorMetaMatches(meta, RUNTIME)).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('rejects when pgMajor differs — this is the PG17→PG18 regression we are healing', () => {
|
|
91
|
+
const stalePg17Meta = {
|
|
92
|
+
pgMajor: '17',
|
|
93
|
+
pgvectorVersion: '0.8.1-2',
|
|
94
|
+
postgresPath: '/home/user/.pgserve/bin/linux-x64/bin/postgres',
|
|
95
|
+
};
|
|
96
|
+
expect(pgvectorMetaMatches(stalePg17Meta, RUNTIME)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('rejects when postgresPath points at a different binary (pgserve upgraded)', () => {
|
|
100
|
+
const meta = {
|
|
101
|
+
pgMajor: '18',
|
|
102
|
+
postgresPath: '/opt/old-pgserve/bin/postgres',
|
|
103
|
+
};
|
|
104
|
+
expect(pgvectorMetaMatches(meta, RUNTIME)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('rejects null metadata (pre-auto-heal install without sidecar)', () => {
|
|
108
|
+
// This is the case that heals every existing broken deployment: they
|
|
109
|
+
// have vector.so on disk but no vector.meta.json, so match returns
|
|
110
|
+
// false → reinstall fires.
|
|
111
|
+
expect(pgvectorMetaMatches(null, RUNTIME)).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('rejects non-object metadata (corrupted sidecar)', () => {
|
|
115
|
+
expect(pgvectorMetaMatches('18', RUNTIME)).toBe(false);
|
|
116
|
+
expect(pgvectorMetaMatches(42, RUNTIME)).toBe(false);
|
|
117
|
+
expect(pgvectorMetaMatches([], RUNTIME)).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('rejects metadata missing pgMajor field', () => {
|
|
121
|
+
expect(pgvectorMetaMatches({ pgvectorVersion: '0.8.1' }, RUNTIME)).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('rejects metadata where pgMajor is not a string', () => {
|
|
125
|
+
// JSON could hand us a number — match must be strict about type to
|
|
126
|
+
// avoid `18 == '18'` false positives masking a corrupted file.
|
|
127
|
+
expect(pgvectorMetaMatches({ pgMajor: 18 }, RUNTIME)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|