Skip to content

Commit 141eb94

Browse files
committed
allow comments with semicolons in queries
1 parent 1aa1016 commit 141eb94

14 files changed

+213
-12
lines changed

index_advisor--0.1.2.sql

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
create or replace function index_advisor(
2+
query text
3+
)
4+
returns table (
5+
startup_cost_before jsonb,
6+
startup_cost_after jsonb,
7+
total_cost_before jsonb,
8+
total_cost_after jsonb,
9+
index_statements text[]
10+
)
11+
volatile
12+
language plpgsql
13+
as $$
14+
declare
15+
n_args int;
16+
prepared_statement_name text = 'index_advisor_working_statement';
17+
hypopg_schema_name text = (select extnamespace::regnamespace::text from pg_extension where extname = 'hypopg');
18+
explain_plan_statement text;
19+
rec record;
20+
plan_initial jsonb;
21+
plan_final jsonb;
22+
statements text[] = '{}';
23+
begin
24+
25+
-- Remove comment lines if they contain a semicolon (this is common is queries through supabase.js)
26+
query := trim(
27+
regexp_replace(
28+
regexp_replace(
29+
regexp_replace(query,'\/\*.+\*\/', '', 'g'),
30+
'--[^\r\n]*', ' ', 'g'),
31+
'\s+', ' ', 'g')
32+
);
33+
34+
-- Disallow multiple statements
35+
if query ilike '%;%' then
36+
raise exception 'query must not contain a semicolon';
37+
end if;
38+
39+
-- Hack to support PostgREST because the prepared statement for args incorrectly defaults to text
40+
query := replace(query, 'WITH pgrst_payload AS (SELECT $1 AS json_data)', 'WITH pgrst_payload AS (SELECT $1::json AS json_data)');
41+
42+
-- Create a prepared statement for the given query
43+
deallocate all;
44+
execute format('prepare %I as %s', prepared_statement_name, query);
45+
46+
-- Detect how many arguments are present in the prepared statement
47+
n_args = (
48+
select
49+
coalesce(array_length(parameter_types, 1), 0)
50+
from
51+
pg_prepared_statements
52+
where
53+
name = prepared_statement_name
54+
limit
55+
1
56+
);
57+
58+
-- Create a SQL statement that can be executed to collect the explain plan
59+
explain_plan_statement = format(
60+
'set local plan_cache_mode = force_generic_plan; explain (format json) execute %I%s',
61+
--'explain (format json) execute %I%s',
62+
prepared_statement_name,
63+
case
64+
when n_args = 0 then ''
65+
else format(
66+
'(%s)', array_to_string(array_fill('null'::text, array[n_args]), ',')
67+
)
68+
end
69+
);
70+
71+
-- Store the query plan before any new indexes
72+
execute explain_plan_statement into plan_initial;
73+
74+
-- Create possible indexes
75+
for rec in (
76+
with extension_regclass as (
77+
select
78+
distinct objid as oid
79+
from
80+
pg_depend
81+
where
82+
deptype = 'e'
83+
)
84+
select
85+
pc.relnamespace::regnamespace::text as schema_name,
86+
pc.relname as table_name,
87+
pa.attname as column_name,
88+
format(
89+
'select %I.hypopg_create_index($i$create index on %I.%I(%I)$i$)',
90+
hypopg_schema_name,
91+
pc.relnamespace::regnamespace::text,
92+
pc.relname,
93+
pa.attname
94+
) hypopg_statement
95+
from
96+
pg_catalog.pg_class pc
97+
join pg_catalog.pg_attribute pa
98+
on pc.oid = pa.attrelid
99+
left join extension_regclass er
100+
on pc.oid = er.oid
101+
left join pg_index pi
102+
on pc.oid = pi.indrelid
103+
and (select array_agg(x) from unnest(pi.indkey) v(x)) = array[pa.attnum]
104+
and pi.indexprs is null -- ignore expression indexes
105+
and pi.indpred is null -- ignore partial indexes
106+
where
107+
pc.relnamespace::regnamespace::text not in ( -- ignore schema list
108+
'pg_catalog', 'pg_toast', 'information_schema'
109+
)
110+
and er.oid is null -- ignore entities owned by extensions
111+
and pc.relkind in ('r', 'm') -- regular tables, and materialized views
112+
and pc.relpersistence = 'p' -- permanent tables (not unlogged or temporary)
113+
and pa.attnum > 0
114+
and not pa.attisdropped
115+
and pi.indrelid is null
116+
and pa.atttypid in (20,16,1082,1184,1114,701,23,21,700,1083,2950,1700,25,18,1042,1043)
117+
)
118+
loop
119+
-- Create the hypothetical index
120+
execute rec.hypopg_statement;
121+
end loop;
122+
123+
/*
124+
for rec in select * from hypopg()
125+
loop
126+
raise notice '%', rec;
127+
end loop;
128+
*/
129+
130+
-- Create a prepared statement for the given query
131+
-- The original prepared statement MUST be dropped because its plan is cached
132+
execute format('deallocate %I', prepared_statement_name);
133+
execute format('prepare %I as %s', prepared_statement_name, query);
134+
135+
-- Store the query plan after new indexes
136+
execute explain_plan_statement into plan_final;
137+
138+
--raise notice '%', plan_final;
139+
140+
-- Idenfity referenced indexes in new plan
141+
execute format(
142+
'select
143+
coalesce(array_agg(hypopg_get_indexdef(indexrelid) order by indrelid, indkey::text), $i${}$i$::text[])
144+
from
145+
%I.hypopg()
146+
where
147+
%s ilike ($i$%%$i$ || indexname || $i$%%$i$)
148+
',
149+
hypopg_schema_name,
150+
quote_literal(plan_final)::text
151+
) into statements;
152+
153+
-- Reset all hypothetical indexes
154+
perform hypopg_reset();
155+
156+
-- Reset prepared statements
157+
deallocate all;
158+
159+
return query values (
160+
(plan_initial -> 0 -> 'Plan' -> 'Startup Cost'),
161+
(plan_final -> 0 -> 'Plan' -> 'Startup Cost'),
162+
(plan_initial -> 0 -> 'Plan' -> 'Total Cost'),
163+
(plan_final -> 0 -> 'Plan' -> 'Total Cost'),
164+
statements::text[]
165+
);
166+
167+
end;
168+
$$;

