Skip to content

Commit 05c97b2

Browse files
authored
feat: implement primary key check
feat: implement primary key check
2 parents 4fbe31b + 20e1edc commit 05c97b2

15 files changed

Lines changed: 2231 additions & 123 deletions

.dockerignore

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1-
data_check/.streamlit/secrets.toml
1+
data_check/.streamlit/secrets.toml
2+
.venv/
3+
build/
4+
.vscode/
5+
.git/
6+
.gitignore
7+
.dockerignore
8+
.DS_Store

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@
66
# Standard Python ignore list
77
*.py[cod]
88
__pycache__/
9+
10+
# Python package
11+
build/
12+
dist/
13+
data_check.egg-info/

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.9

.vscode/launch.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
"configurations": [
44
{
55
"name": "debug streamlit",
6-
"type": "python",
6+
"type": "debugpy",
77
"request": "launch",
8-
"python": "/Users/antoineballiet/Documents/GitHub/data-check/.venv/bin/python",
9-
"program": "/Users/antoineballiet/Documents/GitHub/data-check/.venv/bin/streamlit",
8+
"python": "${workspaceFolder}/.venv/bin/python",
9+
"program": "${workspaceFolder}/.venv/bin/streamlit",
1010
"args": [
1111
"run",
12-
"data_check/streamlit_app.py"
12+
"${workspaceFolder}/data_check/streamlit_app.py"
1313
]
1414
}
1515
]

