Skip to content

Commit

Permalink
fix: explain functionality to show results (#371)
Browse files Browse the repository at this point in the history
Co-authored-by: Rodrigo Mansueli Nunes <rodrigo@mansueli.com>
  • Loading branch information
silentworks and mansueli committed Feb 29, 2024
1 parent 602d66e commit 3e0ea2e
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 197 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Expand Up @@ -136,3 +136,7 @@ dmypy.json

# Poetry local config
poetry.toml

# MacOS annoyance
.DS_Store
**/.DS_Store
37 changes: 20 additions & 17 deletions infra/docker-compose.yaml
@@ -1,25 +1,28 @@
version: "3"
# docker-compose.yml
version: '3'
services:
server:
image: postgrest/postgrest
rest:
image: postgrest/postgrest:v11.2.2
ports:
- "3000:3000"
- '3000:3000'
environment:
PGRST_DB_URI: postgres://app_user:password@db:5432/app_db
PGRST_DB_SCHEMA: public
PGRST_DB_ANON_ROLE: app_user # In production this role should not be the same as the one used for connection
PGRST_OPENAPI_SERVER_PROXY_URI: "http://127.0.0.1:3000"
PGRST_DB_URI: postgres://postgres:postgres@db:5432/postgres
PGRST_DB_SCHEMAS: public,personal
PGRST_DB_EXTRA_SEARCH_PATH: extensions
PGRST_DB_ANON_ROLE: postgres
PGRST_DB_PLAN_ENABLED: 1
PGRST_DB_TX_END: commit-allow-override
depends_on:
- db
db:
image: postgres
image: supabase/postgres:15.1.0.37
ports:
- "5432:5432"
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app_user
POSTGRES_PASSWORD: password
# Uncomment this if you want to persist the data. Create your boostrap SQL file in the project root
- '5432:5432'
volumes:
# - "./pgdata:/var/lib/postgresql/data"
- "./init.sql:/docker-entrypoint-initdb.d/init.sql"
- .:/docker-entrypoint-initdb.d/
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST: /var/run/postgresql
POSTGRES_PORT: 5432
2 changes: 1 addition & 1 deletion infra/init.sql
Expand Up @@ -75,4 +75,4 @@ create or replace function public.list_stored_countries()
language sql
as $function$
select * from countries;
$function$
$function$;
316 changes: 160 additions & 156 deletions poetry.lock

Large diffs are not rendered by default.

16 changes: 10 additions & 6 deletions postgrest/_async/request_builder.py
Expand Up @@ -63,9 +63,16 @@ async def execute(self) -> APIResponse[_ReturnT]:
headers=self.headers,
)
try:
if (
200 <= r.status_code <= 299
): # Response.ok from JS (https://developer.mozilla.org/en-US/docs/Web/API/Response/ok)
if r.is_success:
if self.http_method != "HEAD":
body = r.text
if self.headers.get("Accept") == "text/csv":
return body
if self.headers.get(
"Accept"
) and "application/vnd.pgrst.plan" in self.headers.get("Accept"):
if not "+json" in self.headers.get("Accept"):
return body
return APIResponse[_ReturnT].from_http_request_response(r)
else:
raise APIError(r.json())
Expand Down Expand Up @@ -394,6 +401,3 @@ def delete(
return AsyncFilterRequestBuilder[_ReturnT](
self.session, self.path, method, headers, params, json
)

def stub(self):
return None
16 changes: 10 additions & 6 deletions postgrest/_sync/request_builder.py
Expand Up @@ -63,9 +63,16 @@ def execute(self) -> APIResponse[_ReturnT]:
headers=self.headers,
)
try:
if (
200 <= r.status_code <= 299
): # Response.ok from JS (https://developer.mozilla.org/en-US/docs/Web/API/Response/ok)
if r.is_success:
if self.http_method != "HEAD":
body = r.text
if self.headers.get("Accept") == "text/csv":
return body
if self.headers.get(
"Accept"
) and "application/vnd.pgrst.plan" in self.headers.get("Accept"):
if not "+json" in self.headers.get("Accept"):
return body
return APIResponse[_ReturnT].from_http_request_response(r)
else:
raise APIError(r.json())
Expand Down Expand Up @@ -394,6 +401,3 @@ def delete(
return SyncFilterRequestBuilder[_ReturnT](
self.session, self.path, method, headers, params, json
)

def stub(self):
return None
8 changes: 5 additions & 3 deletions postgrest/base_request_builder.py
Expand Up @@ -9,6 +9,7 @@
Generic,
Iterable,
List,
Literal,
NamedTuple,
Optional,
Tuple,
Expand Down Expand Up @@ -503,16 +504,17 @@ def explain(
settings: bool = False,
buffers: bool = False,
wal: bool = False,
format: str = "",
format: Literal["text", "json"] = "text",
) -> Self:
options = [
key
for key, value in locals().items()
if key not in ["self", "format"] and value
]
options_str = "|".join(options)
options_str = f'options="{options_str};"' if options_str else ""
self.headers["Accept"] = f"application/vnd.pgrst.plan+{format}; {options_str}"
self.headers[
"Accept"
] = f"application/vnd.pgrst.plan+{format}; options={options_str}"
return self

def order(
Expand Down
39 changes: 39 additions & 0 deletions tests/_async/test_filter_request_builder_integration.py
Expand Up @@ -389,6 +389,45 @@ async def test_or_on_reference_table():
]


async def test_explain_json():
res = (
await rest_client()
.from_("countries")
.select("country_name, cities!inner(name)")
.or_("country_id.eq.10,name.eq.Paris", reference_table="cities")
.explain(format="json", analyze=True)
.execute()
)
assert res.data[0]["Plan"]["Node Type"] == "Aggregate"


async def test_csv():
res = (
await rest_client()
.from_("countries")
.select("country_name, iso")
.in_("nicename", ["Albania", "Algeria"])
.csv()
.execute()
)
assert "ALBANIA,AL\nALGERIA,DZ" in res.data


async def test_explain_text():
res = (
await rest_client()
.from_("countries")
.select("country_name, cities!inner(name)")
.or_("country_id.eq.10,name.eq.Paris", reference_table="cities")
.explain(analyze=True, verbose=True, settings=True, buffers=True, wal=True)
.execute()
)
assert (
"((cities_1.country_id = countries.id) AND ((cities_1.country_id = '10'::bigint) OR (cities_1.name = 'Paris'::text)))"
in res
)


async def test_rpc_with_single():
res = (
await rest_client()
Expand Down
8 changes: 3 additions & 5 deletions tests/_async/test_request_builder.py
Expand Up @@ -139,17 +139,15 @@ class TestExplain:
def test_explain_plain(self, request_builder: AsyncRequestBuilder):
builder = request_builder.select("*").explain()
assert builder.params["select"] == "*"
assert "application/vnd.pgrst.plan+" in str(builder.headers.get("accept"))
assert "application/vnd.pgrst.plan" in str(builder.headers.get("accept"))

def test_explain_options(self, request_builder: AsyncRequestBuilder):
builder = request_builder.select("*").explain(
format="json", analyze=True, verbose=True, buffers=True, wal=True
)
assert builder.params["select"] == "*"
assert "application/vnd.pgrst.plan+json" in str(builder.headers.get("accept"))
assert 'options="analyze|verbose|buffers|wal;' in str(
builder.headers.get("accept")
)
assert "application/vnd.pgrst.plan+json;" in str(builder.headers.get("accept"))
assert "options=analyze|verbose|buffers|wal" in str(builder.headers.get("accept"))


class TestRange:
Expand Down
39 changes: 39 additions & 0 deletions tests/_sync/test_filter_request_builder_integration.py
Expand Up @@ -360,6 +360,18 @@ def test_or_in():
]


def test_csv():
res = (
rest_client()
.from_("countries")
.select("country_name, iso")
.in_("nicename", ["Albania", "Algeria"])
.csv()
.execute()
)
assert "ALBANIA,AL\nALGERIA,DZ" in res.data


def test_or_on_reference_table():
res = (
rest_client()
Expand All @@ -382,6 +394,33 @@ def test_or_on_reference_table():
]


def test_explain_json():
res = (
rest_client()
.from_("countries")
.select("country_name, cities!inner(name)")
.or_("country_id.eq.10,name.eq.Paris", reference_table="cities")
.explain(format="json", analyze=True)
.execute()
)
assert res.data[0]["Plan"]["Node Type"] == "Aggregate"


def test_explain_text():
res = (
rest_client()
.from_("countries")
.select("country_name, cities!inner(name)")
.or_("country_id.eq.10,name.eq.Paris", reference_table="cities")
.explain(analyze=True, verbose=True, settings=True, buffers=True, wal=True)
.execute()
)
assert (
"((cities_1.country_id = countries.id) AND ((cities_1.country_id = '10'::bigint) OR (cities_1.name = 'Paris'::text)))"
in res
)


def test_rpc_with_single():
res = (
rest_client()
Expand Down
4 changes: 1 addition & 3 deletions tests/_sync/test_request_builder.py
Expand Up @@ -147,9 +147,7 @@ def test_explain_options(self, request_builder: SyncRequestBuilder):
)
assert builder.params["select"] == "*"
assert "application/vnd.pgrst.plan+json" in str(builder.headers.get("accept"))
assert 'options="analyze|verbose|buffers|wal;' in str(
builder.headers.get("accept")
)
assert "options=analyze|verbose|buffers|wal" in str(builder.headers.get("accept"))


class TestRange:
Expand Down

0 comments on commit 3e0ea2e

Please sign in to comment.