Repository navigation
Fix CSV search download - #17
Conversation
The columns field was removed from CLEANSearchQueryParams in a1ea901, but the CSV branch still read params.columns, so every format=csv request returned a 500.
Array cells were written as Python list reprs, e.g. ['2.7.10.2'].
matthewberry
left a comment
There was a problem hiding this comment.
Two findings are about lines outside the diff hunks, so they go here:
CSV export silently stops at the default page size (search.py:208-215). When limit is not given, it falls back to AUTO_PAGINATION_THRESHOLD (5000). The CSV branch returns only those rows, with no total count and no next-page link. So ?ec_number=1.1.1.1&format=csv with more than 5000 matches produces a file that looks complete but isn't. This existed before the PR, but now that the download works, people will actually hit it.
get_total_count runs on every CSV request and its result is thrown away (search.py:212). That's an extra COUNT over the joined views, with the same WHERE clause, for nothing. Either move it into the JSON branch or send it as an X-Total-Count header, which would also fix the problem above.
These findings come from reading the code; none of them were run against a database.
| if params.columns: | ||
| fieldnames = params.columns | ||
| elif data and len(data) > 0: | ||
| if data and len(data) > 0: |
There was a problem hiding this comment.
No header row when the query matches nothing. Column names now come only from data[0], so an empty result writes just \r\n. A client reading it with csv.DictReader, as the endpoint's docstring example does, gets fieldnames=None instead of an empty table with headers.
The fixed column list is already in CLEANColumn (app/models/clean_data.py:10), so fieldnames = [c.value for c in CLEANColumn] would always produce a header. It would also make this branch unnecessary, including the redundant len(data) > 0, since a non-empty list is already truthy.
There was a problem hiding this comment.
Confirmed, and fixed in ebafcd8. (Small correction: DictReader gives fieldnames == [] rather than None, but the result is the same: no header.) The header now comes from CLEANColumn, which I checked matches the 14 columns in the get_filtered_data SELECT exactly and in order. test_csv_has_header_when_no_rows_match covers it.
| writer.writerow(row) | ||
| # Join array columns into one ';'-separated cell | ||
| writer.writerow({ | ||
| k: ";".join(str(v) for v in value) if isinstance(value, list) else value |
There was a problem hiding this comment.
A NULL element inside an array becomes the text None. str(None) turns e.g. clean_ec_confidence_array = [0.91, NULL] into 0.91;None, which shows up as a value in spreadsheets and parsers. I don't know whether the data allows NULL elements; if it can, something like "" if v is None else str(v) would handle it.
There was a problem hiding this comment.
Confirmed for the CSV, but declining here: the same NULL element already makes the JSON path return a 500, because ECNumberConfidence.score: float and annot_ec_number_array: List[str] reject None (checked with TestClient). JSON already assumes there are no NULL elements and no JSON failures have been reported, so if they do turn up, the fix belongs in both outputs, starting with JSON.
| writer.writerow(row) | ||
| # Join array columns into one ';'-separated cell | ||
| writer.writerow({ | ||
| k: ";".join(str(v) for v in value) if isinstance(value, list) else value |
There was a problem hiding this comment.
An empty array and a NULL array both become an empty cell. annot_ec_number_array comes from a LEFT JOIN (queries.py:132). NULL there means there's no annotation record, and [] means a record with no EC numbers. Both become '' here, while the JSON output keeps them distinct (null vs []).
There was a problem hiding this comment.
Confirmed (CSV writes an empty cell for both, JSON null vs []). Declining: CSV has no standard NULL, and I know of no one who needs to tell "no annotation record" from "record with no EC numbers" in the CSV download.
| @@ -233,7 +231,11 @@ async def get_data( | |||
| ) | |||
| writer.writeheader() | |||
There was a problem hiding this comment.
The CSV and JSON outputs have different shapes. The CSV writes raw DB rows, while JSON goes through the CLEANDataBase mapping. So the column names differ (protein_name/protein, ncbi_taxid/ncbi_tax_id, enzyme_function/function), and EC predictions are parallel ;-joined arrays here but predicted_ec[{ec_number, score}] in JSON. A future column change has to be made in two places. Flattening the model for the CSV, or using array_to_string in SQL, would fix the cause instead of special-casing types in the writer. This doesn't block the PR.
There was a problem hiding this comment.
Agreed, and this was the case before this PR. Deferring to #19, since aligning the two would rename existing CSV columns and needs a decision about CSV consumers.
| writer.writeheader() | ||
| for row in data: | ||
| writer.writerow(row) | ||
| # Join array columns into one ';'-separated cell |
There was a problem hiding this comment.
No regression test. The PR body describes a manual TestClient run with mocked get_total_count/get_filtered_data. Committing that as a test would keep the 500, caused by the field removed in a1ea901, from coming back. The repo has a pytest.ini but no tests yet.
There was a problem hiding this comment.
Added in ebafcd8: tests/test_search_csv.py (TestClient, DB calls mocked), plus pythonpath = . in pytest.ini so app imports. On origin/main both tests fail with the original 500; on this branch both pass.
Take fieldnames from CLEANColumn, which matches the SELECT list in get_filtered_data, so an empty result still has a header row. Add TestClient tests for the CSV branch with the DB calls mocked.
|
Re the two findings in the review summary, both confirmed by reading (the CSV branch returns only
|
Problem
Every `/api/v1/search` request with `format=csv` returned a 500, e.g.
https://cleandb-api.platform.moleculemaker.org/api/v1/search?accession=P51451&format=csv&limit=10
```
{"detail":"Error retrieving data: 'CLEANSearchQueryParams' object has no attribute 'columns'"}
```
a1ea901 removed the `columns` field from `CLEANSearchQueryParams`, but the CSV branch still read `params.columns` before anything else. `parse_query_params` never accepted a `columns` query param, so that branch was dead.
Changes
Testing