postgresai 0.14.0-dev.55 → 0.14.0-dev.57

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,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,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
+