postgresai 0.14.0-beta.3 → 0.14.0-beta.5

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.
Files changed (77) hide show
  1. package/README.md +76 -48
  2. package/bin/postgres-ai.ts +1161 -341
  3. package/bun.lock +258 -0
  4. package/bunfig.toml +19 -0
  5. package/dist/bin/postgres-ai.js +28499 -1771
  6. package/dist/sql/01.role.sql +16 -0
  7. package/dist/sql/02.permissions.sql +37 -0
  8. package/dist/sql/03.optional_rds.sql +6 -0
  9. package/dist/sql/04.optional_self_managed.sql +8 -0
  10. package/dist/sql/05.helpers.sql +439 -0
  11. package/dist/sql/sql/01.role.sql +16 -0
  12. package/dist/sql/sql/02.permissions.sql +37 -0
  13. package/dist/sql/sql/03.optional_rds.sql +6 -0
  14. package/dist/sql/sql/04.optional_self_managed.sql +8 -0
  15. package/dist/sql/sql/05.helpers.sql +439 -0
  16. package/lib/auth-server.ts +124 -106
  17. package/lib/checkup-api.ts +386 -0
  18. package/lib/checkup.ts +1330 -0
  19. package/lib/config.ts +6 -3
  20. package/lib/init.ts +289 -79
  21. package/lib/issues.ts +400 -191
  22. package/lib/mcp-server.ts +213 -90
  23. package/lib/metrics-embedded.ts +79 -0
  24. package/lib/metrics-loader.ts +127 -0
  25. package/lib/util.ts +61 -0
  26. package/package.json +20 -10
  27. package/packages/postgres-ai/README.md +26 -0
  28. package/packages/postgres-ai/bin/postgres-ai.js +27 -0
  29. package/packages/postgres-ai/package.json +27 -0
  30. package/scripts/embed-metrics.ts +154 -0
  31. package/sql/02.permissions.sql +9 -5
  32. package/sql/05.helpers.sql +439 -0
  33. package/test/auth.test.ts +258 -0
  34. package/test/checkup.integration.test.ts +319 -0
  35. package/test/checkup.test.ts +891 -0
  36. package/test/init.integration.test.ts +497 -0
  37. package/test/init.test.ts +417 -0
  38. package/test/issues.cli.test.ts +314 -0
  39. package/test/issues.test.ts +456 -0
  40. package/test/mcp-server.test.ts +988 -0
  41. package/test/schema-validation.test.ts +81 -0
  42. package/test/test-utils.ts +122 -0
  43. package/tsconfig.json +12 -20
  44. package/dist/bin/postgres-ai.d.ts +0 -3
  45. package/dist/bin/postgres-ai.d.ts.map +0 -1
  46. package/dist/bin/postgres-ai.js.map +0 -1
  47. package/dist/lib/auth-server.d.ts +0 -31
  48. package/dist/lib/auth-server.d.ts.map +0 -1
  49. package/dist/lib/auth-server.js +0 -263
  50. package/dist/lib/auth-server.js.map +0 -1
  51. package/dist/lib/config.d.ts +0 -45
  52. package/dist/lib/config.d.ts.map +0 -1
  53. package/dist/lib/config.js +0 -181
  54. package/dist/lib/config.js.map +0 -1
  55. package/dist/lib/init.d.ts +0 -75
  56. package/dist/lib/init.d.ts.map +0 -1
  57. package/dist/lib/init.js +0 -482
  58. package/dist/lib/init.js.map +0 -1
  59. package/dist/lib/issues.d.ts +0 -75
  60. package/dist/lib/issues.d.ts.map +0 -1
  61. package/dist/lib/issues.js +0 -336
  62. package/dist/lib/issues.js.map +0 -1
  63. package/dist/lib/mcp-server.d.ts +0 -9
  64. package/dist/lib/mcp-server.d.ts.map +0 -1
  65. package/dist/lib/mcp-server.js +0 -168
  66. package/dist/lib/mcp-server.js.map +0 -1
  67. package/dist/lib/pkce.d.ts +0 -32
  68. package/dist/lib/pkce.d.ts.map +0 -1
  69. package/dist/lib/pkce.js +0 -101
  70. package/dist/lib/pkce.js.map +0 -1
  71. package/dist/lib/util.d.ts +0 -27
  72. package/dist/lib/util.d.ts.map +0 -1
  73. package/dist/lib/util.js +0 -46
  74. package/dist/lib/util.js.map +0 -1
  75. package/dist/package.json +0 -46
  76. package/test/init.integration.test.cjs +0 -382
  77. package/test/init.test.cjs +0 -323
