collate-data-diff 0.11.12__tar.gz → 0.11.15__tar.gz

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 (53) hide show
  1. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/PKG-INFO +1 -1
  2. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/databricks.py +68 -1
  3. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/snowflake.py +1 -0
  4. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/diff_tables.py +36 -6
  5. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/errors.py +6 -0
  6. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/pyproject.toml +1 -1
  7. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/LICENSE +0 -0
  8. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/README.md +0 -0
  9. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/__init__.py +0 -0
  10. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/__main__.py +0 -0
  11. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/abcs/__init__.py +0 -0
  12. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/abcs/compiler.py +0 -0
  13. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/abcs/database_types.py +0 -0
  14. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/cloud/__init__.py +0 -0
  15. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/cloud/data_source.py +0 -0
  16. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/cloud/datafold_api.py +0 -0
  17. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/config.py +0 -0
  18. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/__init__.py +0 -0
  19. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/_connect.py +0 -0
  20. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/base.py +0 -0
  21. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/bigquery.py +0 -0
  22. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/clickhouse.py +0 -0
  23. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/duckdb.py +0 -0
  24. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/mssql.py +0 -0
  25. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/mysql.py +0 -0
  26. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/oracle.py +0 -0
  27. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/postgresql.py +0 -0
  28. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/presto.py +0 -0
  29. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/redshift.py +0 -0
  30. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/trino.py +0 -0
  31. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/databases/vertica.py +0 -0
  32. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/dbt.py +0 -0
  33. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/dbt_config_validators.py +0 -0
  34. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/dbt_parser.py +0 -0
  35. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/format.py +0 -0
  36. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/hashdiff_tables.py +0 -0
  37. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/info_tree.py +0 -0
  38. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/joindiff_tables.py +0 -0
  39. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/lexicographic_space.py +0 -0
  40. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/parse_time.py +0 -0
  41. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/py.typed +0 -0
  42. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/queries/__init__.py +0 -0
  43. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/queries/api.py +0 -0
  44. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/queries/ast_classes.py +0 -0
  45. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/queries/base.py +0 -0
  46. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/queries/extras.py +0 -0
  47. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/query_utils.py +0 -0
  48. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/schema.py +0 -0
  49. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/table_segment.py +0 -0
  50. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/thread_utils.py +0 -0
  51. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/tracking.py +0 -0
  52. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/utils.py +0 -0
  53. {collate_data_diff-0.11.12 → collate_data_diff-0.11.15}/data_diff/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: collate-data-diff
3
- Version: 0.11.12
3
+ Version: 0.11.15
4
4
  Summary: Command-line tool and Python library to efficiently diff rows across two different databases.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -37,6 +37,18 @@ def import_databricks():
37
37
  return databricks
38
38
 
39
39
 
40
+ @import_helper(text="You can install it using 'pip install databricks-sdk'")
41
+ def import_databricks_sdk():
42
+ from databricks.sdk.core import Config, oauth_service_principal
43
+
44
+ return Config, oauth_service_principal
45
+
46
+
47
+ AUTH_PAT = "pat"
48
+ AUTH_OAUTH_M2M = "oauth-m2m"
49
+ AUTH_AZURE_SP_M2M = "azure-sp-m2m"
50
+
51
+
40
52
  @attrs.define(frozen=False)
41
53
  class Dialect(BaseDialect):
42
54
  name = "Databricks"
@@ -133,6 +145,61 @@ class Databricks(ThreadedDatabase):
133
145
  self.default_schema = kw.get("schema", "default")
134
146
  self.catalog = kw.get("catalog", "hive_metastore")
135
147
 
