Skip to content

Commit 35d2598

Browse files
mdesmetMichiel De Smet
andauthored
Support sql check (#44)
Co-authored-by: Michiel De Smet <[email protected]>
1 parent f20c81b commit 35d2598

File tree

12 files changed

+154
-9
lines changed

12 files changed

+154
-9
lines changed

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def read(*names, **kwargs):
6767
"ruamel.yaml==0.18.6",
6868
"tabulate==0.9.0",
6969
"requests==2.31.0",
70+
"sqlglot==25.30.0",
7071
],
7172
extras_require={
7273
# eg:
Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
11
from abc import abstractmethod
2-
from typing import Optional
32

4-
from datapilot.core.insights.base.insight import Insight
5-
from datapilot.schemas.sql import Dialect
3+
from datapilot.core.platforms.dbt.insights.checks.base import ChecksInsight
64

75

8-
class SqlInsight(Insight):
6+
class SqlInsight(ChecksInsight):
97
NAME = "SqlInsight"
108

11-
def __init__(self, sql: str, dialect: Optional[Dialect], *args, **kwargs):
12-
self.sql = sql
13-
self.dialect = dialect
14-
super().__init__(*args, **kwargs)
15-
169
@abstractmethod
1710
def generate(self, *args, **kwargs) -> dict:
1811
pass

src/datapilot/core/platforms/dbt/executor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(
5151
self.macros = self.manifest_wrapper.get_macros()
5252
self.sources = self.manifest_wrapper.get_sources()
5353
self.exposures = self.manifest_wrapper.get_exposures()
54+
self.adapter_type = self.manifest_wrapper.get_adapter_type()
5455
self.seeds = self.manifest_wrapper.get_seeds()
5556
self.children_map = self.manifest_wrapper.parent_to_child_map(self.nodes)
5657
self.tests = self.manifest_wrapper.get_tests()
@@ -112,6 +113,7 @@ def run(self):
112113
children_map=self.children_map,
113114
tests=self.tests,
114115
project_name=self.project_name,
116+
adapter_type=self.adapter_type,
115117
config=self.config,
116118
selected_models=self.selected_models,
117119
excluded_models=self.excluded_models,

src/datapilot/core/platforms/dbt/insights/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from datapilot.core.platforms.dbt.insights.modelling.unused_sources import DBTUnusedSources
5252
from datapilot.core.platforms.dbt.insights.performance.chain_view_linking import DBTChainViewLinking
5353
from datapilot.core.platforms.dbt.insights.performance.exposure_parent_materializations import DBTExposureParentMaterialization
54+
from datapilot.core.platforms.dbt.insights.sql.sql_check import SqlCheck
5455
from datapilot.core.platforms.dbt.insights.structure.model_directories_structure import DBTModelDirectoryStructure
5556
from datapilot.core.platforms.dbt.insights.structure.model_naming_conventions import DBTModelNamingConvention
5657
from datapilot.core.platforms.dbt.insights.structure.source_directories_structure import DBTSourceDirectoryStructure
@@ -112,4 +113,5 @@
112113
CheckSourceHasTests,
113114
CheckSourceTableHasDescription,
114115
CheckSourceTags,
116+
SqlCheck,
115117
]

src/datapilot/core/platforms/dbt/insights/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import ClassVar
33
from typing import Dict
44
from typing import List
5+
from typing import Optional
56
from typing import Union
67

78
from datapilot.config.utils import get_insight_config
@@ -33,6 +34,7 @@ def __init__(
3334
macros: Dict[str, AltimateManifestMacroNode],
3435
children_map: Dict[str, List[str]],
3536
project_name: str,
37+
adapter_type: Optional[str],
3638
selected_models: Union[List[str], None] = None,
3739
excluded_models: Union[List[str], None] = None,
3840
*args,
@@ -47,6 +49,7 @@ def __init__(
4749
self.seeds = seeds
4850
self.children_map = children_map
4951
self.project_name = project_name
52+
self.adapter_type = adapter_type
5053
self.selected_models = selected_models
5154
self.excluded_models = excluded_models
5255
super().__init__(*args, **kwargs)

src/datapilot/core/platforms/dbt/insights/sql/__init__.py

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from abc import abstractmethod
2+
from typing import Tuple
3+
4+
from datapilot.core.platforms.dbt.insights.base import DBTInsight
5+
6+
7+
class SqlInsight(DBTInsight):
8+
TYPE = "governance"
9+
10+
@abstractmethod
11+
def generate(self, *args, **kwargs) -> dict:
12+
pass
13+
14+
@classmethod
15+
def has_all_required_data(cls, has_manifest: bool, **kwargs) -> Tuple[bool, str]:
16+
"""
17+
Check if all required data is available for the insight to run.
18+
:param has_manifest: A boolean indicating if manifest is available.
19+
:return: A boolean indicating if all required data is available.
20+
"""
21+
if not has_manifest:
22+
return False, "manifest is required for insight to run."
23+
return True, ""
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import inspect
2+
from typing import List
3+
4+
from sqlglot import parse_one
5+
from sqlglot.optimizer.eliminate_ctes import eliminate_ctes
6+
from sqlglot.optimizer.eliminate_joins import eliminate_joins
7+
from sqlglot.optimizer.eliminate_subqueries import eliminate_subqueries
8+
from sqlglot.optimizer.normalize import normalize
9+
from sqlglot.optimizer.pushdown_projections import pushdown_projections
10+
from sqlglot.optimizer.qualify import qualify
11+
from sqlglot.optimizer.unnest_subqueries import unnest_subqueries
12+
13+
from datapilot.core.insights.sql.base.insight import SqlInsight
14+
from datapilot.core.insights.utils import get_severity
15+
from datapilot.core.platforms.dbt.insights.schema import DBTInsightResult
16+
from datapilot.core.platforms.dbt.insights.schema import DBTModelInsightResponse
17+
18+
RULES = (
19+
pushdown_projections,
20+
normalize,
21+
unnest_subqueries,
22+
eliminate_subqueries,
23+
eliminate_joins,
24+
eliminate_ctes,
25+
)
26+
27+
28+
class SqlCheck(SqlInsight):
29+
"""
30+
This class identifies DBT models with SQL optimization issues.
31+
"""
32+
33+
NAME = "sql optimization issues"
34+
ALIAS = "check_sql_optimization"
35+
DESCRIPTION = "Checks if the model has SQL optimization issues. "
36+
REASON_TO_FLAG = "The query can be optimized."
37+
FAILURE_MESSAGE = "The query for model `{model_unique_id}` has optimization opportunities:\n{rule_name}. "
38+
RECOMMENDATION = "Please adapt the query of the model `{model_unique_id}` as in following example:\n{optimized_sql}"
39+
40+
def _build_failure_result(self, model_unique_id: str, rule_name: str, optimized_sql: str) -> DBTInsightResult:
41+
"""
42+
Constructs a failure result for a given model with sql optimization issues.
43+
:param model_unique_id: The unique id of the dbt model.
44+
:param rule_name: The rule that generated this failure result.
45+
:param optimized_sql: The optimized sql.
46+
:return: An instance of DBTInsightResult containing failure details.
47+
"""
48+
failure_message = self.FAILURE_MESSAGE.format(model_unique_id=model_unique_id, rule_name=rule_name)
49+
recommendation = self.RECOMMENDATION.format(model_unique_id=model_unique_id, optimized_sql=optimized_sql)
50+
return DBTInsightResult(
51+
type=self.TYPE,
52+
name=self.NAME,
53+
message=failure_message,
54+
recommendation=recommendation,
55+
reason_to_flag=self.REASON_TO_FLAG,
56+
metadata={"model_unique_id": model_unique_id, "rule_name": rule_name},
57+
)
58+
59+
def generate(self, *args, **kwargs) -> List[DBTModelInsightResponse]:
60+
"""
61+
Generates insights for each DBT model in the project, focusing on sql optimization issues.
62+
63+
:return: A list of DBTModelInsightResponse objects with insights for each model.
64+
"""
65+
self.logger.debug("Generating sql insights for DBT models")
66+
insights = []
67+
68+
possible_kwargs = {
69+
"db": None,
70+
"catalog": None,
71+
"dialect": self.adapter_type,
72+
"isolate_tables": True, # needed for other optimizations to perform well
73+
"quote_identifiers": False,
74+
**kwargs,
75+
}
76+
for node_id, node in self.nodes.items():
77+
try:
78+
compiled_query = node.compiled_code
79+
if compiled_query:
80+
parsed_query = parse_one(compiled_query, dialect=self.adapter_type)
81+
qualified = qualify(parsed_query, **possible_kwargs)
82+
changed = qualified.copy()
83+
for rule in RULES:
84+
original = changed.copy()
85+
rule_params = inspect.getfullargspec(rule).args
86+
rule_kwargs = {param: possible_kwargs[param] for param in rule_params if param in possible_kwargs}
87+
changed = rule(changed, **rule_kwargs)
88+
if changed.sql() != original.sql():
89+
insights.append(
90+
DBTModelInsightResponse(
91+
unique_id=node_id,
92+
package_name=node.package_name,
93+
path=node.original_file_path,
94+
original_file_path=node.original_file_path,
95+
insight=self._build_failure_result(node_id, rule.__name__, changed.sql()),
96+
severity=get_severity(self.config, self.ALIAS, self.DEFAULT_SEVERITY),
97+
)
98+
)
99+
except Exception as e:
100+
self.logger.error(e)
101+
return insights

src/datapilot/core/platforms/dbt/wrappers/manifest/v10/wrapper.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Dict
2+
from typing import Optional
23
from typing import Set
34

45
from dbt_artifacts_parser.parsers.manifest.manifest_v10 import GenericTestNode
@@ -67,6 +68,7 @@ def _get_node(self, node: ManifestNode) -> AltimateManifestNode:
6768
depends_on_macros = node.depends_on.macros if node.depends_on else None
6869
compiled_path = node.compiled_path
6970
compiled = node.compiled
71+
compiled_code = node.compiled_code
7072
raw_code = node.raw_code
7173
language = node.language
7274
contract = AltimateDBTContract(**node.contract.__dict__) if node.contract else None
@@ -381,6 +383,9 @@ def get_seeds(self) -> Dict[str, AltimateSeedNode]:
381383
seeds[seed.unique_id] = self._get_seed(seed)
382384
return seeds
383385

386+
def get_adapter_type(self) -> Optional[str]:
387+
return self.manifest.metadata.adapter_type
388+
384389
def parent_to_child_map(self, nodes: Dict[str, AltimateManifestNode]) -> Dict[str, Set[str]]:
385390
"""
386391
Current manifest contains information about parents

src/datapilot/core/platforms/dbt/wrappers/manifest/v11/wrapper.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Dict
2+
from typing import Optional
23
from typing import Set
34

45
from dbt_artifacts_parser.parsers.manifest.manifest_v11 import GenericTestNode
@@ -67,6 +68,7 @@ def _get_node(self, node: ManifestNode) -> AltimateManifestNode:
6768
depends_on_macros = node.depends_on.macros if node.depends_on else None
6869
compiled_path = node.compiled_path
6970
compiled = node.compiled
71+
compiled_code = node.compiled_code
7072
raw_code = node.raw_code
7173
language = node.language
7274
contract = AltimateDBTContract(**node.contract.__dict__) if node.contract else None
@@ -381,6 +383,9 @@ def get_seeds(self) -> Dict[str, AltimateSeedNode]:
381383
seeds[seed.unique_id] = self._get_seed(seed)
382384
return seeds
383385

386+
def get_adapter_type(self) -> Optional[str]:
387+
return self.manifest.metadata.adapter_type
388+
384389
def parent_to_child_map(self, nodes: Dict[str, AltimateManifestNode]) -> Dict[str, Set[str]]:
385390
"""
386391
Current manifest contains information about parents

0 commit comments

Comments
 (0)