Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update explain functionality to show results #371

Merged
merged 13 commits into from
Feb 29, 2024
Merged
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -63,9 +63,16 @@ async def execute(self) -> APIResponse[_ReturnT]:
headers=self.headers,
)
try:
mansueli marked this conversation as resolved.
Show resolved Hide resolved
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())
@@ -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
Original file line number Diff line number Diff line change
@@ -63,9 +63,16 @@ def execute(self) -> APIResponse[_ReturnT]:
headers=self.headers,
)
try:
mansueli marked this conversation as resolved.
Show resolved Hide resolved
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())
@@ -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
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
Generic,
Iterable,
List,
Literal,
NamedTuple,
Optional,
Tuple,
@@ -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(
39 changes: 39 additions & 0 deletions tests/_async/test_filter_request_builder_integration.py
Original file line number Diff line number Diff line change
@@ -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()
8 changes: 3 additions & 5 deletions tests/_async/test_request_builder.py
Original file line number Diff line number Diff line change
@@ -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:
39 changes: 39 additions & 0 deletions tests/_sync/test_filter_request_builder_integration.py
Original file line number Diff line number Diff line change
@@ -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()
@@ -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()
4 changes: 1 addition & 3 deletions tests/_sync/test_request_builder.py
Original file line number Diff line number Diff line change
@@ -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: