collate-data-diff 0.11.12__tar.gz → 0.11.14__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.
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/PKG-INFO +1 -1
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/snowflake.py +1 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/diff_tables.py +36 -6
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/errors.py +6 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/pyproject.toml +1 -1
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/LICENSE +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/README.md +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/__init__.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/__main__.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/abcs/__init__.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/abcs/compiler.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/abcs/database_types.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/cloud/__init__.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/cloud/data_source.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/cloud/datafold_api.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/config.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/__init__.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/_connect.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/base.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/bigquery.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/clickhouse.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/databricks.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/duckdb.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/mssql.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/mysql.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/oracle.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/postgresql.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/presto.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/redshift.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/trino.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/databases/vertica.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/dbt.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/dbt_config_validators.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/dbt_parser.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/format.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/hashdiff_tables.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/info_tree.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/joindiff_tables.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/lexicographic_space.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/parse_time.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/py.typed +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/queries/__init__.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/queries/api.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/queries/ast_classes.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/queries/base.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/queries/extras.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/query_utils.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/schema.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/table_segment.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/thread_utils.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/tracking.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/utils.py +0 -0
- {collate_data_diff-0.11.12 → collate_data_diff-0.11.14}/data_diff/version.py +0 -0
|
@@ -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
|
|
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.
|
|
3
|
+
version = "0.11.14"
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|