collate-data-diff 0.11.2__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 (54) hide show
  1. collate_data_diff-0.11.2.dist-info/LICENSE +18 -0
  2. collate_data_diff-0.11.2.dist-info/METADATA +77 -0
  3. collate_data_diff-0.11.2.dist-info/RECORD +54 -0
  4. collate_data_diff-0.11.2.dist-info/WHEEL +4 -0
  5. collate_data_diff-0.11.2.dist-info/entry_points.txt +3 -0
  6. data_diff/__init__.py +180 -0
  7. data_diff/__main__.py +618 -0
  8. data_diff/abcs/__init__.py +0 -0
  9. data_diff/abcs/compiler.py +13 -0
  10. data_diff/abcs/database_types.py +308 -0
  11. data_diff/cloud/__init__.py +2 -0
  12. data_diff/cloud/data_source.py +318 -0
  13. data_diff/cloud/datafold_api.py +304 -0
  14. data_diff/config.py +127 -0
  15. data_diff/databases/__init__.py +17 -0
  16. data_diff/databases/_connect.py +306 -0
  17. data_diff/databases/base.py +1291 -0
  18. data_diff/databases/bigquery.py +315 -0
  19. data_diff/databases/clickhouse.py +203 -0
  20. data_diff/databases/databricks.py +248 -0
  21. data_diff/databases/duckdb.py +192 -0
  22. data_diff/databases/mssql.py +229 -0
  23. data_diff/databases/mysql.py +159 -0
  24. data_diff/databases/oracle.py +195 -0
  25. data_diff/databases/postgresql.py +258 -0
  26. data_diff/databases/presto.py +197 -0
  27. data_diff/databases/redshift.py +217 -0
  28. data_diff/databases/snowflake.py +207 -0
  29. data_diff/databases/trino.py +50 -0
  30. data_diff/databases/vertica.py +160 -0
  31. data_diff/dbt.py +604 -0
  32. data_diff/dbt_config_validators.py +65 -0
  33. data_diff/dbt_parser.py +523 -0
  34. data_diff/diff_tables.py +416 -0
  35. data_diff/errors.py +74 -0
  36. data_diff/format.py +359 -0
  37. data_diff/hashdiff_tables.py +264 -0
  38. data_diff/info_tree.py +62 -0
  39. data_diff/joindiff_tables.py +399 -0
  40. data_diff/lexicographic_space.py +240 -0
  41. data_diff/parse_time.py +74 -0
  42. data_diff/py.typed +0 -0
  43. data_diff/queries/__init__.py +0 -0
  44. data_diff/queries/api.py +200 -0
  45. data_diff/queries/ast_classes.py +798 -0
  46. data_diff/queries/base.py +24 -0
  47. data_diff/queries/extras.py +29 -0
  48. data_diff/query_utils.py +56 -0
  49. data_diff/schema.py +52 -0
  50. data_diff/table_segment.py +286 -0
  51. data_diff/thread_utils.py +98 -0
  52. data_diff/tracking.py +237 -0
  53. data_diff/utils.py +625 -0
  54. data_diff/version.py +1 -0
