snowflake-data-validation 1.0.1__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.
Files changed (189) hide show
  1. snowflake/snowflake_data_validation/__init__.py +112 -0
  2. snowflake/snowflake_data_validation/__main__.py +35 -0
  3. snowflake/snowflake_data_validation/__version__.py +16 -0
  4. snowflake/snowflake_data_validation/comparison_orchestrator.py +250 -0
  5. snowflake/snowflake_data_validation/configuration/__init__.py +16 -0
  6. snowflake/snowflake_data_validation/configuration/configuration_loader.py +66 -0
  7. snowflake/snowflake_data_validation/configuration/model/configuration_model.py +122 -0
  8. snowflake/snowflake_data_validation/configuration/model/connection_types.py +46 -0
  9. snowflake/snowflake_data_validation/configuration/model/connections/__init__.py +47 -0
  10. snowflake/snowflake_data_validation/configuration/model/logging_configuration.py +63 -0
  11. snowflake/snowflake_data_validation/configuration/model/table_configuration.py +210 -0
  12. snowflake/snowflake_data_validation/configuration/model/validation_configuration.py +59 -0
  13. snowflake/snowflake_data_validation/configuration/singleton.py +16 -0
  14. snowflake/snowflake_data_validation/connector/__init__.py +32 -0
  15. snowflake/snowflake_data_validation/connector/connector_base.py +183 -0
  16. snowflake/snowflake_data_validation/connector/connector_factory_base.py +161 -0
  17. snowflake/snowflake_data_validation/executer/__init__.py +45 -0
  18. snowflake/snowflake_data_validation/executer/async_generation_executor.py +167 -0
  19. snowflake/snowflake_data_validation/executer/async_validation_executor.py +409 -0
  20. snowflake/snowflake_data_validation/executer/base_validation_executor.py +451 -0
  21. snowflake/snowflake_data_validation/executer/executor_factory.py +248 -0
  22. snowflake/snowflake_data_validation/executer/extractor_types.py +24 -0
  23. snowflake/snowflake_data_validation/executer/sync_validation_executor.py +713 -0
  24. snowflake/snowflake_data_validation/extractor/__init__.py +28 -0
  25. snowflake/snowflake_data_validation/extractor/metadata_extractor_base.py +374 -0
  26. snowflake/snowflake_data_validation/extractor/sql_queries_template_generator.py +570 -0
  27. snowflake/snowflake_data_validation/main_cli.py +92 -0
  28. snowflake/snowflake_data_validation/orchestration/parallel_execution_engine.py +337 -0
  29. snowflake/snowflake_data_validation/orchestration/table_metadata_processor.py +400 -0
  30. snowflake/snowflake_data_validation/orchestration/validation_progress_reporter.py +116 -0
  31. snowflake/snowflake_data_validation/query/__init__.py +22 -0
  32. snowflake/snowflake_data_validation/query/query_generator_base.py +346 -0
  33. snowflake/snowflake_data_validation/redshift/__init__.py +36 -0
  34. snowflake/snowflake_data_validation/redshift/connector/__init__.py +17 -0
  35. snowflake/snowflake_data_validation/redshift/connector/connector_factory_redshift.py +91 -0
  36. snowflake/snowflake_data_validation/redshift/connector/connector_redshift.py +237 -0
  37. snowflake/snowflake_data_validation/redshift/extractor/metadata_extractor_redshift.py +257 -0
  38. snowflake/snowflake_data_validation/redshift/extractor/redshift_cte_generator.py +163 -0
  39. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_chunk_row_concatenated_insert_template.sql.j2 +26 -0
  40. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_chunk_row_concatenated_table_template.sql.j2 +6 -0
  41. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_chunk_row_md5_insert_template.sql.j2 +8 -0
  42. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_chunk_row_md5_table_template.sql.j2 +6 -0
  43. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_chunks_md5_table_template.sql.j2 +4 -0
  44. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_column_metrics_templates.yaml +696 -0
  45. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_columns_cte_template.sql.j2 +8 -0
  46. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_datatypes_normalization_templates.yaml +46 -0
  47. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_extract_chunks_md5_table_template.sql.j2 +1 -0
  48. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_extract_md5_rows_chunk.sql.j2 +10 -0
  49. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_get_columns_metadata.sql.j2 +85 -0
  50. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_insert_chunk_row_md5_template.sql.j2 +5 -0
  51. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_row_count_query.sql.j2 +1 -0
  52. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_table_metadata_query.sql.j2 +29 -0
  53. snowflake/snowflake_data_validation/redshift/extractor/templates/redshift_to_snowflake_datatypes_mapping_template.yaml +44 -0
  54. snowflake/snowflake_data_validation/redshift/model/__init__.py +21 -0
  55. snowflake/snowflake_data_validation/redshift/model/redshift_credentials_connection.py +69 -0
  56. snowflake/snowflake_data_validation/redshift/query/__init__.py +22 -0
  57. snowflake/snowflake_data_validation/redshift/query/query_generator_redshift.py +315 -0
  58. snowflake/snowflake_data_validation/redshift/redshift_arguments_manager.py +129 -0
  59. snowflake/snowflake_data_validation/redshift/redshift_cli.py +618 -0
  60. snowflake/snowflake_data_validation/redshift/script_writer/__init__.py +22 -0
  61. snowflake/snowflake_data_validation/redshift/script_writer/script_writer_redshift.py +142 -0
  62. snowflake/snowflake_data_validation/script_writer/__init__.py +22 -0
  63. snowflake/snowflake_data_validation/script_writer/script_writer_base.py +152 -0
  64. snowflake/snowflake_data_validation/snowflake/__init__.py +38 -0
  65. snowflake/snowflake_data_validation/snowflake/connector/connector_factory_snowflake.py +159 -0
  66. snowflake/snowflake_data_validation/snowflake/connector/connector_snowflake.py +327 -0
  67. snowflake/snowflake_data_validation/snowflake/extractor/metadata_extractor_snowflake.py +352 -0
  68. snowflake/snowflake_data_validation/snowflake/extractor/snowflake_cte_generator.py +155 -0
  69. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_chunk_row_concatenated_template.sql.j2 +33 -0
  70. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_chunk_row_md5_template.sql.j2 +11 -0
  71. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_chunks_md5_table_template.sql.j2 +4 -0
  72. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_column_metrics_templates.yaml +151 -0
  73. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_columns_cte_template.sql.j2 +8 -0
  74. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_datatypes_normalization_templates.yaml +9 -0
  75. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_extract_chunks_md5_table_template.sql.j2 +5 -0
  76. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_extract_md5_rows_chunk.sql.j2 +10 -0
  77. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_get_case_sensitive_columns.sql.j2 +8 -0
  78. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_get_columns_metadata.sql.j2 +76 -0
  79. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_insert_chunk_row_md5_template.sql.j2 +1 -0
  80. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_row_count_query.sql.j2 +1 -0
  81. snowflake/snowflake_data_validation/snowflake/extractor/templates/snowflake_table_metadata_query.sql.j2 +30 -0
  82. snowflake/snowflake_data_validation/snowflake/model/__init__.py +25 -0
  83. snowflake/snowflake_data_validation/snowflake/model/snowflake_credentials_connection.py +62 -0
  84. snowflake/snowflake_data_validation/snowflake/model/snowflake_default_connection.py +31 -0
  85. snowflake/snowflake_data_validation/snowflake/model/snowflake_named_connection.py +36 -0
  86. snowflake/snowflake_data_validation/snowflake/query/__init__.py +22 -0
  87. snowflake/snowflake_data_validation/snowflake/query/query_generator_snowflake.py +223 -0
  88. snowflake/snowflake_data_validation/snowflake/script_writer/__init__.py +22 -0
  89. snowflake/snowflake_data_validation/snowflake/script_writer/script_writer_snowflake.py +96 -0
  90. snowflake/snowflake_data_validation/snowflake/snowflake_arguments_manager.py +204 -0
  91. snowflake/snowflake_data_validation/snowflake/snowflake_cli.py +392 -0
  92. snowflake/snowflake_data_validation/sqlserver/__init__.py +46 -0
  93. snowflake/snowflake_data_validation/sqlserver/connector/__init__.py +17 -0
  94. snowflake/snowflake_data_validation/sqlserver/connector/connector_factory_sql_server.py +92 -0
  95. snowflake/snowflake_data_validation/sqlserver/connector/connector_sql_server.py +312 -0
  96. snowflake/snowflake_data_validation/sqlserver/extractor/__init__.py +16 -0
  97. snowflake/snowflake_data_validation/sqlserver/extractor/metadata_extractor_sqlserver.py +257 -0
  98. snowflake/snowflake_data_validation/sqlserver/extractor/sqlserver_cte_generator.py +161 -0
  99. snowflake/snowflake_data_validation/sqlserver/extractor/templates/sqlserver_chunks_md5_table_template.sql.j2 +4 -0
  100. snowflake/snowflake_data_validation/sqlserver/extractor/templates/sqlserver_column_metrics_templates.yaml +537 -0
  101. snowflake/snowflake_data_validation/sqlserver/extractor/templates/sqlserver_columns_cte_template.sql.j2 +8 -0
  102. snowflake/snowflake_data_validation/sqlserver/extractor/templates/sqlserver_compute_md5_sql.j2 +55 -0
  103. snowflake/snowflake_data_validation/sqlserver/extractor/templates/sqlserver_datatypes_normalization_templates.yaml +26 -0
  104. snowflake/snowflake_data_validation/sqlserver/extractor/templates/sqlserver_extract_chunks_md5_table_template.sql.j2 +1 -0
  105. snowflake/snowflake_data_validation/sqlserver/extractor/templates/sqlserver_extract_md5_rows_chunk.sql.j2 +10 -0
  106. snowflake/snowflake_data_validation/sqlserver/extractor/templates/sqlserver_get_columns_metadata.sql.j2 +70 -0
  107. snowflake/snowflake_data_validation/sqlserver/extractor/templates/sqlserver_row_count_query.sql.j2 +1 -0
  108. snowflake/snowflake_data_validation/sqlserver/extractor/templates/sqlserver_table_metadata_query.sql.j2 +23 -0
  109. snowflake/snowflake_data_validation/sqlserver/extractor/templates/sqlserver_to_snowflake_datatypes_mapping_template.yaml +32 -0
  110. snowflake/snowflake_data_validation/sqlserver/model/__init__.py +21 -0
  111. snowflake/snowflake_data_validation/sqlserver/model/sqlserver_credentials_connection.py +71 -0
  112. snowflake/snowflake_data_validation/sqlserver/query/__init__.py +22 -0
  113. snowflake/snowflake_data_validation/sqlserver/query/query_generator_sqlserver.py +197 -0
  114. snowflake/snowflake_data_validation/sqlserver/script_writer/__init__.py +22 -0
  115. snowflake/snowflake_data_validation/sqlserver/script_writer/script_writer_sqlserver.py +177 -0
  116. snowflake/snowflake_data_validation/sqlserver/sqlserver_arguments_manager.py +147 -0
  117. snowflake/snowflake_data_validation/sqlserver/sqlserver_cli.py +701 -0
  118. snowflake/snowflake_data_validation/table_partitioning_strategy.md +96 -0
  119. snowflake/snowflake_data_validation/teradata/__init__.py +14 -0
  120. snowflake/snowflake_data_validation/teradata/connector/__init__.py +14 -0
  121. snowflake/snowflake_data_validation/teradata/connector/connector_factory_teradata.py +79 -0
  122. snowflake/snowflake_data_validation/teradata/connector/connector_teradata.py +264 -0
  123. snowflake/snowflake_data_validation/teradata/extractor/__init__.py +19 -0
  124. snowflake/snowflake_data_validation/teradata/extractor/metadata_extractor_teradata.py +264 -0
  125. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_chunks_md5_table_template.sql.j2 +4 -0
  126. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_column_metrics_templates.yaml +497 -0
  127. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_columns_cte_template.sql.j2 +8 -0
  128. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_compute_md5_sql.j2 +62 -0
  129. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_create_row_concatenated.sql.j2 +7 -0
  130. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_create_row_md5.sql.j2 +7 -0
  131. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_datatypes_normalization_templates.yaml +15 -0
  132. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_extract_chunks_md5_table_template.sql.j2 +1 -0
  133. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_extract_md5_rows_chunk.sql.j2 +10 -0
  134. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_get_columns_metadata.sql.j2 +70 -0
  135. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_row_count_query.sql.j2 +1 -0
  136. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_table_metadata_query.sql.j2 +92 -0
  137. snowflake/snowflake_data_validation/teradata/extractor/templates/teradata_to_snowflake_datatypes_mapping_template.yaml +48 -0
  138. snowflake/snowflake_data_validation/teradata/extractor/teradata_cte_generator.py +190 -0
  139. snowflake/snowflake_data_validation/teradata/model/__init__.py +21 -0
  140. snowflake/snowflake_data_validation/teradata/model/teradata_credentials_connection.py +53 -0
  141. snowflake/snowflake_data_validation/teradata/query/__init__.py +19 -0
  142. snowflake/snowflake_data_validation/teradata/query/query_generator_teradata.py +231 -0
  143. snowflake/snowflake_data_validation/teradata/script_writer/__init__.py +19 -0
  144. snowflake/snowflake_data_validation/teradata/script_writer/script_writer_teradata.py +50 -0
  145. snowflake/snowflake_data_validation/teradata/teradata_arguments_manager.py +143 -0
  146. snowflake/snowflake_data_validation/teradata/teradata_cli.py +745 -0
  147. snowflake/snowflake_data_validation/utils/__init__.py +16 -0
  148. snowflake/snowflake_data_validation/utils/arguments_manager_base.py +482 -0
  149. snowflake/snowflake_data_validation/utils/arguments_manager_factory.py +130 -0
  150. snowflake/snowflake_data_validation/utils/base_output_handler.py +66 -0
  151. snowflake/snowflake_data_validation/utils/configuration_file_editor.py +86 -0
  152. snowflake/snowflake_data_validation/utils/configuration_file_generator.py +165 -0
  153. snowflake/snowflake_data_validation/utils/connection_pool.py +349 -0
  154. snowflake/snowflake_data_validation/utils/connector_factory.py +71 -0
  155. snowflake/snowflake_data_validation/utils/console_output_handler.py +95 -0
  156. snowflake/snowflake_data_validation/utils/constants.py +350 -0
  157. snowflake/snowflake_data_validation/utils/context.py +126 -0
  158. snowflake/snowflake_data_validation/utils/cpu_optimizer.py +156 -0
  159. snowflake/snowflake_data_validation/utils/helper.py +30 -0
  160. snowflake/snowflake_data_validation/utils/helpers/helper_database.py +54 -0
  161. snowflake/snowflake_data_validation/utils/helpers/helper_dataframe.py +83 -0
  162. snowflake/snowflake_data_validation/utils/helpers/helper_io.py +85 -0
  163. snowflake/snowflake_data_validation/utils/helpers/helper_misc.py +95 -0
  164. snowflake/snowflake_data_validation/utils/helpers/helper_templates.py +294 -0
  165. snowflake/snowflake_data_validation/utils/logging_config.py +197 -0
  166. snowflake/snowflake_data_validation/utils/logging_utils.py +68 -0
  167. snowflake/snowflake_data_validation/utils/model/chunk.py +16 -0
  168. snowflake/snowflake_data_validation/utils/model/column_metadata.py +45 -0
  169. snowflake/snowflake_data_validation/utils/model/table_column_metadata.py +94 -0
  170. snowflake/snowflake_data_validation/utils/model/table_context.py +351 -0
  171. snowflake/snowflake_data_validation/utils/model/templates_loader_manager.py +123 -0
  172. snowflake/snowflake_data_validation/utils/progress_reporter.py +61 -0
  173. snowflake/snowflake_data_validation/utils/run_context.py +59 -0
  174. snowflake/snowflake_data_validation/utils/table_partitioning_strategy.py +148 -0
  175. snowflake/snowflake_data_validation/utils/telemetry.py +863 -0
  176. snowflake/snowflake_data_validation/utils/templates/configuration_file_templates.py +95 -0
  177. snowflake/snowflake_data_validation/utils/thread_safe_singleton.py +49 -0
  178. snowflake/snowflake_data_validation/utils/validation_utils.py +142 -0
  179. snowflake/snowflake_data_validation/validation/__init__.py +16 -0
  180. snowflake/snowflake_data_validation/validation/data_validator_base.py +452 -0
  181. snowflake/snowflake_data_validation/validation/metrics_data_validator.py +251 -0
  182. snowflake/snowflake_data_validation/validation/row_data_validator.py +479 -0
  183. snowflake/snowflake_data_validation/validation/schema_data_validator.py +197 -0
  184. snowflake/snowflake_data_validation/validation/validation_report_buffer.py +196 -0
  185. snowflake_data_validation-1.0.1.dist-info/METADATA +228 -0
  186. snowflake_data_validation-1.0.1.dist-info/RECORD +189 -0
  187. snowflake_data_validation-1.0.1.dist-info/WHEEL +4 -0
  188. snowflake_data_validation-1.0.1.dist-info/entry_points.txt +3 -0
  189. snowflake_data_validation-1.0.1.dist-info/licenses/LICENSE +177 -0