Dockerfile

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@ RUN apt-get update && apt-get install -y \
99
git \
1010
&& rm -rf /var/lib/apt/lists/*
1111

12-
COPY requirements.txt .
12+
COPY . .
1313

14-
RUN pip3 install -r requirements.txt
15-
16-
COPY data_check/ .
14+
RUN pip install .
1715

1816
EXPOSE 8501
1917

2018
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
2119

22-
ENTRYPOINT ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
20+
ENTRYPOINT ["streamlit", "run", "data_check/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]

data_check/data_processor.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
from typing import List, Tuple
33

44
import pandas as pd
5-
from models.table import TableSchema
6-
from query_client import QueryClient
75
from sqlglot import parse_one
86
from sqlglot.expressions import Select
97

8+
from .models.table import TableSchema
9+
from .query_client import QueryClient
10+
1011

1112
class DataProcessor(ABC):
1213
def __init__(
@@ -100,6 +101,11 @@ def get_query_insight_tables_primary_keys(self) -> Select:
100101
"""Compare the primary keys of two tables"""
101102
pass
102103

104+
@abstractmethod
105+
def get_query_check_primary_keys_unique(self, table_name: str) -> Select:
106+
"""Check if the primary keys are unique for a given row"""
107+
pass
108+
103109
@abstractmethod
104110
def get_query_exclusive_primary_keys(self, exclusive_to: str) -> Select:
105111
pass
@@ -247,6 +253,17 @@ def get_plain_diff(
247253
)
248254
df = self.client.run_query_to_dataframe(query)
249255
return query, df
256+
257+
def run_query_check_primary_keys_unique(self, table: str) -> Tuple[bool, str]:
258+
"""Check if the primary keys are unique for a given row"""
259+
query = self.get_query_check_primary_keys_unique(table_name=table)
260+
df = self.client.run_query_to_dataframe(query)
261+
262+
if not df.empty:
263+
error_message = f"Primary key is not unique for {table}: . You can use the query: {query.sql()} to check it."
264+
return False, error_message
265+
266+
return True, ""
250267

251268
def run_query_compare_primary_keys(self) -> pd.DataFrame:
252269
"""Compare the primary keys of two tables"""

data_check/processors/bigquery.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
from data_processor import DataProcessor
2-
from models.table import TableSchema
3-
from processors.utils import add_suffix_to_column_names
4-
from query.query_bq import QueryBigQuery
51
from sqlglot import alias, column, condition, func, parse_one, select
62
from sqlglot.expressions import Select
73

4+
from data_check.data_processor import DataProcessor
5+
from data_check.models.table import TableSchema
6+
from data_check.query.query_bq import QueryBigQuery
7+
8+
from .utils import add_suffix_to_column_names
9+
810

911
class BigQueryProcessor(DataProcessor):
1012
def __init__(self, query1: str, query2: str) -> None:
@@ -70,7 +72,7 @@ def get_query_insight_tables_primary_keys(self) -> Select:
7072
)
7173

7274
query = (
73-
self.with_statement_query.with_("agg_diff_keys", as_=agg_diff_keys)
75+
self.with_statement_query_sampled.with_("agg_diff_keys", as_=agg_diff_keys)
7476
.select(
7577
"total_rows",
7678
"missing_primary_key_in_table1",
@@ -89,6 +91,16 @@ def get_query_insight_tables_primary_keys(self) -> Select:
8991

9092
return query
9193

94+
def get_query_check_primary_keys_unique(self, table_name: str) -> Select:
95+
"""Check if the primary keys are unique for a given row"""
96+
return (
97+
self.with_statement_query_sampled.select(
98+
alias(func("count", "*"), "total_rows"),
99+
).from_(table_name, dialect=self.dialect).group_by(self.primary_key).having(
100+
func("count", "*") > 1
101+
)
102+
)
103+
92104
def get_query_exclusive_primary_keys(
93105
self, exclusive_to: str, limit: int = 500
94106
) -> Select:
@@ -102,7 +114,7 @@ def get_query_exclusive_primary_keys(
102114
)
103115

104116
return (
105-
self.with_statement_query.select(
117+
self.with_statement_query_sampled.select(
106118
column(self.primary_key, table="table1"), *table1_columns_renamed
107119
)
108120
.from_("table1")
@@ -119,7 +131,7 @@ def get_query_exclusive_primary_keys(
119131
)
120132

121133
return (
122-
self.with_statement_query.select(
134+
self.with_statement_query_sampled.select(
123135
column(self.primary_key, table="table2"), *table1_columns_renamed
124136
)
125137
.from_("table2")

data_check/query/query_bq.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
from os import getenv
22
from threading import Thread
3+
34
import pandas as pd
45
import streamlit as st
56
from google.cloud import bigquery
7+
from google.cloud.bigquery.job import QueryJob
68
from google.oauth2 import service_account
7-
from google.cloud.bigquery._helpers import TimeoutType
8-
from models.table import TableSchema
9-
from query_client import QueryClient
109
from sqlglot.expressions import Select
11-
from google.cloud.bigquery.job import (
12-
QueryJob,
13-
)
10+
11+
from data_check.models.table import TableSchema
12+
from data_check.query_client import QueryClient
1413

1514
USE_STREAMLIT_SECRET = getenv("USE_STREAMLIT_SECRET", False)
1615
TIMEOUT_BIGQUERY = 900 # 15 * 60 = 15 minutes

data_check/query_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from abc import ABC, abstractmethod
22

33
import pandas as pd
4-
from models.table import TableSchema
54
from sqlglot.expressions import Select
65

6+
from .models.table import TableSchema
7+
78

89
class QueryClient(ABC):
910
###### ABSTRACT METHODS ######

data_check/streamlit_app.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import pandas as pd
22
import streamlit as st
3-
from data_formatter import (highlight_diff_dataset, style_gradient,
4-
style_percentage)
5-
from processors.bigquery import BigQueryProcessor
3+
4+
from data_check.data_formatter import (highlight_diff_dataset, style_gradient,
5+
style_percentage)
6+
from data_check.processors.bigquery import BigQueryProcessor
67

78

89
class DataDiff:
@@ -152,7 +153,7 @@ def second_step(self):
152153
)
153154

154155
st.selectbox(
155-
"Select primary key:",
156+
"Select primary key (must be unique for a given row):",
156157
common_table_schema.columns_names,
157158
key="temp_primary_key",
158159
index=primary_key_select_index,
@@ -172,7 +173,7 @@ def second_step(self):
172173
)
173174

174175
st.slider(
175-
"Data sampling (only avaible for direct tables as input)",
176+
"Data sampling (only available for direct tables as input)",
176177
min_value=10,
177178
max_value=100,
178179
step=1,
@@ -204,6 +205,19 @@ def window(self):
204205
)
205206

206207
if st.session_state.loaded_tables:
208+
209+
st.write("Checking primary keys are unique for a given row...")
210+
211+
primary_keys_unique_table1, error_message_table1 = processor.run_query_check_primary_keys_unique(table="table1")
212+
primary_keys_unique_table2, error_message_table2 = processor.run_query_check_primary_keys_unique(table="table2")
213+
214+
if not primary_keys_unique_table1 or not primary_keys_unique_table2:
215+
st.write("Primary keys are not unique for a given row ❌")
216+
st.write(error_message_table1)
217+
st.write(error_message_table2)
218+
st.stop()
219+
220+
st.write("Primary keys are unique for a given row ✅")
207221
# Using BigQueryClient to run queries, output primary keys in common and exclusive to each table on streamlit : display rows in table format
208222
st.write("Analyzing primary keys...")
209223
results_primary_keys = processor.run_query_compare_primary_keys()

0 commit comments

Comments
 (0)