postgresai 0.14.0-dev.7 → 0.14.0-dev.70
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.
- package/README.md +161 -61
- package/bin/postgres-ai.ts +1957 -404
- package/bun.lock +258 -0
- package/bunfig.toml +20 -0
- package/dist/bin/postgres-ai.js +29351 -1576
- package/dist/sql/01.role.sql +16 -0
- package/dist/sql/02.permissions.sql +37 -0
- package/dist/sql/03.optional_rds.sql +6 -0
- package/dist/sql/04.optional_self_managed.sql +8 -0
- package/dist/sql/05.helpers.sql +439 -0
- package/dist/sql/sql/01.role.sql +16 -0
- package/dist/sql/sql/02.permissions.sql +37 -0
- package/dist/sql/sql/03.optional_rds.sql +6 -0
- package/dist/sql/sql/04.optional_self_managed.sql +8 -0
- package/dist/sql/sql/05.helpers.sql +439 -0
- package/lib/auth-server.ts +124 -106
- package/lib/checkup-api.ts +386 -0
- package/lib/checkup.ts +1396 -0
- package/lib/config.ts +6 -3
- package/lib/init.ts +512 -156
- package/lib/issues.ts +400 -191
- package/lib/mcp-server.ts +213 -90
- package/lib/metrics-embedded.ts +79 -0
- package/lib/metrics-loader.ts +127 -0
- package/lib/supabase.ts +769 -0
- package/lib/util.ts +61 -0
- package/package.json +20 -10
- package/packages/postgres-ai/README.md +26 -0
- package/packages/postgres-ai/bin/postgres-ai.js +27 -0
- package/packages/postgres-ai/package.json +27 -0
- package/scripts/embed-metrics.ts +154 -0
- package/sql/01.role.sql +16 -0
- package/sql/02.permissions.sql +37 -0
- package/sql/03.optional_rds.sql +6 -0
- package/sql/04.optional_self_managed.sql +8 -0
- package/sql/05.helpers.sql +439 -0
- package/test/auth.test.ts +258 -0
- package/test/checkup.integration.test.ts +321 -0
- package/test/checkup.test.ts +1117 -0
- package/test/init.integration.test.ts +500 -0
- package/test/init.test.ts +527 -0
- package/test/issues.cli.test.ts +314 -0
- package/test/issues.test.ts +456 -0
- package/test/mcp-server.test.ts +988 -0
- package/test/schema-validation.test.ts +81 -0
- package/test/supabase.test.ts +568 -0
- package/test/test-utils.ts +128 -0
- package/tsconfig.json +12 -20
- package/dist/bin/postgres-ai.d.ts +0 -3
- package/dist/bin/postgres-ai.d.ts.map +0 -1
- package/dist/bin/postgres-ai.js.map +0 -1
- package/dist/lib/auth-server.d.ts +0 -31
- package/dist/lib/auth-server.d.ts.map +0 -1
- package/dist/lib/auth-server.js +0 -263
- package/dist/lib/auth-server.js.map +0 -1
- package/dist/lib/config.d.ts +0 -45
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/config.js +0 -181
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/init.d.ts +0 -61
- package/dist/lib/init.d.ts.map +0 -1
- package/dist/lib/init.js +0 -359
- package/dist/lib/init.js.map +0 -1
- package/dist/lib/issues.d.ts +0 -75
- package/dist/lib/issues.d.ts.map +0 -1
- package/dist/lib/issues.js +0 -336
- package/dist/lib/issues.js.map +0 -1
- package/dist/lib/mcp-server.d.ts +0 -9
- package/dist/lib/mcp-server.d.ts.map +0 -1
- package/dist/lib/mcp-server.js +0 -168
- package/dist/lib/mcp-server.js.map +0 -1
- package/dist/lib/pkce.d.ts +0 -32
- package/dist/lib/pkce.d.ts.map +0 -1
- package/dist/lib/pkce.js +0 -101
- package/dist/lib/pkce.js.map +0 -1
- package/dist/lib/util.d.ts +0 -27
- package/dist/lib/util.d.ts.map +0 -1
- package/dist/lib/util.js +0 -46
- package/dist/lib/util.js.map +0 -1
- package/dist/package.json +0 -46
- package/test/init.integration.test.cjs +0 -269
- package/test/init.test.cjs +0 -69
|
@@ -0,0 +1,439 @@
|
|
|
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
|
+
* 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
|
+
/*
|
|
126
|
+
* table_describe
|
|
127
|
+
*
|
|
128
|
+
* Collects comprehensive information about a table for LLM analysis.
|
|
129
|
+
* Returns a compact text format with:
|
|
130
|
+
* - Table metadata (type, size estimates)
|
|
131
|
+
* - Columns (name, type, nullable, default)
|
|
132
|
+
* - Indexes
|
|
133
|
+
* - Constraints (PK, FK, unique, check)
|
|
134
|
+
* - Maintenance stats (vacuum/analyze times)
|
|
135
|
+
*
|
|
136
|
+
* Usage:
|
|
137
|
+
* select postgres_ai.table_describe('public.users');
|
|
138
|
+
* select postgres_ai.table_describe('my_table'); -- uses search_path
|
|
139
|
+
*/
|
|
140
|
+
create or replace function postgres_ai.table_describe(
|
|
141
|
+
in table_name text,
|
|
142
|
+
out result text
|
|
143
|
+
)
|
|
144
|
+
language plpgsql
|
|
145
|
+
security definer
|
|
146
|
+
set search_path = pg_catalog, public
|
|
147
|
+
as $$
|
|
148
|
+
declare
|
|
149
|
+
v_oid oid;
|
|
150
|
+
v_schema text;
|
|
151
|
+
v_table text;
|
|
152
|
+
v_relkind char;
|
|
153
|
+
v_relpages int;
|
|
154
|
+
v_reltuples float;
|
|
155
|
+
v_lines text[] := '{}';
|
|
156
|
+
v_line text;
|
|
157
|
+
v_rec record;
|
|
158
|
+
v_constraint_count int := 0;
|
|
159
|
+
begin
|
|
160
|
+
-- Resolve table name to OID (handles schema-qualified and search_path)
|
|
161
|
+
v_oid := table_name::regclass::oid;
|
|
162
|
+
|
|
163
|
+
-- Get basic table info
|
|
164
|
+
select
|
|
165
|
+
n.nspname,
|
|
166
|
+
c.relname,
|
|
167
|
+
c.relkind,
|
|
168
|
+
c.relpages,
|
|
169
|
+
c.reltuples
|
|
170
|
+
into v_schema, v_table, v_relkind, v_relpages, v_reltuples
|
|
171
|
+
from pg_class c
|
|
172
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
173
|
+
where c.oid = v_oid;
|
|
174
|
+
|
|
175
|
+
-- Validate object type - only tables, views, and materialized views are supported
|
|
176
|
+
if v_relkind not in ('r', 'p', 'v', 'm', 'f') then
|
|
177
|
+
raise exception 'table_describe does not support % (relkind=%)',
|
|
178
|
+
case v_relkind
|
|
179
|
+
when 'i' then 'indexes'
|
|
180
|
+
when 'I' then 'partitioned indexes'
|
|
181
|
+
when 'S' then 'sequences'
|
|
182
|
+
when 't' then 'TOAST tables'
|
|
183
|
+
when 'c' then 'composite types'
|
|
184
|
+
else format('objects of type "%s"', v_relkind)
|
|
185
|
+
end,
|
|
186
|
+
v_relkind;
|
|
187
|
+
end if;
|
|
188
|
+
|
|
189
|
+
-- Header
|
|
190
|
+
v_lines := array_append(v_lines, format('Table: %I.%I', v_schema, v_table));
|
|
191
|
+
v_lines := array_append(v_lines, format('Type: %s | relpages: %s | reltuples: %s',
|
|
192
|
+
case v_relkind
|
|
193
|
+
when 'r' then 'table'
|
|
194
|
+
when 'p' then 'partitioned table'
|
|
195
|
+
when 'v' then 'view'
|
|
196
|
+
when 'm' then 'materialized view'
|
|
197
|
+
when 'f' then 'foreign table'
|
|
198
|
+
end,
|
|
199
|
+
v_relpages,
|
|
200
|
+
case when v_reltuples < 0 then '-1' else v_reltuples::bigint::text end
|
|
201
|
+
));
|
|
202
|
+
|
|
203
|
+
-- Vacuum/analyze stats (only for tables and materialized views, not views)
|
|
204
|
+
if v_relkind in ('r', 'p', 'm', 'f') then
|
|
205
|
+
select
|
|
206
|
+
format('Vacuum: %s (auto: %s) | Analyze: %s (auto: %s)',
|
|
207
|
+
coalesce(to_char(last_vacuum at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never'),
|
|
208
|
+
coalesce(to_char(last_autovacuum at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never'),
|
|
209
|
+
coalesce(to_char(last_analyze at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never'),
|
|
210
|
+
coalesce(to_char(last_autoanalyze at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never')
|
|
211
|
+
)
|
|
212
|
+
into v_line
|
|
213
|
+
from pg_stat_all_tables
|
|
214
|
+
where relid = v_oid;
|
|
215
|
+
|
|
216
|
+
if v_line is not null then
|
|
217
|
+
v_lines := array_append(v_lines, v_line);
|
|
218
|
+
end if;
|
|
219
|
+
end if;
|
|
220
|
+
|
|
221
|
+
v_lines := array_append(v_lines, '');
|
|
222
|
+
|
|
223
|
+
-- Columns
|
|
224
|
+
v_lines := array_append(v_lines, 'Columns:');
|
|
225
|
+
for v_rec in
|
|
226
|
+
select
|
|
227
|
+
a.attname,
|
|
228
|
+
format_type(a.atttypid, a.atttypmod) as data_type,
|
|
229
|
+
a.attnotnull,
|
|
230
|
+
(select pg_get_expr(d.adbin, d.adrelid, true)
|
|
231
|
+
from pg_attrdef d
|
|
232
|
+
where d.adrelid = a.attrelid and d.adnum = a.attnum and a.atthasdef) as default_val,
|
|
233
|
+
a.attidentity,
|
|
234
|
+
a.attgenerated
|
|
235
|
+
from pg_attribute a
|
|
236
|
+
where a.attrelid = v_oid
|
|
237
|
+
and a.attnum > 0
|
|
238
|
+
and not a.attisdropped
|
|
239
|
+
order by a.attnum
|
|
240
|
+
loop
|
|
241
|
+
v_line := format(' %s %s', v_rec.attname, v_rec.data_type);
|
|
242
|
+
|
|
243
|
+
if v_rec.attnotnull then
|
|
244
|
+
v_line := v_line || ' NOT NULL';
|
|
245
|
+
end if;
|
|
246
|
+
|
|
247
|
+
if v_rec.attidentity = 'a' then
|
|
248
|
+
v_line := v_line || ' GENERATED ALWAYS AS IDENTITY';
|
|
249
|
+
elsif v_rec.attidentity = 'd' then
|
|
250
|
+
v_line := v_line || ' GENERATED BY DEFAULT AS IDENTITY';
|
|
251
|
+
elsif v_rec.attgenerated = 's' then
|
|
252
|
+
v_line := v_line || format(' GENERATED ALWAYS AS (%s) STORED', v_rec.default_val);
|
|
253
|
+
elsif v_rec.default_val is not null then
|
|
254
|
+
v_line := v_line || format(' DEFAULT %s', v_rec.default_val);
|
|
255
|
+
end if;
|
|
256
|
+
|
|
257
|
+
v_lines := array_append(v_lines, v_line);
|
|
258
|
+
end loop;
|
|
259
|
+
|
|
260
|
+
-- View definition (for views and materialized views)
|
|
261
|
+
if v_relkind in ('v', 'm') then
|
|
262
|
+
v_lines := array_append(v_lines, '');
|
|
263
|
+
v_lines := array_append(v_lines, 'Definition:');
|
|
264
|
+
v_line := pg_get_viewdef(v_oid, true);
|
|
265
|
+
if v_line is not null then
|
|
266
|
+
-- Indent the view definition
|
|
267
|
+
v_line := ' ' || replace(v_line, e'\n', e'\n ');
|
|
268
|
+
v_lines := array_append(v_lines, v_line);
|
|
269
|
+
end if;
|
|
270
|
+
end if;
|
|
271
|
+
|
|
272
|
+
-- Indexes (tables, partitioned tables, and materialized views can have indexes)
|
|
273
|
+
if v_relkind in ('r', 'p', 'm') then
|
|
274
|
+
v_lines := array_append(v_lines, '');
|
|
275
|
+
v_lines := array_append(v_lines, 'Indexes:');
|
|
276
|
+
for v_rec in
|
|
277
|
+
select
|
|
278
|
+
i.relname as index_name,
|
|
279
|
+
pg_get_indexdef(i.oid) as index_def,
|
|
280
|
+
ix.indisprimary,
|
|
281
|
+
ix.indisunique
|
|
282
|
+
from pg_index ix
|
|
283
|
+
join pg_class i on i.oid = ix.indexrelid
|
|
284
|
+
where ix.indrelid = v_oid
|
|
285
|
+
order by ix.indisprimary desc, ix.indisunique desc, i.relname
|
|
286
|
+
loop
|
|
287
|
+
v_line := ' ';
|
|
288
|
+
if v_rec.indisprimary then
|
|
289
|
+
v_line := v_line || 'PRIMARY KEY: ';
|
|
290
|
+
elsif v_rec.indisunique then
|
|
291
|
+
v_line := v_line || 'UNIQUE: ';
|
|
292
|
+
else
|
|
293
|
+
v_line := v_line || 'INDEX: ';
|
|
294
|
+
end if;
|
|
295
|
+
-- Extract just the column part from index definition
|
|
296
|
+
v_line := v_line || v_rec.index_name || ' ' ||
|
|
297
|
+
regexp_replace(v_rec.index_def, '^CREATE.*INDEX.*ON.*USING\s+\w+\s*', '');
|
|
298
|
+
v_lines := array_append(v_lines, v_line);
|
|
299
|
+
end loop;
|
|
300
|
+
|
|
301
|
+
if not exists (select 1 from pg_index where indrelid = v_oid) then
|
|
302
|
+
v_lines := array_append(v_lines, ' (none)');
|
|
303
|
+
end if;
|
|
304
|
+
end if;
|
|
305
|
+
|
|
306
|
+
-- Constraints (only tables can have constraints)
|
|
307
|
+
if v_relkind in ('r', 'p', 'f') then
|
|
308
|
+
v_lines := array_append(v_lines, '');
|
|
309
|
+
v_lines := array_append(v_lines, 'Constraints:');
|
|
310
|
+
v_constraint_count := 0;
|
|
311
|
+
|
|
312
|
+
for v_rec in
|
|
313
|
+
select
|
|
314
|
+
conname,
|
|
315
|
+
contype,
|
|
316
|
+
pg_get_constraintdef(oid, true) as condef
|
|
317
|
+
from pg_constraint
|
|
318
|
+
where conrelid = v_oid
|
|
319
|
+
and contype != 'p' -- skip primary key (shown with indexes)
|
|
320
|
+
order by
|
|
321
|
+
case contype when 'f' then 1 when 'u' then 2 when 'c' then 3 else 4 end,
|
|
322
|
+
conname
|
|
323
|
+
loop
|
|
324
|
+
v_constraint_count := v_constraint_count + 1;
|
|
325
|
+
v_line := ' ';
|
|
326
|
+
case v_rec.contype
|
|
327
|
+
when 'f' then v_line := v_line || 'FK: ';
|
|
328
|
+
when 'u' then v_line := v_line || 'UNIQUE: ';
|
|
329
|
+
when 'c' then v_line := v_line || 'CHECK: ';
|
|
330
|
+
else v_line := v_line || v_rec.contype || ': ';
|
|
331
|
+
end case;
|
|
332
|
+
v_line := v_line || v_rec.conname || ' ' || v_rec.condef;
|
|
333
|
+
v_lines := array_append(v_lines, v_line);
|
|
334
|
+
end loop;
|
|
335
|
+
|
|
336
|
+
if v_constraint_count = 0 then
|
|
337
|
+
v_lines := array_append(v_lines, ' (none)');
|
|
338
|
+
end if;
|
|
339
|
+
|
|
340
|
+
-- Foreign keys referencing this table
|
|
341
|
+
v_lines := array_append(v_lines, '');
|
|
342
|
+
v_lines := array_append(v_lines, 'Referenced by:');
|
|
343
|
+
v_constraint_count := 0;
|
|
344
|
+
|
|
345
|
+
for v_rec in
|
|
346
|
+
select
|
|
347
|
+
conname,
|
|
348
|
+
conrelid::regclass::text as from_table,
|
|
349
|
+
pg_get_constraintdef(oid, true) as condef
|
|
350
|
+
from pg_constraint
|
|
351
|
+
where confrelid = v_oid
|
|
352
|
+
and contype = 'f'
|
|
353
|
+
order by conrelid::regclass::text, conname
|
|
354
|
+
loop
|
|
355
|
+
v_constraint_count := v_constraint_count + 1;
|
|
356
|
+
v_lines := array_append(v_lines, format(' %s.%s %s',
|
|
357
|
+
v_rec.from_table, v_rec.conname, v_rec.condef));
|
|
358
|
+
end loop;
|
|
359
|
+
|
|
360
|
+
if v_constraint_count = 0 then
|
|
361
|
+
v_lines := array_append(v_lines, ' (none)');
|
|
362
|
+
end if;
|
|
363
|
+
end if;
|
|
364
|
+
|
|
365
|
+
-- Partition info (if partitioned table or partition)
|
|
366
|
+
if v_relkind = 'p' then
|
|
367
|
+
-- This is a partitioned table - show partition key and partitions
|
|
368
|
+
v_lines := array_append(v_lines, '');
|
|
369
|
+
v_lines := array_append(v_lines, 'Partitioning:');
|
|
370
|
+
|
|
371
|
+
select format(' %s BY %s',
|
|
372
|
+
case partstrat
|
|
373
|
+
when 'r' then 'RANGE'
|
|
374
|
+
when 'l' then 'LIST'
|
|
375
|
+
when 'h' then 'HASH'
|
|
376
|
+
else partstrat
|
|
377
|
+
end,
|
|
378
|
+
pg_get_partkeydef(v_oid)
|
|
379
|
+
)
|
|
380
|
+
into v_line
|
|
381
|
+
from pg_partitioned_table
|
|
382
|
+
where partrelid = v_oid;
|
|
383
|
+
|
|
384
|
+
if v_line is not null then
|
|
385
|
+
v_lines := array_append(v_lines, v_line);
|
|
386
|
+
end if;
|
|
387
|
+
|
|
388
|
+
-- List partitions
|
|
389
|
+
v_constraint_count := 0;
|
|
390
|
+
for v_rec in
|
|
391
|
+
select
|
|
392
|
+
c.oid::regclass::text as partition_name,
|
|
393
|
+
pg_get_expr(c.relpartbound, c.oid) as partition_bound,
|
|
394
|
+
c.relpages,
|
|
395
|
+
c.reltuples
|
|
396
|
+
from pg_inherits i
|
|
397
|
+
join pg_class c on c.oid = i.inhrelid
|
|
398
|
+
where i.inhparent = v_oid
|
|
399
|
+
order by c.oid::regclass::text
|
|
400
|
+
loop
|
|
401
|
+
v_constraint_count := v_constraint_count + 1;
|
|
402
|
+
v_lines := array_append(v_lines, format(' %s: %s (relpages: %s, reltuples: %s)',
|
|
403
|
+
v_rec.partition_name, v_rec.partition_bound,
|
|
404
|
+
v_rec.relpages,
|
|
405
|
+
case when v_rec.reltuples < 0 then '-1' else v_rec.reltuples::bigint::text end
|
|
406
|
+
));
|
|
407
|
+
end loop;
|
|
408
|
+
|
|
409
|
+
v_lines := array_append(v_lines, format(' Total partitions: %s', v_constraint_count));
|
|
410
|
+
|
|
411
|
+
elsif exists (select 1 from pg_inherits where inhrelid = v_oid) then
|
|
412
|
+
-- This is a partition - show parent and bound
|
|
413
|
+
v_lines := array_append(v_lines, '');
|
|
414
|
+
v_lines := array_append(v_lines, 'Partition of:');
|
|
415
|
+
|
|
416
|
+
select format(' %s FOR VALUES %s',
|
|
417
|
+
i.inhparent::regclass::text,
|
|
418
|
+
pg_get_expr(c.relpartbound, c.oid)
|
|
419
|
+
)
|
|
420
|
+
into v_line
|
|
421
|
+
from pg_inherits i
|
|
422
|
+
join pg_class c on c.oid = i.inhrelid
|
|
423
|
+
where i.inhrelid = v_oid;
|
|
424
|
+
|
|
425
|
+
if v_line is not null then
|
|
426
|
+
v_lines := array_append(v_lines, v_line);
|
|
427
|
+
end if;
|
|
428
|
+
end if;
|
|
429
|
+
|
|
430
|
+
result := array_to_string(v_lines, e'\n');
|
|
431
|
+
end;
|
|
432
|
+
$$;
|
|
433
|
+
|
|
434
|
+
comment on function postgres_ai.table_describe(text) is
|
|
435
|
+
'Returns comprehensive table information in compact text format for LLM analysis';
|
|
436
|
+
|
|
437
|
+
grant execute on function postgres_ai.table_describe(text) to {{ROLE_IDENT}};
|
|
438
|
+
|
|
439
|
+
|