148
+ def _require(self, key: str) -> str:
149
+ value = self._args.get(key)
150
+ if not value:
151
+ auth_method = self._args.get("auth_method", AUTH_PAT)
152
+ raise ValueError(f"{self.name}: '{key}' is required for auth_method '{auth_method}'")
153
+ return value
154
+
155
+ def _pat_kwargs(self) -> Dict[str, Any]:
156
+ return {"access_token": self._require("access_token")}
157
+
158
+ def _oauth_m2m_kwargs(self) -> Dict[str, Any]:
159
+ Config, oauth_service_principal = import_databricks_sdk()
160
+ hostname = self._args["server_hostname"]
161
+ client_id = self._require("databricks_client_id")
162
+ client_secret = self._require("databricks_client_secret")
163
+
164
+ def credentials_provider():
165
+ provider = oauth_service_principal(
166
+ Config(host=f"https://{hostname}", client_id=client_id, client_secret=client_secret)
167
+ )
168
+ if provider is None:
169
+ raise ConnectionError(f"{self.name}: OAuth is not available on {hostname}")
170
+ return provider
171
+
172
+ return {"credentials_provider": credentials_provider}
173
+
174
+ def _azure_sp_m2m_kwargs(self) -> Dict[str, Any]:
175
+ return {
176
+ "auth_type": AUTH_AZURE_SP_M2M,
177
+ "azure_client_id": self._require("azure_client_id"),
178
+ "azure_client_secret": self._require("azure_client_secret"),
179
+ "azure_tenant_id": self._require("azure_tenant_id"),
180
+ }
181
+
182
+ def _auth_kwargs(self) -> Dict[str, Any]:
183
+ """Credential kwargs for ``databricks.sql.connect``.
184
+
185
+ Raises:
186
+ ValueError: on an unknown ``auth_method`` or a missing credential.
187
+ """
188
+ # Callables cannot travel in the connection config: `Connect` caches on
189
+ # json.dumps of it. Providers are built here, from plain strings.
190
+ auth_method = self._args.get("auth_method", AUTH_PAT)
191
+ builder = {
192
+ AUTH_PAT: self._pat_kwargs,
193
+ AUTH_OAUTH_M2M: self._oauth_m2m_kwargs,
194
+ AUTH_AZURE_SP_M2M: self._azure_sp_m2m_kwargs,
195
+ }.get(auth_method)
196
+ if builder is None:
197
+ raise ValueError(
198
+ f"{self.name}: unknown auth_method '{auth_method}'. "
199
+ f"Expected one of {AUTH_PAT}, {AUTH_OAUTH_M2M}, {AUTH_AZURE_SP_M2M}"
200
+ )
201
+ return builder()
202
+
136
203
  def create_connection(self):
137
204
  databricks = import_databricks()
138
205
 
@@ -140,8 +207,8 @@ class Databricks(ThreadedDatabase):
140
207
  return databricks.sql.connect(
141
208
  server_hostname=self._args["server_hostname"],
142
209
  http_path=self._args["http_path"],
143
- access_token=self._args["access_token"],
144
210
  catalog=self.catalog,
211
+ **self._auth_kwargs(),
145
212
  )
146
213
  except databricks.sql.exc.Error as e:
147
214
  raise ConnectionError(*e.args) from e
@@ -160,6 +160,7 @@ class Snowflake(Database):
160
160
  format=serialization.PrivateFormat.PKCS8,
161
161
  encryption_algorithm=serialization.NoEncryption(),
162
162
  )
163
+ kw.pop("private_key_passphrase", None)
163
164
 
164
165
  self._conn = snowflake.connector.connect(schema=f'"{schema}"', **kw)
165
166
 
@@ -11,13 +11,13 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
11
11
 
12
12
  import attrs
13
13
 
14
- from data_diff.errors import DataDiffMismatchingKeyTypesError
14
+ from data_diff.errors import DataDiffMismatchingKeyTypesError, DataDiffUnsupportedKeyValueError
15
15
  from data_diff.info_tree import InfoTree, SegmentInfo
16
16
  from data_diff.utils import dbt_diff_string_template, run_as_daemon, safezip, getLogger, truncate_error, Vector
17
17
  from data_diff.thread_utils import ThreadedYielder
18
18
  from data_diff.table_segment import TableSegment, create_mesh_from_points
19
19
  from data_diff.tracking import create_end_event_json, create_start_event_json, send_event_json, is_tracking_enabled
20
- from data_diff.abcs.database_types import IKey
20
+ from data_diff.abcs.database_types import IKey, ColType_Alphanum
21
21
 
22
22
  logger = getLogger(__name__)
23
23
 
@@ -309,7 +309,7 @@ class TableDiffer(ThreadBase, ABC):
309
309
  key_ranges = self._threaded_call_as_completed("query_key_range", [table1, table2])
310
310
 
311
311
  # Start with the first completed value, so we don't waste time waiting
312
- min_key1, max_key1 = self._parse_key_range_result(key_types1, next(key_ranges))
312
+ min_key1, max_key1 = self._parse_key_range_result(key_types1, next(key_ranges), table1.key_columns)
313
313
 
314
314
  btable1 = table1.new_key_bounds(min_key=min_key1, max_key=max_key1, key_types=key_types1)
315
315
  btable2 = table2.new_key_bounds(min_key=min_key1, max_key=max_key1, key_types=key_types2)
@@ -337,7 +337,7 @@ class TableDiffer(ThreadBase, ABC):
337
337
  # Overall, the max number of new regions in this 2nd pass is 3^|k| - 1
338
338
 
