postgresai 0.14.0-dev.8 → 0.14.0-dev.81

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.
Files changed (96) hide show
  1. package/README.md +161 -61
  2. package/bin/postgres-ai.ts +2596 -428
  3. package/bun.lock +258 -0
  4. package/bunfig.toml +20 -0
  5. package/dist/bin/postgres-ai.js +31277 -1575
  6. package/dist/sql/01.role.sql +16 -0
  7. package/dist/sql/02.extensions.sql +8 -0
  8. package/dist/sql/03.permissions.sql +38 -0
  9. package/dist/sql/04.optional_rds.sql +6 -0
  10. package/dist/sql/05.optional_self_managed.sql +8 -0
  11. package/dist/sql/06.helpers.sql +439 -0
  12. package/dist/sql/sql/01.role.sql +16 -0
  13. package/dist/sql/sql/02.extensions.sql +8 -0
  14. package/dist/sql/sql/03.permissions.sql +38 -0
  15. package/dist/sql/sql/04.optional_rds.sql +6 -0
  16. package/dist/sql/sql/05.optional_self_managed.sql +8 -0
  17. package/dist/sql/sql/06.helpers.sql +439 -0
  18. package/dist/sql/sql/uninit/01.helpers.sql +5 -0
  19. package/dist/sql/sql/uninit/02.permissions.sql +30 -0
  20. package/dist/sql/sql/uninit/03.role.sql +27 -0
  21. package/dist/sql/uninit/01.helpers.sql +5 -0
  22. package/dist/sql/uninit/02.permissions.sql +30 -0
  23. package/dist/sql/uninit/03.role.sql +27 -0
  24. package/lib/auth-server.ts +124 -106
  25. package/lib/checkup-api.ts +386 -0
  26. package/lib/checkup-dictionary.ts +113 -0
  27. package/lib/checkup.ts +1512 -0
  28. package/lib/config.ts +6 -3
  29. package/lib/init.ts +655 -189
  30. package/lib/issues.ts +848 -193
  31. package/lib/mcp-server.ts +391 -91
  32. package/lib/metrics-loader.ts +127 -0
  33. package/lib/supabase.ts +824 -0
  34. package/lib/util.ts +61 -0
  35. package/package.json +22 -10
  36. package/packages/postgres-ai/README.md +26 -0
  37. package/packages/postgres-ai/bin/postgres-ai.js +27 -0
  38. package/packages/postgres-ai/package.json +27 -0
  39. package/scripts/embed-checkup-dictionary.ts +106 -0
  40. package/scripts/embed-metrics.ts +154 -0
  41. package/sql/01.role.sql +16 -0
  42. package/sql/02.extensions.sql +8 -0
  43. package/sql/03.permissions.sql +38 -0
  44. package/sql/04.optional_rds.sql +6 -0
  45. package/sql/05.optional_self_managed.sql +8 -0
  46. package/sql/06.helpers.sql +439 -0
  47. package/sql/uninit/01.helpers.sql +5 -0
  48. package/sql/uninit/02.permissions.sql +30 -0
  49. package/sql/uninit/03.role.sql +27 -0
  50. package/test/auth.test.ts +258 -0
  51. package/test/checkup.integration.test.ts +321 -0
  52. package/test/checkup.test.ts +1116 -0
  53. package/test/config-consistency.test.ts +36 -0
  54. package/test/init.integration.test.ts +508 -0
  55. package/test/init.test.ts +916 -0
  56. package/test/issues.cli.test.ts +538 -0
  57. package/test/issues.test.ts +456 -0
  58. package/test/mcp-server.test.ts +1527 -0
  59. package/test/schema-validation.test.ts +81 -0
  60. package/test/supabase.test.ts +568 -0
  61. package/test/test-utils.ts +128 -0
  62. package/tsconfig.json +12 -20
  63. package/dist/bin/postgres-ai.d.ts +0 -3
  64. package/dist/bin/postgres-ai.d.ts.map +0 -1
  65. package/dist/bin/postgres-ai.js.map +0 -1
  66. package/dist/lib/auth-server.d.ts +0 -31
  67. package/dist/lib/auth-server.d.ts.map +0 -1
  68. package/dist/lib/auth-server.js +0 -263
  69. package/dist/lib/auth-server.js.map +0 -1
  70. package/dist/lib/config.d.ts +0 -45
  71. package/dist/lib/config.d.ts.map +0 -1
  72. package/dist/lib/config.js +0 -181
  73. package/dist/lib/config.js.map +0 -1
  74. package/dist/lib/init.d.ts +0 -64
  75. package/dist/lib/init.d.ts.map +0 -1
  76. package/dist/lib/init.js +0 -399
  77. package/dist/lib/init.js.map +0 -1
  78. package/dist/lib/issues.d.ts +0 -75
  79. package/dist/lib/issues.d.ts.map +0 -1
  80. package/dist/lib/issues.js +0 -336
  81. package/dist/lib/issues.js.map +0 -1
  82. package/dist/lib/mcp-server.d.ts +0 -9
  83. package/dist/lib/mcp-server.d.ts.map +0 -1
  84. package/dist/lib/mcp-server.js +0 -168
  85. package/dist/lib/mcp-server.js.map +0 -1
  86. package/dist/lib/pkce.d.ts +0 -32
  87. package/dist/lib/pkce.d.ts.map +0 -1
  88. package/dist/lib/pkce.js +0 -101
  89. package/dist/lib/pkce.js.map +0 -1
  90. package/dist/lib/util.d.ts +0 -27
  91. package/dist/lib/util.d.ts.map +0 -1
  92. package/dist/lib/util.js +0 -46
  93. package/dist/lib/util.js.map +0 -1
  94. package/dist/package.json +0 -46
  95. package/test/init.integration.test.cjs +0 -269
  96. package/test/init.test.cjs +0 -76
