Skip to content

Commit 1bac179

Browse files
committed
billing: iterate EXTERNAL_LINKS chunks by index, not via next_chunk_index
The previous fix used the EXTERNAL_LINKS disposition but assumed the API would chain chunks via ``next_chunk_index`` / ``next_chunk_internal_link``. For multi-chunk responses the Statements API does not actually populate those fields on the initial response — total_chunk_count says 3 but only chunk 0's link is included, and next_chunk_index is null. Result: we only downloaded ~327k of the ~807k product rows (about 40% data loss). Fix: explicitly loop chunk_index from 1 to manifest.total_chunk_count-1 and fetch each via /api/2.0/sql/statements/{sid}/result/chunks/{i}. Chunk 0 still comes from the initial response (no extra round-trip). Log expected vs collected row count for visibility. Co-authored-by: Isaac
1 parent dd084ed commit 1bac179

1 file changed

Lines changed: 26 additions & 25 deletions

File tree

workflows/09_discover_billing.py

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,14 @@
131131
def _execute_sql(sql: str) -> List[Dict[str, Any]]:
132132
"""Run SQL via the Statement Execution API with EXTERNAL_LINKS disposition
133133
so large result sets (e.g. the all-products query that returns ~800k rows)
134-
don't get silently truncated by the INLINE response size limit (~16MB)."""
134+
don't get truncated by INLINE's ~16MB response cap.
135+
136+
Chunks are fetched by index from 0 to manifest.total_chunk_count - 1.
137+
The API does NOT chain ``next_chunk_index`` reliably for multi-chunk
138+
EXTERNAL_LINKS responses — we have to iterate explicitly. The initial
139+
response contains chunk 0's link; chunks 1..N-1 are fetched via
140+
``/api/2.0/sql/statements/{sid}/result/chunks/{i}``.
141+
"""
135142
if not WAREHOUSE_ID:
136143
print(" No warehouse ID")
137144
return []
@@ -166,19 +173,16 @@ def _execute_sql(sql: str) -> List[Dict[str, Any]]:
166173
return []
167174

168175
cols = [c["name"] for c in resp.get("manifest", {}).get("schema", {}).get("columns", [])]
169-
result = resp.get("result", {}) or {}
176+
total_chunks = int(resp.get("manifest", {}).get("total_chunk_count") or 0)
177+
total_rows = int(resp.get("manifest", {}).get("total_row_count") or 0)
178+
print(f" manifest: {total_rows} rows in {total_chunks} chunk(s)")
170179

171-
# EXTERNAL_LINKS returns chunked presigned URLs in result.external_links.
172-
# First chunk is in the initial response; further chunks must be fetched
173-
# via subsequent GETs on /result/chunks/{chunk_index}.
174180
import json as _json
175181
import urllib.request as _ureq
176182
rows: List[Dict[str, Any]] = []
177-
chunk = result
178-
chunk_count = 0
179-
while chunk is not None:
180-
links = chunk.get("external_links") or []
181-
for link in links:
183+
184+
def _download_chunk_links(chunk_obj: dict, chunk_label: str):
185+
for link in chunk_obj.get("external_links") or []:
182186
url = link.get("external_link") or ""
183187
if not url:
184188
continue
@@ -188,23 +192,20 @@ def _execute_sql(sql: str) -> List[Dict[str, Any]]:
188192
for row in data:
189193
rows.append(dict(zip(cols, row)))
190194
except Exception as exc:
191-
print(f" ⚠️ chunk download failed: {exc}")
192-
chunk_count += 1
193-
next_idx = chunk.get("next_chunk_index")
194-
next_url = chunk.get("next_chunk_internal_link")
195-
if next_idx is None and not next_url:
196-
break
195+
print(f" ⚠️ {chunk_label} download failed: {exc}")
196+
197+
# Chunk 0's link is in the initial result. Re-use it.
198+
_download_chunk_links(resp.get("result") or {}, "chunk 0")
199+
200+
# Chunks 1..N-1 must be fetched explicitly by index.
201+
for i in range(1, total_chunks):
197202
try:
198-
if next_url:
199-
chunk = w.api_client.do("GET", next_url)
200-
else:
201-
chunk = w.api_client.do("GET", f"/api/2.0/sql/statements/{sid}/result/chunks/{next_idx}")
203+
chunk = w.api_client.do("GET", f"/api/2.0/sql/statements/{sid}/result/chunks/{i}")
204+
_download_chunk_links(chunk, f"chunk {i}")
202205
except Exception as exc:
203-
print(f" ⚠️ next chunk fetch failed: {exc}")
204-
break
205-
if chunk_count > 200:
206-
print(f" ⚠️ too many chunks, bailing at {chunk_count}")
207-
break
206+
print(f" ⚠️ chunk {i} fetch failed: {exc}")
207+
208+
print(f" collected {len(rows)}/{total_rows} rows")
208209
return rows
209210

210211
# COMMAND ----------

0 commit comments

Comments
 (0)