postgresai 0.14.0-dev.51 → 0.14.0-dev.53

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.
@@ -0,0 +1,16 @@
1
+ -- Role creation / password update (template-filled by cli/lib/init.ts)
2
+ --
3
+ -- Always uses a race-safe pattern (create if missing, then always alter to set the password):
4
+ -- do $$ begin
5
+ -- if not exists (select 1 from pg_catalog.pg_roles where rolname = '...') then
6
+ -- begin
7
+ -- create user "..." with password '...';
8
+ -- exception when duplicate_object then
9
+ -- null;
10
+ -- end;
11
+ -- end if;
12
+ -- alter user "..." with password '...';
13
+ -- end $$;
14
+ {{ROLE_STMT}}
15
+
16
+
@@ -0,0 +1,37 @@
1
+ -- Required permissions for postgres_ai monitoring user (template-filled by cli/lib/init.ts)
2
+
3
+ -- Allow connect
4
+ grant connect on database {{DB_IDENT}} to {{ROLE_IDENT}};
5
+
6
+ -- Standard monitoring privileges
7
+ grant pg_monitor to {{ROLE_IDENT}};
8
+ grant select on pg_catalog.pg_index to {{ROLE_IDENT}};
9
+
10
+ -- Create postgres_ai schema for our objects
11
+ create schema if not exists postgres_ai;
12
+ grant usage on schema postgres_ai to {{ROLE_IDENT}};
13
+
14
+ -- For bloat analysis: expose pg_statistic via a view
15
+ create or replace view postgres_ai.pg_statistic as
16
+ select
17
+ n.nspname as schemaname,
18
+ c.relname as tablename,
19
+ a.attname,
20
+ s.stanullfrac as null_frac,
21
+ s.stawidth as avg_width,
22
+ false as inherited
23
+ from pg_catalog.pg_statistic s
24
+ join pg_catalog.pg_class c on c.oid = s.starelid
25
+ join pg_catalog.pg_namespace n on n.oid = c.relnamespace
26
+ join pg_catalog.pg_attribute a on a.attrelid = s.starelid and a.attnum = s.staattnum
27
+ where a.attnum > 0 and not a.attisdropped;
28
+
29
+ grant select on postgres_ai.pg_statistic to {{ROLE_IDENT}};
30
+
31
+ -- Hardened clusters sometimes revoke PUBLIC on schema public
32
+ grant usage on schema public to {{ROLE_IDENT}};
33
+
34
+ -- Keep search_path predictable; postgres_ai first so our objects are found
35
+ alter user {{ROLE_IDENT}} set search_path = postgres_ai, "$user", public, pg_catalog;
36
+
37
+
@@ -0,0 +1,6 @@
1
+ -- Optional permissions for RDS Postgres / Aurora (best effort)
2
+
3
+ create extension if not exists rds_tools;
4
+ grant execute on function rds_tools.pg_ls_multixactdir() to {{ROLE_IDENT}};
5
+
6
+
@@ -0,0 +1,8 @@
1
+ -- Optional permissions for self-managed Postgres (best effort)
2
+
3
+ grant execute on function pg_catalog.pg_stat_file(text) to {{ROLE_IDENT}};
4
+ grant execute on function pg_catalog.pg_stat_file(text, boolean) to {{ROLE_IDENT}};
5
+ grant execute on function pg_catalog.pg_ls_dir(text) to {{ROLE_IDENT}};
6
+ grant execute on function pg_catalog.pg_ls_dir(text, boolean, boolean) to {{ROLE_IDENT}};
7
+
8
+
@@ -0,0 +1,415 @@
1
+ -- Helper functions for postgres_ai monitoring user (template-filled by cli/lib/init.ts)
2
+ -- These functions use SECURITY DEFINER to allow the monitoring user to perform
3
+ -- operations they don't have direct permissions for.
4
+
5
+ /*
6
+ * pgai_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
+ * Usage examples:
12
+ * -- Basic generic plan
13
+ * select postgres_ai.explain_generic('select * from users where id = $1');
14
+ *
15
+ * -- JSON format
16
+ * select postgres_ai.explain_generic('select * from users where id = $1', 'json');
17
+ *
18
+ * -- Test a hypothetical index
19
+ * select postgres_ai.explain_generic(
20
+ * 'select * from users where email = $1',
21
+ * 'text',
22
+ * 'create index on users (email)'
23
+ * );
24
+ */
25
+ create or replace function postgres_ai.explain_generic(
26
+ in query text,
27
+ in format text default 'text',
28
+ in hypopg_index text default null,
29
+ out result text
30
+ )
31
+ language plpgsql
32
+ security definer
33
+ set search_path = pg_catalog, public
34
+ as $$
35
+ declare
36
+ v_line record;
37
+ v_lines text[] := '{}';
38
+ v_explain_query text;
39
+ v_hypo_result record;
40
+ v_version int;
41
+ v_hypopg_available boolean;
42
+ begin
43
+ -- Check PostgreSQL version (generic_plan requires 16+)
44
+ select current_setting('server_version_num')::int into v_version;
45
+
46
+ if v_version < 160000 then
47
+ raise exception 'generic_plan requires PostgreSQL 16+, current version: %',
48
+ current_setting('server_version');
49
+ end if;
50
+
51
+ -- Check if HypoPG extension is available
52
+ if hypopg_index is not null then
53
+ select exists(
54
+ select 1 from pg_extension where extname = 'hypopg'
55
+ ) into v_hypopg_available;
56
+
57
+ if not v_hypopg_available then
58
+ raise exception 'HypoPG extension is required for hypothetical index testing but is not installed';
59
+ end if;
60
+
61
+ -- Create hypothetical index
62
+ select * into v_hypo_result from hypopg_create_index(hypopg_index);
63
+ raise notice 'Created hypothetical index: % (oid: %)',
64
+ v_hypo_result.indexname, v_hypo_result.indexrelid;
65
+ end if;
66
+
67
+ -- Build and execute explain query based on format
68
+ -- Output is preserved exactly as EXPLAIN returns it
69
+ begin
70
+ if lower(format) = 'json' then
71
+ v_explain_query := 'explain (verbose, settings, generic_plan, format json) ' || query;
72
+ execute v_explain_query into result;
73
+ else
74
+ v_explain_query := 'explain (verbose, settings, generic_plan) ' || query;
75
+ for v_line in execute v_explain_query loop
76
+ v_lines := array_append(v_lines, v_line."QUERY PLAN");
77
+ end loop;
78
+ result := array_to_string(v_lines, e'\n');
79
+ end if;
80
+ exception when others then
81
+ -- Clean up hypothetical index before re-raising
82
+ if hypopg_index is not null then
83
+ perform hypopg_reset();
84
+ end if;
85
+ raise;
86
+ end;
87
+
88
+ -- Clean up hypothetical index
89
+ if hypopg_index is not null then
90
+ perform hypopg_reset();
91
+ end if;
92
+ end;
93
+ $$;
94
+
95
+ comment on function postgres_ai.explain_generic(text, text, text) is
96
+ 'Returns generic EXPLAIN plan with optional HypoPG index testing (requires PG16+)';
97
+
98
+ -- Grant execute to the monitoring user
99
+ grant execute on function postgres_ai.explain_generic(text, text, text) to {{ROLE_IDENT}};
100
+
101
+ /*
102
+ * table_describe
103
+ *
104
+ * Collects comprehensive information about a table for LLM analysis.
105
+ * Returns a compact text format with:
106
+ * - Table metadata (type, size estimates)
107
+ * - Columns (name, type, nullable, default)
108
+ * - Indexes
109
+ * - Constraints (PK, FK, unique, check)
110
+ * - Maintenance stats (vacuum/analyze times)
111
+ *
112
+ * Usage:
113
+ * select postgres_ai.table_describe('public.users');
114
+ * select postgres_ai.table_describe('my_table'); -- uses search_path
115
+ */
116
+ create or replace function postgres_ai.table_describe(
117
+ in table_name text,
118
+ out result text
119
+ )
120
+ language plpgsql
121
+ security definer
122
+ set search_path = pg_catalog, public
123
+ as $$
124
+ declare
125
+ v_oid oid;
126
+ v_schema text;
127
+ v_table text;
128
+ v_relkind char;
129
+ v_relpages int;
130
+ v_reltuples float;
131
+ v_lines text[] := '{}';
132
+ v_line text;
133
+ v_rec record;
134
+ v_constraint_count int := 0;
135
+ begin
136
+ -- Resolve table name to OID (handles schema-qualified and search_path)
137
+ v_oid := table_name::regclass::oid;
138
+
139
+ -- Get basic table info
140
+ select
141
+ n.nspname,
142
+ c.relname,
143
+ c.relkind,
144
+ c.relpages,
145
+ c.reltuples
146
+ into v_schema, v_table, v_relkind, v_relpages, v_reltuples
147
+ from pg_class c
148
+ join pg_namespace n on n.oid = c.relnamespace
149
+ where c.oid = v_oid;
150
+
151
+ -- Validate object type - only tables, views, and materialized views are supported
152
+ if v_relkind not in ('r', 'p', 'v', 'm', 'f') then
153
+ raise exception 'table_describe does not support % (relkind=%)',
154
+ case v_relkind
155
+ when 'i' then 'indexes'
156
+ when 'I' then 'partitioned indexes'
157
+ when 'S' then 'sequences'
158
+ when 't' then 'TOAST tables'
159
+ when 'c' then 'composite types'
160
+ else format('objects of type "%s"', v_relkind)
161
+ end,
162
+ v_relkind;
163
+ end if;
164
+
165
+ -- Header
166
+ v_lines := array_append(v_lines, format('Table: %I.%I', v_schema, v_table));
167
+ v_lines := array_append(v_lines, format('Type: %s | relpages: %s | reltuples: %s',
168
+ case v_relkind
169
+ when 'r' then 'table'
170
+ when 'p' then 'partitioned table'
171
+ when 'v' then 'view'
172
+ when 'm' then 'materialized view'
173
+ when 'f' then 'foreign table'
174
+ end,
175
+ v_relpages,
176
+ case when v_reltuples < 0 then '-1' else v_reltuples::bigint::text end
177
+ ));
178
+
179
+ -- Vacuum/analyze stats (only for tables and materialized views, not views)
180
+ if v_relkind in ('r', 'p', 'm', 'f') then
181
+ select
182
+ format('Vacuum: %s (auto: %s) | Analyze: %s (auto: %s)',
183
+ coalesce(to_char(last_vacuum at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never'),
184
+ coalesce(to_char(last_autovacuum at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never'),
185
+ coalesce(to_char(last_analyze at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never'),
186
+ coalesce(to_char(last_autoanalyze at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never')
187
+ )
188
+ into v_line
189
+ from pg_stat_all_tables
190
+ where relid = v_oid;
191
+
192
+ if v_line is not null then
193
+ v_lines := array_append(v_lines, v_line);
194
+ end if;
195
+ end if;
196
+
197
+ v_lines := array_append(v_lines, '');
198
+
199
+ -- Columns
200
+ v_lines := array_append(v_lines, 'Columns:');
201
+ for v_rec in
202
+ select
203
+ a.attname,
204
+ format_type(a.atttypid, a.atttypmod) as data_type,
205
+ a.attnotnull,
206
+ (select pg_get_expr(d.adbin, d.adrelid, true)
207
+ from pg_attrdef d
208
+ where d.adrelid = a.attrelid and d.adnum = a.attnum and a.atthasdef) as default_val,
209
+ a.attidentity,
210
+ a.attgenerated
211
+ from pg_attribute a
212
+ where a.attrelid = v_oid
213
+ and a.attnum > 0
214
+ and not a.attisdropped
215
+ order by a.attnum
216
+ loop
217
+ v_line := format(' %s %s', v_rec.attname, v_rec.data_type);
218
+
219
+ if v_rec.attnotnull then
220
+ v_line := v_line || ' NOT NULL';
221
+ end if;
222
+
223
+ if v_rec.attidentity = 'a' then
224
+ v_line := v_line || ' GENERATED ALWAYS AS IDENTITY';
225
+ elsif v_rec.attidentity = 'd' then
226
+ v_line := v_line || ' GENERATED BY DEFAULT AS IDENTITY';
227
+ elsif v_rec.attgenerated = 's' then
228
+ v_line := v_line || format(' GENERATED ALWAYS AS (%s) STORED', v_rec.default_val);
229
+ elsif v_rec.default_val is not null then
230
+ v_line := v_line || format(' DEFAULT %s', v_rec.default_val);
231
+ end if;
232
+
233
+ v_lines := array_append(v_lines, v_line);
234
+ end loop;
235
+
236
+ -- View definition (for views and materialized views)
237
+ if v_relkind in ('v', 'm') then
238
+ v_lines := array_append(v_lines, '');
239
+ v_lines := array_append(v_lines, 'Definition:');
240
+ v_line := pg_get_viewdef(v_oid, true);
241
+ if v_line is not null then
242
+ -- Indent the view definition
243
+ v_line := ' ' || replace(v_line, e'\n', e'\n ');
244
+ v_lines := array_append(v_lines, v_line);
245
+ end if;
246
+ end if;
247
+
248
+ -- Indexes (tables, partitioned tables, and materialized views can have indexes)
249
+ if v_relkind in ('r', 'p', 'm') then
250
+ v_lines := array_append(v_lines, '');
251
+ v_lines := array_append(v_lines, 'Indexes:');
252
+ for v_rec in
253
+ select
254
+ i.relname as index_name,
255
+ pg_get_indexdef(i.oid) as index_def,
256
+ ix.indisprimary,
257
+ ix.indisunique
258
+ from pg_index ix
259
+ join pg_class i on i.oid = ix.indexrelid
260
+ where ix.indrelid = v_oid
261
+ order by ix.indisprimary desc, ix.indisunique desc, i.relname
262
+ loop
263
+ v_line := ' ';
264
+ if v_rec.indisprimary then
265
+ v_line := v_line || 'PRIMARY KEY: ';
266
+ elsif v_rec.indisunique then
267
+ v_line := v_line || 'UNIQUE: ';
268
+ else
269
+ v_line := v_line || 'INDEX: ';
270
+ end if;
271
+ -- Extract just the column part from index definition
272
+ v_line := v_line || v_rec.index_name || ' ' ||
273
+ regexp_replace(v_rec.index_def, '^CREATE.*INDEX.*ON.*USING\s+\w+\s*', '');
274
+ v_lines := array_append(v_lines, v_line);
275
+ end loop;
276
+
277
+ if not exists (select 1 from pg_index where indrelid = v_oid) then
278
+ v_lines := array_append(v_lines, ' (none)');
279
+ end if;
280
+ end if;
281
+
282
+ -- Constraints (only tables can have constraints)
283
+ if v_relkind in ('r', 'p', 'f') then
284
+ v_lines := array_append(v_lines, '');
285
+ v_lines := array_append(v_lines, 'Constraints:');
286
+ v_constraint_count := 0;
287
+
288
+ for v_rec in
289
+ select
290
+ conname,
291
+ contype,
292
+ pg_get_constraintdef(oid, true) as condef
293
+ from pg_constraint
294
+ where conrelid = v_oid
295
+ and contype != 'p' -- skip primary key (shown with indexes)
296
+ order by
297
+ case contype when 'f' then 1 when 'u' then 2 when 'c' then 3 else 4 end,
298
+ conname
299
+ loop
300
+ v_constraint_count := v_constraint_count + 1;
301
+ v_line := ' ';
302
+ case v_rec.contype
303
+ when 'f' then v_line := v_line || 'FK: ';
304
+ when 'u' then v_line := v_line || 'UNIQUE: ';
305
+ when 'c' then v_line := v_line || 'CHECK: ';
306
+ else v_line := v_line || v_rec.contype || ': ';
307
+ end case;
308
+ v_line := v_line || v_rec.conname || ' ' || v_rec.condef;
309
+ v_lines := array_append(v_lines, v_line);
310
+ end loop;
311
+
312
+ if v_constraint_count = 0 then
313
+ v_lines := array_append(v_lines, ' (none)');
314
+ end if;
315
+
316
+ -- Foreign keys referencing this table
317
+ v_lines := array_append(v_lines, '');
318
+ v_lines := array_append(v_lines, 'Referenced by:');
319
+ v_constraint_count := 0;
320
+
321
+ for v_rec in
322
+ select
323
+ conname,
324
+ conrelid::regclass::text as from_table,
325
+ pg_get_constraintdef(oid, true) as condef
326
+ from pg_constraint
327
+ where confrelid = v_oid
328
+ and contype = 'f'
329
+ order by conrelid::regclass::text, conname
330
+ loop
331
+ v_constraint_count := v_constraint_count + 1;
332
+ v_lines := array_append(v_lines, format(' %s.%s %s',
333
+ v_rec.from_table, v_rec.conname, v_rec.condef));
334
+ end loop;
335
+
336
+ if v_constraint_count = 0 then
337
+ v_lines := array_append(v_lines, ' (none)');
338
+ end if;
339
+ end if;
340
+
341
+ -- Partition info (if partitioned table or partition)
342
+ if v_relkind = 'p' then
343
+ -- This is a partitioned table - show partition key and partitions
344
+ v_lines := array_append(v_lines, '');
345
+ v_lines := array_append(v_lines, 'Partitioning:');
346
+
347
+ select format(' %s BY %s',
348
+ case partstrat
349
+ when 'r' then 'RANGE'
350
+ when 'l' then 'LIST'
351
+ when 'h' then 'HASH'
352
+ else partstrat
353
+ end,
354
+ pg_get_partkeydef(v_oid)
355
+ )
356
+ into v_line
357
+ from pg_partitioned_table
358
+ where partrelid = v_oid;
359
+
360
+ if v_line is not null then
361
+ v_lines := array_append(v_lines, v_line);
362
+ end if;
363
+
364
+ -- List partitions
365
+ v_constraint_count := 0;
366
+ for v_rec in
367
+ select
368
+ c.oid::regclass::text as partition_name,
369
+ pg_get_expr(c.relpartbound, c.oid) as partition_bound,
370
+ c.relpages,
371
+ c.reltuples
372
+ from pg_inherits i
373
+ join pg_class c on c.oid = i.inhrelid
374
+ where i.inhparent = v_oid
375
+ order by c.oid::regclass::text
376
+ loop
377
+ v_constraint_count := v_constraint_count + 1;
378
+ v_lines := array_append(v_lines, format(' %s: %s (relpages: %s, reltuples: %s)',
379
+ v_rec.partition_name, v_rec.partition_bound,
380
+ v_rec.relpages,
381
+ case when v_rec.reltuples < 0 then '-1' else v_rec.reltuples::bigint::text end
382
+ ));
383
+ end loop;
384
+
385
+ v_lines := array_append(v_lines, format(' Total partitions: %s', v_constraint_count));
386
+
387
+ elsif exists (select 1 from pg_inherits where inhrelid = v_oid) then
388
+ -- This is a partition - show parent and bound
389
+ v_lines := array_append(v_lines, '');
390
+ v_lines := array_append(v_lines, 'Partition of:');
391
+
392
+ select format(' %s FOR VALUES %s',
393
+ i.inhparent::regclass::text,
394
+ pg_get_expr(c.relpartbound, c.oid)
395
+ )
396
+ into v_line
397
+ from pg_inherits i
398
+ join pg_class c on c.oid = i.inhrelid
399
+ where i.inhrelid = v_oid;
400
+
401
+ if v_line is not null then
402
+ v_lines := array_append(v_lines, v_line);
403
+ end if;
404
+ end if;
405
+
406
+ result := array_to_string(v_lines, e'\n');
407
+ end;
408
+ $$;
409
+
410
+ comment on function postgres_ai.table_describe(text) is
411
+ 'Returns comprehensive table information in compact text format for LLM analysis';
412
+
413
+ grant execute on function postgres_ai.table_describe(text) to {{ROLE_IDENT}};
414
+
415
+