Skip to content

Commit 6a27739

Browse files
authored
feat: add build_table_metrics_query method and improve fetch_sample_value (#343)
* feat: add build_table_metrics_query method and improve fetch_sample_values_from_database for MSSQL and Postgres * fix: update datetime import and usage for consistency in MssqlDataSource * fix: minor fix * fix: ensure cursor is closed properly in fetch_rows and fetchall methods * fix: minor bug
1 parent fec3972 commit 6a27739

2 files changed

Lines changed: 145 additions & 6 deletions

File tree

dcs_core/integrations/databases/mssql.py

Lines changed: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from datetime import datetime
15+
import datetime
16+
from decimal import Decimal
1617
from typing import Any, Dict, List, Optional, Tuple, Union
18+
from uuid import UUID
1719

1820
import pyodbc
1921
from loguru import logger
22+
from sqlalchemy import text
2023

2124
from dcs_core.core.common.errors import DataChecksDataSourcesConnectionError
2225
from dcs_core.core.common.models.data_source_resource import RawColumnInfo
@@ -683,6 +686,137 @@ def query_get_time_diff(self, table: str, field: str) -> int:
683686
if result:
684687
updated_time = result[0]
685688
if isinstance(updated_time, str):
686-
updated_time = datetime.strptime(updated_time, "%Y-%m-%d %H:%M:%S.%f")
687-
return int((datetime.utcnow() - updated_time).total_seconds())
689+
updated_time = datetime.datetime.strptime(
690+
updated_time, "%Y-%m-%d %H:%M:%S.%f"
691+
)
692+
return int((datetime.datetime.utcnow() - updated_time).total_seconds())
688693
return 0
694+
695+
def build_table_metrics_query(
696+
self,
697+
table_name: str,
698+
column_info: list[dict],
699+
additional_queries: Optional[List[str]] = None,
700+
) -> list[dict]:
701+
query_parts = []
702+
if not column_info:
703+
return []
704+
705+
for col in column_info:
706+
name = col["column_name"]
707+
dtype = col["data_type"].lower()
708+
709+
quoted_name = self.quote_column(name)
710+
711+
query_parts.append(f"COUNT(DISTINCT {quoted_name}) AS [{name}_distinct]")
712+
query_parts.append(
713+
f"COUNT({quoted_name}) - COUNT(DISTINCT {quoted_name}) AS [{name}_duplicate]"
714+
)
715+
query_parts.append(
716+
f"SUM(CASE WHEN {quoted_name} IS NULL THEN 1 ELSE 0 END) AS [{name}_is_null]"
717+
)
718+
719+
if dtype in (
720+
"int",
721+
"integer",
722+
"bigint",
723+
"smallint",
724+
"tinyint",
725+
"decimal",
726+
"numeric",
727+
"float",
728+
"real",
729+
"money",
730+
"smallmoney",
731+
):
732+
query_parts.append(f"MIN({quoted_name}) AS [{name}_min]")
733+
query_parts.append(f"MAX({quoted_name}) AS [{name}_max]")
734+
query_parts.append(
735+
f"AVG(CAST({quoted_name} AS FLOAT)) AS [{name}_average]"
736+
)
737+
738+
elif dtype in ("varchar", "nvarchar", "char", "nchar", "text", "ntext"):
739+
query_parts.append(
740+
f"MAX(LEN({quoted_name})) AS [{name}_max_character_length]"
741+
)
742+
743+
if additional_queries:
744+
query_parts.extend(additional_queries)
745+
746+
qualified_table = self.qualified_table_name(table_name)
747+
query = f'SELECT\n {",\n ".join(query_parts)}\nFROM {qualified_table};'
748+
749+
cursor = self.connection.cursor()
750+
try:
751+
cursor.execute(query)
752+
columns = [column[0] for column in cursor.description]
753+
result_row = cursor.fetchone()
754+
finally:
755+
cursor.close()
756+
757+
row = dict(zip(columns, result_row))
758+
759+
def _normalize_metrics(value):
760+
"""Safely normalize DB metric values for JSON serialization."""
761+
if value is None:
762+
return None
763+
if isinstance(value, Decimal):
764+
return float(value)
765+
if isinstance(value, (int, float, bool)):
766+
return value
767+
if isinstance(value, (datetime.datetime, datetime.date)):
768+
return value.isoformat()
769+
if isinstance(value, UUID):
770+
return str(value)
771+
if isinstance(value, list):
772+
return [_normalize_metrics(v) for v in value]
773+
if isinstance(value, dict):
774+
return {k: _normalize_metrics(v) for k, v in value.items()}
775+
return str(value)
776+
777+
column_wise = []
778+
for col in column_info:
779+
name = col["column_name"]
780+
col_metrics = {}
781+
782+
for key, value in row.items():
783+
if key.startswith(f"{name}_"):
784+
metric_name = key[len(name) + 1 :]
785+
col_metrics[metric_name] = _normalize_metrics(value)
786+
787+
column_wise.append({"column_name": name, "metrics": col_metrics})
788+
return column_wise
789+
790+
def fetch_sample_values_from_database(
791+
self,
792+
table_name: str,
793+
column_names: list[str],
794+
limit: int = 5,
795+
) -> Tuple[List[Tuple], List[str]]:
796+
"""
797+
Fetch sample rows for specific columns from the given table (MSSQL version).
798+
799+
:param table_name: The name of the table.
800+
:param column_names: List of column names to fetch.
801+
:param limit: Number of rows to fetch.
802+
:return: Tuple of (list of row tuples, list of column names)
803+
"""
804+
qualified_table_name = self.qualified_table_name(table_name)
805+
806+
if not column_names:
807+
raise ValueError("At least one column name must be provided")
808+
809+
if len(column_names) == 1 and column_names[0] == "*":
810+
query = f"SELECT TOP {limit} * FROM {qualified_table_name}"
811+
else:
812+
columns = ", ".join([self.quote_column(col) for col in column_names])
813+
query = f"SELECT TOP {limit} {columns} FROM {qualified_table_name}"
814+
815+
cursor = self.connection.cursor()
816+
try:
817+
cursor.execute(query)
818+
column_names = [desc[0] for desc in cursor.description]
819+
rows = cursor.fetchall()
820+
finally:
821+
cursor.close()
822+
return rows, column_names

dcs_core/integrations/databases/postgres.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,16 @@ def fetch_sample_values_from_database(
307307
if not column_names:
308308
raise ValueError("At least one column name must be provided")
309309

310-
columns = ", ".join([self.quote_column(col) for col in column_names])
311-
query = f"SELECT {columns} FROM {table_name} LIMIT {limit}"
310+
if len(column_names) == 1 and column_names[0] == "*":
311+
query = f"SELECT * FROM {table_name} LIMIT {limit}"
312+
else:
313+
columns = ", ".join([self.quote_column(col) for col in column_names])
314+
query = f"SELECT {columns} FROM {table_name} LIMIT {limit}"
315+
312316
result = self.connection.execute(text(query))
317+
column_names = list(result.keys())
313318
rows = result.fetchall()
314-
return rows
319+
return rows, column_names
315320

316321
def build_table_metrics_query(
317322
self,

0 commit comments

Comments
 (0)