|
2 | 2 | -- These functions use SECURITY DEFINER to allow the monitoring user to perform |
3 | 3 | -- operations they don't have direct permissions for. |
4 | 4 |
|
5 | | -/* |
6 | | - * explain_generic |
7 | | - * |
8 | | - * Function to get generic explain plans with optional HypoPG index testing. |
9 | | - * Requires: PostgreSQL 16+ (for generic_plan option), HypoPG extension (optional). |
10 | | - * |
11 | | - * Security notes: |
12 | | - * - EXPLAIN without ANALYZE is read-only (plans but doesn't execute the query) |
13 | | - * - PostgreSQL's EXPLAIN only accepts a single statement (primary protection) |
14 | | - * - Input validation uses a simple heuristic to detect multiple statements |
15 | | - * (Note: may reject valid queries containing semicolons in string literals) |
16 | | - * |
17 | | - * Usage examples: |
18 | | - * -- Basic generic plan |
19 | | - * select postgres_ai.explain_generic('select * from users where id = $1'); |
20 | | - * |
21 | | - * -- JSON format |
22 | | - * select postgres_ai.explain_generic('select * from users where id = $1', 'json'); |
23 | | - * |
24 | | - * -- Test a hypothetical index |
25 | | - * select postgres_ai.explain_generic( |
26 | | - * 'select * from users where email = $1', |
27 | | - * 'text', |
28 | | - * 'create index on users (email)' |
29 | | - * ); |
30 | | - */ |
31 | | -create or replace function postgres_ai.explain_generic( |
32 | | - in query text, |
33 | | - in format text default 'text', |
34 | | - in hypopg_index text default null, |
35 | | - out result text |
36 | | -) |
37 | | -language plpgsql |
38 | | -security definer |
39 | | -set search_path = pg_catalog, public |
40 | | -as $$ |
41 | | -declare |
42 | | - v_line record; |
43 | | - v_lines text[] := '{}'; |
44 | | - v_explain_query text; |
45 | | - v_hypo_result record; |
46 | | - v_version int; |
47 | | - v_hypopg_available boolean; |
48 | | - v_clean_query text; |
49 | | -begin |
50 | | - -- Check PostgreSQL version (generic_plan requires 16+) |
51 | | - select current_setting('server_version_num')::int into v_version; |
52 | | - |
53 | | - if v_version < 160000 then |
54 | | - raise exception 'generic_plan requires PostgreSQL 16+, current version: %', |
55 | | - current_setting('server_version'); |
56 | | - end if; |
57 | | - |
58 | | - -- Input validation: reject empty queries |
59 | | - if query is null or trim(query) = '' then |
60 | | - raise exception 'query cannot be empty'; |
61 | | - end if; |
62 | | - |
63 | | - -- Input validation: detect multiple statements (defense-in-depth) |
64 | | - -- Note: This is a simple heuristic - EXPLAIN itself only accepts single statements |
65 | | - -- Limitation: Queries with semicolons inside string literals will be rejected |
66 | | - v_clean_query := trim(query); |
67 | | - if v_clean_query like '%;%' then |
68 | | - -- Strip trailing semicolon if present (common user convenience) |
69 | | - v_clean_query := regexp_replace(v_clean_query, ';\s*$', ''); |
70 | | - -- If there's still a semicolon, reject (likely multiple statements or semicolon in string) |
71 | | - if v_clean_query like '%;%' then |
72 | | - raise exception 'query contains semicolon (multiple statements not allowed; note: semicolons in string literals are also not supported)'; |
73 | | - end if; |
74 | | - end if; |
75 | | - |
76 | | - -- Check if HypoPG extension is available |
77 | | - if hypopg_index is not null then |
78 | | - select exists( |
79 | | - select 1 from pg_extension where extname = 'hypopg' |
80 | | - ) into v_hypopg_available; |
81 | | - |
82 | | - if not v_hypopg_available then |
83 | | - raise exception 'HypoPG extension is required for hypothetical index testing but is not installed'; |
84 | | - end if; |
85 | | - |
86 | | - -- Create hypothetical index |
87 | | - select * into v_hypo_result from hypopg_create_index(hypopg_index); |
88 | | - raise notice 'Created hypothetical index: % (oid: %)', |
89 | | - v_hypo_result.indexname, v_hypo_result.indexrelid; |
90 | | - end if; |
91 | | - |
92 | | - -- Build and execute EXPLAIN query |
93 | | - -- Note: EXPLAIN is read-only (plans but doesn't execute), making this safe |
94 | | - begin |
95 | | - if lower(format) = 'json' then |
96 | | - execute 'explain (verbose, settings, generic_plan, format json) ' || v_clean_query |
97 | | - into result; |
98 | | - else |
99 | | - for v_line in execute 'explain (verbose, settings, generic_plan) ' || v_clean_query loop |
100 | | - v_lines := array_append(v_lines, v_line."QUERY PLAN"); |
101 | | - end loop; |
102 | | - result := array_to_string(v_lines, e'\n'); |
103 | | - end if; |
104 | | - exception when others then |
105 | | - -- Clean up hypothetical index before re-raising |
106 | | - if hypopg_index is not null then |
107 | | - perform hypopg_reset(); |
108 | | - end if; |
109 | | - raise; |
110 | | - end; |
111 | | - |
112 | | - -- Clean up hypothetical index |
113 | | - if hypopg_index is not null then |
114 | | - perform hypopg_reset(); |
115 | | - end if; |
116 | | -end; |
117 | | -$$; |
118 | | - |
119 | | -comment on function postgres_ai.explain_generic(text, text, text) is |
120 | | - 'Returns generic EXPLAIN plan with optional HypoPG index testing (requires PG16+)'; |
121 | | - |
122 | | --- Grant execute to the monitoring user |
123 | | -grant execute on function postgres_ai.explain_generic(text, text, text) to {{ROLE_IDENT}}; |
124 | | - |
125 | 5 | /* |
126 | 6 | * table_describe |
127 | 7 | * |
@@ -435,5 +315,3 @@ comment on function postgres_ai.table_describe(text) is |
435 | 315 | 'Returns comprehensive table information in compact text format for LLM analysis'; |
436 | 316 |
|
437 | 317 | grant execute on function postgres_ai.table_describe(text) to {{ROLE_IDENT}}; |
438 | | - |
439 | | - |
0 commit comments