339
339
  # Note: python types can be the same, but the rendering parameters (e.g. casing) can differ.
340
- min_key2, max_key2 = self._parse_key_range_result(key_types2, next(key_ranges))
340
+ min_key2, max_key2 = self._parse_key_range_result(key_types2, next(key_ranges), table2.key_columns)
341
341
 
342
342
  points = [list(sorted(p)) for p in safezip(min_key1, min_key2, max_key1, max_key2)]
343
343
  box_mesh = create_mesh_from_points(*points)
@@ -351,18 +351,48 @@ class TableDiffer(ThreadBase, ABC):
351
351
 
352
352
  return ti
353
353
 
354
- def _parse_key_range_result(self, key_types, key_range) -> Tuple[Vector, Vector]:
354
+ def _parse_key_range_result(self, key_types, key_range, key_columns=None) -> Tuple[Vector, Vector]:
355
355
  min_key_values, max_key_values = key_range
356
356
 
357
357
  # We add 1 because our ranges are exclusive of the end (like in Python)
358
358
  try:
359
359
  min_key = Vector(key_type.make_value(mn) for key_type, mn in safezip(key_types, min_key_values))
360
360
  max_key = Vector(key_type.make_value(mx) + 1 for key_type, mx in safezip(key_types, max_key_values))
361
- except (TypeError, ValueError) as e:
361
+ except ValueError as e:
362
+ # A key value could not be mapped to a bisectable range. For an
363
+ # alphanumeric string key this means its min/max contain characters
364
+ # outside data-diff's bisection alphabet (e.g. '.', accents, other
365
+ # non-ASCII) — such keys can't be bisected safely, so we surface an
366
+ # actionable message. Other value errors (e.g. a malformed UUID) keep
367
+ # the original generic error.
368
+ raise self._key_range_error(key_types, min_key_values, max_key_values, key_columns) from e
369
+ except TypeError as e:
362
370
  raise type(e)(f"Cannot apply {key_types} to '{min_key_values}', '{max_key_values}'.") from e
363
371
 
364
372
  return min_key, max_key
365
373
 
374
+ @staticmethod
375
+ def _key_range_error(key_types, min_key_values, max_key_values, key_columns) -> ValueError:
376
+ names = list(key_columns) if key_columns else [f"#{i}" for i in range(len(key_types))]
377
+ offenders = []
378
+ for name, key_type, mn, mx in safezip(names, key_types, min_key_values, max_key_values):
379
+ if not isinstance(key_type, ColType_Alphanum):
380
+ continue # only alphanum keys get the tailored, actionable guidance
381
+ for bound, value in (("min", mn), ("max", mx)):
382
+ try:
383
+ key_type.make_value(value)
384
+ except ValueError as inner:
385
+ offenders.append(f"column '{name}' ({bound}={value!r}): {inner}")
386
+ if offenders:
387
+ return DataDiffUnsupportedKeyValueError(
388
+ f"Cannot bisect on key {'; '.join(offenders)}. data-diff can only bisect string keys built "
389
+ "from spaces, hyphens, underscores, digits and unaccented ASCII letters (a-z, A-Z); keys "
390
+ "containing other characters (e.g. '.', accented or non-ASCII characters) cannot be mapped to "
391
+ "a comparable range. Use a numeric or UUID key column for this diff instead."
392
+ )
393
+ # Non-alphanum failure (e.g. a malformed UUID): preserve the original error.
394
+ return ValueError(f"Cannot apply {key_types} to '{min_key_values}', '{max_key_values}'.")
395
+
366
396
  def _bisect_and_diff_segments(
367
397
  self,
368
398
  ti: ThreadedYielder,
@@ -72,3 +72,9 @@ class DataDiffSimpleSelectNotFound(Exception):
72
72
 
73
73
  class DataDiffMismatchingKeyTypesError(Exception):
74
74
  "Raised when the key types of two tables do not match, like VARCHAR and INT."
75
+
76
+
77
+ # Subclasses ValueError because it replaces the ValueError previously raised from
78
+ # _parse_key_range_result — keeps the exception contract for existing callers.
79
+ class DataDiffUnsupportedKeyValueError(ValueError):
80
+ "Raised when a key column holds values that cannot be mapped to a bisectable range (e.g. text keys with '.', accents or other non-ASCII characters)."
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "collate-data-diff"
3
- version = "0.11.12"
3
+ version = "0.11.15"
4
4
  description = "Command-line tool and Python library to efficiently diff rows across two different databases."
5
5
  authors = ["Collate <info@getcollate.io>", "Datafold <data-diff@datafold.com>"]
6
6
  license = "MIT"