-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy path06.helpers.sql
More file actions
317 lines (286 loc) · 9.95 KB
/
Copy path06.helpers.sql
File metadata and controls
317 lines (286 loc) · 9.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
-- Helper functions for postgres_ai monitoring user (template-filled by cli/lib/init.ts)
-- These functions use SECURITY DEFINER to allow the monitoring user to perform
-- operations they don't have direct permissions for.
/*
* table_describe
*
* Collects comprehensive information about a table for LLM analysis.
* Returns a compact text format with:
* - Table metadata (type, size estimates)
* - Columns (name, type, nullable, default)
* - Indexes
* - Constraints (PK, FK, unique, check)
* - Maintenance stats (vacuum/analyze times)
*
* Usage:
* select postgres_ai.table_describe('public.users');
* select postgres_ai.table_describe('my_table'); -- uses search_path
*/
create or replace function postgres_ai.table_describe(
in table_name text,
out result text
)
language plpgsql
security definer
set search_path = pg_catalog, public
as $$
declare
v_oid oid;
v_schema text;
v_table text;
v_relkind char;
v_relpages int;
v_reltuples float;
v_lines text[] := '{}';
v_line text;
v_rec record;
v_constraint_count int := 0;
begin
-- Resolve table name to OID (handles schema-qualified and search_path)
v_oid := table_name::regclass::oid;
-- Get basic table info
select
n.nspname,
c.relname,
c.relkind,
c.relpages,
c.reltuples
into v_schema, v_table, v_relkind, v_relpages, v_reltuples
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where c.oid = v_oid;
-- Validate object type - only tables, views, and materialized views are supported
if v_relkind not in ('r', 'p', 'v', 'm', 'f') then
raise exception 'table_describe does not support % (relkind=%)',
case v_relkind
when 'i' then 'indexes'
when 'I' then 'partitioned indexes'
when 'S' then 'sequences'
when 't' then 'TOAST tables'
when 'c' then 'composite types'
else format('objects of type "%s"', v_relkind)
end,
v_relkind;
end if;
-- Header
v_lines := array_append(v_lines, format('Table: %I.%I', v_schema, v_table));
v_lines := array_append(v_lines, format('Type: %s | relpages: %s | reltuples: %s',
case v_relkind
when 'r' then 'table'
when 'p' then 'partitioned table'
when 'v' then 'view'
when 'm' then 'materialized view'
when 'f' then 'foreign table'
end,
v_relpages,
case when v_reltuples < 0 then '-1' else v_reltuples::bigint::text end
));
-- Vacuum/analyze stats (only for tables and materialized views, not views)
if v_relkind in ('r', 'p', 'm', 'f') then
select
format('Vacuum: %s (auto: %s) | Analyze: %s (auto: %s)',
coalesce(to_char(last_vacuum at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never'),
coalesce(to_char(last_autovacuum at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never'),
coalesce(to_char(last_analyze at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never'),
coalesce(to_char(last_autoanalyze at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never')
)
into v_line
from pg_stat_all_tables
where relid = v_oid;
if v_line is not null then
v_lines := array_append(v_lines, v_line);
end if;
end if;
v_lines := array_append(v_lines, '');
-- Columns
v_lines := array_append(v_lines, 'Columns:');
for v_rec in
select
a.attname,
format_type(a.atttypid, a.atttypmod) as data_type,
a.attnotnull,
(select pg_get_expr(d.adbin, d.adrelid, true)
from pg_attrdef d
where d.adrelid = a.attrelid and d.adnum = a.attnum and a.atthasdef) as default_val,
a.attidentity,
a.attgenerated
from pg_attribute a
where a.attrelid = v_oid
and a.attnum > 0
and not a.attisdropped
order by a.attnum
loop
v_line := format(' %s %s', v_rec.attname, v_rec.data_type);
if v_rec.attnotnull then
v_line := v_line || ' NOT NULL';
end if;
if v_rec.attidentity = 'a' then
v_line := v_line || ' GENERATED ALWAYS AS IDENTITY';
elsif v_rec.attidentity = 'd' then
v_line := v_line || ' GENERATED BY DEFAULT AS IDENTITY';
elsif v_rec.attgenerated = 's' then
v_line := v_line || format(' GENERATED ALWAYS AS (%s) STORED', v_rec.default_val);
elsif v_rec.default_val is not null then
v_line := v_line || format(' DEFAULT %s', v_rec.default_val);
end if;
v_lines := array_append(v_lines, v_line);
end loop;
-- View definition (for views and materialized views)
if v_relkind in ('v', 'm') then
v_lines := array_append(v_lines, '');
v_lines := array_append(v_lines, 'Definition:');
v_line := pg_get_viewdef(v_oid, true);
if v_line is not null then
-- Indent the view definition
v_line := ' ' || replace(v_line, e'\n', e'\n ');
v_lines := array_append(v_lines, v_line);
end if;
end if;
-- Indexes (tables, partitioned tables, and materialized views can have indexes)
if v_relkind in ('r', 'p', 'm') then
v_lines := array_append(v_lines, '');
v_lines := array_append(v_lines, 'Indexes:');
for v_rec in
select
i.relname as index_name,
pg_get_indexdef(i.oid) as index_def,
ix.indisprimary,
ix.indisunique
from pg_index ix
join pg_class i on i.oid = ix.indexrelid
where ix.indrelid = v_oid
order by ix.indisprimary desc, ix.indisunique desc, i.relname
loop
v_line := ' ';
if v_rec.indisprimary then
v_line := v_line || 'PRIMARY KEY: ';
elsif v_rec.indisunique then
v_line := v_line || 'UNIQUE: ';
else
v_line := v_line || 'INDEX: ';
end if;
-- Extract just the column part from index definition
v_line := v_line || v_rec.index_name || ' ' ||
regexp_replace(v_rec.index_def, '^CREATE.*INDEX.*ON.*USING\s+\w+\s*', '');
v_lines := array_append(v_lines, v_line);
end loop;
if not exists (select 1 from pg_index where indrelid = v_oid) then
v_lines := array_append(v_lines, ' (none)');
end if;
end if;
-- Constraints (only tables can have constraints)
if v_relkind in ('r', 'p', 'f') then
v_lines := array_append(v_lines, '');
v_lines := array_append(v_lines, 'Constraints:');
v_constraint_count := 0;
for v_rec in
select
conname,
contype,
pg_get_constraintdef(oid, true) as condef
from pg_constraint
where conrelid = v_oid
and contype != 'p' -- skip primary key (shown with indexes)
order by
case contype when 'f' then 1 when 'u' then 2 when 'c' then 3 else 4 end,
conname
loop
v_constraint_count := v_constraint_count + 1;
v_line := ' ';
case v_rec.contype
when 'f' then v_line := v_line || 'FK: ';
when 'u' then v_line := v_line || 'UNIQUE: ';
when 'c' then v_line := v_line || 'CHECK: ';
else v_line := v_line || v_rec.contype || ': ';
end case;
v_line := v_line || v_rec.conname || ' ' || v_rec.condef;
v_lines := array_append(v_lines, v_line);
end loop;
if v_constraint_count = 0 then
v_lines := array_append(v_lines, ' (none)');
end if;
-- Foreign keys referencing this table
v_lines := array_append(v_lines, '');
v_lines := array_append(v_lines, 'Referenced by:');
v_constraint_count := 0;
for v_rec in
select
conname,
conrelid::regclass::text as from_table,
pg_get_constraintdef(oid, true) as condef
from pg_constraint
where confrelid = v_oid
and contype = 'f'
order by conrelid::regclass::text, conname
loop
v_constraint_count := v_constraint_count + 1;
v_lines := array_append(v_lines, format(' %s.%s %s',
v_rec.from_table, v_rec.conname, v_rec.condef));
end loop;
if v_constraint_count = 0 then
v_lines := array_append(v_lines, ' (none)');
end if;
end if;
-- Partition info (if partitioned table or partition)
if v_relkind = 'p' then
-- This is a partitioned table - show partition key and partitions
v_lines := array_append(v_lines, '');
v_lines := array_append(v_lines, 'Partitioning:');
select format(' %s BY %s',
case partstrat
when 'r' then 'RANGE'
when 'l' then 'LIST'
when 'h' then 'HASH'
else partstrat
end,
pg_get_partkeydef(v_oid)
)
into v_line
from pg_partitioned_table
where partrelid = v_oid;
if v_line is not null then
v_lines := array_append(v_lines, v_line);
end if;
-- List partitions
v_constraint_count := 0;
for v_rec in
select
c.oid::regclass::text as partition_name,
pg_get_expr(c.relpartbound, c.oid) as partition_bound,
c.relpages,
c.reltuples
from pg_inherits i
join pg_class c on c.oid = i.inhrelid
where i.inhparent = v_oid
order by c.oid::regclass::text
loop
v_constraint_count := v_constraint_count + 1;
v_lines := array_append(v_lines, format(' %s: %s (relpages: %s, reltuples: %s)',
v_rec.partition_name, v_rec.partition_bound,
v_rec.relpages,
case when v_rec.reltuples < 0 then '-1' else v_rec.reltuples::bigint::text end
));
end loop;
v_lines := array_append(v_lines, format(' Total partitions: %s', v_constraint_count));
elsif exists (select 1 from pg_inherits where inhrelid = v_oid) then
-- This is a partition - show parent and bound
v_lines := array_append(v_lines, '');
v_lines := array_append(v_lines, 'Partition of:');
select format(' %s FOR VALUES %s',
i.inhparent::regclass::text,
pg_get_expr(c.relpartbound, c.oid)
)
into v_line
from pg_inherits i
join pg_class c on c.oid = i.inhrelid
where i.inhrelid = v_oid;
if v_line is not null then
v_lines := array_append(v_lines, v_line);
end if;
end if;
result := array_to_string(v_lines, e'\n');
end;
$$;
comment on function postgres_ai.table_describe(text) is
'Returns comprehensive table information in compact text format for LLM analysis';
grant execute on function postgres_ai.table_describe(text) to {{ROLE_IDENT}};