package/lib/config.ts CHANGED
@@ -9,6 +9,7 @@ export interface Config {
9
9
  apiKey: string | null;
10
10
  baseUrl: string | null;
11
11
  orgId: number | null;
12
+ defaultProject: string | null;
12
13
  }
13
14
 
14
15
  /**
@@ -46,6 +47,7 @@ export function readConfig(): Config {
46
47
  apiKey: null,
47
48
  baseUrl: null,
48
49
  orgId: null,
50
+ defaultProject: null,
49
51
  };
50
52
 
51
53
  // Try user-level config first
@@ -54,9 +56,10 @@ export function readConfig(): Config {
54
56
  try {
55
57
  const content = fs.readFileSync(userConfigPath, "utf8");
56
58
  const parsed = JSON.parse(content);
57
- config.apiKey = parsed.apiKey || null;
58
- config.baseUrl = parsed.baseUrl || null;
59
- config.orgId = parsed.orgId || null;
59
+ config.apiKey = parsed.apiKey ?? null;
60
+ config.baseUrl = parsed.baseUrl ?? null;
61
+ config.orgId = parsed.orgId ?? null;
62
+ config.defaultProject = parsed.defaultProject ?? null;
60
63
  return config;
61
64
  } catch (err) {
62
65
  const message = err instanceof Error ? err.message : String(err);
package/lib/init.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "crypto";
2
- import { URL } from "url";
2
+ import { URL, fileURLToPath } from "url";
3
3
  import type { ConnectionOptions as TlsConnectionOptions } from "tls";
4
4
  import type { Client as PgClient } from "pg";
5
5
  import * as fs from "fs";
@@ -17,11 +17,135 @@ export type PgClientConfig = {
17
17
  ssl?: boolean | TlsConnectionOptions;
18
18
  };
19
19
 
20
+ /**
21
+ * Convert PostgreSQL sslmode to node-postgres ssl config.
22
+ */
23
+ function sslModeToConfig(mode: string): boolean | TlsConnectionOptions {
24
+ if (mode.toLowerCase() === "disable") return false;
25
+ if (mode.toLowerCase() === "verify-full" || mode.toLowerCase() === "verify-ca") return true;
26
+ // For require/prefer/allow: encrypt without certificate verification
27
+ return { rejectUnauthorized: false };
28
+ }
29
+
30
+ /** Extract sslmode from a PostgreSQL connection URI. */
31
+ function extractSslModeFromUri(uri: string): string | undefined {
32
+ try {
33
+ return new URL(uri).searchParams.get("sslmode") ?? undefined;
34
+ } catch {
35
+ return uri.match(/[?&]sslmode=([^&]+)/i)?.[1];
36
+ }
37
+ }
38
+
39
+ /** Remove sslmode parameter from a PostgreSQL connection URI. */
40
+ function stripSslModeFromUri(uri: string): string {
41
+ try {
42
+ const u = new URL(uri);
43
+ u.searchParams.delete("sslmode");
44
+ return u.toString();
45
+ } catch {
46
+ // Fallback regex for malformed URIs
47
+ return uri
48
+ .replace(/[?&]sslmode=[^&]*/gi, "")
49
+ .replace(/\?&/, "?")
50
+ .replace(/\?$/, "");
51
+ }
52
+ }
53
+
20
54
  export type AdminConnection = {
21
55
  clientConfig: PgClientConfig;
22
56
  display: string;
57
+ /** True if SSL fallback is enabled (try SSL first, fall back to non-SSL on failure). */
58
+ sslFallbackEnabled?: boolean;
23
59
  };