@@ -0,0 +1,416 @@
1
+ """Provides classes for performing a table diff"""
2
+
3
+ import threading
4
+ import time
5
+ from abc import ABC, abstractmethod
6
+ from enum import Enum
7
+ from contextlib import contextmanager
8
+ from operator import methodcaller
9
+ from typing import Any, Dict, Set, List, Tuple, Iterator, Optional, Union
10
+ from concurrent.futures import ThreadPoolExecutor, as_completed
11
+
12
+ import attrs
13
+
14
+ from data_diff.errors import DataDiffMismatchingKeyTypesError
15
+ from data_diff.info_tree import InfoTree, SegmentInfo
16
+ from data_diff.utils import dbt_diff_string_template, run_as_daemon, safezip, getLogger, truncate_error, Vector
17
+ from data_diff.thread_utils import ThreadedYielder
18
+ from data_diff.table_segment import TableSegment, create_mesh_from_points
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
21
+
22
+ logger = getLogger(__name__)
23
+
24
+
25
+ class Algorithm(Enum):
26
+ AUTO = "auto"
27
+ JOINDIFF = "joindiff"
28
+ HASHDIFF = "hashdiff"
29
+
30
+
31
+ DiffResult = Iterator[Tuple[str, tuple]] # Iterator[Tuple[Literal["+", "-"], tuple]]
32
+ DiffResultList = Iterator[List[Tuple[str, tuple]]]
33
+
34
+
35
+ @attrs.define(frozen=False)
36
+ class ThreadBase:
37
+ "Provides utility methods for optional threading"
38
+
39
+ threaded: bool = True
40
+ max_threadpool_size: Optional[int] = 1
41
+
42
+ def _thread_map(self, func, iterable):
43
+ if not self.threaded:
44
+ return map(func, iterable)
45
+
46
+ with ThreadPoolExecutor(max_workers=self.max_threadpool_size) as task_pool:
47
+ return task_pool.map(func, iterable)
48
+
49
+ def _threaded_call(self, func, iterable):
50
+ "Calls a method for each object in iterable."
51
+ return list(self._thread_map(methodcaller(func), iterable))
52
+
53
+ def _thread_as_completed(self, func, iterable):
54
+ if not self.threaded:
55
+ yield from map(func, iterable)
56
+ return
57
+
58
+ with ThreadPoolExecutor(max_workers=self.max_threadpool_size) as task_pool:
59
+ futures = [task_pool.submit(func, item) for item in iterable]
60
+ for future in as_completed(futures):
61
+ yield future.result()
62
+
63
+ def _threaded_call_as_completed(self, func, iterable):
64
+ "Calls a method for each object in iterable. Returned in order of completion."
65
+ return self._thread_as_completed(methodcaller(func), iterable)
66
+
67
+ @contextmanager
68
+ def _run_in_background(self, *funcs):
69
+ with ThreadPoolExecutor(max_workers=self.max_threadpool_size) as task_pool:
70
+ futures = [task_pool.submit(f) for f in funcs if f is not None]
71
+ yield futures
72
+ for f in futures:
73
+ f.result()
74
+
75
+
76
+ @attrs.define(frozen=True)
77
+ class DiffStats:
78
+ diff_by_sign: Dict[str, int]
79
+ table1_count: int
80
+ table2_count: int
81
+ unchanged: int
82
+ diff_percent: float
83
+ extra_column_diffs: Optional[Dict[str, int]]
84
+
85
+
86
+ @attrs.define(frozen=True)
87
+ class DiffResultWrapper:
88
+ diff: iter # DiffResult
89
+ info_tree: InfoTree
90
+ stats: dict
91
+ result_list: list = attrs.field(factory=list)
92
+
93
+ def __iter__(self) -> Iterator[Any]:
94
+ yield from self.result_list
95
+ for i in self.diff:
96
+ self.result_list.append(i)
97
+ yield i
98
+
99
+ def _get_stats(self, is_dbt: bool = False) -> DiffStats:
100
+ list(self) # Consume the iterator into result_list, if we haven't already
101
+
102
+ key_columns = self.info_tree.info.tables[0].key_columns
103
+ len_key_columns = len(key_columns)
104
+ diff_by_key = {}
105
+ extra_column_diffs = None
106
+ if is_dbt:
107
+ extra_column_values_store = {}
108
+ extra_columns = self.info_tree.info.tables[0].extra_columns
109
+ extra_column_diffs = {k: 0 for k in extra_columns}
110
+
111
+ for sign, values in self.result_list:
112
+ k = values[:len_key_columns]
113
+ if is_dbt:
114
+ extra_column_values = values[len_key_columns:]
115
+ if k in diff_by_key:
116
+ assert sign != diff_by_key[k]
117
+ diff_by_key[k] = "!"
118
+ if is_dbt:
119
+ for i in range(0, len(extra_columns)):
120
+ if extra_column_values[i] != extra_column_values_store[k][i]:
121
+ extra_column_diffs[extra_columns[i]] += 1
122
+ else:
123
+ diff_by_key[k] = sign
124
+ if is_dbt:
125
+ extra_column_values_store[k] = extra_column_values
126
+
127
+ diff_by_sign = {k: 0 for k in "+-!"}
128
+ for sign in diff_by_key.values():
129
+ diff_by_sign[sign] += 1
130
+
131
+ table1_count = self.info_tree.info.rowcounts[1]
132
+ table2_count = self.info_tree.info.rowcounts[2]
133
+ unchanged = table1_count - diff_by_sign["-"] - diff_by_sign["!"]
134
+ diff_percent = 1 - unchanged / max(table1_count, table2_count)
135
+
136
+ return DiffStats(diff_by_sign, table1_count, table2_count, unchanged, diff_percent, extra_column_diffs)
137
+
138
+ def get_stats_string(self, is_dbt: bool = False):
139
+ diff_stats = self._get_stats(is_dbt)
140
+
141
+ total_rows_diff = diff_stats.table2_count - diff_stats.table1_count
142
+
143
+ if is_dbt:
144
+ string_output = dbt_diff_string_template(
145
+ total_rows_table1=diff_stats.table1_count,
146
+ total_rows_table2=diff_stats.table2_count,
147
+ total_rows_diff=total_rows_diff,
148
+ rows_added=diff_stats.diff_by_sign["+"],
149
+ rows_removed=diff_stats.diff_by_sign["-"],
150
+ rows_updated=diff_stats.diff_by_sign["!"],
151
+ rows_unchanged=diff_stats.unchanged,
152
+ extra_info_dict=diff_stats.extra_column_diffs,
153
+ extra_info_str="[u]Values Changed[/u]",
154
+ )
155
+
156
+ else:
157
+ string_output = ""
158
+ string_output += f"{diff_stats.table1_count} rows in table A\n"
159
+ string_output += f"{diff_stats.table2_count} rows in table B\n"
160
+ string_output += f"{diff_stats.diff_by_sign['-']} rows exclusive to table A (not present in B)\n"
161
+ string_output += f"{diff_stats.diff_by_sign['+']} rows exclusive to table B (not present in A)\n"
162
+ string_output += f"{diff_stats.diff_by_sign['!']} rows updated\n"
163
+ string_output += f"{diff_stats.unchanged} rows unchanged\n"
164
+ string_output += f"{100*diff_stats.diff_percent:.2f}% difference score\n"
165
+
166
+ if self.stats:
167
+ string_output += "\nExtra-Info:\n"
168
+ for k, v in sorted(self.stats.items()):
169
+ string_output += f" {k} = {v}\n"
170
+
171
+ return string_output
172
+
173
+ def get_stats_dict(self, is_dbt: bool = False):
174
+ diff_stats = self._get_stats(is_dbt)
175
+ json_output = {
176
+ "rows_A": diff_stats.table1_count,
177
+ "rows_B": diff_stats.table2_count,
178
+ "exclusive_A": diff_stats.diff_by_sign["-"],
179
+ "exclusive_B": diff_stats.diff_by_sign["+"],
180
+ "updated": diff_stats.diff_by_sign["!"],
181
+ "unchanged": diff_stats.unchanged,
182
+ "total": sum(diff_stats.diff_by_sign.values()),
183
+ "stats": self.stats,
184
+ }
185
+ json_output["values"] = diff_stats.extra_column_diffs or {}
186
+ return json_output
187
+
188
+
189
+ @attrs.define(frozen=False)
190
+ class TableDiffer(ThreadBase, ABC):
191
+ INFO_TREE_CLASS = InfoTree
192
+
193
+ bisection_factor = 32
194
+ stats: dict = {}
195
+
196
+ ignored_columns1: Set[str] = attrs.field(factory=set)
197
+ ignored_columns2: Set[str] = attrs.field(factory=set)
198
+ _ignored_columns_lock: threading.Lock = attrs.field(factory=threading.Lock, init=False)
199
+ yield_list: bool = False
200
+
201
+ def diff_tables(self, table1: TableSegment, table2: TableSegment, info_tree: InfoTree = None) -> DiffResultWrapper:
202
+ """Diff the given tables.
203
+
204
+ Parameters:
205
+ table1 (TableSegment): The "before" table to compare. Or: source table
206
+ table2 (TableSegment): The "after" table to compare. Or: target table
207
+
208
+ Returns:
209
+ An iterator that yield pair-tuples, representing the diff. Items can be either -
210
+ ('-', row) for items in table1 but not in table2.
211
+ ('+', row) for items in table2 but not in table1.
212
+ Where `row` is a tuple of values, corresponding to the diffed columns.
213
+ """
214
+ if info_tree is None:
215
+ segment_info = self.INFO_TREE_CLASS.SEGMENT_INFO_CLASS([table1, table2])
216
+ info_tree = self.INFO_TREE_CLASS(segment_info)
217
+ return DiffResultWrapper(self._diff_tables_wrapper(table1, table2, info_tree), info_tree, self.stats)
218
+
219
+ def _diff_tables_wrapper(self, table1: TableSegment, table2: TableSegment, info_tree: InfoTree) -> DiffResult:
220
+ if is_tracking_enabled():
221
+ options = attrs.asdict(self, recurse=False)
222
+ # not a useful event attribute
223
+ options.pop("_ignored_columns_lock")
224
+ options["differ_name"] = type(self).__name__
225
+ event_json = create_start_event_json(options)
226
+ run_as_daemon(send_event_json, event_json)
227
+
228
+ if table1.database.dialect.PREVENT_OVERFLOW_WHEN_CONCAT or table2.database.dialect.PREVENT_OVERFLOW_WHEN_CONCAT:
229
+ table1.database.dialect.enable_preventing_type_overflow()
230
+ table2.database.dialect.enable_preventing_type_overflow()
231
+
232
+ start = time.monotonic()
233
+ error = None
234
+ try:
235
+ # Query and validate schema
236
+ table1, table2 = self._threaded_call("with_schema", [table1, table2])
237
+ self._validate_and_adjust_columns(table1, table2)
238
+
239
+ yield from self._diff_tables_root(table1, table2, info_tree)
240
+
241
+ except BaseException as e: # Catch KeyboardInterrupt too
242
+ error = e
243
+ finally:
244
+ info_tree.aggregate_info()
245
+
246
+ if is_tracking_enabled():
247
+ runtime = time.monotonic() - start
248
+ rowcounts = info_tree.info.rowcounts
249
+ table1_count = rowcounts[1] if rowcounts else None
250
+ table2_count = rowcounts[2] if rowcounts else None
251
+ diff_count = info_tree.info.diff_count
252
+ err_message = truncate_error(repr(error))
253
+ event_json = create_end_event_json(
254
+ error is None,
255
+ runtime,
256
+ table1.database.name,
257
+ table2.database.name,
258
+ table1_count,
259
+ table2_count,
260
+ diff_count,
261
+ err_message,
262
+ )
263
+ send_event_json(event_json)
264
+
265
+ if error:
266
+ raise error
267
+
268
+ def _validate_and_adjust_columns(self, table1: TableSegment, table2: TableSegment) -> None:
269
+ pass
270
+
271
+ def _diff_tables_root(
272
+ self, table1: TableSegment, table2: TableSegment, info_tree: InfoTree
273
+ ) -> Union[DiffResult, DiffResultList]:
274
+ return self._bisect_and_diff_tables(table1, table2, info_tree)
275
+
276
+ @abstractmethod
277
+ def _diff_segments(
278
+ self,
279
+ ti: ThreadedYielder,
280
+ table1: TableSegment,
281
+ table2: TableSegment,
282
+ info_tree: InfoTree,
283
+ max_rows: int,
284
+ level=0,
285
+ segment_index=None,
286
+ segment_count=None,
287
+ ): ...
288
+
289
+ def _bisect_and_diff_tables(self, table1: TableSegment, table2: TableSegment, info_tree):
290
+ if len(table1.key_columns) != len(table2.key_columns):
291
+ raise ValueError("Tables should have an equivalent number of key columns!")
292
+
293
+ key_types1 = [table1._schema[i] for i in table1.key_columns]
294
+ key_types2 = [table2._schema[i] for i in table2.key_columns]
295
+
296
+ for kt in key_types1 + key_types2:
297
+ if not isinstance(kt, IKey):
298
+ raise NotImplementedError(f"Cannot use a column of type {kt} as a key")
299
+
300
+ for i, (kt1, kt2) in enumerate(safezip(key_types1, key_types2)):
301
+ if kt1.python_type is not kt2.python_type:
302
+ k1 = table1.key_columns[i]
303
+ k2 = table2.key_columns[i]
304
+ raise DataDiffMismatchingKeyTypesError(
305
+ f"Key columns {k1} and {k2} can't be compared due to different types."
306
+ )
307
+
308
+ # Query min/max values
309
+ key_ranges = self._threaded_call_as_completed("query_key_range", [table1, table2])
310
+
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))
313
+
314
+ btable1 = table1.new_key_bounds(min_key=min_key1, max_key=max_key1, key_types=key_types1)
315
+ btable2 = table2.new_key_bounds(min_key=min_key1, max_key=max_key1, key_types=key_types2)
316
+
317
+ logger.info(
318
+ f"Diffing segments at key-range: {btable1.min_key}..{btable2.max_key}. "
319
+ f"size: table1 <= {btable1.approximate_size()}, table2 <= {btable2.approximate_size()}"
320
+ )
321
+
322
+ ti = ThreadedYielder(self.max_threadpool_size, self.yield_list)
323
+ # Bisect (split) the table into segments, and diff them recursively.
324
+ ti.submit(self._bisect_and_diff_segments, ti, btable1, btable2, info_tree, priority=999)
325
+
326
+ # Now we check for the second min-max, to diff the portions we "missed".
327
+ # This is achieved by subtracting the table ranges, and dividing the resulting space into aligned boxes.
328
+ # For example, given tables A & B, and a 2D compound key, where A was queried first for key-range,
329
+ # the regions of B we need to diff in this second pass are marked by B1..8:
330
+ # ┌──┬──────┬──┐
331
+ # │B1│ B2 │B3│
332
+ # ├──┼──────┼──┤
333
+ # │B4│ A │B5│
334
+ # ├──┼──────┼──┤
335
+ # │B6│ B7 │B8│
336
+ # └──┴──────┴──┘
337
+ # Overall, the max number of new regions in this 2nd pass is 3^|k| - 1
338
+
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))
341
+
342
+ points = [list(sorted(p)) for p in safezip(min_key1, min_key2, max_key1, max_key2)]
343
+ box_mesh = create_mesh_from_points(*points)
344
+
345
+ new_regions = [(p1, p2) for p1, p2 in box_mesh if p1 < p2 and not (p1 >= min_key1 and p2 <= max_key1)]
346
+
347
+ for p1, p2 in new_regions:
348
+ extra_table1 = table1.new_key_bounds(min_key=p1, max_key=p2, key_types=key_types1)
349
+ extra_table2 = table2.new_key_bounds(min_key=p1, max_key=p2, key_types=key_types2)
350
+ ti.submit(self._bisect_and_diff_segments, ti, extra_table1, extra_table2, info_tree, priority=999)
351
+
352
+ return ti
353
+
354
+ def _parse_key_range_result(self, key_types, key_range) -> Tuple[Vector, Vector]:
355
+ min_key_values, max_key_values = key_range
356
+
357
+ # We add 1 because our ranges are exclusive of the end (like in Python)
358
+ try:
359
+ min_key = Vector(key_type.make_value(mn) for key_type, mn in safezip(key_types, min_key_values))
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:
362
+ raise type(e)(f"Cannot apply {key_types} to '{min_key_values}', '{max_key_values}'.") from e
363
+
364
+ return min_key, max_key
365
+
366
+ def _bisect_and_diff_segments(
367
+ self,
368
+ ti: ThreadedYielder,
369
+ table1: TableSegment,
370
+ table2: TableSegment,
371
+ info_tree: InfoTree,
372
+ level=0,
373
+ max_rows=None,
374
+ ):
375
+ assert table1.is_bounded and table2.is_bounded
376
+
377
+ # Choose evenly spaced checkpoints (according to min_key and max_key)
378
+ biggest_table = max(table1, table2, key=methodcaller("approximate_size"))
379
+ checkpoints = biggest_table.choose_checkpoints(self.bisection_factor - 1)
380
+
381
+ # Get it thread-safe, to avoid segment misalignment because of bad timing.
382
+ with self._ignored_columns_lock:
383
+ table1 = attrs.evolve(table1, ignored_columns=frozenset(self.ignored_columns1))
384
+ table2 = attrs.evolve(table2, ignored_columns=frozenset(self.ignored_columns2))
385
+
386
+ # Create new instances of TableSegment between each checkpoint
387
+ segmented1 = table1.segment_by_checkpoints(checkpoints)
388
+ segmented2 = table2.segment_by_checkpoints(checkpoints)
389
+
390
+ # Recursively compare each pair of corresponding segments between table1 and table2
391
+ for i, (t1, t2) in enumerate(safezip(segmented1, segmented2)):
392
+ info_node = info_tree.add_node(t1, t2, max_rows=max_rows)
393
+ ti.submit(
394
+ self._diff_segments, ti, t1, t2, info_node, max_rows, level + 1, i + 1, len(segmented1), priority=level
395
+ )
396
+
397
+ def ignore_column(self, column_name1: str, column_name2: str) -> None:
398
+ """
399
+ Ignore the column (by name on sides A & B) in md5s & diffs from now on.
400
+
401
+ This affects 2 places:
402
+
403
+ - The columns are not checksumed for new(!) segments.
404
+ - The columns are ignored in in-memory diffing for running segments.
405
+
406
+ The columns are never ignored in the fetched values, whether they are
407
+ the same or different — for data consistency.
408
+
409
+ Use this feature to collect relatively well-represented differences
410
+ across all columns if one of them is highly different in the beginning
411
+ of a table (as per the order of segmentation/bisection). Otherwise,
412
+ that one column might easily hit the limit and stop the whole diff.
413
+ """
414
+ with self._ignored_columns_lock:
415
+ self.ignored_columns1.add(column_name1)
416
+ self.ignored_columns2.add(column_name2)
data_diff/errors.py ADDED
@@ -0,0 +1,74 @@
1
+ class DataDiffDbtProjectVarsNotFoundError(Exception):
2
+ "Raised when an expected dbt_project.yml section is missing."
3
+
4
+
5
+ class DataDiffDbtProfileNotFoundError(Exception):
6
+ "Raised when an expected profiles.yml section is missing."
7
+
8
+
9
+ class DataDiffDbtNoSuccessfulModelsInRunError(Exception):
10
+ "Raised when there are no successful model runs in the run_results.json"
11
+
12
+
13
+ class DataDiffDbtRunResultsVersionError(Exception):
14
+ "Raised when the dbt version in run_results.json is lower than the minimum version."
15
+
16
+
17
+ class DataDiffDbtSelectNoMatchingModelsError(Exception):
18
+ "Raised when the `--select` flag returns no models."
19
+
20
+
21
+ class DataDiffDbtSelectUnexpectedError(Exception):
22
+ "Catch all for unexpected dbt list --select results."
23
+
24
+
25
+ class DataDiffDbtSnowflakeSetConnectionError(Exception):
26
+ "Raised when a dbt snowflake profile has unexpected values."
27
+
28
+
29
+ class DataDiffDbtBigQueryUnsupportedMethodError(Exception):
30
+ "Raised when trying to use an unsupported connection with BigQuery."
31
+
32
+
33
+ class DataDiffDbtRedshiftPasswordOnlyError(Exception):
34
+ "Raised when using a non-password connection method with Redshift."
35
+
36
+
37
+ class DataDiffDbtConnectionNotImplementedError(Exception):
38
+ "Raised when trying to use an unsupported dbt connection method."
39
+
40
+
41
+ class DataDiffDbtCoreNoRunnerError(Exception):
42
+ "Raised when the manifest version >= 1.5, but the dbt-core package is < 1.5. This is an edge case most likely to occur in development."
43
+
44
+
45
+ class DataDiffCustomSchemaNoConfigError(Exception):
46
+ "Raised when a model has a custom schema, but there is no prod_custom_schema config. (And not using --state)."
47
+
48
+
49
+ class DataDiffNoAPIKeyError(Exception):
50
+ "Raised when using --cloud but no API key is present in the DATAFOLD_API_KEY env var or keyring"
51
+
52
+
53
+ class DataDiffNoDatasourceIdError(Exception):
54
+ "Raised when using --cloud but no datasource_id was found in dbt_project.yml"
55
+
56
+
57
+ class DataDiffDatasourceIdNotFoundError(Exception):
58
+ "Raised when using --cloud but the datasource_id is not found for a particular org."
59
+
60
+
61
+ class DataDiffCloudDiffFailed(Exception):
62
+ "Raised when using --cloud and the remote diff fails."
63
+
64
+
65
+ class DataDiffCloudDiffTimedOut(Exception):
66
+ "Raised when using --cloud and the diff did not return finish before the timeout value."
67
+
68
+
69
+ class DataDiffSimpleSelectNotFound(Exception):
70
+ "Raised when using --select on dbt < 1.5 and a model node is not found in the manifest."
71
+
72
+
73
+ class DataDiffMismatchingKeyTypesError(Exception):
74
+ "Raised when the key types of two tables do not match, like VARCHAR and INT."