icsDataValidation 1.0.430__py3-none-any.whl → 1.0.439__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- icsDataValidation/connection_setups/sqlserver_connection_setup.py +4 -3
- icsDataValidation/input_parameters/testing_tool_params.py +0 -1
- icsDataValidation/services/database_services/snowflake_service.py +170 -67
- icsDataValidation/services/database_services/sqlserver_service.py +196 -88
- {icsdatavalidation-1.0.430.dist-info → icsdatavalidation-1.0.439.dist-info}/METADATA +1 -1
- {icsdatavalidation-1.0.430.dist-info → icsdatavalidation-1.0.439.dist-info}/RECORD +22 -8
- {icsdatavalidation-1.0.430.dist-info → icsdatavalidation-1.0.439.dist-info}/WHEEL +1 -1
- {icsdatavalidation-1.0.430.dist-info → icsdatavalidation-1.0.439.dist-info}/top_level.txt +1 -0
- tests/snowflake_service/test_create_checksums.py +146 -0
- tests/snowflake_service/test_create_pandas_df_from_group_by.py +485 -0
- tests/snowflake_service/test_create_pandas_df_from_sample.py +444 -0
- tests/snowflake_service/test_get_checksum_statement.py +243 -0
- tests/snowflake_service/test_get_column_clause.py +305 -0
- tests/snowflake_service/test_get_countnulls_statement.py +128 -0
- tests/snowflake_service/test_get_in_clause.py +66 -0
- tests/sqlserver_service/test_create_checksums.py +153 -0
- tests/sqlserver_service/test_create_pandas_df_from_group_by.py +427 -0
- tests/sqlserver_service/test_create_pandas_df_from_sample.py +286 -0
- tests/sqlserver_service/test_get_checksum_statement.py +160 -0
- tests/sqlserver_service/test_get_column_clause.py +182 -0
- tests/sqlserver_service/test_get_countnulls_statement.py +121 -0
- tests/sqlserver_service/test_get_in_clause.py +87 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from icsDataValidation.core.database_objects import DatabaseObject, DatabaseObjectType
|
|
7
|
+
from icsDataValidation.services.database_services.sqlserver_service import SQLServerService
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def sqlserver_service():
|
|
12
|
+
"""Create a SQLServerService instance with mocked connection."""
|
|
13
|
+
connection_params = {
|
|
14
|
+
'Driver': 'ODBC Driver 18 for SQL Server',
|
|
15
|
+
'Server': 'localhost',
|
|
16
|
+
'Port': '1433',
|
|
17
|
+
'Database': 'testdb',
|
|
18
|
+
'User': 'sa',
|
|
19
|
+
'Password': 'password',
|
|
20
|
+
'Encrypt': True,
|
|
21
|
+
'TrustServerCertificate': True
|
|
22
|
+
}
|
|
23
|
+
service = SQLServerService(connection_params=connection_params)
|
|
24
|
+
service.sqlserver_connection = MagicMock()
|
|
25
|
+
return service
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def mock_database_object():
|
|
30
|
+
"""Create a mock DatabaseObject."""
|
|
31
|
+
obj = DatabaseObject(
|
|
32
|
+
object_identifier="TEST_DB.TEST_SCHEMA.TEST_TABLE",
|
|
33
|
+
object_type=DatabaseObjectType.TABLE
|
|
34
|
+
)
|
|
35
|
+
return obj
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestCreatePandasDfFromSampleParametrized:
|
|
39
|
+
"""Parametrized tests for create_pandas_df_from_sample method."""
|
|
40
|
+
|
|
41
|
+
@pytest.mark.parametrize(
|
|
42
|
+
"column_intersections,key_columns,exclude_columns,dedicated_columns,"
|
|
43
|
+
"where_clause,key_filters,sample_count,numeric_scale,enclose_quotes,"
|
|
44
|
+
"mock_datatypes,mock_column_clause,mock_in_clause,expected_contains,expected_not_in",
|
|
45
|
+
[
|
|
46
|
+
( # simple case with key columns
|
|
47
|
+
['id', 'name', 'amount'],
|
|
48
|
+
['id'],
|
|
49
|
+
[],
|
|
50
|
+
[],
|
|
51
|
+
"",
|
|
52
|
+
{},
|
|
53
|
+
10,
|
|
54
|
+
None,
|
|
55
|
+
False,
|
|
56
|
+
[
|
|
57
|
+
{"COLUMN_NAME": "id", "DATA_TYPE": "int"},
|
|
58
|
+
{"COLUMN_NAME": "name", "DATA_TYPE": "varchar"},
|
|
59
|
+
{"COLUMN_NAME": "amount", "DATA_TYPE": "int"}
|
|
60
|
+
],
|
|
61
|
+
("[id] as [id], [name] AS [name], [amount] as [amount]", ['id', 'amount'], ['id', 'name', 'amount']),
|
|
62
|
+
None,
|
|
63
|
+
[
|
|
64
|
+
"SELECT TOP (10) [id] as [id], [name] AS [name], [amount] as [amount]",
|
|
65
|
+
"FROM TEST_SCHEMA.TEST_TABLE",
|
|
66
|
+
"WHERE 1=1",
|
|
67
|
+
"ORDER BY [id];"
|
|
68
|
+
],
|
|
69
|
+
[]
|
|
70
|
+
),
|
|
71
|
+
( # multiple key columns
|
|
72
|
+
['id', 'region', 'amount'],
|
|
73
|
+
['id', 'region'],
|
|
74
|
+
[],
|
|
75
|
+
[],
|
|
76
|
+
"",
|
|
77
|
+
{},
|
|
78
|
+
10,
|
|
79
|
+
None,
|
|
80
|
+
False,
|
|
81
|
+
[
|
|
82
|
+
{"COLUMN_NAME": "id", "DATA_TYPE": "int"},
|
|
83
|
+
{"COLUMN_NAME": "region", "DATA_TYPE": "varchar"},
|
|
84
|
+
{"COLUMN_NAME": "amount", "DATA_TYPE": "int"}
|
|
85
|
+
],
|
|
86
|
+
("[id] as [id], [region] AS [region], [amount] as [amount]", ['id', 'amount'], ['id', 'region', 'amount']),
|
|
87
|
+
None,
|
|
88
|
+
["ORDER BY [id], [region];"],
|
|
89
|
+
[]
|
|
90
|
+
),
|
|
91
|
+
( # with where clause
|
|
92
|
+
['id', 'status'],
|
|
93
|
+
['id'],
|
|
94
|
+
[],
|
|
95
|
+
[],
|
|
96
|
+
"WHERE status = 'active'",
|
|
97
|
+
{},
|
|
98
|
+
10,
|
|
99
|
+
None,
|
|
100
|
+
False,
|
|
101
|
+
[
|
|
102
|
+
{"COLUMN_NAME": "id", "DATA_TYPE": "int"},
|
|
103
|
+
{"COLUMN_NAME": "status", "DATA_TYPE": "varchar"}
|
|
104
|
+
],
|
|
105
|
+
("[id] as [id], [status] AS [status]", ['id'], ['id', 'status']),
|
|
106
|
+
None,
|
|
107
|
+
["WHERE status = 'active'", "ORDER BY [id];"],
|
|
108
|
+
[]
|
|
109
|
+
),
|
|
110
|
+
( # excluded columns
|
|
111
|
+
['id', 'name', 'secret'],
|
|
112
|
+
['id'],
|
|
113
|
+
['secret'],
|
|
114
|
+
[],
|
|
115
|
+
"",
|
|
116
|
+
{},
|
|
117
|
+
10,
|
|
118
|
+
None,
|
|
119
|
+
False,
|
|
120
|
+
[
|
|
121
|
+
{"COLUMN_NAME": "id", "DATA_TYPE": "int"},
|
|
122
|
+
{"COLUMN_NAME": "name", "DATA_TYPE": "varchar"}
|
|
123
|
+
],
|
|
124
|
+
("[id] as [id], [name] AS [name]", ['id'], ['id', 'name']),
|
|
125
|
+
None,
|
|
126
|
+
["[id]", "[name]", "ORDER BY [id];"],
|
|
127
|
+
["[secret]"]
|
|
128
|
+
),
|
|
129
|
+
( # with key filters
|
|
130
|
+
['id', 'region', 'amount'],
|
|
131
|
+
['id', 'region'],
|
|
132
|
+
[],
|
|
133
|
+
[],
|
|
134
|
+
"",
|
|
135
|
+
{'id': [1, 2], 'region': ['US', 'EU']},
|
|
136
|
+
10,
|
|
137
|
+
None,
|
|
138
|
+
False,
|
|
139
|
+
[
|
|
140
|
+
{"COLUMN_NAME": "id", "DATA_TYPE": "int"},
|
|
141
|
+
{"COLUMN_NAME": "region", "DATA_TYPE": "varchar"},
|
|
142
|
+
{"COLUMN_NAME": "amount", "DATA_TYPE": "int"}
|
|
143
|
+
],
|
|
144
|
+
("[id] as [id], [region] AS [region], [amount] as [amount]", ['id', 'amount'], ['id', 'region', 'amount']),
|
|
145
|
+
" AND (CONCAT(cast(ROUND([id], 0) as numeric(38, 0)), '|' ,[region], '|') in ('1|US|','2|EU|'))",
|
|
146
|
+
[
|
|
147
|
+
"AND (CONCAT(cast(ROUND([id], 0) as numeric(38, 0)), '|' ,[region], '|') in ('1|US|','2|EU|'))",
|
|
148
|
+
"ORDER BY [id], [region];"
|
|
149
|
+
],
|
|
150
|
+
[]
|
|
151
|
+
),
|
|
152
|
+
( # dedicated columns
|
|
153
|
+
['id', 'name', 'amount', 'description'],
|
|
154
|
+
['id'],
|
|
155
|
+
[],
|
|
156
|
+
['id', 'name'],
|
|
157
|
+
"",
|
|
158
|
+
{},
|
|
159
|
+
10,
|
|
160
|
+
None,
|
|
161
|
+
False,
|
|
162
|
+
[
|
|
163
|
+
{"COLUMN_NAME": "id", "DATA_TYPE": "int"},
|
|
164
|
+
{"COLUMN_NAME": "name", "DATA_TYPE": "varchar"}
|
|
165
|
+
],
|
|
166
|
+
("[id] as [id], [name] AS [name]", ['id'], ['id', 'name']),
|
|
167
|
+
None,
|
|
168
|
+
["[id]", "[name]", "ORDER BY [id];"],
|
|
169
|
+
["[amount]", "[description]"]
|
|
170
|
+
),
|
|
171
|
+
( # custom sample count
|
|
172
|
+
['id', 'name'],
|
|
173
|
+
['id'],
|
|
174
|
+
[],
|
|
175
|
+
[],
|
|
176
|
+
"",
|
|
177
|
+
{},
|
|
178
|
+
50,
|
|
179
|
+
None,
|
|
180
|
+
False,
|
|
181
|
+
[
|
|
182
|
+
{"COLUMN_NAME": "id", "DATA_TYPE": "int"},
|
|
183
|
+
{"COLUMN_NAME": "name", "DATA_TYPE": "varchar"}
|
|
184
|
+
],
|
|
185
|
+
("[id] as [id], [name] AS [name]", ['id'], ['id', 'name']),
|
|
186
|
+
None,
|
|
187
|
+
["SELECT TOP (50)"] ,
|
|
188
|
+
[]
|
|
189
|
+
),
|
|
190
|
+
( # no key columns (uses ORDER BY NEWID())
|
|
191
|
+
['id', 'name', 'amount'],
|
|
192
|
+
[],
|
|
193
|
+
[],
|
|
194
|
+
[],
|
|
195
|
+
"",
|
|
196
|
+
{},
|
|
197
|
+
10,
|
|
198
|
+
None,
|
|
199
|
+
False,
|
|
200
|
+
[
|
|
201
|
+
{"COLUMN_NAME": "id", "DATA_TYPE": "int"},
|
|
202
|
+
{"COLUMN_NAME": "name", "DATA_TYPE": "varchar"},
|
|
203
|
+
{"COLUMN_NAME": "amount", "DATA_TYPE": "int"}
|
|
204
|
+
],
|
|
205
|
+
("[id] as [id], [name] AS [name], [amount] as [amount]", ['id', 'amount'], ['id', 'name', 'amount']),
|
|
206
|
+
None,
|
|
207
|
+
["SELECT TOP (10) [id] as [id], [name] AS [name], [amount] as [amount]", "WHERE 1=1", "ORDER BY NEWID();"],
|
|
208
|
+
["ORDER BY ["]
|
|
209
|
+
),
|
|
210
|
+
( # with numeric scale, no double quotes
|
|
211
|
+
['id', 'price'],
|
|
212
|
+
['id'],
|
|
213
|
+
[],
|
|
214
|
+
[],
|
|
215
|
+
"",
|
|
216
|
+
{},
|
|
217
|
+
10,
|
|
218
|
+
2,
|
|
219
|
+
False,
|
|
220
|
+
[
|
|
221
|
+
{"COLUMN_NAME": "id", "DATA_TYPE": "int"},
|
|
222
|
+
{"COLUMN_NAME": "price", "DATA_TYPE": "decimal"}
|
|
223
|
+
],
|
|
224
|
+
("[id] as [id], CAST(ROUND([price], 2) as decimal(38,2)) as [price]", ['id', 'price'], ['id', 'price']),
|
|
225
|
+
None,
|
|
226
|
+
["CAST(ROUND([price], 2) as decimal(38,2)) as [price]"],
|
|
227
|
+
[]
|
|
228
|
+
),
|
|
229
|
+
( # special characters
|
|
230
|
+
['User ID', 'Full Name', 'Email-Address'],
|
|
231
|
+
['User ID'],
|
|
232
|
+
[],
|
|
233
|
+
[],
|
|
234
|
+
"",
|
|
235
|
+
{},
|
|
236
|
+
10,
|
|
237
|
+
None,
|
|
238
|
+
False,
|
|
239
|
+
[
|
|
240
|
+
{"COLUMN_NAME": "User ID", "DATA_TYPE": "int"},
|
|
241
|
+
{"COLUMN_NAME": "Full Name", "DATA_TYPE": "varchar"},
|
|
242
|
+
{"COLUMN_NAME": "Email-Address", "DATA_TYPE": "varchar"}
|
|
243
|
+
],
|
|
244
|
+
("[User ID] as [User ID], [Full Name] AS [Full Name], [Email-Address] AS [Email-Address]", ['User ID'], ['User ID', 'Full Name', 'Email-Address']),
|
|
245
|
+
None,
|
|
246
|
+
["[User ID]", "[Full Name]", "[Email-Address]", "ORDER BY [User ID];"],
|
|
247
|
+
[]
|
|
248
|
+
),
|
|
249
|
+
],
|
|
250
|
+
)
|
|
251
|
+
def test_create_pandas_df_from_sample(
|
|
252
|
+
self, sqlserver_service, mock_database_object,
|
|
253
|
+
column_intersections, key_columns, exclude_columns, dedicated_columns,
|
|
254
|
+
where_clause, key_filters, sample_count, numeric_scale, enclose_quotes,
|
|
255
|
+
mock_datatypes, mock_column_clause, mock_in_clause,
|
|
256
|
+
expected_contains, expected_not_in
|
|
257
|
+
):
|
|
258
|
+
"""Test create_pandas_df_from_sample with various configurations."""
|
|
259
|
+
with patch.object(sqlserver_service, 'get_data_types_from_object') as mock_get_datatypes, \
|
|
260
|
+
patch.object(sqlserver_service, '_get_column_clause') as mock_get_column, \
|
|
261
|
+
patch.object(sqlserver_service, '_get_in_clause') as mock_get_in, \
|
|
262
|
+
patch.object(sqlserver_service, 'execute_queries') as mock_execute:
|
|
263
|
+
|
|
264
|
+
mock_get_datatypes.return_value = mock_datatypes
|
|
265
|
+
mock_get_column.return_value = mock_column_clause
|
|
266
|
+
if mock_in_clause:
|
|
267
|
+
mock_get_in.return_value = mock_in_clause
|
|
268
|
+
mock_execute.return_value = pd.DataFrame({'id': [1], 'name': ['A'], 'region': ['US'], 'User ID': [123]})
|
|
269
|
+
|
|
270
|
+
result_list, key_dict, used_columns, sample_query = sqlserver_service.create_pandas_df_from_sample(
|
|
271
|
+
object=mock_database_object,
|
|
272
|
+
column_intersections=column_intersections,
|
|
273
|
+
key_columns=key_columns,
|
|
274
|
+
exclude_columns=exclude_columns,
|
|
275
|
+
dedicated_columns=dedicated_columns,
|
|
276
|
+
where_clause=where_clause,
|
|
277
|
+
key_filters=key_filters,
|
|
278
|
+
sample_count=sample_count,
|
|
279
|
+
numeric_scale=numeric_scale,
|
|
280
|
+
enclose_column_by_double_quotes=enclose_quotes
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
for expected in expected_contains:
|
|
284
|
+
assert expected in sample_query
|
|
285
|
+
for expected in expected_not_in:
|
|
286
|
+
assert expected not in sample_query
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, Mock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from icsDataValidation.core.database_objects import DatabaseObject
|
|
6
|
+
from icsDataValidation.services.database_services.sqlserver_service import SQLServerService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def sqlserver_service():
|
|
11
|
+
"""Create a SQLServerService instance with mocked connection."""
|
|
12
|
+
connection_params = {
|
|
13
|
+
'Driver': 'ODBC Driver 18 for SQL Server',
|
|
14
|
+
'Server': 'localhost',
|
|
15
|
+
'Port': '1433',
|
|
16
|
+
'Database': 'testdb',
|
|
17
|
+
'User': 'sa',
|
|
18
|
+
'Password': 'password',
|
|
19
|
+
'Encrypt': True,
|
|
20
|
+
'TrustServerCertificate': True
|
|
21
|
+
}
|
|
22
|
+
service = SQLServerService(connection_params=connection_params)
|
|
23
|
+
service.sqlserver_connection = MagicMock()
|
|
24
|
+
return service
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def mock_database_object():
|
|
29
|
+
"""Create a mock DatabaseObject."""
|
|
30
|
+
obj = Mock(spec=DatabaseObject)
|
|
31
|
+
obj.database = "TestDB"
|
|
32
|
+
obj.schema = "dbo"
|
|
33
|
+
obj.name = "TestTable"
|
|
34
|
+
obj.type = "table"
|
|
35
|
+
return obj
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestGetChecksumStatementParametrized:
|
|
39
|
+
"""Parametrized tests for _get_checksum_statement method."""
|
|
40
|
+
|
|
41
|
+
@pytest.mark.parametrize(
|
|
42
|
+
"columns,mock_datatypes,exclude_columns,where_clause,numeric_scale," \
|
|
43
|
+
"expected_contains,expected_not_in",
|
|
44
|
+
[
|
|
45
|
+
( # numeric column with scale
|
|
46
|
+
["Amount"],
|
|
47
|
+
[{"COLUMN_NAME": "AMOUNT", "DATA_TYPE": "decimal"}],
|
|
48
|
+
[],
|
|
49
|
+
"",
|
|
50
|
+
2,
|
|
51
|
+
["SUM([AMOUNT])", "DECIMAL(38, 2)", "AS [SUM_AMOUNT]", "FROM dbo.TestTable"],
|
|
52
|
+
[]
|
|
53
|
+
),
|
|
54
|
+
( # numeric column without scale
|
|
55
|
+
["Amount"],
|
|
56
|
+
[{"COLUMN_NAME": "AMOUNT", "DATA_TYPE": "int"}],
|
|
57
|
+
[],
|
|
58
|
+
"",
|
|
59
|
+
None,
|
|
60
|
+
["SUM([AMOUNT])", "AS [SUM_AMOUNT]"],
|
|
61
|
+
["DECIMAL(38,"]
|
|
62
|
+
),
|
|
63
|
+
( # string column
|
|
64
|
+
["Name"],
|
|
65
|
+
[{"COLUMN_NAME": "NAME", "DATA_TYPE": "varchar"}],
|
|
66
|
+
[],
|
|
67
|
+
"",
|
|
68
|
+
None,
|
|
69
|
+
["COUNT(DISTINCT LOWER([NAME]))", "AS [COUNTDISTINCT_NAME]"],
|
|
70
|
+
[]
|
|
71
|
+
),
|
|
72
|
+
( # boolean column
|
|
73
|
+
["IsActive"],
|
|
74
|
+
[{"COLUMN_NAME": "ISACTIVE", "DATA_TYPE": "bit"}],
|
|
75
|
+
[],
|
|
76
|
+
"",
|
|
77
|
+
None,
|
|
78
|
+
["COUNT(CASE WHEN [ISACTIVE] = 1", "COUNT(CASE WHEN [ISACTIVE] = 0", "AS [AGGREGATEBOOLEAN_ISACTIVE]"],
|
|
79
|
+
[]
|
|
80
|
+
),
|
|
81
|
+
( # with where clause
|
|
82
|
+
["Amount"],
|
|
83
|
+
[{"COLUMN_NAME": "AMOUNT", "DATA_TYPE": "int"}],
|
|
84
|
+
[],
|
|
85
|
+
"WHERE Amount > 100",
|
|
86
|
+
None,
|
|
87
|
+
["WHERE Amount > 100"],
|
|
88
|
+
[]
|
|
89
|
+
),
|
|
90
|
+
( # exclude columns
|
|
91
|
+
["Amount", "Price"],
|
|
92
|
+
[{"COLUMN_NAME": "AMOUNT", "DATA_TYPE": "int"}],
|
|
93
|
+
["Price"],
|
|
94
|
+
"",
|
|
95
|
+
None,
|
|
96
|
+
["AMOUNT"],
|
|
97
|
+
["PRICE"]
|
|
98
|
+
),
|
|
99
|
+
( # multiple columns mixed types
|
|
100
|
+
["Amount", "Name", "IsActive"],
|
|
101
|
+
[
|
|
102
|
+
{"COLUMN_NAME": "AMOUNT", "DATA_TYPE": "decimal"},
|
|
103
|
+
{"COLUMN_NAME": "NAME", "DATA_TYPE": "varchar"},
|
|
104
|
+
{"COLUMN_NAME": "ISACTIVE", "DATA_TYPE": "bit"}
|
|
105
|
+
],
|
|
106
|
+
[],
|
|
107
|
+
"",
|
|
108
|
+
2,
|
|
109
|
+
["SUM([AMOUNT])", "COUNT(DISTINCT LOWER([NAME]))", "AGGREGATEBOOLEAN_ISACTIVE"],
|
|
110
|
+
[]
|
|
111
|
+
),
|
|
112
|
+
( # binary column
|
|
113
|
+
["BinaryData"],
|
|
114
|
+
[{"COLUMN_NAME": "BINARYDATA", "DATA_TYPE": "varbinary"}],
|
|
115
|
+
[],
|
|
116
|
+
"",
|
|
117
|
+
None,
|
|
118
|
+
["TRY_CONVERT(VARCHAR,[BINARYDATA])", "COUNT(DISTINCT LOWER("],
|
|
119
|
+
[]
|
|
120
|
+
),
|
|
121
|
+
( # date column
|
|
122
|
+
["CreatedDate"],
|
|
123
|
+
[{"COLUMN_NAME": "CREATEDDATE", "DATA_TYPE": "datetime"}],
|
|
124
|
+
[],
|
|
125
|
+
"",
|
|
126
|
+
None,
|
|
127
|
+
["COUNT(DISTINCT LOWER([CREATEDDATE]))", "AS [COUNTDISTINCT_CREATEDDATE]"],
|
|
128
|
+
[]
|
|
129
|
+
),
|
|
130
|
+
( # special characters in column names
|
|
131
|
+
["/ISDFPS/OBJNR"],
|
|
132
|
+
[{"COLUMN_NAME": "/ISDFPS/OBJNR", "DATA_TYPE": "varchar"}],
|
|
133
|
+
[],
|
|
134
|
+
"",
|
|
135
|
+
None,
|
|
136
|
+
["[/ISDFPS/OBJNR]", "AS [COUNTDISTINCT_/ISDFPS/OBJNR]"],
|
|
137
|
+
[]
|
|
138
|
+
),
|
|
139
|
+
],
|
|
140
|
+
)
|
|
141
|
+
def test_get_checksum_statement(
|
|
142
|
+
self, sqlserver_service, mock_database_object,
|
|
143
|
+
columns, mock_datatypes, exclude_columns, where_clause, numeric_scale,
|
|
144
|
+
expected_contains, expected_not_in
|
|
145
|
+
):
|
|
146
|
+
"""Test checksum statement with various configurations."""
|
|
147
|
+
sqlserver_service.get_data_types_from_object = MagicMock(return_value=mock_datatypes)
|
|
148
|
+
|
|
149
|
+
result = sqlserver_service._get_checksum_statement(
|
|
150
|
+
object=mock_database_object,
|
|
151
|
+
column_intersections=columns,
|
|
152
|
+
exclude_columns=exclude_columns,
|
|
153
|
+
where_clause=where_clause,
|
|
154
|
+
numeric_scale=numeric_scale
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
for expected in expected_contains:
|
|
158
|
+
assert expected in result
|
|
159
|
+
for expected in expected_not_in:
|
|
160
|
+
assert expected not in result
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from unittest.mock import MagicMock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from icsDataValidation.services.database_services.sqlserver_service import SQLServerService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def sqlserver_service():
|
|
10
|
+
"""Create a SQLServerService instance with mocked connection."""
|
|
11
|
+
connection_params = {
|
|
12
|
+
'Driver': 'ODBC Driver 18 for SQL Server',
|
|
13
|
+
'Server': 'localhost',
|
|
14
|
+
'Port': '1433',
|
|
15
|
+
'Database': 'testdb',
|
|
16
|
+
'User': 'sa',
|
|
17
|
+
'Password': 'password',
|
|
18
|
+
'Encrypt': True,
|
|
19
|
+
'TrustServerCertificate': True
|
|
20
|
+
}
|
|
21
|
+
service = SQLServerService(connection_params=connection_params)
|
|
22
|
+
service.sqlserver_connection = MagicMock()
|
|
23
|
+
return service
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestGetColumnClauseParametrized:
|
|
27
|
+
"""Parametrized tests for _get_column_clause method."""
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize(
|
|
30
|
+
"column_list,columns_datatype,numeric_scale,key_columns," \
|
|
31
|
+
"expected_in_clause,expected_numeric,expected_used",
|
|
32
|
+
[
|
|
33
|
+
( # numeric with scale
|
|
34
|
+
["Amount"],
|
|
35
|
+
[{"COLUMN_NAME": "Amount", "DATA_TYPE": "decimal"}],
|
|
36
|
+
2,
|
|
37
|
+
[],
|
|
38
|
+
["CAST(ROUND([Amount], 2) as decimal(38,2)) as [Amount]"],
|
|
39
|
+
["Amount"],
|
|
40
|
+
["Amount"]
|
|
41
|
+
),
|
|
42
|
+
( # numeric without scale
|
|
43
|
+
["Price"],
|
|
44
|
+
[{"COLUMN_NAME": "Price", "DATA_TYPE": "int"}],
|
|
45
|
+
None,
|
|
46
|
+
[],
|
|
47
|
+
["[Price] as [Price]"],
|
|
48
|
+
["Price"],
|
|
49
|
+
["Price"]
|
|
50
|
+
),
|
|
51
|
+
( # string column
|
|
52
|
+
["Name"],
|
|
53
|
+
[{"COLUMN_NAME": "Name", "DATA_TYPE": "varchar"}],
|
|
54
|
+
2,
|
|
55
|
+
[],
|
|
56
|
+
["[Name] AS [Name]"],
|
|
57
|
+
[],
|
|
58
|
+
["Name"]
|
|
59
|
+
),
|
|
60
|
+
( # date excluded by default
|
|
61
|
+
["CreatedDate"],
|
|
62
|
+
[{"COLUMN_NAME": "CreatedDate", "DATA_TYPE": "datetime"}],
|
|
63
|
+
2,
|
|
64
|
+
[],
|
|
65
|
+
[],
|
|
66
|
+
[],
|
|
67
|
+
[]
|
|
68
|
+
),
|
|
69
|
+
( # date as key column
|
|
70
|
+
["CreatedDate"],
|
|
71
|
+
[{"COLUMN_NAME": "CreatedDate", "DATA_TYPE": "datetime"}],
|
|
72
|
+
2,
|
|
73
|
+
["CreatedDate"],
|
|
74
|
+
["CreatedDate"],
|
|
75
|
+
[],
|
|
76
|
+
["CreatedDate"]
|
|
77
|
+
),
|
|
78
|
+
( # multiple mixed types
|
|
79
|
+
["Amount", "Name", "IsActive"],
|
|
80
|
+
[
|
|
81
|
+
{"COLUMN_NAME": "Amount", "DATA_TYPE": "decimal"},
|
|
82
|
+
{"COLUMN_NAME": "Name", "DATA_TYPE": "varchar"},
|
|
83
|
+
{"COLUMN_NAME": "IsActive", "DATA_TYPE": "bit"}
|
|
84
|
+
],
|
|
85
|
+
2,
|
|
86
|
+
[],
|
|
87
|
+
["CAST(ROUND([Amount], 2)", "[Name] AS [Name]", "IsActive"],
|
|
88
|
+
["Amount"],
|
|
89
|
+
["Amount", "Name", "IsActive"]
|
|
90
|
+
),
|
|
91
|
+
( # special characters in column names
|
|
92
|
+
["/ISDFPS/OBJNR", "MANDT"],
|
|
93
|
+
[
|
|
94
|
+
{"COLUMN_NAME": "/ISDFPS/OBJNR", "DATA_TYPE": "varchar"},
|
|
95
|
+
{"COLUMN_NAME": "MANDT", "DATA_TYPE": "varchar"}
|
|
96
|
+
],
|
|
97
|
+
2,
|
|
98
|
+
[],
|
|
99
|
+
["[/ISDFPS/OBJNR]", "[MANDT]"],
|
|
100
|
+
[],
|
|
101
|
+
["/ISDFPS/OBJNR", "MANDT"]
|
|
102
|
+
),
|
|
103
|
+
( # binary column
|
|
104
|
+
["BinaryData"],
|
|
105
|
+
[{"COLUMN_NAME": "BinaryData", "DATA_TYPE": "varbinary"}],
|
|
106
|
+
2,
|
|
107
|
+
[],
|
|
108
|
+
["BinaryData"],
|
|
109
|
+
[],
|
|
110
|
+
["BinaryData"]
|
|
111
|
+
),
|
|
112
|
+
( # empty column list
|
|
113
|
+
[],
|
|
114
|
+
[],
|
|
115
|
+
2,
|
|
116
|
+
[],
|
|
117
|
+
[],
|
|
118
|
+
[],
|
|
119
|
+
[]
|
|
120
|
+
),
|
|
121
|
+
( # all numeric columns with scale
|
|
122
|
+
["Amount", "Price", "Quantity"],
|
|
123
|
+
[
|
|
124
|
+
{"COLUMN_NAME": "Amount", "DATA_TYPE": "decimal"},
|
|
125
|
+
{"COLUMN_NAME": "Price", "DATA_TYPE": "money"},
|
|
126
|
+
{"COLUMN_NAME": "Quantity", "DATA_TYPE": "int"}
|
|
127
|
+
],
|
|
128
|
+
3,
|
|
129
|
+
[],
|
|
130
|
+
["decimal(38,3)"],
|
|
131
|
+
["Amount", "Price", "Quantity"],
|
|
132
|
+
["Amount", "Price", "Quantity"]
|
|
133
|
+
),
|
|
134
|
+
( # numeric with zero scale
|
|
135
|
+
["Count"],
|
|
136
|
+
[{"COLUMN_NAME": "Count", "DATA_TYPE": "int"}],
|
|
137
|
+
0,
|
|
138
|
+
[],
|
|
139
|
+
["[Count] as [Count]"],
|
|
140
|
+
["Count"],
|
|
141
|
+
["Count"]
|
|
142
|
+
),
|
|
143
|
+
( # text and ntext columns
|
|
144
|
+
["Description", "Notes"],
|
|
145
|
+
[
|
|
146
|
+
{"COLUMN_NAME": "Description", "DATA_TYPE": "text"},
|
|
147
|
+
{"COLUMN_NAME": "Notes", "DATA_TYPE": "ntext"}
|
|
148
|
+
],
|
|
149
|
+
2,
|
|
150
|
+
[],
|
|
151
|
+
["[Description] AS [Description]", "[Notes] AS [Notes]"],
|
|
152
|
+
[],
|
|
153
|
+
["Description", "Notes"]
|
|
154
|
+
),
|
|
155
|
+
( # multiple date columns with one as key
|
|
156
|
+
["CreatedDate", "ModifiedDate", "Id"],
|
|
157
|
+
[
|
|
158
|
+
{"COLUMN_NAME": "CreatedDate", "DATA_TYPE": "datetime"},
|
|
159
|
+
{"COLUMN_NAME": "ModifiedDate", "DATA_TYPE": "datetime2"},
|
|
160
|
+
{"COLUMN_NAME": "Id", "DATA_TYPE": "int"}
|
|
161
|
+
],
|
|
162
|
+
2,
|
|
163
|
+
["CreatedDate"],
|
|
164
|
+
["CreatedDate", "Id"],
|
|
165
|
+
["Id"],
|
|
166
|
+
["CreatedDate", "Id"]
|
|
167
|
+
),
|
|
168
|
+
],
|
|
169
|
+
)
|
|
170
|
+
def test_get_column_clause(
|
|
171
|
+
self, sqlserver_service, column_list, columns_datatype, numeric_scale,
|
|
172
|
+
key_columns, expected_in_clause, expected_numeric, expected_used
|
|
173
|
+
):
|
|
174
|
+
"""Test column clause with various configurations."""
|
|
175
|
+
column_clause, numeric_columns, used_columns = sqlserver_service._get_column_clause(
|
|
176
|
+
column_list, columns_datatype, numeric_scale, key_columns
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
for expected in expected_in_clause:
|
|
180
|
+
assert expected in column_clause
|
|
181
|
+
assert set(numeric_columns) == set(expected_numeric)
|
|
182
|
+
assert set(used_columns) == set(expected_used)
|