@@ -485,6 +485,12 @@ end $$;`;
485485 sql : applyTemplate ( loadSqlTemplate ( "02.permissions.sql" ) , vars ) ,
486486 } ) ;
487487
488+ // Helper functions (SECURITY DEFINER) for plan analysis and table info
489+ steps . push ( {
490+ name : "05.helpers" ,
491+ sql : applyTemplate ( loadSqlTemplate ( "05.helpers.sql" ) , vars ) ,
492+ } ) ;
493+
488494 if ( params . includeOptionalPermissions ) {
489495 steps . push (
490496 {
@@ -511,78 +517,70 @@ export async function applyInitPlan(params: {
511517 const applied : string [ ] = [ ] ;
512518 const skippedOptional : string [ ] = [ ] ;
513519
514- // Apply non-optional steps in a single transaction.
515- await params . client . query ( "begin;" ) ;
516- try {
517- for ( const step of params . plan . steps . filter ( ( s ) => ! s . optional ) ) {
520+ // Helper to wrap a step execution in begin/commit
521+ const executeStep = async ( step : InitStep ) : Promise < void > => {
522+ await params . client . query ( "begin;" ) ;
523+ try {
524+ await params . client . query ( step . sql , step . params as any ) ;
525+ await params . client . query ( "commit;" ) ;
526+ } catch ( e ) {
527+ // Rollback errors should never mask the original failure.
518528 try {
519- await params . client . query ( step . sql , step . params as any ) ;
520- applied . push ( step . name ) ;
521- } catch ( e ) {
522- const msg = e instanceof Error ? e . message : String ( e ) ;
523- const errAny = e as any ;
524- const wrapped : any = new Error ( `Failed at step "${ step . name } ": ${ msg } ` ) ;
525- // Preserve useful Postgres error fields so callers can provide better hints / diagnostics.
526- const pgErrorFields = [
527- "code" ,
528- "detail" ,
529- "hint" ,
530- "position" ,
531- "internalPosition" ,
532- "internalQuery" ,
533- "where" ,
534- "schema" ,
535- "table" ,
536- "column" ,
537- "dataType" ,
538- "constraint" ,
539- "file" ,
540- "line" ,
541- "routine" ,
542- ] as const ;
543- if ( errAny && typeof errAny === "object" ) {
544- for ( const field of pgErrorFields ) {
545- if ( errAny [ field ] !== undefined ) wrapped [ field ] = errAny [ field ] ;
546- }
547- }
548- if ( e instanceof Error && e . stack ) {
549- wrapped . stack = e . stack ;
550- }
551- throw wrapped ;
529+ await params . client . query ( "rollback;" ) ;
530+ } catch {
531+ // ignore
552532 }
533+ throw e ;
553534 }
554- await params . client . query ( "commit;" ) ;
555- } catch ( e ) {
556- // Rollback errors should never mask the original failure.
535+ } ;
536+
537+ // Apply non-optional steps, each in its own transaction
538+ for ( const step of params . plan . steps . filter ( ( s ) => ! s . optional ) ) {
557539 try {
558- await params . client . query ( "rollback;" ) ;
559- } catch {
560- // ignore
540+ await executeStep ( step ) ;
541+ applied . push ( step . name ) ;
542+ } catch ( e ) {
543+ const msg = e instanceof Error ? e . message : String ( e ) ;
544+ const errAny = e as any ;
545+ const wrapped : any = new Error ( `Failed at step "${ step . name } ": ${ msg } ` ) ;
546+ // Preserve useful Postgres error fields so callers can provide better hints / diagnostics.
547+ const pgErrorFields = [
548+ "code" ,
549+ "detail" ,
550+ "hint" ,
551+ "position" ,
552+ "internalPosition" ,
553+ "internalQuery" ,
554+ "where" ,
555+ "schema" ,
556+ "table" ,
557+ "column" ,
558+ "dataType" ,
559+ "constraint" ,
560+ "file" ,
561+ "line" ,
562+ "routine" ,
563+ ] as const ;
564+ if ( errAny && typeof errAny === "object" ) {
565+ for ( const field of pgErrorFields ) {
566+ if ( errAny [ field ] !== undefined ) wrapped [ field ] = errAny [ field ] ;
567+ }
568+ }
569+ if ( e instanceof Error && e . stack ) {
570+ wrapped . stack = e . stack ;
571+ }
572+ throw wrapped ;
561573 }
562- throw e ;
563574 }
564575
565- // Apply optional steps outside of the transaction so a failure doesn't abort everything.
576+ // Apply optional steps, each in its own transaction ( failure doesn't abort)
566577 for ( const step of params . plan . steps . filter ( ( s ) => s . optional ) ) {
567578 try {
568- // Run each optional step in its own mini-transaction to avoid partial application.
569- await params . client . query ( "begin;" ) ;
570- try {
571- await params . client . query ( step . sql , step . params as any ) ;
572- await params . client . query ( "commit;" ) ;
573- applied . push ( step . name ) ;
574- } catch {
575- try {
576- await params . client . query ( "rollback;" ) ;
577- } catch {
578- // ignore rollback errors
579- }
580- skippedOptional . push ( step . name ) ;
581- // best-effort: ignore
582- }
579+ await executeStep ( step ) ;
580+ applied . push ( step . name ) ;
583581 } catch {
584- // If we can't even begin/commit, treat as skipped.
585582 skippedOptional . push ( step . name ) ;
583+ // best-effort: ignore
586584 }
587585 }
588586
@@ -642,16 +640,25 @@ export async function verifyInitSetup(params: {
642640 missingRequired . push ( "SELECT on pg_catalog.pg_index" ) ;
643641 }
644642
645- const viewExistsRes = await params . client . query ( "select to_regclass('public.pg_statistic') is not null as ok" ) ;
643+ // Check postgres_ai schema exists and is usable
644+ const schemaExistsRes = await params . client . query (
645+ "select has_schema_privilege($1, 'postgres_ai', 'USAGE') as ok" ,
646+ [ role ]
647+ ) ;
648+ if ( ! schemaExistsRes . rows ?. [ 0 ] ?. ok ) {
649+ missingRequired . push ( "USAGE on schema postgres_ai" ) ;
650+ }
651+
652+ const viewExistsRes = await params . client . query ( "select to_regclass('postgres_ai.pg_statistic') is not null as ok" ) ;
646653 if ( ! viewExistsRes . rows ?. [ 0 ] ?. ok ) {
647- missingRequired . push ( "view public .pg_statistic exists" ) ;
654+ missingRequired . push ( "view postgres_ai .pg_statistic exists" ) ;
648655 } else {
649656 const viewPrivRes = await params . client . query (
650- "select has_table_privilege($1, 'public .pg_statistic', 'SELECT') as ok" ,
657+ "select has_table_privilege($1, 'postgres_ai .pg_statistic', 'SELECT') as ok" ,
651658 [ role ]
652659 ) ;
653660 if ( ! viewPrivRes . rows ?. [ 0 ] ?. ok ) {
654- missingRequired . push ( "SELECT on view public .pg_statistic" ) ;
661+ missingRequired . push ( "SELECT on view postgres_ai .pg_statistic" ) ;
655662 }
656663 }
657664
@@ -669,13 +676,22 @@ export async function verifyInitSetup(params: {
669676 if ( typeof spLine !== "string" || ! spLine ) {
670677 missingRequired . push ( "role search_path is set" ) ;
671678 } else {
672- // We accept any ordering as long as public and pg_catalog are included.
679+ // We accept any ordering as long as postgres_ai, public, and pg_catalog are included.
673680 const sp = spLine . toLowerCase ( ) ;
674- if ( ! sp . includes ( "public" ) || ! sp . includes ( "pg_catalog" ) ) {
675- missingRequired . push ( "role search_path includes public and pg_catalog" ) ;
681+ if ( ! sp . includes ( "postgres_ai" ) || ! sp . includes ( " public") || ! sp . includes ( "pg_catalog" ) ) {
682+ missingRequired . push ( "role search_path includes postgres_ai, public and pg_catalog" ) ;
676683 }
677684 }
678685
686+ // Check for explain_generic helper function
687+ const explainFnRes = await params . client . query (
688+ "select has_function_privilege($1, 'postgres_ai.explain_generic(text, text, text)', 'EXECUTE') as ok" ,
689+ [ role ]
690+ ) ;
691+ if ( ! explainFnRes . rows ?. [ 0 ] ?. ok ) {
692+ missingRequired . push ( "EXECUTE on postgres_ai.explain_generic(text, text, text)" ) ;
693+ }
694+
679695 if ( params . includeOptionalPermissions ) {
680696 // Optional RDS/Aurora extras
681697 {
0 commit comments