034: fix double JSON serialization in AsyncAPIClient.request

Root cause: AsyncAPIClient.request() passes its  parameter directly
to httpx.AsyncClient.request(json=data). When callers pass a pre-serialized
JSON string (data=json.dumps(dict)), httpx re-encodes it via json.dumps(),
resulting in a double-encoded JSON string body instead of a JSON object.

This caused ALL POST/PUT requests with string data to fail — Superset received
a JSON string instead of a JSON object, returning GENERIC_BACKEND_ERROR
('dictionary update sequence element #0 has length 1; 2 is required').

Fix: if data is a string, pass it via httpx  parameter (raw body);
if it's a dict/list, pass via  for automatic encoding.

Affected callers (6 files) now correctly send JSON objects:
  - preview_executor.py: chart data requests
  - superset_executor.py
  - _run_source.py
  - _datasets.py: update_dataset
  - _datasets_preview.py: compile_dataset_preview
  - _dashboards_write.py

Also simplified preview_executor.fetch_sample_rows back to single-strategy
(chart data API only) since the root cause is now fixed.
This commit is contained in:
2026-06-05 12:07:55 +03:00
parent 72fcf698af
commit 5d9d214bd6
2 changed files with 30 additions and 19 deletions

View File

@@ -37,7 +37,7 @@ class PreviewExecutor:
# region fetch_sample_rows [TYPE Function]
# @PURPOSE: Fetch sample rows from Superset dataset for preview.
# @SIDE_EFFECT Calls Superset chart data endpoint.
# @SIDE_EFFECT Calls Superset chart data API.
async def fetch_sample_rows(
self,
job: TranslationJob,
@@ -60,13 +60,20 @@ class PreviewExecutor:
dataset_id=int(job.source_datasource_id), dataset_record=dataset_detail,
template_params={}, effective_filters=[],
)
# Build column list from dataset schema for explicit column projection
column_names = [
col["name"] for col in dataset_detail.get("columns", [])
if col.get("name") and col.get("is_active", True)
]
queries = query_context.get("queries", [])
if queries:
queries[0]["row_limit"] = sample_size
queries[0].pop("result_type", None)
queries[0].pop("columns", None)
queries[0]["columns"] = column_names or []
queries[0]["metrics"] = []
query_context["result_type"] = "samples"
queries[0].pop("result_type", None)
query_context["result_type"] = "query"
form_data = query_context.get("form_data", {})
form_data.pop("query_mode", None)