@@ -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
+
@@ -0,0 +1,5 @@
1
+ -- Drop helper functions created by prepare-db (template-filled by cli/lib/init.ts)
2
+ -- Run before dropping the postgres_ai schema.
3
+
4
+ drop function if exists postgres_ai.explain_generic(text, text, text);
5
+ drop function if exists postgres_ai.table_describe(text);
@@ -0,0 +1,30 @@
1
+ -- Revoke permissions and drop objects created by prepare-db (template-filled by cli/lib/init.ts)
2
+
3
+ -- Drop the postgres_ai.pg_statistic view
4
+ drop view if exists postgres_ai.pg_statistic;
5
+
6
+ -- Drop the postgres_ai schema (CASCADE to handle any remaining objects)
7
+ drop schema if exists postgres_ai cascade;
8
+
9
+ -- Revoke permissions from the monitoring role
10
+ -- Use a DO block to handle the case where the role doesn't exist
11
+ do $$ begin
12
+ revoke pg_monitor from {{ROLE_IDENT}};
13
+ exception when undefined_object then
14
+ null; -- Role doesn't exist, nothing to revoke
15
+ end $$;
16
+
17
+ do $$ begin
18
+ revoke select on pg_catalog.pg_index from {{ROLE_IDENT}};
19
+ exception when undefined_object then
20
+ null; -- Role doesn't exist
21
+ end $$;
22
+
23
+ do $$ begin
24
+ revoke connect on database {{DB_IDENT}} from {{ROLE_IDENT}};
25
+ exception when undefined_object then
26
+ null; -- Role doesn't exist
27
+ end $$;
28
+
29
+ -- Note: USAGE on public is typically granted by default; we don't revoke it
30
+ -- to avoid breaking other applications that may rely on it.
@@ -0,0 +1,27 @@
1
+ -- Drop the monitoring role created by prepare-db (template-filled by cli/lib/init.ts)
2
+ -- This must run after revoking all permissions from the role.
3
+
4
+ -- Use a DO block to handle the case where the role doesn't exist
5
+ do $$ begin
6
+ -- Reassign owned objects to current user before dropping
7
+ -- This handles any objects that might have been created by the role
8
+ begin
9
+ execute format('reassign owned by %I to current_user', {{ROLE_LITERAL}});
10
+ exception when undefined_object then
11
+ null; -- Role doesn't exist, nothing to reassign
12
+ end;
13
+
14
+ -- Drop owned objects (in case reassign didn't work for some objects)
15
+ begin
16
+ execute format('drop owned by %I', {{ROLE_LITERAL}});
17
+ exception when undefined_object then
18
+ null; -- Role doesn't exist
19
+ end;
20
+
21
+ -- Drop the role
22
+ begin
23
+ execute format('drop role %I', {{ROLE_LITERAL}});
24
+ exception when undefined_object then
25
+ null; -- Role doesn't exist, that's fine
26
+ end;
27
+ end $$;