postgresai 0.17.0-dev.1 → 0.17.0-dev.11
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 +236 -8
- package/dist/bin/postgres-ai.js +22275 -15524
- package/dist/sql/01.role.sql +2 -1
- package/dist/sql/02.extensions.sql +1 -1
- package/dist/sql/04.optional_rds.sql +2 -1
- package/dist/sql/06.helpers.sql +29 -8
- package/package.json +2 -2
- package/sql/01.role.sql +2 -1
- package/sql/02.extensions.sql +1 -1
- package/sql/04.optional_rds.sql +2 -1
- package/sql/06.helpers.sql +29 -8
- package/dist/sql/sql/01.role.sql +0 -16
- package/dist/sql/sql/02.extensions.sql +0 -8
- package/dist/sql/sql/03.permissions.sql +0 -86
- package/dist/sql/sql/04.optional_rds.sql +0 -6
- package/dist/sql/sql/05.optional_self_managed.sql +0 -8
- package/dist/sql/sql/06.helpers.sql +0 -317
- package/dist/sql/sql/uninit/01.helpers.sql +0 -5
- package/dist/sql/sql/uninit/02.permissions.sql +0 -30
- package/dist/sql/sql/uninit/03.role.sql +0 -27
package/dist/sql/01.role.sql
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
-- Role creation / password update (template-filled by cli/lib/init.ts)
|
|
2
2
|
--
|
|
3
|
-
--
|
|
3
|
+
-- A race-safe pattern: create if missing, then alter to set the password.
|
|
4
|
+
-- `pgai connect` omits the alter, so an existing role keeps its password:
|
|
4
5
|
-- do $$ begin
|
|
5
6
|
-- if not exists (select 1 from pg_catalog.pg_roles where rolname = '...') then
|
|
6
7
|
-- begin
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
-- Enable pg_stat_statements for query performance monitoring
|
|
4
4
|
-- Note: Uses IF NOT EXISTS because extension may already be installed.
|
|
5
5
|
-- We do NOT drop this extension in unprepare-db since it may have been pre-existing.
|
|
6
|
-
create extension if not exists pg_stat_statements;
|
|
6
|
+
create extension if not exists pg_stat_statements with schema public;
|
|
7
7
|
|
|
8
8
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
-- Optional permissions for RDS Postgres / Aurora (best effort)
|
|
2
2
|
|
|
3
|
-
create
|
|
3
|
+
create schema if not exists rds_tools;
|
|
4
|
+
create extension if not exists rds_tools with schema rds_tools;
|
|
4
5
|
grant execute on function rds_tools.pg_ls_multixactdir() to {{ROLE_IDENT}};
|
|
5
6
|
|
|
6
7
|
|
package/dist/sql/06.helpers.sql
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
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
2
|
|
|
5
3
|
/*
|
|
6
4
|
* table_describe
|
|
@@ -15,18 +13,25 @@
|
|
|
15
13
|
*
|
|
16
14
|
* Usage:
|
|
17
15
|
* select postgres_ai.table_describe('public.users');
|
|
18
|
-
* select postgres_ai.table_describe('my_table'); --
|
|
16
|
+
* select postgres_ai.table_describe('my_table'); -- pg_catalog, then public
|
|
17
|
+
*
|
|
18
|
+
* SECURITY INVOKER: it reads only the world-readable catalog, so it needs no
|
|
19
|
+
* privilege the caller lacks. Names are resolved by catalog query, not regclass
|
|
20
|
+
* (which demands USAGE on the schema): unqualified in pg_catalog, then public.
|
|
21
|
+
* search_path is pg_catalog, pg_temp so objects in a writable schema cannot
|
|
22
|
+
* shadow a built-in the function calls.
|
|
19
23
|
*/
|
|
20
24
|
create or replace function postgres_ai.table_describe(
|
|
21
25
|
in table_name text,
|
|
22
26
|
out result text
|
|
23
27
|
)
|
|
24
28
|
language plpgsql
|
|
25
|
-
security
|
|
26
|
-
set search_path = pg_catalog,
|
|
29
|
+
security invoker
|
|
30
|
+
set search_path = pg_catalog, pg_temp
|
|
27
31
|
as $$
|
|
28
32
|
declare
|
|
29
33
|
v_oid oid;
|
|
34
|
+
v_ident text[];
|
|
30
35
|
v_schema text;
|
|
31
36
|
v_table text;
|
|
32
37
|
v_relkind char;
|
|
@@ -37,8 +42,24 @@ declare
|
|
|
37
42
|
v_rec record;
|
|
38
43
|
v_constraint_count int := 0;
|
|
39
44
|
begin
|
|
40
|
-
-- Resolve table name to OID
|
|
41
|
-
|
|
45
|
+
-- Resolve table name to OID: [current database.]schema.table (pg_temp is the
|
|
46
|
+
-- caller's temp schema), or an unqualified name in pg_catalog, then public
|
|
47
|
+
v_ident := parse_ident(table_name);
|
|
48
|
+
select c.oid into v_oid
|
|
49
|
+
from pg_class c
|
|
50
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
51
|
+
where (cardinality(v_ident) <= 2 or (cardinality(v_ident) = 3 and v_ident[1] = current_database()))
|
|
52
|
+
and c.relname = v_ident[cardinality(v_ident)]::name
|
|
53
|
+
and case
|
|
54
|
+
when cardinality(v_ident) = 1 then n.nspname in ('pg_catalog', 'public')
|
|
55
|
+
when v_ident[cardinality(v_ident) - 1] = 'pg_temp' then n.oid = pg_my_temp_schema()
|
|
56
|
+
else n.nspname = v_ident[cardinality(v_ident) - 1]::name
|
|
57
|
+
end
|
|
58
|
+
order by n.nspname = 'public'
|
|
59
|
+
limit 1;
|
|
60
|
+
if v_oid is null then
|
|
61
|
+
raise exception 'relation "%" does not exist', table_name using errcode = '42P01';
|
|
62
|
+
end if;
|
|
42
63
|
|
|
43
64
|
-- Get basic table info
|
|
44
65
|
select
|
|
@@ -207,7 +228,7 @@ begin
|
|
|
207
228
|
when 'f' then v_line := v_line || 'FK: ';
|
|
208
229
|
when 'u' then v_line := v_line || 'UNIQUE: ';
|
|
209
230
|
when 'c' then v_line := v_line || 'CHECK: ';
|
|
210
|
-
else v_line := v_line || v_rec.contype || ': ';
|
|
231
|
+
else v_line := v_line || v_rec.contype::text || ': ';
|
|
211
232
|
end case;
|
|
212
233
|
v_line := v_line || v_rec.conname || ' ' || v_rec.condef;
|
|
213
234
|
v_lines := array_append(v_lines, v_line);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresai",
|
|
3
|
-
"version": "0.17.0-dev.
|
|
3
|
+
"version": "0.17.0-dev.11",
|
|
4
4
|
"description": "postgres_ai CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"private": false,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"embed-checkup-dictionary": "bun run scripts/embed-checkup-dictionary.ts",
|
|
37
37
|
"embed-all": "bun run embed-metrics && bun run embed-checkup-dictionary",
|
|
38
38
|
"sync-schemas": "node scripts/sync-schemas.mjs",
|
|
39
|
-
"build": "bun run sync-schemas && bun run embed-all && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e \"const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))\" && cp -r ./sql ./dist/sql && cp ../instances.demo.yml ./instances.demo.yml",
|
|
39
|
+
"build": "bun run sync-schemas && bun run embed-all && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e \"const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))\" && rm -rf ./dist/sql && cp -r ./sql ./dist/sql && cp ../instances.demo.yml ./instances.demo.yml",
|
|
40
40
|
"prepack": "npm run sync-schemas",
|
|
41
41
|
"prepublishOnly": "npm run build",
|
|
42
42
|
"start": "bun ./bin/postgres-ai.ts --help",
|
package/sql/01.role.sql
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
-- Role creation / password update (template-filled by cli/lib/init.ts)
|
|
2
2
|
--
|
|
3
|
-
--
|
|
3
|
+
-- A race-safe pattern: create if missing, then alter to set the password.
|
|
4
|
+
-- `pgai connect` omits the alter, so an existing role keeps its password:
|
|
4
5
|
-- do $$ begin
|
|
5
6
|
-- if not exists (select 1 from pg_catalog.pg_roles where rolname = '...') then
|
|
6
7
|
-- begin
|
package/sql/02.extensions.sql
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
-- Enable pg_stat_statements for query performance monitoring
|
|
4
4
|
-- Note: Uses IF NOT EXISTS because extension may already be installed.
|
|
5
5
|
-- We do NOT drop this extension in unprepare-db since it may have been pre-existing.
|
|
6
|
-
create extension if not exists pg_stat_statements;
|
|
6
|
+
create extension if not exists pg_stat_statements with schema public;
|
|
7
7
|
|
|
8
8
|
|
package/sql/04.optional_rds.sql
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
-- Optional permissions for RDS Postgres / Aurora (best effort)
|
|
2
2
|
|
|
3
|
-
create
|
|
3
|
+
create schema if not exists rds_tools;
|
|
4
|
+
create extension if not exists rds_tools with schema rds_tools;
|
|
4
5
|
grant execute on function rds_tools.pg_ls_multixactdir() to {{ROLE_IDENT}};
|
|
5
6
|
|
|
6
7
|
|
package/sql/06.helpers.sql
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
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
2
|
|
|
5
3
|
/*
|
|
6
4
|
* table_describe
|
|
@@ -15,18 +13,25 @@
|
|
|
15
13
|
*
|
|
16
14
|
* Usage:
|
|
17
15
|
* select postgres_ai.table_describe('public.users');
|
|
18
|
-
* select postgres_ai.table_describe('my_table'); --
|
|
16
|
+
* select postgres_ai.table_describe('my_table'); -- pg_catalog, then public
|
|
17
|
+
*
|
|
18
|
+
* SECURITY INVOKER: it reads only the world-readable catalog, so it needs no
|
|
19
|
+
* privilege the caller lacks. Names are resolved by catalog query, not regclass
|
|
20
|
+
* (which demands USAGE on the schema): unqualified in pg_catalog, then public.
|
|
21
|
+
* search_path is pg_catalog, pg_temp so objects in a writable schema cannot
|
|
22
|
+
* shadow a built-in the function calls.
|
|
19
23
|
*/
|
|
20
24
|
create or replace function postgres_ai.table_describe(
|
|
21
25
|
in table_name text,
|
|
22
26
|
out result text
|
|
23
27
|
)
|
|
24
28
|
language plpgsql
|
|
25
|
-
security
|
|
26
|
-
set search_path = pg_catalog,
|
|
29
|
+
security invoker
|
|
30
|
+
set search_path = pg_catalog, pg_temp
|
|
27
31
|
as $$
|
|
28
32
|
declare
|
|
29
33
|
v_oid oid;
|
|
34
|
+
v_ident text[];
|
|
30
35
|
v_schema text;
|
|
31
36
|
v_table text;
|
|
32
37
|
v_relkind char;
|
|
@@ -37,8 +42,24 @@ declare
|
|
|
37
42
|
v_rec record;
|
|
38
43
|
v_constraint_count int := 0;
|
|
39
44
|
begin
|
|
40
|
-
-- Resolve table name to OID
|
|
41
|
-
|
|
45
|
+
-- Resolve table name to OID: [current database.]schema.table (pg_temp is the
|
|
46
|
+
-- caller's temp schema), or an unqualified name in pg_catalog, then public
|
|
47
|
+
v_ident := parse_ident(table_name);
|
|
48
|
+
select c.oid into v_oid
|
|
49
|
+
from pg_class c
|
|
50
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
51
|
+
where (cardinality(v_ident) <= 2 or (cardinality(v_ident) = 3 and v_ident[1] = current_database()))
|
|
52
|
+
and c.relname = v_ident[cardinality(v_ident)]::name
|
|
53
|
+
and case
|
|
54
|
+
when cardinality(v_ident) = 1 then n.nspname in ('pg_catalog', 'public')
|
|
55
|
+
when v_ident[cardinality(v_ident) - 1] = 'pg_temp' then n.oid = pg_my_temp_schema()
|
|
56
|
+
else n.nspname = v_ident[cardinality(v_ident) - 1]::name
|
|
57
|
+
end
|
|
58
|
+
order by n.nspname = 'public'
|
|
59
|
+
limit 1;
|
|
60
|
+
if v_oid is null then
|
|
61
|
+
raise exception 'relation "%" does not exist', table_name using errcode = '42P01';
|
|
62
|
+
end if;
|
|
42
63
|
|
|
43
64
|
-- Get basic table info
|
|
44
65
|
select
|
|
@@ -207,7 +228,7 @@ begin
|
|
|
207
228
|
when 'f' then v_line := v_line || 'FK: ';
|
|
208
229
|
when 'u' then v_line := v_line || 'UNIQUE: ';
|
|
209
230
|
when 'c' then v_line := v_line || 'CHECK: ';
|
|
210
|
-
else v_line := v_line || v_rec.contype || ': ';
|
|
231
|
+
else v_line := v_line || v_rec.contype::text || ': ';
|
|
211
232
|
end case;
|
|
212
233
|
v_line := v_line || v_rec.conname || ' ' || v_rec.condef;
|
|
213
234
|
v_lines := array_append(v_lines, v_line);
|
package/dist/sql/sql/01.role.sql
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
-- Extensions required for postgres_ai monitoring
|
|
2
|
-
|
|
3
|
-
-- Enable pg_stat_statements for query performance monitoring
|
|
4
|
-
-- Note: Uses IF NOT EXISTS because extension may already be installed.
|
|
5
|
-
-- We do NOT drop this extension in unprepare-db since it may have been pre-existing.
|
|
6
|
-
create extension if not exists pg_stat_statements;
|
|
7
|
-
|
|
8
|
-
|
|
@@ -1,86 +0,0 @@
|
|
|
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
|
-
-- Required for cluster-wide pg_stat_statements collection.
|
|
11
|
-
-- pg_stat_statements is backed by shared memory and holds entries for every database
|
|
12
|
-
-- in the cluster, but a role without pg_read_all_stats sees other users' rows with
|
|
13
|
-
-- queryid and query text redacted to NULL -- which collapses the per-queryid grouping
|
|
14
|
-
-- in the pg_stat_statements metric. pg_monitor already implies pg_read_all_stats;
|
|
15
|
-
-- granting it explicitly documents the dependency and keeps the metric working if the
|
|
16
|
-
-- pg_monitor grant above is ever narrowed. Idempotent.
|
|
17
|
-
grant pg_read_all_stats to {{ROLE_IDENT}};
|
|
18
|
-
|
|
19
|
-
-- Create postgres_ai schema for our objects
|
|
20
|
-
-- Using IF NOT EXISTS for idempotency - prepare-db can be run multiple times
|
|
21
|
-
create schema if not exists postgres_ai;
|
|
22
|
-
grant usage on schema postgres_ai to {{ROLE_IDENT}};
|
|
23
|
-
|
|
24
|
-
-- For bloat analysis: expose pg_statistic via a view
|
|
25
|
-
create or replace view postgres_ai.pg_statistic as
|
|
26
|
-
select
|
|
27
|
-
n.nspname as schemaname,
|
|
28
|
-
c.relname as tablename,
|
|
29
|
-
a.attname,
|
|
30
|
-
s.stanullfrac as null_frac,
|
|
31
|
-
s.stawidth as avg_width,
|
|
32
|
-
false as inherited
|
|
33
|
-
from pg_catalog.pg_statistic s
|
|
34
|
-
join pg_catalog.pg_class c on c.oid = s.starelid
|
|
35
|
-
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
|
|
36
|
-
join pg_catalog.pg_attribute a on a.attrelid = s.starelid and a.attnum = s.staattnum
|
|
37
|
-
where a.attnum > 0 and not a.attisdropped;
|
|
38
|
-
|
|
39
|
-
grant select on postgres_ai.pg_statistic to {{ROLE_IDENT}};
|
|
40
|
-
|
|
41
|
-
-- Hardened clusters sometimes revoke PUBLIC on schema public
|
|
42
|
-
grant usage on schema public to {{ROLE_IDENT}};
|
|
43
|
-
|
|
44
|
-
-- Grant access to the schema where pg_stat_statements is installed.
|
|
45
|
-
-- Some providers (e.g., Supabase) install extensions in a separate 'extensions' schema
|
|
46
|
-
-- rather than pg_catalog. This DO block detects the schema and grants USAGE if needed.
|
|
47
|
-
do $$
|
|
48
|
-
declare
|
|
49
|
-
ext_schema text;
|
|
50
|
-
begin
|
|
51
|
-
select n.nspname into ext_schema
|
|
52
|
-
from pg_extension e
|
|
53
|
-
join pg_namespace n on e.extnamespace = n.oid
|
|
54
|
-
where e.extname = 'pg_stat_statements';
|
|
55
|
-
|
|
56
|
-
-- Only grant if extension exists and is in a non-standard schema
|
|
57
|
-
if ext_schema is not null and ext_schema not in ('pg_catalog', 'public') then
|
|
58
|
-
execute format('grant usage on schema %I to {{ROLE_IDENT}}', ext_schema);
|
|
59
|
-
end if;
|
|
60
|
-
end $$;
|
|
61
|
-
|
|
62
|
-
-- [SEARCH_PATH_BLOCK_START] Keep search_path predictable; postgres_ai first so our objects are found.
|
|
63
|
-
-- Dynamically include the pg_stat_statements extension schema if it's in a non-standard location.
|
|
64
|
-
do $$
|
|
65
|
-
declare
|
|
66
|
-
ext_schema text;
|
|
67
|
-
sp text;
|
|
68
|
-
begin
|
|
69
|
-
-- Detect pg_stat_statements extension schema
|
|
70
|
-
select n.nspname into ext_schema
|
|
71
|
-
from pg_extension e
|
|
72
|
-
join pg_namespace n on e.extnamespace = n.oid
|
|
73
|
-
where e.extname = 'pg_stat_statements';
|
|
74
|
-
|
|
75
|
-
-- Build search_path: include extension schema if in non-standard location
|
|
76
|
-
if ext_schema is not null and ext_schema not in ('pg_catalog', 'public') then
|
|
77
|
-
sp := 'postgres_ai, ' || quote_ident(ext_schema) || ', "$user", public, pg_catalog';
|
|
78
|
-
else
|
|
79
|
-
sp := 'postgres_ai, "$user", public, pg_catalog';
|
|
80
|
-
end if;
|
|
81
|
-
|
|
82
|
-
execute format('alter user {{ROLE_IDENT}} set search_path = %s', sp);
|
|
83
|
-
end $$;
|
|
84
|
-
-- [SEARCH_PATH_BLOCK_END]
|
|
85
|
-
|
|
86
|
-
|
|
@@ -1,8 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,317 +0,0 @@
|
|
|
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
|
-
* table_describe
|
|
7
|
-
*
|
|
8
|
-
* Collects comprehensive information about a table for LLM analysis.
|
|
9
|
-
* Returns a compact text format with:
|
|
10
|
-
* - Table metadata (type, size estimates)
|
|
11
|
-
* - Columns (name, type, nullable, default)
|
|
12
|
-
* - Indexes
|
|
13
|
-
* - Constraints (PK, FK, unique, check)
|
|
14
|
-
* - Maintenance stats (vacuum/analyze times)
|
|
15
|
-
*
|
|
16
|
-
* Usage:
|
|
17
|
-
* select postgres_ai.table_describe('public.users');
|
|
18
|
-
* select postgres_ai.table_describe('my_table'); -- uses search_path
|
|
19
|
-
*/
|
|
20
|
-
create or replace function postgres_ai.table_describe(
|
|
21
|
-
in table_name text,
|
|
22
|
-
out result text
|
|
23
|
-
)
|
|
24
|
-
language plpgsql
|
|
25
|
-
security definer
|
|
26
|
-
set search_path = pg_catalog, public
|
|
27
|
-
as $$
|
|
28
|
-
declare
|
|
29
|
-
v_oid oid;
|
|
30
|
-
v_schema text;
|
|
31
|
-
v_table text;
|
|
32
|
-
v_relkind char;
|
|
33
|
-
v_relpages int;
|
|
34
|
-
v_reltuples float;
|
|
35
|
-
v_lines text[] := '{}';
|
|
36
|
-
v_line text;
|
|
37
|
-
v_rec record;
|
|
38
|
-
v_constraint_count int := 0;
|
|
39
|
-
begin
|
|
40
|
-
-- Resolve table name to OID (handles schema-qualified and search_path)
|
|
41
|
-
v_oid := table_name::regclass::oid;
|
|
42
|
-
|
|
43
|
-
-- Get basic table info
|
|
44
|
-
select
|
|
45
|
-
n.nspname,
|
|
46
|
-
c.relname,
|
|
47
|
-
c.relkind,
|
|
48
|
-
c.relpages,
|
|
49
|
-
c.reltuples
|
|
50
|
-
into v_schema, v_table, v_relkind, v_relpages, v_reltuples
|
|
51
|
-
from pg_class c
|
|
52
|
-
join pg_namespace n on n.oid = c.relnamespace
|
|
53
|
-
where c.oid = v_oid;
|
|
54
|
-
|
|
55
|
-
-- Validate object type - only tables, views, and materialized views are supported
|
|
56
|
-
if v_relkind not in ('r', 'p', 'v', 'm', 'f') then
|
|
57
|
-
raise exception 'table_describe does not support % (relkind=%)',
|
|
58
|
-
case v_relkind
|
|
59
|
-
when 'i' then 'indexes'
|
|
60
|
-
when 'I' then 'partitioned indexes'
|
|
61
|
-
when 'S' then 'sequences'
|
|
62
|
-
when 't' then 'TOAST tables'
|
|
63
|
-
when 'c' then 'composite types'
|
|
64
|
-
else format('objects of type "%s"', v_relkind)
|
|
65
|
-
end,
|
|
66
|
-
v_relkind;
|
|
67
|
-
end if;
|
|
68
|
-
|
|
69
|
-
-- Header
|
|
70
|
-
v_lines := array_append(v_lines, format('Table: %I.%I', v_schema, v_table));
|
|
71
|
-
v_lines := array_append(v_lines, format('Type: %s | relpages: %s | reltuples: %s',
|
|
72
|
-
case v_relkind
|
|
73
|
-
when 'r' then 'table'
|
|
74
|
-
when 'p' then 'partitioned table'
|
|
75
|
-
when 'v' then 'view'
|
|
76
|
-
when 'm' then 'materialized view'
|
|
77
|
-
when 'f' then 'foreign table'
|
|
78
|
-
end,
|
|
79
|
-
v_relpages,
|
|
80
|
-
case when v_reltuples < 0 then '-1' else v_reltuples::bigint::text end
|
|
81
|
-
));
|
|
82
|
-
|
|
83
|
-
-- Vacuum/analyze stats (only for tables and materialized views, not views)
|
|
84
|
-
if v_relkind in ('r', 'p', 'm', 'f') then
|
|
85
|
-
select
|
|
86
|
-
format('Vacuum: %s (auto: %s) | Analyze: %s (auto: %s)',
|
|
87
|
-
coalesce(to_char(last_vacuum at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never'),
|
|
88
|
-
coalesce(to_char(last_autovacuum at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never'),
|
|
89
|
-
coalesce(to_char(last_analyze at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never'),
|
|
90
|
-
coalesce(to_char(last_autoanalyze at time zone 'UTC', 'YYYY-MM-DD HH24:MI:SS UTC'), 'never')
|
|
91
|
-
)
|
|
92
|
-
into v_line
|
|
93
|
-
from pg_stat_all_tables
|
|
94
|
-
where relid = v_oid;
|
|
95
|
-
|
|
96
|
-
if v_line is not null then
|
|
97
|
-
v_lines := array_append(v_lines, v_line);
|
|
98
|
-
end if;
|
|
99
|
-
end if;
|
|
100
|
-
|
|
101
|
-
v_lines := array_append(v_lines, '');
|
|
102
|
-
|
|
103
|
-
-- Columns
|
|
104
|
-
v_lines := array_append(v_lines, 'Columns:');
|
|
105
|
-
for v_rec in
|
|
106
|
-
select
|
|
107
|
-
a.attname,
|
|
108
|
-
format_type(a.atttypid, a.atttypmod) as data_type,
|
|
109
|
-
a.attnotnull,
|
|
110
|
-
(select pg_get_expr(d.adbin, d.adrelid, true)
|
|
111
|
-
from pg_attrdef d
|
|
112
|
-
where d.adrelid = a.attrelid and d.adnum = a.attnum and a.atthasdef) as default_val,
|
|
113
|
-
a.attidentity,
|
|
114
|
-
a.attgenerated
|
|
115
|
-
from pg_attribute a
|
|
116
|
-
where a.attrelid = v_oid
|
|
117
|
-
and a.attnum > 0
|
|
118
|
-
and not a.attisdropped
|
|
119
|
-
order by a.attnum
|
|
120
|
-
loop
|
|
121
|
-
v_line := format(' %s %s', v_rec.attname, v_rec.data_type);
|
|
122
|
-
|
|
123
|
-
if v_rec.attnotnull then
|
|
124
|
-
v_line := v_line || ' NOT NULL';
|
|
125
|
-
end if;
|
|
126
|
-
|
|
127
|
-
if v_rec.attidentity = 'a' then
|
|
128
|
-
v_line := v_line || ' GENERATED ALWAYS AS IDENTITY';
|
|
129
|
-
elsif v_rec.attidentity = 'd' then
|
|
130
|
-
v_line := v_line || ' GENERATED BY DEFAULT AS IDENTITY';
|
|
131
|
-
elsif v_rec.attgenerated = 's' then
|
|
132
|
-
v_line := v_line || format(' GENERATED ALWAYS AS (%s) STORED', v_rec.default_val);
|
|
133
|
-
elsif v_rec.default_val is not null then
|
|
134
|
-
v_line := v_line || format(' DEFAULT %s', v_rec.default_val);
|
|
135
|
-
end if;
|
|
136
|
-
|
|
137
|
-
v_lines := array_append(v_lines, v_line);
|
|
138
|
-
end loop;
|
|
139
|
-
|
|
140
|
-
-- View definition (for views and materialized views)
|
|
141
|
-
if v_relkind in ('v', 'm') then
|
|
142
|
-
v_lines := array_append(v_lines, '');
|
|
143
|
-
v_lines := array_append(v_lines, 'Definition:');
|
|
144
|
-
v_line := pg_get_viewdef(v_oid, true);
|
|
145
|
-
if v_line is not null then
|
|
146
|
-
-- Indent the view definition
|
|
147
|
-
v_line := ' ' || replace(v_line, e'\n', e'\n ');
|
|
148
|
-
v_lines := array_append(v_lines, v_line);
|
|
149
|
-
end if;
|
|
150
|
-
end if;
|
|
151
|
-
|
|
152
|
-
-- Indexes (tables, partitioned tables, and materialized views can have indexes)
|
|
153
|
-
if v_relkind in ('r', 'p', 'm') then
|
|
154
|
-
v_lines := array_append(v_lines, '');
|
|
155
|
-
v_lines := array_append(v_lines, 'Indexes:');
|
|
156
|
-
for v_rec in
|
|
157
|
-
select
|
|
158
|
-
i.relname as index_name,
|
|
159
|
-
pg_get_indexdef(i.oid) as index_def,
|
|
160
|
-
ix.indisprimary,
|
|
161
|
-
ix.indisunique
|
|
162
|
-
from pg_index ix
|
|
163
|
-
join pg_class i on i.oid = ix.indexrelid
|
|
164
|
-
where ix.indrelid = v_oid
|
|
165
|
-
order by ix.indisprimary desc, ix.indisunique desc, i.relname
|
|
166
|
-
loop
|
|
167
|
-
v_line := ' ';
|
|
168
|
-
if v_rec.indisprimary then
|
|
169
|
-
v_line := v_line || 'PRIMARY KEY: ';
|
|
170
|
-
elsif v_rec.indisunique then
|
|
171
|
-
v_line := v_line || 'UNIQUE: ';
|
|
172
|
-
else
|
|
173
|
-
v_line := v_line || 'INDEX: ';
|
|
174
|
-
end if;
|
|
175
|
-
-- Extract just the column part from index definition
|
|
176
|
-
v_line := v_line || v_rec.index_name || ' ' ||
|
|
177
|
-
regexp_replace(v_rec.index_def, '^CREATE.*INDEX.*ON.*USING\s+\w+\s*', '');
|
|
178
|
-
v_lines := array_append(v_lines, v_line);
|
|
179
|
-
end loop;
|
|
180
|
-
|
|
181
|
-
if not exists (select 1 from pg_index where indrelid = v_oid) then
|
|
182
|
-
v_lines := array_append(v_lines, ' (none)');
|
|
183
|
-
end if;
|
|
184
|
-
end if;
|
|
185
|
-
|
|
186
|
-
-- Constraints (only tables can have constraints)
|
|
187
|
-
if v_relkind in ('r', 'p', 'f') then
|
|
188
|
-
v_lines := array_append(v_lines, '');
|
|
189
|
-
v_lines := array_append(v_lines, 'Constraints:');
|
|
190
|
-
v_constraint_count := 0;
|
|
191
|
-
|
|
192
|
-
for v_rec in
|
|
193
|
-
select
|
|
194
|
-
conname,
|
|
195
|
-
contype,
|
|
196
|
-
pg_get_constraintdef(oid, true) as condef
|
|
197
|
-
from pg_constraint
|
|
198
|
-
where conrelid = v_oid
|
|
199
|
-
and contype != 'p' -- skip primary key (shown with indexes)
|
|
200
|
-
order by
|
|
201
|
-
case contype when 'f' then 1 when 'u' then 2 when 'c' then 3 else 4 end,
|
|
202
|
-
conname
|
|
203
|
-
loop
|
|
204
|
-
v_constraint_count := v_constraint_count + 1;
|
|
205
|
-
v_line := ' ';
|
|
206
|
-
case v_rec.contype
|
|
207
|
-
when 'f' then v_line := v_line || 'FK: ';
|
|
208
|
-
when 'u' then v_line := v_line || 'UNIQUE: ';
|
|
209
|
-
when 'c' then v_line := v_line || 'CHECK: ';
|
|
210
|
-
else v_line := v_line || v_rec.contype || ': ';
|
|
211
|
-
end case;
|
|
212
|
-
v_line := v_line || v_rec.conname || ' ' || v_rec.condef;
|
|
213
|
-
v_lines := array_append(v_lines, v_line);
|
|
214
|
-
end loop;
|
|
215
|
-
|
|
216
|
-
if v_constraint_count = 0 then
|
|
217
|
-
v_lines := array_append(v_lines, ' (none)');
|
|
218
|
-
end if;
|
|
219
|
-
|
|
220
|
-
-- Foreign keys referencing this table
|
|
221
|
-
v_lines := array_append(v_lines, '');
|
|
222
|
-
v_lines := array_append(v_lines, 'Referenced by:');
|
|
223
|
-
v_constraint_count := 0;
|
|
224
|
-
|
|
225
|
-
for v_rec in
|
|
226
|
-
select
|
|
227
|
-
conname,
|
|
228
|
-
conrelid::regclass::text as from_table,
|
|
229
|
-
pg_get_constraintdef(oid, true) as condef
|
|
230
|
-
from pg_constraint
|
|
231
|
-
where confrelid = v_oid
|
|
232
|
-
and contype = 'f'
|
|
233
|
-
order by conrelid::regclass::text, conname
|
|
234
|
-
loop
|
|
235
|
-
v_constraint_count := v_constraint_count + 1;
|
|
236
|
-
v_lines := array_append(v_lines, format(' %s.%s %s',
|
|
237
|
-
v_rec.from_table, v_rec.conname, v_rec.condef));
|
|
238
|
-
end loop;
|
|
239
|
-
|
|
240
|
-
if v_constraint_count = 0 then
|
|
241
|
-
v_lines := array_append(v_lines, ' (none)');
|
|
242
|
-
end if;
|
|
243
|
-
end if;
|
|
244
|
-
|
|
245
|
-
-- Partition info (if partitioned table or partition)
|
|
246
|
-
if v_relkind = 'p' then
|
|
247
|
-
-- This is a partitioned table - show partition key and partitions
|
|
248
|
-
v_lines := array_append(v_lines, '');
|
|
249
|
-
v_lines := array_append(v_lines, 'Partitioning:');
|
|
250
|
-
|
|
251
|
-
select format(' %s BY %s',
|
|
252
|
-
case partstrat
|
|
253
|
-
when 'r' then 'RANGE'
|
|
254
|
-
when 'l' then 'LIST'
|
|
255
|
-
when 'h' then 'HASH'
|
|
256
|
-
else partstrat
|
|
257
|
-
end,
|
|
258
|
-
pg_get_partkeydef(v_oid)
|
|
259
|
-
)
|
|
260
|
-
into v_line
|
|
261
|
-
from pg_partitioned_table
|
|
262
|
-
where partrelid = v_oid;
|
|
263
|
-
|
|
264
|
-
if v_line is not null then
|
|
265
|
-
v_lines := array_append(v_lines, v_line);
|
|
266
|
-
end if;
|
|
267
|
-
|
|
268
|
-
-- List partitions
|
|
269
|
-
v_constraint_count := 0;
|
|
270
|
-
for v_rec in
|
|
271
|
-
select
|
|
272
|
-
c.oid::regclass::text as partition_name,
|
|
273
|
-
pg_get_expr(c.relpartbound, c.oid) as partition_bound,
|
|
274
|
-
c.relpages,
|
|
275
|
-
c.reltuples
|
|
276
|
-
from pg_inherits i
|
|
277
|
-
join pg_class c on c.oid = i.inhrelid
|
|
278
|
-
where i.inhparent = v_oid
|
|
279
|
-
order by c.oid::regclass::text
|
|
280
|
-
loop
|
|
281
|
-
v_constraint_count := v_constraint_count + 1;
|
|
282
|
-
v_lines := array_append(v_lines, format(' %s: %s (relpages: %s, reltuples: %s)',
|
|
283
|
-
v_rec.partition_name, v_rec.partition_bound,
|
|
284
|
-
v_rec.relpages,
|
|
285
|
-
case when v_rec.reltuples < 0 then '-1' else v_rec.reltuples::bigint::text end
|
|
286
|
-
));
|
|
287
|
-
end loop;
|
|
288
|
-
|
|
289
|
-
v_lines := array_append(v_lines, format(' Total partitions: %s', v_constraint_count));
|
|
290
|
-
|
|
291
|
-
elsif exists (select 1 from pg_inherits where inhrelid = v_oid) then
|
|
292
|
-
-- This is a partition - show parent and bound
|
|
293
|
-
v_lines := array_append(v_lines, '');
|
|
294
|
-
v_lines := array_append(v_lines, 'Partition of:');
|
|
295
|
-
|
|
296
|
-
select format(' %s FOR VALUES %s',
|
|
297
|
-
i.inhparent::regclass::text,
|
|
298
|
-
pg_get_expr(c.relpartbound, c.oid)
|
|
299
|
-
)
|
|
300
|
-
into v_line
|
|
301
|
-
from pg_inherits i
|
|
302
|
-
join pg_class c on c.oid = i.inhrelid
|
|
303
|
-
where i.inhrelid = v_oid;
|
|
304
|
-
|
|
305
|
-
if v_line is not null then
|
|
306
|
-
v_lines := array_append(v_lines, v_line);
|
|
307
|
-
end if;
|
|
308
|
-
end if;
|
|
309
|
-
|
|
310
|
-
result := array_to_string(v_lines, e'\n');
|
|
311
|
-
end;
|
|
312
|
-
$$;
|
|
313
|
-
|
|
314
|
-
comment on function postgres_ai.table_describe(text) is
|
|
315
|
-
'Returns comprehensive table information in compact text format for LLM analysis';
|
|
316
|
-
|
|
317
|
-
grant execute on function postgres_ai.table_describe(text) to {{ROLE_IDENT}};
|