@@ -0,0 +1,47 @@
1
+ # Copyright 2025 Snowflake Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ """Connection models for different database dialects."""
14
+
15
+ # Import SQL Server connection models
16
+ from snowflake.snowflake_data_validation.sqlserver.model.sqlserver_credentials_connection import (
17
+ SqlServerCredentialsConnection,
18
+ )
19
+
20
+ from snowflake.snowflake_data_validation.redshift.model.redshift_credentials_connection import (
21
+ RedshiftCredentialsConnection,
22
+ )
23
+
24
+ # Import Teradata connection models
25
+ from snowflake.snowflake_data_validation.teradata.model.teradata_credentials_connection import (
26
+ TeradataCredentialsConnection,
27
+ )
28
+
29
+ # Import Snowflake connection models
30
+ from snowflake.snowflake_data_validation.snowflake.model.snowflake_default_connection import (
31
+ SnowflakeDefaultConnection,
32
+ )
33
+ from snowflake.snowflake_data_validation.snowflake.model.snowflake_named_connection import (
34
+ SnowflakeNamedConnection,
35
+ )
36
+ from snowflake.snowflake_data_validation.snowflake.model.snowflake_credentials_connection import (
37
+ SnowflakeCredentialsConnection,
38
+ )
39
+
40
+ __all__ = [
41
+ "SqlServerCredentialsConnection",
42
+ "TeradataCredentialsConnection",
43
+ "SnowflakeDefaultConnection",
44
+ "SnowflakeNamedConnection",
45
+ "SnowflakeCredentialsConnection",
46
+ "RedshiftCredentialsConnection",
47
+ ]
@@ -0,0 +1,63 @@
1
+ # Copyright 2025 Snowflake Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ """Logging configuration model for Snowflake Data Validation."""
14
+
15
+ import logging
16
+
17
+ from pydantic import BaseModel, field_validator
18
+
19
+
20
+ class LoggingConfiguration(BaseModel):
21
+ """Logging configuration model.
22
+
23
+ Args:
24
+ level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
25
+ console_level: Console logging level (defaults to level if not specified)
26
+ file_level: File logging level (defaults to level if not specified)
27
+
28
+ """
29
+
30
+ level: str = "INFO"
31
+ console_level: str | None = None
32
+ file_level: str | None = None
33
+
34
+ @field_validator("level", "console_level", "file_level")
35
+ @classmethod
36
+ def validate_log_level(cls, value: str | None) -> str | None:
37
+ """Validate logging level values."""
38
+ if value is None:
39
+ return value
40
+
41
+ valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
42
+ if value.upper() not in valid_levels:
43
+ raise ValueError(
44
+ f"Invalid logging level: {value}. "
45
+ f"Valid levels are: {', '.join(valid_levels)}"
46
+ )
47
+ return value.upper()
48
+
49
+ def get_console_level(self) -> str:
50
+ """Get the console logging level."""
51
+ return self.console_level or self.level
52
+
53
+ def get_file_level(self) -> str:
54
+ """Get the file logging level."""
55
+ return self.file_level or self.level
56
+
57
+ def get_console_level_int(self) -> int:
58
+ """Get the console logging level as integer."""
59
+ return getattr(logging, self.get_console_level())
60
+
61
+ def get_file_level_int(self) -> int:
62
+ """Get the file logging level as integer."""
63
+ return getattr(logging, self.get_file_level())
@@ -0,0 +1,210 @@
1
+ import logging
2
+
3
+ from typing import ClassVar
4
+
5
+ from pydantic import BaseModel, model_validator
6
+ from typing_extensions import Self
7
+
8
+ from snowflake.snowflake_data_validation.configuration.model.validation_configuration import (
9
+ ValidationConfiguration,
10
+ )
11
+ from snowflake.snowflake_data_validation.utils.constants import NEWLINE
12
+ from snowflake.snowflake_data_validation.utils.helper import Helper
13
+ from snowflake.snowflake_data_validation.utils.helpers.helper_database import (
14
+ HelperDatabase,
15
+ )
16
+
17
+
18
+ LOGGER = logging.getLogger(__name__)
19
+
20
+ MANDATORY_PROPERTIES: set[str] = {
21
+ "target_name",
22
+ "use_column_selection_as_exclude_list",
23
+ "column_selection_list",
24
+ }
25
+
26
+ VISIBLE_PROPERTIES: set[str] = {
27
+ "apply_metric_column_modifier",
28
+ "chunk_number",
29
+ "column_mappings",
30
+ "exclude_metrics",
31
+ "index_column_list",
32
+ "is_case_sensitive",
33
+ "max_failed_rows_number",
34
+ "target_database",
35
+ "target_index_column_list",
36
+ "target_schema",
37
+ "target_where_clause",
38
+ "where_clause",
39
+ }
40
+
41
+
42
+ class TableConfiguration(BaseModel):
43
+ """Table configuration model.
44
+
45
+ Args:
46
+ pydantic.BaseModel (pydantic.BaseModel): pydantic BaseModel
47
+
48
+ """
49
+
50
+ _id_counter: ClassVar[int] = 0
51
+ id: int | None = None
52
+
53
+ fully_qualified_name: str
54
+ use_column_selection_as_exclude_list: bool
55
+ column_selection_list: list[str]
56
+ validation_configuration: ValidationConfiguration | None = None
57
+
58
+ source_database: str | None = None
59
+ source_schema: str | None = None
60
+ source_table: str | None = None
61
+ target_name: str | None = None
62
+
63
+ target_fully_qualified_name: str = ""
64
+ where_clause: str = ""
65
+ target_where_clause: str = ""
66
+ has_where_clause: bool = False
67
+ has_target_where_clause: bool = False
68
+ target_database: str | None = None
69
+ target_schema: str | None = None
70
+ index_column_list: list[str] = []
71
+ target_index_column_list: list[str] = []
72
+ is_case_sensitive: bool = False
73
+ chunk_number: int | None = None
74
+ column_mappings: dict[str, str] = {}
75
+ max_failed_rows_number: int | None = None
76
+ exclude_metrics: bool | None = None
77
+ apply_metric_column_modifier: bool | None = None
78
+
79
+ @model_validator(mode="after")
80
+ def load(self) -> Self:
81
+ self._assign_id()
82
+ self._load_source_decomposed_fully_qualified_name()
83
+ self._load_target_fully_qualified_name()
84
+ self._set_has_where_clause()
85
+ self._set_has_target_where_clause()
86
+ self._normalize_where_clause()
87
+ self._set_target_index_column_list()
88
+ self._check_chunk_number()
89
+ self._check_target_where_clause()
90
+ self._set_column_mappings()
91
+ self._check_max_failed_rows_number()
92
+ return self
93
+
94
+ def _assign_id(self) -> None:
95
+ if self.id is None:
96
+ TableConfiguration._id_counter += 1
97
+ self.id = TableConfiguration._id_counter
98
+
99
+ def _load_source_decomposed_fully_qualified_name(self) -> None:
100
+ self.fully_qualified_name = self.fully_qualified_name.replace('\\"', '"')
101
+
102
+ decomposed_tuple = Helper.get_decomposed_fully_qualified_name(
103
+ self.fully_qualified_name
104
+ )
105
+
106
+ if len(decomposed_tuple) == 2:
107
+ self.source_database = None
108
+ self.source_schema = decomposed_tuple[0]
109
+ self.source_table = decomposed_tuple[1]
110
+
111
+ else:
112
+ self.source_database = decomposed_tuple[0]
113
+ self.source_schema = decomposed_tuple[1]
114
+ self.source_table = decomposed_tuple[2]
115
+
116
+ def _load_target_fully_qualified_name(self) -> None:
117
+ if self.target_database is None:
118
+ self.target_database = self.source_database
119
+
120
+ if self.target_schema is None:
121
+ self.target_schema = self.source_schema
122
+
123
+ if self.target_name is None:
124
+ self.target_name = self.source_table
125
+ else:
126
+ self.target_name = HelperDatabase.normalize_escape_quotes(self.target_name)
127
+
128
+ self.target_fully_qualified_name = (
129
+ f"{self.target_database}.{self.target_schema}.{self.target_name}"
130
+ )
131
+
132
+ def _set_has_where_clause(self) -> None:
133
+ self.has_where_clause = self.where_clause != ""
134
+
135
+ def _set_has_target_where_clause(self) -> None:
136
+ self.has_target_where_clause = self.target_where_clause != ""
137
+
138
+ def _check_target_where_clause(self) -> None:
139
+ if self.has_where_clause and not self.has_target_where_clause:
140
+ LOGGER.warning(
141
+ "Target where clause was not set for table %s. This may lead to unexpected results.",
142
+ self.target_fully_qualified_name,
143
+ )
144
+
145
+ def _set_target_index_column_list(self) -> None:
146
+ if self.target_index_column_list == []:
147
+ self.target_index_column_list = self.index_column_list.copy()
148
+
149
+ local_target_index_column_list = []
150
+ for target_column_name in self.target_index_column_list:
151
+ new_column_name = self.column_mappings.get(
152
+ target_column_name, target_column_name
153
+ )
154
+ local_target_index_column_list.append(new_column_name)
155
+
156
+ self.target_index_column_list = local_target_index_column_list
157
+
158
+ def _check_chunk_number(self) -> None:
159
+ if self.chunk_number is None:
160
+ return
161
+ elif self.chunk_number < 1:
162
+ raise ValueError("Chunk number must be greater than or equal to 1.")
163
+
164
+ def _set_column_mappings(self) -> None:
165
+ """Normalize the keys and values of the column mappings dictionary to uppercase."""
166
+ local_column_mapping = {}
167
+ for key in self.column_mappings.keys():
168
+ if self.is_case_sensitive:
169
+ local_column_mapping[key] = self.column_mappings[key]
170
+ else:
171
+ local_column_mapping[key.upper()] = self.column_mappings[key].upper()
172
+ self.column_mappings = local_column_mapping
173
+
174
+ def _check_max_failed_rows_number(self) -> None:
175
+ # Accept None, because it will be overwritten by ConfigurationModel
176
+ # based on the value of ValidationConfiguration model.
177
+ if self.max_failed_rows_number is None:
178
+ return
179
+ elif self.max_failed_rows_number < 1:
180
+ raise ValueError(
181
+ f"Invalid value for max failed rows number in table f{self.fully_qualified_name}. "
182
+ "Value must be greater than or equal to 1."
183
+ )
184
+
185
+ def _normalize_where_clause(self) -> None:
186
+ if self.has_where_clause:
187
+ self.where_clause = HelperDatabase.remove_escape_quotes(self.where_clause)
188
+
189
+ if self.has_target_where_clause:
190
+ self.target_where_clause = HelperDatabase.remove_escape_quotes(
191
+ self.target_where_clause
192
+ )
193
+
194
+ def __str__(self) -> str:
195
+ """Return a formatted string representation of the table configuration."""
196
+ properties = []
197
+
198
+ # Add mandatory properties
199
+ for attr in MANDATORY_PROPERTIES:
200
+ properties.append(f" {attr}: {getattr(self, attr)}")
201
+
202
+ # Add visible properties with non-empty values
203
+ for attr in VISIBLE_PROPERTIES:
204
+ value = getattr(self, attr)
205
+ if value not in (None, [], {}, "", 0):
206
+ properties.append(f" {attr}: {value}")
207
+
208
+ properties.sort()
209
+ properties_str = NEWLINE.join(properties)
210
+ return f" - fully_qualified_name: {self.fully_qualified_name}{NEWLINE}{properties_str}"
@@ -0,0 +1,59 @@
1
+ # Copyright 2025 Snowflake Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from pathlib import Path
17
+
18
+ from pydantic import BaseModel, model_validator
19
+
20
+ from snowflake.snowflake_data_validation.utils.constants import (
21
+ MAX_FAILED_ROWS_NUMBER_DEFAULT_VALUE,
22
+ )
23
+
24
+
25
+ class ValidationConfiguration(BaseModel):
26
+ """Class representing the validation levels to be applied to a table."""
27
+
28
+ schema_validation: bool | None = False
29
+ metrics_validation: bool | None = False
30
+ row_validation: bool | None = False
31
+ # Custom templates path for validation scripts.
32
+ custom_templates_path: Path | None = None
33
+ max_failed_rows_number: int = MAX_FAILED_ROWS_NUMBER_DEFAULT_VALUE
34
+ exclude_metrics: bool = False
35
+ apply_metric_column_modifier: bool = False
36
+
37
+ @model_validator(mode="after")
38
+ def validate_configuration(self) -> "ValidationConfiguration":
39
+ """Validate the configuration after initialization.
40
+
41
+ This method checks if at least one validation type is enabled.
42
+
43
+ Raises:
44
+ ValueError: If no validation type is enabled.
45
+
46
+ """
47
+ if not self.model_dump(exclude_none=True):
48
+ raise ValueError(
49
+ "At least one validation type must be enabled in case of adding the property."
50
+ )
51
+ self._check_max_failed_rows_number()
52
+ return self
53
+
54
+ def _check_max_failed_rows_number(self) -> None:
55
+ if self.max_failed_rows_number < 1:
56
+ raise ValueError(
57
+ "Invalid value for max failed rows number in validation configuration. "
58
+ "Value must be greater than or equal to 1."
59
+ )
@@ -0,0 +1,16 @@
1
+ class Singleton(type):
2
+
3
+ """Singleton metaclass.
4
+
5
+ This metaclass ensures that only one instance of a class exists.
6
+ Any class using this metaclass will return the same instance
7
+ every time it is instantiated, effectively implementing the
8
+ Singleton design pattern.
9
+ """
10
+
11
+ _instances = {}
12
+
13
+ def __call__(cls, *args, **kwargs):
14
+ if cls not in cls._instances:
15
+ cls._instances[cls] = super().__call__(*args, **kwargs)
16
+ return cls._instances[cls]
@@ -0,0 +1,32 @@
1
+ # Copyright 2025 Snowflake Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ # Description: This file is used to expose the ConnectorBase class to the outside world.
18
+ # The ConnectorBase class is used to define the interface for the Snowflake connector.
19
+ # It is used to define the methods that the connector must implement.
20
+ # The methods defined in the ConnectorBase class are connect
21
+ # execute_query, and close.
22
+ # The connect method is used to establish a connection to the Snowflake database.
23
+
24
+ __all__ = ["ConnectorBase", "NullConnector", "ConnectorFactoryBase"]
25
+
26
+ from snowflake.snowflake_data_validation.connector.connector_base import (
27
+ ConnectorBase,
28
+ NullConnector,
29
+ )
30
+ from snowflake.snowflake_data_validation.connector.connector_factory_base import (
31
+ ConnectorFactoryBase,
32
+ )
@@ -0,0 +1,183 @@
1
+ # Copyright 2025 Snowflake Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ import logging
18
+ import time
19
+
20
+ from abc import ABC, abstractmethod
21
+
22
+ from snowflake.snowflake_data_validation.utils.constants import (
23
+ DEFAULT_CONNECTION_MODE,
24
+ )
25
+
26
+
27
+ class ConnectorBase(ABC):
28
+ """Abstract base class for database connectors."""
29
+
30
+ def __init__(self, output_path: str | None = None):
31
+ """
32
+ Initialize the database connector base class.
33
+
34
+ Args:
35
+ output_path (Optional[str], optional): Path for output files. Defaults to None.
36
+
37
+ """
38
+ self.connection: object | None = None
39
+ self.output_path = output_path
40
+ self.logger = logging.getLogger(__name__)
41
+
42
+ @abstractmethod
43
+ def connect(
44
+ self, mode: str = DEFAULT_CONNECTION_MODE, connection_name: str = ""
45
+ ) -> None:
46
+ """Establish a connection to the database."""
47
+ pass
48
+
49
+ def connect_with_retry(
50
+ self,
51
+ connect_func,
52
+ max_attempts: int = 3,
53
+ delay_seconds: float = 1.0,
54
+ delay_multiplier: float = 2.0,
55
+ *args,
56
+ **kwargs,
57
+ ) -> None:
58
+ """
59
+ Establish a connection with retry logic.
60
+
61
+ Args:
62
+ connect_func: The connection function to call
63
+ max_attempts: Maximum number of connection attempts
64
+ delay_seconds: Initial delay between retries in seconds
65
+ delay_multiplier: Multiplier for exponential backoff
66
+ *args: Arguments to pass to connect_func
67
+ **kwargs: Keyword arguments to pass to connect_func
68
+
69
+ Raises:
70
+ ConnectionError: If all connection attempts fail
71
+
72
+ """
73
+ last_exception = None
74
+ current_delay = delay_seconds
75
+
76
+ for attempt in range(1, max_attempts + 1):
77
+ try:
78
+ self.logger.info(f"Connection attempt {attempt}/{max_attempts}")
79
+ connect_func(*args, **kwargs)
80
+ self.logger.info("Connection established successfully")
81
+ return
82
+
83
+ except Exception as e:
84
+ last_exception = e
85
+ self.logger.warning(
86
+ f"Connection attempt {attempt}/{max_attempts} failed: {str(e)}"
87
+ )
88
+ if attempt < max_attempts:
89
+ self.logger.info(f"Retrying in {current_delay:.1f} seconds...")
90
+ time.sleep(current_delay)
91
+ current_delay *= delay_multiplier
92
+ else:
93
+ self.logger.error(f"All {max_attempts} connection attempts failed")
94
+ raise ConnectionError(
95
+ f"Failed to establish connection after {max_attempts} attempts. "
96
+ f"Last error: {last_exception}"
97
+ ) from last_exception
98
+
99
+ def _verify_connection(self) -> None:
100
+ """
101
+ Verify the connection by executing a simple test query.
102
+
103
+ Default implementation that can be overridden by child classes.
104
+ Child classes should implement their own verification logic.
105
+
106
+ Raises:
107
+ ConnectionError: If connection verification fails
108
+
109
+ """
110
+ if self.connection is None:
111
+ raise ConnectionError("Connection is None")
112
+
113
+ # Default implementation - child classes should override this
114
+ self.logger.debug("Using default connection verification (no-op)")
115
+
116
+ def _attempt_connection_and_verify(self, connect_func, *args, **kwargs) -> None:
117
+ """
118
+ Attempt connection and verify it.
119
+
120
+ Args:
121
+ connect_func: The connection function to call
122
+ *args: Arguments to pass to connect_func
123
+ **kwargs: Keyword arguments to pass to connect_func
124
+
125
+ Raises:
126
+ ConnectionError: If connection or verification fails
127
+
128
+ """
129
+ try:
130
+ connect_func(*args, **kwargs)
131
+ self._verify_connection()
132
+
133
+ except Exception:
134
+ if self.connection is not None:
135
+ try:
136
+ close_method = getattr(self.connection, "close", None)
137
+ if close_method is not None:
138
+ close_method()
139
+ except Exception:
140
+ pass # Ignore errors during cleanup
141
+ self.connection = None
142
+ raise
143
+
144
+ @abstractmethod
145
+ def execute_query(self, query: str) -> list[tuple]:
146
+ """Execute a query on the database."""
147
+ pass
148
+
149
+ @abstractmethod
150
+ def execute_statement(self, statement: str) -> None:
151
+ """Execute a statement on the database."""
152
+ pass
153
+
154
+ @abstractmethod
155
+ def execute_query_no_return(self, query: str) -> None:
156
+ """Execute a query on the database without returning results."""
157
+ pass
158
+
159
+ @abstractmethod
160
+ def close(self) -> None:
161
+ """Close the connection to the database."""
162
+ pass
163
+
164
+
165
+ class NullConnector(ConnectorBase):
166
+ """A connector that does nothing. Used as a null object for async validation scenarios."""
167
+
168
+ def connect(
169
+ self, mode: str = DEFAULT_CONNECTION_MODE, connection_name: str = ""
170
+ ) -> None:
171
+ pass
172
+
173
+ def execute_query(self, query: str) -> list[tuple]:
174
+ return []
175
+
176
+ def execute_query_no_return(self, query: str) -> None:
177
+ pass
178
+
179
+ def execute_statement(self, statement: str) -> None:
180
+ pass
181
+
182
+ def close(self) -> None:
183
+ pass