|
1 | 1 | """Query functionality for Lightdash models.""" |
2 | 2 | import time |
| 3 | +import uuid |
3 | 4 | import warnings |
4 | 5 | from typing import Any, Dict, List, Optional, Union, Sequence, Iterator |
5 | 6 |
|
|
12 | 13 | from .results import BaseResult |
13 | 14 |
|
14 | 15 |
|
| 16 | +def _inject_filter_ids(filters: Dict[str, Any]) -> Dict[str, Any]: |
| 17 | + """Add the ``id`` fields the v1 ``compileQuery`` endpoint requires on every |
| 18 | + filter group and rule. |
| 19 | +
|
| 20 | + The v2 query endpoint injects these server-side, but v1 validates them |
| 21 | + strictly. Returns a new dict; the input is not mutated. |
| 22 | + """ |
| 23 | + def visit_group(group: Dict[str, Any]) -> Dict[str, Any]: |
| 24 | + out: Dict[str, Any] = {"id": str(uuid.uuid4())} |
| 25 | + for key in ("and", "or"): |
| 26 | + if key in group: |
| 27 | + out[key] = [visit_item(item) for item in group[key]] |
| 28 | + return out |
| 29 | + |
| 30 | + def visit_item(item: Dict[str, Any]) -> Dict[str, Any]: |
| 31 | + if "and" in item or "or" in item: # nested group |
| 32 | + return visit_group(item) |
| 33 | + return {**item, "id": str(uuid.uuid4())} # leaf rule |
| 34 | + |
| 35 | + return {ftype: visit_group(group) for ftype, group in filters.items()} |
| 36 | + |
| 37 | + |
15 | 38 | class _QueryExecutor: |
16 | 39 | """Internal class that handles V2 async query submission and polling.""" |
17 | 40 |
|
@@ -693,6 +716,54 @@ def execute( |
693 | 716 | ) |
694 | 717 | return self._result |
695 | 718 |
|
| 719 | + def compile(self) -> str: |
| 720 | + """ |
| 721 | + Compile the query to warehouse SQL without executing it. |
| 722 | +
|
| 723 | + Returns the SQL Lightdash would run for this query. Nothing is executed |
| 724 | + and no rows are fetched — useful for inspecting or debugging a query, or |
| 725 | + for running it directly against your warehouse (e.g. BigQuery/bigframes, |
| 726 | + dbt, or a data pipeline) when you need more rows than the query API |
| 727 | + returns. |
| 728 | +
|
| 729 | + The query's ``limit`` is included in the SQL (as ``LIMIT n``); set a |
| 730 | + large limit, or strip the trailing clause, if you intend to run it |
| 731 | + yourself without Lightdash's row cap. |
| 732 | +
|
| 733 | + Returns: |
| 734 | + The compiled SQL as a string. |
| 735 | +
|
| 736 | + Example: |
| 737 | + sql = ( |
| 738 | + model.query() |
| 739 | + .metrics(model.metrics.revenue) |
| 740 | + .dimensions(model.dimensions.country) |
| 741 | + .filter(model.dimensions.status == "active") |
| 742 | + .compile() |
| 743 | + ) |
| 744 | + """ |
| 745 | + if self._model._client is None: |
| 746 | + raise RuntimeError("Model not properly initialized with client reference") |
| 747 | + |
| 748 | + client = self._model._client |
| 749 | + payload = self._build_payload() |
| 750 | + |
| 751 | + # The v1 compileQuery endpoint is stricter than the v2 query endpoint: |
| 752 | + # it requires additionalMetrics and an `id` on every filter group/rule. |
| 753 | + payload.setdefault("additionalMetrics", []) |
| 754 | + filters = payload.get("filters", {}) |
| 755 | + has_rules = any( |
| 756 | + group.get("and") or group.get("or") for group in filters.values() |
| 757 | + ) |
| 758 | + payload["filters"] = _inject_filter_ids(filters) if has_rules else {} |
| 759 | + |
| 760 | + result = client._make_request( |
| 761 | + "POST", |
| 762 | + f"/api/v1/projects/{client.project_uuid}/explores/{self._model.name}/compileQuery", |
| 763 | + json=payload, |
| 764 | + ) |
| 765 | + return result["query"] |
| 766 | + |
696 | 767 | def to_records(self) -> List[Dict[str, Any]]: |
697 | 768 | """Get all query results as a list of dictionaries.""" |
698 | 769 | return self.execute().to_records() |
|
0 commit comments