24
60
 
61
+ /**
62
+ * Check if an error indicates SSL negotiation failed and fallback to non-SSL should be attempted.
63
+ * This mimics libpq's sslmode=prefer behavior.
64
+ *
65
+ * IMPORTANT: This should NOT match certificate errors (expired, invalid, self-signed)
66
+ * as those are real errors the user needs to fix, not negotiation failures.
67
+ */
68
+ function isSslNegotiationError(err: unknown): boolean {
69
+ if (!err || typeof err !== "object") return false;
70
+ const e = err as any;
71
+ const msg = typeof e.message === "string" ? e.message.toLowerCase() : "";
72
+ const code = typeof e.code === "string" ? e.code : "";
73
+
74
+ // Specific patterns that indicate server doesn't support SSL (should fallback)
75
+ const fallbackPatterns = [
76
+ "the server does not support ssl",
77
+ "ssl off",
78
+ "server does not support ssl connections",
79
+ ];
80
+
81
+ for (const pattern of fallbackPatterns) {
82
+ if (msg.includes(pattern)) return true;
83
+ }
84
+
85
+ // PostgreSQL error code 08P01 (protocol violation) during initial connection
86
+ // often indicates SSL negotiation mismatch, but only if the message suggests it
87
+ if (code === "08P01" && (msg.includes("ssl") || msg.includes("unsupported"))) {
88
+ return true;
89
+ }
90
+
91
+ return false;
92
+ }
93
+
94
+ /**
95
+ * Connect to PostgreSQL with sslmode=prefer-like behavior.
96
+ * If sslFallbackEnabled is true, tries SSL first, then falls back to non-SSL on failure.
97
+ */
98
+ export async function connectWithSslFallback(
99
+ ClientClass: new (config: PgClientConfig) => PgClient,
100
+ adminConn: AdminConnection,
101
+ verbose?: boolean
102
+ ): Promise<{ client: PgClient; usedSsl: boolean }> {
103
+ const tryConnect = async (config: PgClientConfig): Promise<PgClient> => {
104
+ const client = new ClientClass(config);
105
+ await client.connect();
106
+ return client;
107
+ };
108
+
109
+ // If SSL was explicitly set or no SSL configured, just try once
110
+ if (!adminConn.sslFallbackEnabled) {
111
+ const client = await tryConnect(adminConn.clientConfig);
112
+ return { client, usedSsl: !!adminConn.clientConfig.ssl };
113
+ }
114
+
115
+ // sslmode=prefer behavior: try SSL first, fallback to non-SSL
116
+ try {
117
+ const client = await tryConnect(adminConn.clientConfig);
118
+ return { client, usedSsl: true };
119
+ } catch (sslErr) {
120
+ if (!isSslNegotiationError(sslErr)) {
121
+ // Not an SSL error, don't retry
122
+ throw sslErr;
123
+ }
124
+
125
+ if (verbose) {
126
+ console.log("SSL connection failed, retrying without SSL...");
127
+ }
128
+
129
+ // Retry without SSL
130
+ const noSslConfig: PgClientConfig = { ...adminConn.clientConfig, ssl: false };
131
+ try {
132
+ const client = await tryConnect(noSslConfig);
133
+ return { client, usedSsl: false };
134
+ } catch (noSslErr) {
135
+ // If non-SSL also fails, check if it's "SSL required" - throw that instead
136
+ if (isSslNegotiationError(noSslErr)) {
137
+ const msg = (noSslErr as any)?.message || "";
138
+ if (msg.toLowerCase().includes("ssl") && msg.toLowerCase().includes("required")) {
139
+ // Server requires SSL but SSL attempt failed - throw original SSL error
140
+ throw sslErr;
141
+ }
142
+ }
143
+ // Throw the non-SSL error (it's more relevant since SSL attempt also failed)
144
+ throw noSslErr;
145
+ }
146
+ }
147
+ }
148
+
25
149
  export type InitStep = {
26
150
  name: string;
27
151
  sql: string;
@@ -35,13 +159,27 @@ export type InitPlan = {
35
159
  steps: InitStep[];
36
160
  };
37
161
 
38
- function packageRootDirFromCompiled(): string {
39
- // dist/lib/init.js -> <pkg>/dist/lib ; package root is ../..
40
- return path.resolve(__dirname, "..", "..");
41
- }
42
-
43
162
  function sqlDir(): string {
44
- return path.join(packageRootDirFromCompiled(), "sql");
163
+ // Handle both development and production paths
164
+ // Development: lib/init.ts -> ../sql
165
+ // Production (bundled): dist/bin/postgres-ai.js -> ../sql (copied during build)
166
+ //
167
+ // IMPORTANT: Use import.meta.url instead of __dirname because bundlers (bun/esbuild)
168
+ // bake in __dirname at build time, while import.meta.url resolves at runtime.
169
+ const currentFile = fileURLToPath(import.meta.url);
170
+ const currentDir = path.dirname(currentFile);
171
+
172
+ const candidates = [
173
+ path.resolve(currentDir, "..", "sql"), // bundled: dist/bin -> dist/sql
174
+ path.resolve(currentDir, "..", "..", "sql"), // dev from lib: lib -> ../sql
175
+ ];
176
+
177
+ for (const candidate of candidates) {
178
+ if (fs.existsSync(candidate)) {
179
+ return candidate;
180
+ }
181
+ }
182
+ throw new Error(`SQL directory not found. Searched: ${candidates.join(", ")}`);
45
183
  }
46
184
 
47
185
  function loadSqlTemplate(filename: string): string {
@@ -142,6 +280,7 @@ function tokenizeConninfo(input: string): string[] {
142
280
  export function parseLibpqConninfo(input: string): PgClientConfig {
143
281
  const tokens = tokenizeConninfo(input);
144
282
  const cfg: PgClientConfig = {};
283
+ let sslmode: string | undefined;
145
284
 
146
285
  for (const t of tokens) {
147
286
  const eq = t.indexOf("=");
@@ -170,12 +309,20 @@ export function parseLibpqConninfo(input: string): PgClientConfig {
170
309
  case "database":
171
310
  cfg.database = val;
172
311
  break;
173
- // ignore everything else (sslmode, options, application_name, etc.)
312
+ case "sslmode":
313
+ sslmode = val;
314
+ break;
315
+ // ignore everything else (options, application_name, etc.)
174
316
  default:
175
317
  break;
176
318
  }
177
319
  }
178
320
 
321
+ // Apply SSL configuration based on sslmode
322
+ if (sslmode) {
323
+ cfg.ssl = sslModeToConfig(sslmode);
324
+ }
325
+
179
326
  return cfg;
180
327
  }
181
328
 
@@ -202,6 +349,9 @@ export function resolveAdminConnection(opts: {
202
349
  const conn = (opts.conn || "").trim();
203
350
  const dbUrlFlag = (opts.dbUrlFlag || "").trim();
204
351
 
352
+ // Resolve explicit SSL setting from environment (undefined = auto-detect)
353
+ const explicitSsl = process.env.PGSSLMODE;
354
+
205
355
  // NOTE: passwords alone (PGPASSWORD / --admin-password) do NOT constitute a connection.
206
356
  // We require at least some connection addressing (host/port/user/db) if no positional arg / --db-url is provided.
207
357
  const hasConnDetails = !!(opts.host || opts.port || opts.username || opts.dbname);
@@ -213,12 +363,40 @@ export function resolveAdminConnection(opts: {
213
363
  if (conn || dbUrlFlag) {
214
364
  const v = conn || dbUrlFlag;
215
365
  if (isLikelyUri(v)) {
216
- return { clientConfig: { connectionString: v }, display: maskConnectionString(v) };
366
+ const urlSslMode = extractSslModeFromUri(v);
367
+ const effectiveSslMode = explicitSsl || urlSslMode;
368
+ // SSL priority: PGSSLMODE env > URL param > auto (sslmode=prefer behavior)
369
+ const sslConfig = effectiveSslMode
370
+ ? sslModeToConfig(effectiveSslMode)
371
+ : { rejectUnauthorized: false }; // Default: try SSL (with fallback)
372
+ // Enable fallback for: no explicit mode OR explicit "prefer"/"allow"
373
+ const shouldFallback = !effectiveSslMode ||
374
+ effectiveSslMode.toLowerCase() === "prefer" ||
375
+ effectiveSslMode.toLowerCase() === "allow";
376
+ // Strip sslmode from URI so pg uses our ssl config object instead
377
+ const cleanUri = stripSslModeFromUri(v);
378
+ return {
379
+ clientConfig: { connectionString: cleanUri, ssl: sslConfig },
380
+ display: maskConnectionString(v),
381
+ sslFallbackEnabled: shouldFallback,
382
+ };
217
383
  }
218
384
  // libpq conninfo (dbname=... host=...)
219
385
  const cfg = parseLibpqConninfo(v);
220
386
  if (opts.envPassword && !cfg.password) cfg.password = opts.envPassword;
221
- return { clientConfig: cfg, display: describePgConfig(cfg) };
387
+ const cfgHadSsl = cfg.ssl !== undefined;
388
+ if (cfg.ssl === undefined) {
389
+ if (explicitSsl) cfg.ssl = sslModeToConfig(explicitSsl);
390
+ else cfg.ssl = { rejectUnauthorized: false }; // Default: try SSL (with fallback)
391
+ }
392
+ // Enable fallback for: no explicit mode OR explicit "prefer"/"allow"
393
+ const shouldFallback = (!explicitSsl && !cfgHadSsl) ||
394
+ (!!explicitSsl && (explicitSsl.toLowerCase() === "prefer" || explicitSsl.toLowerCase() === "allow"));
395
+ return {
396
+ clientConfig: cfg,
397
+ display: describePgConfig(cfg),
398
+ sslFallbackEnabled: shouldFallback,
399
+ };
222
400
  }
223
401
 
224
402
  if (!hasConnDetails) {
@@ -239,7 +417,15 @@ export function resolveAdminConnection(opts: {
239
417
  if (opts.dbname) cfg.database = opts.dbname;
240
418
  if (opts.adminPassword) cfg.password = opts.adminPassword;
241
419
  if (opts.envPassword && !cfg.password) cfg.password = opts.envPassword;
242
- return { clientConfig: cfg, display: describePgConfig(cfg) };
420
+ if (explicitSsl) {
421
+ cfg.ssl = sslModeToConfig(explicitSsl);
422
+ // Enable fallback for explicit "prefer"/"allow"
423
+ const shouldFallback = explicitSsl.toLowerCase() === "prefer" || explicitSsl.toLowerCase() === "allow";
424
+ return { clientConfig: cfg, display: describePgConfig(cfg), sslFallbackEnabled: shouldFallback };
425
+ }
426
+ // Default: try SSL with fallback (sslmode=prefer behavior)
427
+ cfg.ssl = { rejectUnauthorized: false };
428
+ return { clientConfig: cfg, display: describePgConfig(cfg), sslFallbackEnabled: true };
243
429
  }
244
430
 
245
431
  function generateMonitoringPassword(): string {
@@ -313,6 +499,12 @@ end $$;`;
313
499
  sql: applyTemplate(loadSqlTemplate("02.permissions.sql"), vars),
314
500
  });
315
501
 
502
+ // Helper functions (SECURITY DEFINER) for plan analysis and table info
503
+ steps.push({
504
+ name: "05.helpers",
505
+ sql: applyTemplate(loadSqlTemplate("05.helpers.sql"), vars),
506
+ });
507
+
316
508
  if (params.includeOptionalPermissions) {
317
509
  steps.push(
318
510
  {
@@ -339,78 +531,70 @@ export async function applyInitPlan(params: {
339
531
  const applied: string[] = [];
340
532
  const skippedOptional: string[] = [];
341
533
 
342
- // Apply non-optional steps in a single transaction.
343
- await params.client.query("begin;");
344
- try {
345
- for (const step of params.plan.steps.filter((s) => !s.optional)) {
534
+ // Helper to wrap a step execution in begin/commit
535
+ const executeStep = async (step: InitStep): Promise<void> => {
536
+ await params.client.query("begin;");
537
+ try {
538
+ await params.client.query(step.sql, step.params as any);
539
+ await params.client.query("commit;");
540
+ } catch (e) {
541
+ // Rollback errors should never mask the original failure.
346
542
  try {
347
- await params.client.query(step.sql, step.params as any);
348
- applied.push(step.name);
349
- } catch (e) {
350
- const msg = e instanceof Error ? e.message : String(e);
351
- const errAny = e as any;
352
- const wrapped: any = new Error(`Failed at step "${step.name}": ${msg}`);
353
- // Preserve useful Postgres error fields so callers can provide better hints / diagnostics.
354
- const pgErrorFields = [
355
- "code",
356
- "detail",
357
- "hint",
358
- "position",
359
- "internalPosition",
360
- "internalQuery",
361
- "where",
362
- "schema",
363
- "table",
364
- "column",
365
- "dataType",
366
- "constraint",
367
- "file",
368
- "line",
369
- "routine",
370
- ] as const;
371
- if (errAny && typeof errAny === "object") {
372
- for (const field of pgErrorFields) {
373
- if (errAny[field] !== undefined) wrapped[field] = errAny[field];
374
- }
375
- }
376
- if (e instanceof Error && e.stack) {
377
- wrapped.stack = e.stack;
378
- }
379
- throw wrapped;
543
+ await params.client.query("rollback;");
544
+ } catch {
545
+ // ignore
380
546
  }
547
+ throw e;
381
548
  }
382
- await params.client.query("commit;");
383
- } catch (e) {
384
- // Rollback errors should never mask the original failure.
549
+ };
550
+
551
+ // Apply non-optional steps, each in its own transaction
552
+ for (const step of params.plan.steps.filter((s) => !s.optional)) {
385
553
  try {
386
- await params.client.query("rollback;");
387
- } catch {
388
- // ignore
554
+ await executeStep(step);
555
+ applied.push(step.name);
556
+ } catch (e) {
557
+ const msg = e instanceof Error ? e.message : String(e);
558
+ const errAny = e as any;
559
+ const wrapped: any = new Error(`Failed at step "${step.name}": ${msg}`);
560
+ // Preserve useful Postgres error fields so callers can provide better hints / diagnostics.
561
+ const pgErrorFields = [
562
+ "code",
563
+ "detail",
564
+ "hint",
565
+ "position",
566
+ "internalPosition",
567
+ "internalQuery",
568
+ "where",
569
+ "schema",
570
+ "table",
571
+ "column",
572
+ "dataType",
573
+ "constraint",
574
+ "file",
575
+ "line",
576
+ "routine",
577
+ ] as const;
578
+ if (errAny && typeof errAny === "object") {
579
+ for (const field of pgErrorFields) {
580
+ if (errAny[field] !== undefined) wrapped[field] = errAny[field];
581
+ }
582
+ }
583
+ if (e instanceof Error && e.stack) {
584
+ wrapped.stack = e.stack;
585
+ }
586
+ throw wrapped;
389
587
  }
390
- throw e;
391
588
  }
392
589
 
393
- // Apply optional steps outside of the transaction so a failure doesn't abort everything.
590
+ // Apply optional steps, each in its own transaction (failure doesn't abort)
394
591
  for (const step of params.plan.steps.filter((s) => s.optional)) {
395
592
  try {
396
- // Run each optional step in its own mini-transaction to avoid partial application.
397
- await params.client.query("begin;");
398
- try {
399
- await params.client.query(step.sql, step.params as any);
400
- await params.client.query("commit;");
401
- applied.push(step.name);
402
- } catch {
403
- try {
404
- await params.client.query("rollback;");
405
- } catch {
406
- // ignore rollback errors
407
- }
408
- skippedOptional.push(step.name);
409
- // best-effort: ignore
410
- }
593
+ await executeStep(step);
594
+ applied.push(step.name);
411
595
  } catch {
412
- // If we can't even begin/commit, treat as skipped.
413
596
  skippedOptional.push(step.name);
597
+ // best-effort: ignore
414
598
  }
415
599
  }
416
600
 
@@ -470,16 +654,25 @@ export async function verifyInitSetup(params: {
470
654
  missingRequired.push("SELECT on pg_catalog.pg_index");
471
655
  }
472
656
 
473
- const viewExistsRes = await params.client.query("select to_regclass('public.pg_statistic') is not null as ok");
657
+ // Check postgres_ai schema exists and is usable
658
+ const schemaExistsRes = await params.client.query(
659
+ "select has_schema_privilege($1, 'postgres_ai', 'USAGE') as ok",
660
+ [role]
661
+ );
662
+ if (!schemaExistsRes.rows?.[0]?.ok) {
663
+ missingRequired.push("USAGE on schema postgres_ai");
664
+ }
665
+
666
+ const viewExistsRes = await params.client.query("select to_regclass('postgres_ai.pg_statistic') is not null as ok");
474
667
  if (!viewExistsRes.rows?.[0]?.ok) {
475
- missingRequired.push("view public.pg_statistic exists");
668
+ missingRequired.push("view postgres_ai.pg_statistic exists");
476
669
  } else {
477
670
  const viewPrivRes = await params.client.query(
478
- "select has_table_privilege($1, 'public.pg_statistic', 'SELECT') as ok",
671
+ "select has_table_privilege($1, 'postgres_ai.pg_statistic', 'SELECT') as ok",
479
672
  [role]
480
673
  );
481
674
  if (!viewPrivRes.rows?.[0]?.ok) {
482
- missingRequired.push("SELECT on view public.pg_statistic");
675
+ missingRequired.push("SELECT on view postgres_ai.pg_statistic");
483
676
  }
484
677
  }
485
678
 
@@ -497,13 +690,30 @@ export async function verifyInitSetup(params: {
497
690
  if (typeof spLine !== "string" || !spLine) {
498
691
  missingRequired.push("role search_path is set");
499
692
  } else {
500
- // We accept any ordering as long as public and pg_catalog are included.
693
+ // We accept any ordering as long as postgres_ai, public, and pg_catalog are included.
501
694
  const sp = spLine.toLowerCase();
502
- if (!sp.includes("public") || !sp.includes("pg_catalog")) {
503
- missingRequired.push("role search_path includes public and pg_catalog");
695
+ if (!sp.includes("postgres_ai") || !sp.includes("public") || !sp.includes("pg_catalog")) {
696
+ missingRequired.push("role search_path includes postgres_ai, public and pg_catalog");
504
697
  }
505
698
  }
506
699
 
700
+ // Check for helper functions
701
+ const explainFnRes = await params.client.query(
702
+ "select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok",
703
+ [role]
704
+ );
705
+ if (!explainFnRes.rows?.[0]?.ok) {
706
+ missingRequired.push("EXECUTE on postgres_ai.explain_generic(text, text, text)");
707
+ }
708
+
709
+ const tableDescribeFnRes = await params.client.query(
710
+ "select has_function_privilege($1, 'postgres_ai.table_describe(text)', 'EXECUTE') as ok",
711
+ [role]
712
+ );
713
+ if (!tableDescribeFnRes.rows?.[0]?.ok) {
714
+ missingRequired.push("EXECUTE on postgres_ai.table_describe(text)");
715
+ }
716
+
507
717
  if (params.includeOptionalPermissions) {
508
718
  // Optional RDS/Aurora extras
509
719
  {