Skip to content

Commit 3e0ea2e

Browse files
fix: explain functionality to show results (#371)
Co-authored-by: Rodrigo Mansueli Nunes <[email protected]>
1 parent 602d66e commit 3e0ea2e

11 files changed

+292
-197
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,7 @@ dmypy.json
136136

137137
# Poetry local config
138138
poetry.toml
139+
140+
# MacOS annoyance
141+
.DS_Store
142+
**/.DS_Store

infra/docker-compose.yaml

+20-17
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
1-
version: "3"
1+
# docker-compose.yml
2+
version: '3'
23
services:
3-
server:
4-
image: postgrest/postgrest
4+
rest:
5+
image: postgrest/postgrest:v11.2.2
56
ports:
6-
- "3000:3000"
7+
- '3000:3000'
78
environment:
8-
PGRST_DB_URI: postgres://app_user:password@db:5432/app_db
9-
PGRST_DB_SCHEMA: public
10-
PGRST_DB_ANON_ROLE: app_user # In production this role should not be the same as the one used for connection
11-
PGRST_OPENAPI_SERVER_PROXY_URI: "http://127.0.0.1:3000"
9+
PGRST_DB_URI: postgres://postgres:postgres@db:5432/postgres
10+
PGRST_DB_SCHEMAS: public,personal
11+
PGRST_DB_EXTRA_SEARCH_PATH: extensions
12+
PGRST_DB_ANON_ROLE: postgres
13+
PGRST_DB_PLAN_ENABLED: 1
14+
PGRST_DB_TX_END: commit-allow-override
1215
depends_on:
1316
- db
1417
db:
15-
image: postgres
18+
image: supabase/postgres:15.1.0.37
1619
ports:
17-
- "5432:5432"
18-
environment:
19-
POSTGRES_DB: app_db
20-
POSTGRES_USER: app_user
21-
POSTGRES_PASSWORD: password
22-
# Uncomment this if you want to persist the data. Create your boostrap SQL file in the project root
20+
- '5432:5432'
2321
volumes:
24-
# - "./pgdata:/var/lib/postgresql/data"
25-
- "./init.sql:/docker-entrypoint-initdb.d/init.sql"
22+
- .:/docker-entrypoint-initdb.d/
23+
environment:
24+
POSTGRES_DB: postgres
25+
POSTGRES_USER: postgres
26+
POSTGRES_PASSWORD: postgres
27+
POSTGRES_HOST: /var/run/postgresql
28+
POSTGRES_PORT: 5432

infra/init.sql

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,4 @@ create or replace function public.list_stored_countries()
7575
language sql
7676
as $function$
7777
select * from countries;
78-
$function$
78+
$function$;

poetry.lock

+160-156
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

postgrest/_async/request_builder.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,16 @@ async def execute(self) -> APIResponse[_ReturnT]:
6363
headers=self.headers,
6464
)
6565
try:
66-
if (
67-
200 <= r.status_code <= 299
68-
): # Response.ok from JS (https://developer.mozilla.org/en-US/docs/Web/API/Response/ok)
66+
if r.is_success:
67+
if self.http_method != "HEAD":
68+
body = r.text
69+
if self.headers.get("Accept") == "text/csv":
70+
return body
71+
if self.headers.get(
72+
"Accept"
73+
) and "application/vnd.pgrst.plan" in self.headers.get("Accept"):
74+
if not "+json" in self.headers.get("Accept"):
75+
return body
6976
return APIResponse[_ReturnT].from_http_request_response(r)
7077
else:
7178
raise APIError(r.json())
@@ -394,6 +401,3 @@ def delete(
394401
return AsyncFilterRequestBuilder[_ReturnT](
395402
self.session, self.path, method, headers, params, json
396403
)
397-
398-
def stub(self):
399-
return None

postgrest/_sync/request_builder.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,16 @@ def execute(self) -> APIResponse[_ReturnT]:
6363
headers=self.headers,
6464
)
6565
try:
66-
if (
67-
200 <= r.status_code <= 299
68-
): # Response.ok from JS (https://developer.mozilla.org/en-US/docs/Web/API/Response/ok)
66+
if r.is_success:
67+
if self.http_method != "HEAD":
68+
body = r.text
69+
if self.headers.get("Accept") == "text/csv":
70+
return body
71+
if self.headers.get(
72+
"Accept"
73+
) and "application/vnd.pgrst.plan" in self.headers.get("Accept"):
74+
if not "+json" in self.headers.get("Accept"):
75+
return body
6976
return APIResponse[_ReturnT].from_http_request_response(r)
7077
else:
7178
raise APIError(r.json())
@@ -394,6 +401,3 @@ def delete(
394401
return SyncFilterRequestBuilder[_ReturnT](
395402
self.session, self.path, method, headers, params, json
396403
)
397-
398-
def stub(self):
399-
return None

postgrest/base_request_builder.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
Generic,
1010
Iterable,
1111
List,
12+
Literal,
1213
NamedTuple,
1314
Optional,
1415
Tuple,
@@ -503,16 +504,17 @@ def explain(
503504
settings: bool = False,
504505
buffers: bool = False,
505506
wal: bool = False,
506-
format: str = "",
507+
format: Literal["text", "json"] = "text",
507508
) -> Self:
508509
options = [
509510
key
510511
for key, value in locals().items()
511512
if key not in ["self", "format"] and value
512513
]
513514
options_str = "|".join(options)
514-
options_str = f'options="{options_str};"' if options_str else ""
515-
self.headers["Accept"] = f"application/vnd.pgrst.plan+{format}; {options_str}"
515+
self.headers[
516+
"Accept"
517+
] = f"application/vnd.pgrst.plan+{format}; options={options_str}"
516518
return self
517519

518520
def order(

tests/_async/test_filter_request_builder_integration.py

+39
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,45 @@ async def test_or_on_reference_table():
389389
]
390390

391391

392+
async def test_explain_json():
393+
res = (
394+
await rest_client()
395+
.from_("countries")
396+
.select("country_name, cities!inner(name)")
397+
.or_("country_id.eq.10,name.eq.Paris", reference_table="cities")
398+
.explain(format="json", analyze=True)
399+
.execute()
400+
)
401+
assert res.data[0]["Plan"]["Node Type"] == "Aggregate"
402+
403+
404+
async def test_csv():
405+
res = (
406+
await rest_client()
407+
.from_("countries")
408+
.select("country_name, iso")
409+
.in_("nicename", ["Albania", "Algeria"])
410+
.csv()
411+
.execute()
412+
)
413+
assert "ALBANIA,AL\nALGERIA,DZ" in res.data
414+
415+
416+
async def test_explain_text():
417+
res = (
418+
await rest_client()
419+
.from_("countries")
420+
.select("country_name, cities!inner(name)")
421+
.or_("country_id.eq.10,name.eq.Paris", reference_table="cities")
422+
.explain(analyze=True, verbose=True, settings=True, buffers=True, wal=True)
423+
.execute()
424+
)
425+
assert (
426+
"((cities_1.country_id = countries.id) AND ((cities_1.country_id = '10'::bigint) OR (cities_1.name = 'Paris'::text)))"
427+
in res
428+
)
429+
430+
392431
async def test_rpc_with_single():
393432
res = (
394433
await rest_client()

tests/_async/test_request_builder.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,15 @@ class TestExplain:
139139
def test_explain_plain(self, request_builder: AsyncRequestBuilder):
140140
builder = request_builder.select("*").explain()
141141
assert builder.params["select"] == "*"
142-
assert "application/vnd.pgrst.plan+" in str(builder.headers.get("accept"))
142+
assert "application/vnd.pgrst.plan" in str(builder.headers.get("accept"))
143143

144144
def test_explain_options(self, request_builder: AsyncRequestBuilder):
145145
builder = request_builder.select("*").explain(
146146
format="json", analyze=True, verbose=True, buffers=True, wal=True
147147
)
148148
assert builder.params["select"] == "*"
149-
assert "application/vnd.pgrst.plan+json" in str(builder.headers.get("accept"))
150-
assert 'options="analyze|verbose|buffers|wal;' in str(
151-
builder.headers.get("accept")
152-
)
149+
assert "application/vnd.pgrst.plan+json;" in str(builder.headers.get("accept"))
150+
assert "options=analyze|verbose|buffers|wal" in str(builder.headers.get("accept"))
153151

154152

155153
class TestRange:

tests/_sync/test_filter_request_builder_integration.py

+39
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,18 @@ def test_or_in():
360360
]
361361

362362

363+
def test_csv():
364+
res = (
365+
rest_client()
366+
.from_("countries")
367+
.select("country_name, iso")
368+
.in_("nicename", ["Albania", "Algeria"])
369+
.csv()
370+
.execute()
371+
)
372+
assert "ALBANIA,AL\nALGERIA,DZ" in res.data
373+
374+
363375
def test_or_on_reference_table():
364376
res = (
365377
rest_client()
@@ -382,6 +394,33 @@ def test_or_on_reference_table():
382394
]
383395

384396

397+
def test_explain_json():
398+
res = (
399+
rest_client()
400+
.from_("countries")
401+
.select("country_name, cities!inner(name)")
402+
.or_("country_id.eq.10,name.eq.Paris", reference_table="cities")
403+
.explain(format="json", analyze=True)
404+
.execute()
405+
)
406+
assert res.data[0]["Plan"]["Node Type"] == "Aggregate"
407+
408+
409+
def test_explain_text():
410+
res = (
411+
rest_client()
412+
.from_("countries")
413+
.select("country_name, cities!inner(name)")
414+
.or_("country_id.eq.10,name.eq.Paris", reference_table="cities")
415+
.explain(analyze=True, verbose=True, settings=True, buffers=True, wal=True)
416+
.execute()
417+
)
418+
assert (
419+
"((cities_1.country_id = countries.id) AND ((cities_1.country_id = '10'::bigint) OR (cities_1.name = 'Paris'::text)))"
420+
in res
421+
)
422+
423+
385424
def test_rpc_with_single():
386425
res = (
387426
rest_client()

tests/_sync/test_request_builder.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,7 @@ def test_explain_options(self, request_builder: SyncRequestBuilder):
147147
)
148148
assert builder.params["select"] == "*"
149149
assert "application/vnd.pgrst.plan+json" in str(builder.headers.get("accept"))
150-
assert 'options="analyze|verbose|buffers|wal;' in str(
151-
builder.headers.get("accept")
152-
)
150+
assert "options=analyze|verbose|buffers|wal" in str(builder.headers.get("accept"))
153151

154152

155153
class TestRange:

0 commit comments

Comments
 (0)