collate-data-diff 0.11.9__tar.gz → 0.11.11__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.9 → collate_data_diff-0.11.11}/PKG-INFO +6 -6
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/__init__.py +1 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/__main__.py +1 -3
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/cloud/datafold_api.py +9 -9
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/base.py +2 -2
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/bigquery.py +2 -4
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/databricks.py +1 -1
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/duckdb.py +15 -11
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/mysql.py +4 -2
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/oracle.py +2 -2
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/postgresql.py +4 -4
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/presto.py +21 -10
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/redshift.py +1 -1
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/trino.py +26 -14
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/vertica.py +12 -3
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/dbt.py +4 -3
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/dbt_config_validators.py +10 -10
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/dbt_parser.py +2 -2
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/diff_tables.py +1 -1
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/parse_time.py +1 -1
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/schema.py +1 -1
- collate_data_diff-0.11.11/data_diff/version.py +1 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/pyproject.toml +8 -8
- collate_data_diff-0.11.9/data_diff/version.py +0 -1
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/LICENSE +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/README.md +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/abcs/__init__.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/abcs/compiler.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/abcs/database_types.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/cloud/__init__.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/cloud/data_source.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/config.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/__init__.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/_connect.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/clickhouse.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/mssql.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/databases/snowflake.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/errors.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/format.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/hashdiff_tables.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/info_tree.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/joindiff_tables.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/lexicographic_space.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/py.typed +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/queries/__init__.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/queries/api.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/queries/ast_classes.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/queries/base.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/queries/extras.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/query_utils.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/table_segment.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/thread_utils.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/tracking.py +0 -0
- {collate_data_diff-0.11.9 → collate_data_diff-0.11.11}/data_diff/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: collate-data-diff
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.11
|
|
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,9 +37,9 @@ Provides-Extra: snowflake
|
|
|
37
37
|
Provides-Extra: trino
|
|
38
38
|
Provides-Extra: vertica
|
|
39
39
|
Requires-Dist: attrs (>=23.1.0)
|
|
40
|
-
Requires-Dist: click (>=8.
|
|
40
|
+
Requires-Dist: click (>=8.3.3)
|
|
41
41
|
Requires-Dist: clickhouse-driver ; extra == "clickhouse" or extra == "all-dbs"
|
|
42
|
-
Requires-Dist: cryptography ; extra == "snowflake" or extra == "all-dbs"
|
|
42
|
+
Requires-Dist: cryptography (>=46.0.5) ; extra == "snowflake" or extra == "all-dbs"
|
|
43
43
|
Requires-Dist: dbt-core (>=1.0.0,<2.0.0)
|
|
44
44
|
Requires-Dist: deepdiff (>=8.6.1,<9.0)
|
|
45
45
|
Requires-Dist: dsnparse (<0.2.0)
|
|
@@ -52,15 +52,15 @@ Requires-Dist: oracledb ; extra == "oracle" or extra == "all-dbs"
|
|
|
52
52
|
Requires-Dist: preql (>=0.2.19) ; extra == "preql" or extra == "all-dbs"
|
|
53
53
|
Requires-Dist: presto-python-client ; extra == "presto" or extra == "all-dbs"
|
|
54
54
|
Requires-Dist: psycopg2 ; extra == "postgresql" or extra == "redshift" or extra == "all-dbs"
|
|
55
|
-
Requires-Dist: pydantic (>=
|
|
55
|
+
Requires-Dist: pydantic (>=2.0,<3)
|
|
56
56
|
Requires-Dist: pyodbc (>=4.0.39) ; extra == "mssql" or extra == "all-dbs"
|
|
57
57
|
Requires-Dist: rich
|
|
58
|
-
Requires-Dist: snowflake-connector-python (>=
|
|
58
|
+
Requires-Dist: snowflake-connector-python (>=4.4.0,<5.0.0) ; (python_version >= "3.10") and (extra == "snowflake" or extra == "all-dbs")
|
|
59
59
|
Requires-Dist: tabulate (>=0.9.0)
|
|
60
60
|
Requires-Dist: toml (>=0.10.2)
|
|
61
61
|
Requires-Dist: trino (>=0.314.0) ; extra == "trino" or extra == "all-dbs"
|
|
62
62
|
Requires-Dist: typing-extensions (>=4.0.1)
|
|
63
|
-
Requires-Dist: urllib3 (
|
|
63
|
+
Requires-Dist: urllib3 (>=2.7.0,<3)
|
|
64
64
|
Requires-Dist: vertica-python ; extra == "vertica" or extra == "all-dbs"
|
|
65
65
|
Project-URL: Repository, https://github.com/datafold/data-diff
|
|
66
66
|
Description-Content-Type: text/markdown
|
|
@@ -253,14 +253,12 @@ click.Context.formatter_class = MyHelpFormatter
|
|
|
253
253
|
)
|
|
254
254
|
@click.option(
|
|
255
255
|
"--select",
|
|
256
|
-
"-s",
|
|
257
256
|
default=None,
|
|
258
257
|
metavar="SELECTION or MODEL_NAME",
|
|
259
258
|
help="--select dbt resources to compare using dbt selection syntax in dbt versions >= 1.5.\nIn versions < 1.5, it will naively search for a model with MODEL_NAME as the name.",
|
|
260
259
|
)
|
|
261
260
|
@click.option(
|
|
262
261
|
"--state",
|
|
263
|
-
"-s",
|
|
264
262
|
default=None,
|
|
265
263
|
metavar="PATH",
|
|
266
264
|
help="Specify manifest to utilize for 'prod' comparison paths instead of using configuration.",
|
|
@@ -611,7 +609,7 @@ def _data_diff(
|
|
|
611
609
|
_print_result(stats, json_output, diff_iter)
|
|
612
610
|
|
|
613
611
|
end = time.monotonic()
|
|
614
|
-
logging.info(f"Duration: {end-start:.2f} seconds.")
|
|
612
|
+
logging.info(f"Duration: {end - start:.2f} seconds.")
|
|
615
613
|
|
|
616
614
|
|
|
617
615
|
if __name__ == "__main__":
|
|
@@ -103,8 +103,8 @@ class TCloudApiDataDiff(pydantic.BaseModel):
|
|
|
103
103
|
pk_columns: List[str]
|
|
104
104
|
filter1: Optional[str] = None
|
|
105
105
|
filter2: Optional[str] = None
|
|
106
|
-
include_columns: Optional[List[str]]
|
|
107
|
-
exclude_columns: Optional[List[str]]
|
|
106
|
+
include_columns: Optional[List[str]] = None
|
|
107
|
+
exclude_columns: Optional[List[str]] = None
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
class TCloudApiOrgMeta(pydantic.BaseModel):
|
|
@@ -150,10 +150,10 @@ class TSummaryResultDependencyDetails(pydantic.BaseModel):
|
|
|
150
150
|
|
|
151
151
|
class TCloudApiDataDiffSummaryResult(pydantic.BaseModel):
|
|
152
152
|
status: str
|
|
153
|
-
pks: Optional[TSummaryResultPrimaryKeyStats]
|
|
154
|
-
values: Optional[TSummaryResultValueStats]
|
|
155
|
-
schema_: Optional[TSummaryResultSchemaStats]
|
|
156
|
-
deps: Optional[TSummaryResultDependencyDetails]
|
|
153
|
+
pks: Optional[TSummaryResultPrimaryKeyStats] = None
|
|
154
|
+
values: Optional[TSummaryResultValueStats] = None
|
|
155
|
+
schema_: Optional[TSummaryResultSchemaStats] = None
|
|
156
|
+
deps: Optional[TSummaryResultDependencyDetails] = None
|
|
157
157
|
|
|
158
158
|
@classmethod
|
|
159
159
|
def from_orm(cls, obj: Any) -> Self:
|
|
@@ -179,7 +179,7 @@ class TCloudDataSourceTestResult(pydantic.BaseModel):
|
|
|
179
179
|
class TCloudApiDataSourceTestResult(pydantic.BaseModel):
|
|
180
180
|
name: str
|
|
181
181
|
status: str
|
|
182
|
-
result: Optional[TCloudDataSourceTestResult]
|
|
182
|
+
result: Optional[TCloudDataSourceTestResult] = None
|
|
183
183
|
|
|
184
184
|
|
|
185
185
|
@attrs.define(frozen=False)
|
|
@@ -223,7 +223,7 @@ class DatafoldAPI:
|
|
|
223
223
|
return TCloudApiDataSource(**datasource)
|
|
224
224
|
|
|
225
225
|
def create_data_source(self, config: TDsConfig) -> TCloudApiDataSource:
|
|
226
|
-
payload = config.
|
|
226
|
+
payload = config.model_dump()
|
|
227
227
|
if config.type == "bigquery":
|
|
228
228
|
json_string = payload["options"]["jsonKeyFile"].encode("utf-8")
|
|
229
229
|
payload["options"]["jsonKeyFile"] = base64.b64encode(json_string).decode("utf-8")
|
|
@@ -245,7 +245,7 @@ class DatafoldAPI:
|
|
|
245
245
|
]
|
|
246
246
|
|
|
247
247
|
def create_data_diff(self, payload: TCloudApiDataDiff) -> int:
|
|
248
|
-
rv = self.make_post_request(url="api/v1/datadiffs", payload=payload.
|
|
248
|
+
rv = self.make_post_request(url="api/v1/datadiffs", payload=payload.model_dump())
|
|
249
249
|
return rv.json()["id"]
|
|
250
250
|
|
|
251
251
|
def poll_data_diff_results(self, diff_id: int) -> TCloudApiDataDiffSummaryResult:
|
|
@@ -137,7 +137,7 @@ class Compiler(AbstractCompiler):
|
|
|
137
137
|
|
|
138
138
|
def new_unique_table_name(self, prefix="tmp") -> DbPath:
|
|
139
139
|
self._counter[0] += 1
|
|
140
|
-
table_name = f"{prefix}{self._counter[0]}_{'%x'%random.randrange(2**32)}"
|
|
140
|
+
table_name = f"{prefix}{self._counter[0]}_{'%x' % random.randrange(2**32)}"
|
|
141
141
|
return self.database.dialect.parse_table_name(table_name)
|
|
142
142
|
|
|
143
143
|
def add_table_context(self, *tables: Sequence, **kw) -> Self:
|
|
@@ -706,7 +706,7 @@ class BaseDialect(abc.ABC):
|
|
|
706
706
|
return f"'{v.uuid}'"
|
|
707
707
|
elif isinstance(v, decimal.Decimal):
|
|
708
708
|
return str(v)
|
|
709
|
-
elif isinstance(v, bytearray):
|
|
709
|
+
elif isinstance(v, (bytes, bytearray)):
|
|
710
710
|
return f"'{v.decode()}'"
|
|
711
711
|
elif isinstance(v, Code):
|
|
712
712
|
return v.code
|
|
@@ -158,7 +158,7 @@ class Dialect(BaseDialect):
|
|
|
158
158
|
return tuple(i for i in path if i is not None)
|
|
159
159
|
|
|
160
160
|
def md5_as_int(self, s: str) -> str:
|
|
161
|
-
return f"cast(cast( ('0x' || substr(TO_HEX(md5({s})), {1+MD5_HEXDIGITS-CHECKSUM_HEXDIGITS})) as int64) as numeric) - {CHECKSUM_OFFSET}"
|
|
161
|
+
return f"cast(cast( ('0x' || substr(TO_HEX(md5({s})), {1 + MD5_HEXDIGITS - CHECKSUM_HEXDIGITS})) as int64) as numeric) - {CHECKSUM_OFFSET}"
|
|
162
162
|
|
|
163
163
|
def md5_as_hex(self, s: str) -> str:
|
|
164
164
|
return f"md5({s})"
|
|
@@ -189,9 +189,7 @@ class Dialect(BaseDialect):
|
|
|
189
189
|
return f"FORMAT_TIMESTAMP('%F %H:%M:%E6S', {value})"
|
|
190
190
|
|
|
191
191
|
timestamp6 = f"FORMAT_TIMESTAMP('%F %H:%M:%E6S', {value})"
|
|
192
|
-
return (
|
|
193
|
-
f"RPAD(LEFT({timestamp6}, {TIMESTAMP_PRECISION_POS+coltype.precision}), {TIMESTAMP_PRECISION_POS+6}, '0')"
|
|
194
|
-
)
|
|
192
|
+
return f"RPAD(LEFT({timestamp6}, {TIMESTAMP_PRECISION_POS + coltype.precision}), {TIMESTAMP_PRECISION_POS + 6}, '0')"
|
|
195
193
|
|
|
196
194
|
def normalize_number(self, value: str, coltype: FractionalType) -> str:
|
|
197
195
|
return f"format('%.{coltype.precision}f', {value})"
|
|
@@ -85,7 +85,7 @@ class Dialect(BaseDialect):
|
|
|
85
85
|
return tuple(i for i in path if i is not None)
|
|
86
86
|
|
|
87
87
|
def md5_as_int(self, s: str) -> str:
|
|
88
|
-
return f"cast(conv(substr(md5({s}), {1+MD5_HEXDIGITS-CHECKSUM_HEXDIGITS}), 16, 10) as decimal(38, 0)) - {CHECKSUM_OFFSET}"
|
|
88
|
+
return f"cast(conv(substr(md5({s}), {1 + MD5_HEXDIGITS - CHECKSUM_HEXDIGITS}), 16, 10) as decimal(38, 0)) - {CHECKSUM_OFFSET}"
|
|
89
89
|
|
|
90
90
|
def md5_as_hex(self, s: str) -> str:
|
|
91
91
|
return f"md5({s})"
|
|
@@ -97,7 +97,7 @@ class Dialect(BaseDialect):
|
|
|
97
97
|
return "current_timestamp"
|
|
98
98
|
|
|
99
99
|
def md5_as_int(self, s: str) -> str:
|
|
100
|
-
return f"('0x' || SUBSTRING(md5({s}), {1+MD5_HEXDIGITS-CHECKSUM_HEXDIGITS},{CHECKSUM_HEXDIGITS}))::BIGINT - {CHECKSUM_OFFSET}"
|
|
100
|
+
return f"('0x' || SUBSTRING(md5({s}), {1 + MD5_HEXDIGITS - CHECKSUM_HEXDIGITS},{CHECKSUM_HEXDIGITS}))::BIGINT - {CHECKSUM_OFFSET}"
|
|
101
101
|
|
|
102
102
|
def md5_as_hex(self, s: str) -> str:
|
|
103
103
|
return f"md5({s})"
|
|
@@ -105,9 +105,9 @@ class Dialect(BaseDialect):
|
|
|
105
105
|
def normalize_timestamp(self, value: str, coltype: TemporalType) -> str:
|
|
106
106
|
# It's precision 6 by default. If precision is less than 6 -> we remove the trailing numbers.
|
|
107
107
|
if coltype.rounds and coltype.precision > 0:
|
|
108
|
-
return f"CONCAT(SUBSTRING(STRFTIME({value}::TIMESTAMP, '%Y-%m-%d %H:%M:%S.'),1,23), LPAD(((ROUND(strftime({value}::timestamp, '%f')::DECIMAL(15,7)/100000,{coltype.precision-1})*100000)::INT)::VARCHAR,6,'0'))"
|
|
108
|
+
return f"CONCAT(SUBSTRING(STRFTIME({value}::TIMESTAMP, '%Y-%m-%d %H:%M:%S.'),1,23), LPAD(((ROUND(strftime({value}::timestamp, '%f')::DECIMAL(15,7)/100000,{coltype.precision - 1})*100000)::INT)::VARCHAR,6,'0'))"
|
|
109
109
|
|
|
110
|
-
return f"rpad(substring(strftime({value}::timestamp, '%Y-%m-%d %H:%M:%S.%f'),1,{TIMESTAMP_PRECISION_POS+coltype.precision}),26,'0')"
|
|
110
|
+
return f"rpad(substring(strftime({value}::timestamp, '%Y-%m-%d %H:%M:%S.%f'),1,{TIMESTAMP_PRECISION_POS + coltype.precision}),26,'0')"
|
|
111
111
|
|
|
112
112
|
def normalize_number(self, value: str, coltype: FractionalType) -> str:
|
|
113
113
|
return self.to_string(f"{value}::DECIMAL(38, {coltype.precision})")
|
|
@@ -115,6 +115,11 @@ class Dialect(BaseDialect):
|
|
|
115
115
|
def normalize_boolean(self, value: str, _coltype: Boolean) -> str:
|
|
116
116
|
return self.to_string(f"{value}::INTEGER")
|
|
117
117
|
|
|
118
|
+
def type_repr(self, t) -> str:
|
|
119
|
+
if isinstance(t, TimestampTZ):
|
|
120
|
+
return "TIMESTAMP WITH TIME ZONE"
|
|
121
|
+
return super().type_repr(t)
|
|
122
|
+
|
|
118
123
|
|
|
119
124
|
@attrs.define(frozen=False, init=False, kw_only=True)
|
|
120
125
|
class DuckDB(Database):
|
|
@@ -166,16 +171,15 @@ class DuckDB(Database):
|
|
|
166
171
|
def select_table_schema(self, path: DbPath) -> str:
|
|
167
172
|
database, schema, table = self._normalize_table_path(path)
|
|
168
173
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
else:
|
|
175
|
-
dynamic_database_clause = "current_catalog()"
|
|
174
|
+
# Newer DuckDB rejects the <catalog>.information_schema.columns three
|
|
175
|
+
# part form that older versions accepted. Query the unqualified
|
|
176
|
+
# information_schema.columns view which already returns rows for every
|
|
177
|
+
# attached catalog and scope it with table_catalog.
|
|
178
|
+
dynamic_database_clause = f"'{database}'" if database else "current_catalog()"
|
|
176
179
|
|
|
177
180
|
return (
|
|
178
|
-
|
|
181
|
+
"SELECT column_name, data_type, datetime_precision, numeric_precision, numeric_scale "
|
|
182
|
+
"FROM information_schema.columns "
|
|
179
183
|
f"WHERE table_name = '{table}' AND table_schema = '{schema}' and table_catalog = {dynamic_database_clause}"
|
|
180
184
|
)
|
|
181
185
|
|
|
@@ -100,7 +100,7 @@ class Dialect(BaseDialect):
|
|
|
100
100
|
return "SET @@session.time_zone='+00:00'"
|
|
101
101
|
|
|
102
102
|
def md5_as_int(self, s: str) -> str:
|
|
103
|
-
return f"conv(substring(md5({s}), {1+MD5_HEXDIGITS-CHECKSUM_HEXDIGITS}), 16, 10) - {CHECKSUM_OFFSET}"
|
|
103
|
+
return f"conv(substring(md5({s}), {1 + MD5_HEXDIGITS - CHECKSUM_HEXDIGITS}), 16, 10) - {CHECKSUM_OFFSET}"
|
|
104
104
|
|
|
105
105
|
def md5_as_hex(self, s: str) -> str:
|
|
106
106
|
return f"md5({s})"
|
|
@@ -110,7 +110,9 @@ class Dialect(BaseDialect):
|
|
|
110
110
|
return self.to_string(f"cast( cast({value} as datetime({coltype.precision})) as datetime(6))")
|
|
111
111
|
|
|
112
112
|
s = self.to_string(f"cast({value} as datetime(6))")
|
|
113
|
-
return
|
|
113
|
+
return (
|
|
114
|
+
f"RPAD(RPAD({s}, {TIMESTAMP_PRECISION_POS + coltype.precision}, '.'), {TIMESTAMP_PRECISION_POS + 6}, '0')"
|
|
115
|
+
)
|
|
114
116
|
|
|
115
117
|
def normalize_number(self, value: str, coltype: FractionalType) -> str:
|
|
116
118
|
return self.to_string(f"cast({value} as decimal(38, {coltype.precision}))")
|
|
@@ -128,7 +128,7 @@ class Dialect(
|
|
|
128
128
|
def md5_as_int(self, s: str) -> str:
|
|
129
129
|
# standard_hash is faster than DBMS_CRYPTO.Hash
|
|
130
130
|
# TODO: Find a way to use UTL_RAW.CAST_TO_BINARY_INTEGER ?
|
|
131
|
-
return f"to_number(substr(standard_hash({s}, 'MD5'), {1+MD5_HEXDIGITS-CHECKSUM_HEXDIGITS}), 'xxxxxxxxxxxxxxx') - {CHECKSUM_OFFSET}"
|
|
131
|
+
return f"to_number(substr(standard_hash({s}, 'MD5'), {1 + MD5_HEXDIGITS - CHECKSUM_HEXDIGITS}), 'xxxxxxxxxxxxxxx') - {CHECKSUM_OFFSET}"
|
|
132
132
|
|
|
133
133
|
def md5_as_hex(self, s: str) -> str:
|
|
134
134
|
return f"standard_hash({s}, 'MD5')"
|
|
@@ -145,7 +145,7 @@ class Dialect(
|
|
|
145
145
|
truncated = f"to_char({value}, 'YYYY-MM-DD HH24:MI:SS.FF{coltype.precision}')"
|
|
146
146
|
else:
|
|
147
147
|
truncated = f"to_char({value}, 'YYYY-MM-DD HH24:MI:SS.')"
|
|
148
|
-
return f"RPAD({truncated}, {TIMESTAMP_PRECISION_POS+6}, '0')"
|
|
148
|
+
return f"RPAD({truncated}, {TIMESTAMP_PRECISION_POS + 6}, '0')"
|
|
149
149
|
|
|
150
150
|
def normalize_number(self, value: str, coltype: FractionalType) -> str:
|
|
151
151
|
# FM999.9990
|
|
@@ -105,14 +105,14 @@ class PostgresqlDialect(BaseDialect):
|
|
|
105
105
|
return super().type_repr(t)
|
|
106
106
|
|
|
107
107
|
def md5_as_int(self, s: str) -> str:
|
|
108
|
-
return f"('x' || substring(md5({s}), {1+MD5_HEXDIGITS-CHECKSUM_HEXDIGITS}))::bit({_CHECKSUM_BITSIZE})::bigint - {CHECKSUM_OFFSET}"
|
|
108
|
+
return f"('x' || substring(md5({s}), {1 + MD5_HEXDIGITS - CHECKSUM_HEXDIGITS}))::bit({_CHECKSUM_BITSIZE})::bigint - {CHECKSUM_OFFSET}"
|
|
109
109
|
|
|
110
110
|
def md5_as_hex(self, s: str) -> str:
|
|
111
111
|
return f"md5({s})"
|
|
112
112
|
|
|
113
113
|
def normalize_timestamp(self, value: str, coltype: TemporalType) -> str:
|
|
114
114
|
def _add_padding(coltype: TemporalType, timestamp6: str):
|
|
115
|
-
return f"RPAD(LEFT({timestamp6}, {TIMESTAMP_PRECISION_POS+coltype.precision}), {TIMESTAMP_PRECISION_POS+6}, '0')"
|
|
115
|
+
return f"RPAD(LEFT({timestamp6}, {TIMESTAMP_PRECISION_POS + coltype.precision}), {TIMESTAMP_PRECISION_POS + 6}, '0')"
|
|
116
116
|
|
|
117
117
|
try:
|
|
118
118
|
is_date = coltype.is_date
|
|
@@ -143,7 +143,7 @@ class PostgresqlDialect(BaseDialect):
|
|
|
143
143
|
timestamp = f"least('{max_timestamp}'::timestamp(6), {value}::timestamp(6))"
|
|
144
144
|
timestamp = f"greatest('{min_timestamp}'::timestamp(6), {timestamp})"
|
|
145
145
|
|
|
146
|
-
interval = format((0.5 * (10 ** (-coltype.precision))), f".{coltype.precision+1}f")
|
|
146
|
+
interval = format((0.5 * (10 ** (-coltype.precision))), f".{coltype.precision + 1}f")
|
|
147
147
|
|
|
148
148
|
rounded_timestamp = (
|
|
149
149
|
f"left(to_char(least('{max_timestamp}'::timestamp, {timestamp})"
|
|
@@ -223,7 +223,7 @@ class PostgreSQL(ThreadedDatabase):
|
|
|
223
223
|
THEN coalesce(numeric_scale, {self.dialect.DEFAULT_NUMERIC_PRECISION})
|
|
224
224
|
ELSE numeric_scale
|
|
225
225
|
END AS numeric_scale
|
|
226
|
-
FROM {
|
|
226
|
+
FROM {".".join(info_schema_path)}
|
|
227
227
|
WHERE table_name = '{table}' AND table_schema = '{schema}'
|
|
228
228
|
"""
|
|
229
229
|
|
|
@@ -113,13 +113,18 @@ class Dialect(BaseDialect):
|
|
|
113
113
|
return super().parse_type(table_path, info)
|
|
114
114
|
|
|
115
115
|
def set_timezone_to_utc(self) -> str:
|
|
116
|
-
|
|
116
|
+
# PrestoDB has no SQL command to change the session timezone and no
|
|
117
|
+
# timezone session property. The session timezone is fixed at connect
|
|
118
|
+
# time. Raise so the connection setup logs and moves on, matching the
|
|
119
|
+
# base class pattern for databases without session timezone support.
|
|
120
|
+
# The Trino dialect overrides this since Trino does have SET TIME ZONE.
|
|
121
|
+
raise NotImplementedError("No support for session tz.")
|
|
117
122
|
|
|
118
123
|
def current_timestamp(self) -> str:
|
|
119
124
|
return "current_timestamp"
|
|
120
125
|
|
|
121
126
|
def md5_as_int(self, s: str) -> str:
|
|
122
|
-
return f"cast(from_base(substr(to_hex(md5(to_utf8({s}))), {1+MD5_HEXDIGITS-CHECKSUM_HEXDIGITS}), 16) as decimal(38, 0)) - {CHECKSUM_OFFSET}"
|
|
127
|
+
return f"cast(from_base(substr(to_hex(md5(to_utf8({s}))), {1 + MD5_HEXDIGITS - CHECKSUM_HEXDIGITS}), 16) as decimal(38, 0)) - {CHECKSUM_OFFSET}"
|
|
123
128
|
|
|
124
129
|
def md5_as_hex(self, s: str) -> str:
|
|
125
130
|
return f"to_hex(md5(to_utf8({s})))"
|
|
@@ -129,13 +134,15 @@ class Dialect(BaseDialect):
|
|
|
129
134
|
return f"TRIM(CAST({value} AS VARCHAR))"
|
|
130
135
|
|
|
131
136
|
def normalize_timestamp(self, value: str, coltype: TemporalType) -> str:
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return
|
|
137
|
+
# PrestoDB has no parameterized TIMESTAMP(p) type, only plain TIMESTAMP
|
|
138
|
+
# which is fixed at millisecond precision. Casting to TIMESTAMP(6) here
|
|
139
|
+
# fails with "Unknown type: timestamp(6)" on real PrestoDB even though
|
|
140
|
+
# Trino accepts it. The Trino dialect provides its own precision aware
|
|
141
|
+
# override so this only affects PrestoDB.
|
|
142
|
+
s = f"date_format(cast({value} as timestamp), '%Y-%m-%d %H:%i:%S.%f')"
|
|
143
|
+
return (
|
|
144
|
+
f"RPAD(RPAD({s}, {TIMESTAMP_PRECISION_POS + coltype.precision}, '.'), {TIMESTAMP_PRECISION_POS + 6}, '0')"
|
|
145
|
+
)
|
|
139
146
|
|
|
140
147
|
def normalize_number(self, value: str, coltype: FractionalType) -> str:
|
|
141
148
|
return self.to_string(f"cast({value} as decimal(38,{coltype.precision}))")
|
|
@@ -197,4 +204,8 @@ class Presto(Database):
|
|
|
197
204
|
|
|
198
205
|
@property
|
|
199
206
|
def is_autocommit(self) -> bool:
|
|
200
|
-
|
|
207
|
+
# PrestoDB and Trino both run every statement in autocommit mode by
|
|
208
|
+
# default, with no implicit transaction. Emitting a bare COMMIT then
|
|
209
|
+
# fails with NOT_IN_TRANSACTION. Telling the base dialect that we are
|
|
210
|
+
# autocommit makes render_commit a no op so the COMMIT is never sent.
|
|
211
|
+
return True
|
|
@@ -50,7 +50,7 @@ class Dialect(PostgresqlDialect):
|
|
|
50
50
|
return super().type_repr(t)
|
|
51
51
|
|
|
52
52
|
def md5_as_int(self, s: str) -> str:
|
|
53
|
-
return f"strtol(substring(md5({s}), {1+MD5_HEXDIGITS-CHECKSUM_HEXDIGITS}), 16)::decimal(38) - {CHECKSUM_OFFSET}"
|
|
53
|
+
return f"strtol(substring(md5({s}), {1 + MD5_HEXDIGITS - CHECKSUM_HEXDIGITS}), 16)::decimal(38) - {CHECKSUM_OFFSET}"
|
|
54
54
|
|
|
55
55
|
def md5_as_hex(self, s: str) -> str:
|
|
56
56
|
return f"md5({s})"
|
|
@@ -3,7 +3,7 @@ from typing import Any, ClassVar, Type
|
|
|
3
3
|
import attrs
|
|
4
4
|
from requests import Session
|
|
5
5
|
|
|
6
|
-
from data_diff.abcs.database_types import TemporalType, ColType_UUID
|
|
6
|
+
from data_diff.abcs.database_types import TemporalType, TimestampTZ, ColType_UUID
|
|
7
7
|
from data_diff.databases import presto
|
|
8
8
|
from data_diff.databases.base import import_helper
|
|
9
9
|
from data_diff.databases.base import TIMESTAMP_PRECISION_POS, BaseDialect
|
|
@@ -19,12 +19,29 @@ def import_trino():
|
|
|
19
19
|
class Dialect(presto.Dialect):
|
|
20
20
|
name = "Trino"
|
|
21
21
|
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
def set_timezone_to_utc(self) -> str:
|
|
23
|
+
# Trino has its own statement form that PrestoDB does not accept.
|
|
24
|
+
return "SET TIME ZONE 'UTC'"
|
|
25
|
+
|
|
26
|
+
def type_repr(self, t) -> str:
|
|
27
|
+
# The base dialect emits TIMESTAMP(p) for TimestampTZ, which Trino
|
|
28
|
+
# interprets as TIMESTAMP WITHOUT TIME ZONE. That silently drops the
|
|
29
|
+
# offset on insert, so be explicit and emit TIMESTAMP(p) WITH TIME ZONE.
|
|
30
|
+
if isinstance(t, TimestampTZ):
|
|
31
|
+
return f"TIMESTAMP({t.precision}) WITH TIME ZONE"
|
|
32
|
+
return super().type_repr(t)
|
|
27
33
|
|
|
34
|
+
def normalize_timestamp(self, value: str, coltype: TemporalType) -> str:
|
|
35
|
+
# Trino's date_format truncates fractional seconds to millisecond
|
|
36
|
+
# precision and loses any trailing microseconds. Casting the value to
|
|
37
|
+
# TIMESTAMP(p) and then to VARCHAR keeps the full precision. For
|
|
38
|
+
# TIMESTAMP WITH TIME ZONE columns the cast would preserve the source
|
|
39
|
+
# offset instead of using the session timezone, so apply
|
|
40
|
+
# AT TIME ZONE 'UTC' first to normalize.
|
|
41
|
+
inner_prec = coltype.precision if coltype.rounds else 6
|
|
42
|
+
if isinstance(coltype, TimestampTZ):
|
|
43
|
+
value = f"({value}) AT TIME ZONE 'UTC'"
|
|
44
|
+
s = f"CAST(CAST({value} AS TIMESTAMP({inner_prec})) AS VARCHAR)"
|
|
28
45
|
return (
|
|
29
46
|
f"RPAD(RPAD({s}, {TIMESTAMP_PRECISION_POS + coltype.precision}, '.'), {TIMESTAMP_PRECISION_POS + 6}, '0')"
|
|
30
47
|
)
|
|
@@ -58,20 +75,15 @@ class Trino(presto.Presto):
|
|
|
58
75
|
|
|
59
76
|
if auth:
|
|
60
77
|
if auth.get("authType") == "basic":
|
|
61
|
-
kw["auth"] = trino.auth.BasicAuthentication(
|
|
62
|
-
auth.get("username"),
|
|
63
|
-
auth.get("password")
|
|
64
|
-
)
|
|
78
|
+
kw["auth"] = trino.auth.BasicAuthentication(auth.get("username"), auth.get("password"))
|
|
65
79
|
kw["http_scheme"] = "https"
|
|
66
80
|
|
|
67
81
|
elif auth.get("authType") == "jwt":
|
|
68
|
-
kw["auth"] = trino.auth.JWTAuthentication(
|
|
69
|
-
auth.get("jwt")
|
|
70
|
-
)
|
|
82
|
+
kw["auth"] = trino.auth.JWTAuthentication(auth.get("jwt"))
|
|
71
83
|
kw["http_scheme"] = "https"
|
|
72
84
|
|
|
73
85
|
elif auth.get("authType") == "oauth2":
|
|
74
86
|
kw["auth"] = trino.auth.OAuth2Authentication()
|
|
75
87
|
kw["http_scheme"] = "https"
|
|
76
|
-
|
|
88
|
+
|
|
77
89
|
self._conn = trino.dbapi.connect(**kw)
|
|
@@ -114,9 +114,7 @@ class Dialect(BaseDialect):
|
|
|
114
114
|
return f"TO_CHAR({value}::TIMESTAMP({coltype.precision}), 'YYYY-MM-DD HH24:MI:SS.US')"
|
|
115
115
|
|
|
116
116
|
timestamp6 = f"TO_CHAR({value}::TIMESTAMP(6), 'YYYY-MM-DD HH24:MI:SS.US')"
|
|
117
|
-
return (
|
|
118
|
-
f"RPAD(LEFT({timestamp6}, {TIMESTAMP_PRECISION_POS+coltype.precision}), {TIMESTAMP_PRECISION_POS+6}, '0')"
|
|
119
|
-
)
|
|
117
|
+
return f"RPAD(LEFT({timestamp6}, {TIMESTAMP_PRECISION_POS + coltype.precision}), {TIMESTAMP_PRECISION_POS + 6}, '0')"
|
|
120
118
|
|
|
121
119
|
def normalize_number(self, value: str, coltype: FractionalType) -> str:
|
|
122
120
|
return self.to_string(f"CAST({value} AS NUMERIC(38, {coltype.precision}))")
|
|
@@ -128,6 +126,17 @@ class Dialect(BaseDialect):
|
|
|
128
126
|
def normalize_boolean(self, value: str, _coltype: Boolean) -> str:
|
|
129
127
|
return self.to_string(f"cast ({value} as int)")
|
|
130
128
|
|
|
129
|
+
def timestamp_value(self, t) -> str:
|
|
130
|
+
# In the UNION ALL SELECT path emitted by constant_values, Vertica
|
|
131
|
+
# refuses to implicitly cast a varchar to TIMESTAMP. Emit a typed
|
|
132
|
+
# literal so the value is unambiguously a TIMESTAMP everywhere.
|
|
133
|
+
return f"TIMESTAMP '{t.isoformat()}'"
|
|
134
|
+
|
|
135
|
+
def constant_values(self, rows) -> str:
|
|
136
|
+
# Vertica before 11.0 does not accept multi-row INSERT ... VALUES (a), (b), (c).
|
|
137
|
+
# Emit UNION ALL SELECT instead, which is accepted by every Vertica version.
|
|
138
|
+
return " UNION ALL ".join("SELECT " + ", ".join(self._constant_value(v) for v in row) for row in rows)
|
|
139
|
+
|
|
131
140
|
|
|
132
141
|
@attrs.define(frozen=False, init=False, kw_only=True)
|
|
133
142
|
class Vertica(ThreadedDatabase):
|
|
@@ -116,9 +116,10 @@ def dbt_diff(
|
|
|
116
116
|
|
|
117
117
|
futures = {}
|
|
118
118
|
|
|
119
|
-
with
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
with (
|
|
120
|
+
log_status_handler.status if log_status_handler else nullcontext(),
|
|
121
|
+
ThreadPoolExecutor(max_workers=dbt_parser.threads) as executor,
|
|
122
|
+
):
|
|
122
123
|
for model in models:
|
|
123
124
|
if log_status_handler:
|
|
124
125
|
log_status_handler.set_prefix(f"Diffing {model.alias} \n")
|
|
@@ -5,13 +5,13 @@ from pydantic import BaseModel, Field
|
|
|
5
5
|
|
|
6
6
|
class ManifestJsonConfig(BaseModel):
|
|
7
7
|
class Metadata(BaseModel):
|
|
8
|
-
dbt_version: str = Field(...,
|
|
9
|
-
project_id: Optional[str]
|
|
10
|
-
user_id: Optional[str]
|
|
8
|
+
dbt_version: str = Field(..., pattern=r"^\d+\.\d+\.\d+([a-zA-Z0-9]+)?$")
|
|
9
|
+
project_id: Optional[str] = None
|
|
10
|
+
user_id: Optional[str] = None
|
|
11
11
|
|
|
12
12
|
class Nodes(BaseModel):
|
|
13
|
-
class
|
|
14
|
-
database: Optional[str]
|
|
13
|
+
class NodeConfig(BaseModel):
|
|
14
|
+
database: Optional[str] = None
|
|
15
15
|
schema_: Optional[str] = Field(..., alias="schema")
|
|
16
16
|
tags: List[str]
|
|
17
17
|
|
|
@@ -31,13 +31,13 @@ class ManifestJsonConfig(BaseModel):
|
|
|
31
31
|
resource_type: str
|
|
32
32
|
name: str
|
|
33
33
|
alias: str
|
|
34
|
-
database: Optional[str]
|
|
34
|
+
database: Optional[str] = None
|
|
35
35
|
schema_: str = Field(..., alias="schema")
|
|
36
|
-
columns: Optional[Dict[str, Column]]
|
|
36
|
+
columns: Optional[Dict[str, Column]] = None
|
|
37
37
|
meta: Dict[str, Any]
|
|
38
|
-
config:
|
|
38
|
+
config: NodeConfig
|
|
39
39
|
tags: List[str]
|
|
40
|
-
test_metadata: Optional[TestMetadata]
|
|
40
|
+
test_metadata: Optional[TestMetadata] = None
|
|
41
41
|
depends_on: DependsOn
|
|
42
42
|
|
|
43
43
|
metadata: Metadata
|
|
@@ -46,7 +46,7 @@ class ManifestJsonConfig(BaseModel):
|
|
|
46
46
|
|
|
47
47
|
class RunResultsJsonConfig(BaseModel):
|
|
48
48
|
class Metadata(BaseModel):
|
|
49
|
-
dbt_version: str = Field(...,
|
|
49
|
+
dbt_version: str = Field(..., pattern=r"^\d+\.\d+\.\d+([a-zA-Z0-9]+)?$")
|
|
50
50
|
|
|
51
51
|
class Results(BaseModel):
|
|
52
52
|
class Status(Enum):
|
|
@@ -252,7 +252,7 @@ class DbtParser:
|
|
|
252
252
|
with open(self.project_dir / RUN_RESULTS_PATH) as run_results:
|
|
253
253
|
logger.info(f"Parsing file {RUN_RESULTS_PATH}")
|
|
254
254
|
run_results_dict = json.load(run_results)
|
|
255
|
-
run_results_validated = RunResultsJsonConfig.
|
|
255
|
+
run_results_validated = RunResultsJsonConfig.model_validate(run_results_dict)
|
|
256
256
|
|
|
257
257
|
dbt_version = parse_version(run_results_validated.metadata.dbt_version)
|
|
258
258
|
|
|
@@ -279,7 +279,7 @@ class DbtParser:
|
|
|
279
279
|
with open(path) as manifest:
|
|
280
280
|
logger.info(f"Parsing file {path}")
|
|
281
281
|
manifest_dict = json.load(manifest)
|
|
282
|
-
manifest_obj = ManifestJsonConfig.
|
|
282
|
+
manifest_obj = ManifestJsonConfig.model_validate(manifest_dict)
|
|
283
283
|
return manifest_obj
|
|
284
284
|
|
|
285
285
|
def get_project_dict(self):
|
|
@@ -161,7 +161,7 @@ class DiffResultWrapper:
|
|
|
161
161
|
string_output += f"{diff_stats.diff_by_sign['+']} rows exclusive to table B (not present in A)\n"
|
|
162
162
|
string_output += f"{diff_stats.diff_by_sign['!']} rows updated\n"
|
|
163
163
|
string_output += f"{diff_stats.unchanged} rows unchanged\n"
|
|
164
|
-
string_output += f"{100*diff_stats.diff_percent:.2f}% difference score\n"
|
|
164
|
+
string_output += f"{100 * diff_stats.diff_percent:.2f}% difference score\n"
|
|
165
165
|
|
|
166
166
|
if self.stats:
|
|
167
167
|
string_output += "\nExtra-Info:\n"
|
|
@@ -44,7 +44,7 @@ def parse_time_atom(count, unit):
|
|
|
44
44
|
except KeyError:
|
|
45
45
|
most_similar = max(TIME_UNITS, key=lambda k: string_similarity(k, unit))
|
|
46
46
|
raise ParseError(
|
|
47
|
-
f"'{unit}' is not a recognized time unit. Did you mean '{most_similar}'
|
|
47
|
+
f"'{unit}' is not a recognized time unit. Did you mean '{most_similar}'?\nSupported units: {UNITS_STR}"
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
if unit in EXTRAPOLATED:
|
|
@@ -47,6 +47,6 @@ def create_schema(db_name: str, table_path: DbPath, schema: dict, case_sensitive
|
|
|
47
47
|
return CaseSensitiveDict(schema)
|
|
48
48
|
|
|
49
49
|
if len({k.lower() for k in schema}) < len(schema):
|
|
50
|
-
logger.warning(f
|
|
50
|
+
logger.warning(f"Ambiguous schema for {db_name}:{'.'.join(table_path)} | Columns = {', '.join(list(schema))}")
|
|
51
51
|
logger.warning("We recommend to disable case-insensitivity (set --case-sensitive).")
|
|
52
52
|
return CaseInsensitiveDict(schema)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.11.11"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "collate-data-diff"
|
|
3
|
-
version = "0.11.
|
|
3
|
+
version = "0.11.11"
|
|
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"
|
|
@@ -21,10 +21,10 @@ classifiers = [
|
|
|
21
21
|
]
|
|
22
22
|
packages = [{ include = "data_diff" }]
|
|
23
23
|
[tool.poetry.dependencies]
|
|
24
|
-
pydantic = ">=
|
|
24
|
+
pydantic = ">=2.0,<3"
|
|
25
25
|
python = ">=3.10,<4.0"
|
|
26
26
|
dsnparse = "<0.2.0"
|
|
27
|
-
click = ">=8.
|
|
27
|
+
click = ">=8.3.3" # Security fix for CVE-2026-7246
|
|
28
28
|
rich = "*"
|
|
29
29
|
toml = ">=0.10.2"
|
|
30
30
|
mysql-connector-python = [
|
|
@@ -33,9 +33,9 @@ mysql-connector-python = [
|
|
|
33
33
|
]
|
|
34
34
|
psycopg2 = {version="*", optional=true}
|
|
35
35
|
snowflake-connector-python = [
|
|
36
|
-
{version = ">=
|
|
36
|
+
{version = ">=4.4.0,<5.0.0", python = ">=3.10", optional=true}
|
|
37
37
|
]
|
|
38
|
-
cryptography = {version="
|
|
38
|
+
cryptography = {version=">=46.0.5", optional=true} # Security fix for CVE-2026-26007
|
|
39
39
|
trino = {version=">=0.314.0", optional=true}
|
|
40
40
|
presto-python-client = {version="*", optional=true}
|
|
41
41
|
clickhouse-driver = {version="*", optional=true}
|
|
@@ -46,7 +46,7 @@ keyring = "*"
|
|
|
46
46
|
tabulate = ">=0.9.0"
|
|
47
47
|
preql = {version=">=0.2.19", optional=true}
|
|
48
48
|
vertica-python = {version="*", optional=true}
|
|
49
|
-
urllib3 = "
|
|
49
|
+
urllib3 = ">=2.7.0,<3" # Security fix for CVE-2026-44431, CVE-2026-21441, CVE-2025-66418, CVE-2025-66471
|
|
50
50
|
oracledb = {version = "*", optional=true}
|
|
51
51
|
pyodbc = {version=">=4.0.39", optional=true}
|
|
52
52
|
typing-extensions = ">=4.0.1"
|
|
@@ -84,9 +84,9 @@ mysql-connector-python = [
|
|
|
84
84
|
]
|
|
85
85
|
psycopg2 = "*"
|
|
86
86
|
snowflake-connector-python = [
|
|
87
|
-
{version=">=
|
|
87
|
+
{version=">=4.4.0,<5.0.0", python = ">=3.10"}
|
|
88
88
|
]
|
|
89
|
-
cryptography = "
|
|
89
|
+
cryptography = ">=46.0.5" # Security fix for CVE-2026-26007
|
|
90
90
|
trino = ">=0.314.0"
|
|
91
91
|
presto-python-client = ">=0.8.3"
|
|
92
92
|
clickhouse-driver = "*"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.11.1"
|
|
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
|