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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgserve",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "description": "Embedded PostgreSQL server with true concurrent connections - zero config, auto-provision databases",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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
- const vectorSo = path.join(libDir, 'vector.so');
960
- const vectorControl = path.join(extDir, 'vector.control');
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
- // Already installed
963
- if (fs.existsSync(vectorSo) && fs.existsSync(vectorControl)) return;
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
- this.logger.info('pgvector extension files not found — downloading prebuilt binary...');
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
- // Detect PG major version from the postgres binary.
969
- // `postgres --version` output is `postgres (PostgreSQL) 18.2`, so the
970
- // regex must tolerate the `)` that separates the product name from the
971
- // version number. The previous pattern `/PostgreSQL (\d+)/` expected a
972
- // digit immediately after `PostgreSQL ` and silently fell back to '17'
973
- // on PG 14+, causing the wrong pgvector .deb to be downloaded and a
974
- // later "incompatible library version mismatch" at CREATE EXTENSION time.
975
- const { execSync } = await import('node:child_process');
976
- const pgVersion = execSync(`${this.binaries.postgres} --version`, { encoding: 'utf-8' }).trim();
977
- const majorMatch = pgVersion.match(/PostgreSQL\)?\s+(\d+)/);
978
- if (!majorMatch) {
979
- throw new Error(
980
- `Could not detect PostgreSQL major version from: ${JSON.stringify(pgVersion)}`
981
- );
982
- }
983
- const pgMajor = majorMatch[1];
984
- this.logger.info({ pgMajor, pgVersion }, 'Detected PostgreSQL major version for pgvector install');
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
- // Download prebuilt pgvector .deb from apt.postgresql.org (HTTPS)
997
- // Version 0.8.1-2 update when new releases ship
998
- const debUrl = `https://apt.postgresql.org/pub/repos/apt/pool/main/p/pgvector/postgresql-${pgMajor}-pgvector_0.8.1-2.pgdg%2B1_${arch}.deb`;
999
- this.logger.info({ url: debUrl }, 'Downloading pgvector...');
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
- const res = await fetch(debUrl);
1002
- if (!res.ok) throw new Error(`Download failed: ${res.status}`);
1131
+ const res = await fetch(debUrl);
1132
+ if (!res.ok) throw new Error(`Download failed: ${res.status}`);
1003
1133
 
1004
- const buffer = Buffer.from(await res.arrayBuffer());
1134
+ const buffer = Buffer.from(await res.arrayBuffer());
1005
1135
 
1006
- // Extract .deb (it's an ar archive containing data.tar.xz)
1007
- const tmpDir = path.join(os.tmpdir(), `pgserve-pgvector-${process.pid}-${Date.now()}`);
1008
- fs.mkdirSync(tmpDir, { recursive: true });
1009
- const debPath = path.join(tmpDir, 'pgvector.deb');
1010
- fs.writeFileSync(debPath, buffer);
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
- fs.copyFileSync(soSrc, vectorSo);
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
- // Cleanup
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
- try {
1073
- // Create temporary connection to the specific database
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
- // Enable pgvector extension
1086
- await dbPool.unsafe('CREATE EXTENSION IF NOT EXISTS vector');
1249
+ try {
1250
+ await tryCreateExtension();
1087
1251
  this.logger.info({ dbName }, 'pgvector extension enabled');
1252
+ return;
1088
1253
  } catch (error) {
1089
- // Log but don't fail database creation - pgvector might not be available
1090
- this.logger.warn({ dbName, err: error.message }, 'Failed to enable pgvector extension (non-fatal)');
1091
- } finally {
1092
- // Always close the temporary connection
1093
- if (dbPool) {
1094
- await dbPool.close().catch(() => {});
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 `ensurePgvectorFiles()` in src/postgres.js
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
+ });