Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 1 addition & 16 deletions app/dataapi/domain/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,7 @@
catalogs.RawCatalog.NATURE,
]

METADATA_ALLOWED_SCHEMAS = frozenset(
{
"common",
"rawdata",
"designation",
"icrs",
"cz",
"layer2",
"layer0",
"nature",
"note",
"photometry",
"distance",
"morphology",
},
)
METADATA_ALLOWED_SCHEMAS = frozenset({"layer2"})


def _json_cell(value: object) -> object:
Expand Down
6 changes: 3 additions & 3 deletions app/dataapi/presentation/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ def __init__(
"/v1/tap/tables",
http.HTTPMethod.GET,
api.tap_tables,
"List TAP table metadata for whitelisted schemas.",
"List TAP table metadata for LEDA tables.",
),
server.Route(
"/v1/tap/sync",
http.HTTPMethod.GET,
api.tap_sync,
"Execute an arbitrary SQL query (TAP /sync).",
"Runs a read-only SQL query against whitelisted schemas and returns a VOTable-like JSON payload.",
"Execute a read-only SQL query against layer2 (TAP /sync).",
"Runs a read-only SQL query with search_path set to layer2 and returns a VOTable-like JSON payload.",
rate_limit="60/minute",
),
server.Route(
Expand Down
1 change: 1 addition & 0 deletions app/dataapi/repository/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ def query_with_metadata(
wrapped,
timeout_seconds=timeout_seconds,
read_only=True,
search_path="layer2",
)
if not dict_rows:
return repo_model.QueryWithMetadataResult(columns=[], rows=[])
Expand Down
5 changes: 5 additions & 0 deletions app/lib/storage/postgres/postgres_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ def query(
params: list[Any] | None = None,
timeout_seconds: float | None = None,
read_only: bool = False,
search_path: str | None = None,
) -> list[rows.DictRow]:
log.debug("SQL query", query=self.query_str(query).replace("\n", " "), args=params or [])

Expand Down Expand Up @@ -173,6 +174,10 @@ def _execute(cursor: psycopg.Cursor) -> list[rows.DictRow]:
cursor.execute(
sql.SQL("SET LOCAL statement_timeout = {}").format(sql.Literal(f"{timeout_ms}ms"))
)
if search_path is not None:
cursor.execute(
sql.SQL("SET LOCAL search_path TO {}").format(sql.Identifier(search_path))
)
result = _execute(cursor)
finally:
if read_only:
Expand Down
44 changes: 21 additions & 23 deletions tests/dataapi/integration/metadata_api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ def test_tap_tables_default_max(self) -> None:
data = response.json()["data"]
self.assertIn("schemas", data)
self.assertGreater(len(data["schemas"]), 0)
common = next(s for s in data["schemas"] if s["schema_name"] == "common")
bib = next(t for t in common["tables"] if t["name"] == 'common."bib"')
self.assertEqual(bib["type"], "table")
self.assertIn("columns", bib)
self.assertIsInstance(bib["columns"], list)
self.assertGreater(len(bib["columns"]), 0)
id_col = next(c for c in bib["columns"] if c["name"] == "id")
self.assertEqual(id_col["datatype"], "int")
layer2 = next(s for s in data["schemas"] if s["schema_name"] == "layer2")
icrs = next(t for t in layer2["tables"] if t["name"] == 'layer2."icrs"')
self.assertEqual(icrs["type"], "table")
self.assertIn("columns", icrs)
self.assertIsInstance(icrs["columns"], list)
self.assertGreater(len(icrs["columns"]), 0)
pgc_col = next(c for c in icrs["columns"] if c["name"] == "pgc")
self.assertEqual(pgc_col["datatype"], "int")

def test_tap_tables_min(self) -> None:
response = self.client.get("/api/v1/tap/tables", params={"detail": "min"})
Expand All @@ -84,24 +84,24 @@ def test_tap_sync_basic(self) -> None:
response = self.client.get(
"/api/v1/tap/sync",
params={
"query": "SELECT type_name, objclass, description FROM nature.object_type ORDER BY type_name LIMIT 1",
"query": "SELECT catalog FROM last_update ORDER BY catalog LIMIT 1",
},
)
self.assertEqual(response.status_code, 200)
table = response.json()["data"]["resource"]["table"]
col_names = [c["name"] for c in table["columns"]]
self.assertEqual(col_names, ["type_name", "objclass", "description"])
type_name_col = table["columns"][0]
self.assertEqual(type_name_col["datatype"], "char")
self.assertEqual(type_name_col["arraysize"], "*")
self.assertEqual(col_names, ["catalog"])
catalog_col = table["columns"][0]
self.assertEqual(catalog_col["datatype"], "char")
self.assertEqual(catalog_col["arraysize"], "*")
self.assertEqual(len(table["data"]), 1)
self.assertEqual(len(table["data"][0]), 3)
self.assertEqual(len(table["data"][0]), 1)

def test_tap_sync_maxrec(self) -> None:
response = self.client.get(
"/api/v1/tap/sync",
params={
"query": "SELECT type_name FROM nature.object_type ORDER BY type_name",
"query": "SELECT catalog FROM last_update ORDER BY catalog",
"maxrec": 2,
},
)
Expand All @@ -113,7 +113,7 @@ def test_tap_sync_maxrec_not_bypassed_by_line_comment(self) -> None:
response = self.client.get(
"/api/v1/tap/sync",
params={
"query": "SELECT type_name FROM nature.object_type ORDER BY type_name --",
"query": "SELECT catalog FROM last_update ORDER BY catalog --",
"maxrec": 2,
},
)
Expand All @@ -125,7 +125,7 @@ def test_tap_sync_maxrec_not_bypassed_by_unterminated_block_comment(self) -> Non
response = self.client.get(
"/api/v1/tap/sync",
params={
"query": ("SELECT type_name FROM nature.object_type ORDER BY type_name) AS _tap_sync LIMIT 10000 /*"),
"query": ("SELECT catalog FROM last_update ORDER BY catalog) AS _tap_sync LIMIT 10000 /*"),
"maxrec": 2,
},
)
Expand All @@ -135,9 +135,7 @@ def test_tap_sync_rejects_semicolon_separated_queries(self) -> None:
response = self.client.get(
"/api/v1/tap/sync",
params={
"query": (
"SELECT type_name FROM nature.object_type LIMIT 1; SELECT type_name FROM nature.object_type LIMIT 1"
),
"query": ("SELECT catalog FROM last_update LIMIT 1; SELECT catalog FROM last_update LIMIT 1"),
},
)
self.assertEqual(response.status_code, 500)
Expand All @@ -146,13 +144,13 @@ def test_tap_sync_like_with_percent_wildcard(self) -> None:
response = self.client.get(
"/api/v1/tap/sync",
params={
"query": "SELECT type_name FROM nature.object_type WHERE type_name NOT LIKE '%gal%'",
"query": "SELECT catalog FROM last_update WHERE catalog NOT LIKE '%icrs%'",
},
)
self.assertEqual(response.status_code, 200)
table = response.json()["data"]["resource"]["table"]
self.assertEqual([c["name"] for c in table["columns"]], ["type_name"])
self.assertTrue(all("gal" not in row[0].lower() for row in table["data"]))
self.assertEqual([c["name"] for c in table["columns"]], ["catalog"])
self.assertTrue(all("icrs" not in row[0] for row in table["data"]))

def test_tap_sync_query_timeout(self) -> None:
with self.assertRaises(psycopg.errors.QueryCanceled):
Expand Down
8 changes: 5 additions & 3 deletions tests/regression/tap_compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,18 @@ def check_tap_sync(session: requests.Session) -> None:
response = session.get(
"/v1/tap/sync",
params={
"query": "SELECT type_name, objclass, description FROM nature.object_type ORDER BY type_name",
"query": "SELECT pgc, ra, dec FROM icrs ORDER BY pgc",
"lang": "PostgreSQL",
"format": "json",
"maxrec": 5,
},
)
response.raise_for_status()
table = response.json()["data"]["resource"]["table"]
assert [c["name"] for c in table["columns"]] == ["type_name", "objclass", "description"]
assert all(c["datatype"] == "char" for c in table["columns"])
assert [c["name"] for c in table["columns"]] == ["pgc", "ra", "dec"]
assert table["columns"][0]["datatype"] == "int"
assert table["columns"][1]["datatype"] == "double"
assert table["columns"][2]["datatype"] == "double"
assert len(table["data"]) == 5
assert all(len(row) == 3 for row in table["data"])

Expand Down