index_advisor.control

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
comment = 'Query index advisor'
2-
default_version = '0.1.1'
2+
default_version = '0.1.2'
33
relocatable = true
44
requires = hypopg
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
begin;
2-
create extension index_advisor version '0.1.1' cascade;
2+
create extension index_advisor version '0.1.2' cascade;
33
NOTICE: installing required extension "hypopg"
44
select index_advisor($$ select 1; $$);
55
ERROR: query must not contain a semicolon
6-
CONTEXT: PL/pgSQL function index_advisor(text) line 15 at RAISE
6+
CONTEXT: PL/pgSQL function index_advisor(text) line 24 at RAISE
77
rollback;

test/expected/integration.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
begin;
2-
create extension index_advisor version '0.1.1' cascade;
2+
create extension index_advisor version '0.1.2' cascade;
33
NOTICE: installing required extension "hypopg"
44
create table public.book(
55
id int,

test/expected/issue_1.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
begin;
2-
create extension index_advisor version '0.1.1' cascade;
2+
create extension index_advisor version '0.1.2' cascade;
33
NOTICE: installing required extension "hypopg"
44
create table public.book(
55
id int,

test/expected/multi_index.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
begin;
2-
create extension index_advisor version '0.1.1' cascade;
2+
create extension index_advisor version '0.1.2' cascade;
33
NOTICE: installing required extension "hypopg"
44
create table author(
55
id serial primary key,

test/expected/postgrest_query.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
begin;
2-
create extension index_advisor version '0.1.1' cascade;
2+
create extension index_advisor version '0.1.2' cascade;
33
NOTICE: installing required extension "hypopg"
44
create function get_info(x int) returns text language sql as $$ select 'foo' $$;
55
select index_advisor($$
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
begin;
2+
-- Semicolons should be allowed in comments because they are common in prep stmts
3+
create extension index_advisor version '0.1.2' cascade;
4+
NOTICE: installing required extension "hypopg"
5+
create table public.book(
6+
id int,
7+
name text
8+
);
9+
select index_advisor($$
10+
-- some comment with a semicolon;
11+
select * from book where id = $1
12+
$$);
13+
index_advisor
14+
------------------------------------------------------------------------------
15+
(0.00,4.07,25.88,13.54,"{""CREATE INDEX ON public.book USING btree (id)""}")
16+
(1 row)
17+
18+
rollback;

test/sql/disallow_semicolon.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
begin;
22

3-
create extension index_advisor version '0.1.1' cascade;
3+
create extension index_advisor version '0.1.2' cascade;
44

55
select index_advisor($$ select 1; $$);
66

test/sql/integration.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
begin;
22

3-
create extension index_advisor version '0.1.1' cascade;
3+
create extension index_advisor version '0.1.2' cascade;
44

55
create table public.book(
66
id int,

0 commit comments

Comments
 (0)