dvt-core 0.59.0a51__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 (299) hide show
  1. dbt/__init__.py +7 -0
  2. dbt/_pydantic_shim.py +26 -0
  3. dbt/artifacts/__init__.py +0 -0
  4. dbt/artifacts/exceptions/__init__.py +1 -0
  5. dbt/artifacts/exceptions/schemas.py +31 -0
  6. dbt/artifacts/resources/__init__.py +116 -0
  7. dbt/artifacts/resources/base.py +67 -0
  8. dbt/artifacts/resources/types.py +93 -0
  9. dbt/artifacts/resources/v1/analysis.py +10 -0
  10. dbt/artifacts/resources/v1/catalog.py +23 -0
  11. dbt/artifacts/resources/v1/components.py +274 -0
  12. dbt/artifacts/resources/v1/config.py +277 -0
  13. dbt/artifacts/resources/v1/documentation.py +11 -0
  14. dbt/artifacts/resources/v1/exposure.py +51 -0
  15. dbt/artifacts/resources/v1/function.py +52 -0
  16. dbt/artifacts/resources/v1/generic_test.py +31 -0
  17. dbt/artifacts/resources/v1/group.py +21 -0
  18. dbt/artifacts/resources/v1/hook.py +11 -0
  19. dbt/artifacts/resources/v1/macro.py +29 -0
  20. dbt/artifacts/resources/v1/metric.py +172 -0
  21. dbt/artifacts/resources/v1/model.py +145 -0
  22. dbt/artifacts/resources/v1/owner.py +10 -0
  23. dbt/artifacts/resources/v1/saved_query.py +111 -0
  24. dbt/artifacts/resources/v1/seed.py +41 -0
  25. dbt/artifacts/resources/v1/semantic_layer_components.py +72 -0
  26. dbt/artifacts/resources/v1/semantic_model.py +314 -0
  27. dbt/artifacts/resources/v1/singular_test.py +14 -0
  28. dbt/artifacts/resources/v1/snapshot.py +91 -0
  29. dbt/artifacts/resources/v1/source_definition.py +84 -0
  30. dbt/artifacts/resources/v1/sql_operation.py +10 -0
  31. dbt/artifacts/resources/v1/unit_test_definition.py +77 -0
  32. dbt/artifacts/schemas/__init__.py +0 -0
  33. dbt/artifacts/schemas/base.py +191 -0
  34. dbt/artifacts/schemas/batch_results.py +24 -0
  35. dbt/artifacts/schemas/catalog/__init__.py +11 -0
  36. dbt/artifacts/schemas/catalog/v1/__init__.py +0 -0
  37. dbt/artifacts/schemas/catalog/v1/catalog.py +59 -0
  38. dbt/artifacts/schemas/freshness/__init__.py +1 -0
  39. dbt/artifacts/schemas/freshness/v3/__init__.py +0 -0
  40. dbt/artifacts/schemas/freshness/v3/freshness.py +158 -0
  41. dbt/artifacts/schemas/manifest/__init__.py +2 -0
  42. dbt/artifacts/schemas/manifest/v12/__init__.py +0 -0
  43. dbt/artifacts/schemas/manifest/v12/manifest.py +211 -0
  44. dbt/artifacts/schemas/results.py +147 -0
  45. dbt/artifacts/schemas/run/__init__.py +2 -0
  46. dbt/artifacts/schemas/run/v5/__init__.py +0 -0
  47. dbt/artifacts/schemas/run/v5/run.py +184 -0
  48. dbt/artifacts/schemas/upgrades/__init__.py +4 -0
  49. dbt/artifacts/schemas/upgrades/upgrade_manifest.py +174 -0
  50. dbt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py +2 -0
  51. dbt/artifacts/utils/validation.py +153 -0
  52. dbt/cli/__init__.py +1 -0
  53. dbt/cli/context.py +17 -0
  54. dbt/cli/exceptions.py +57 -0
  55. dbt/cli/flags.py +560 -0
  56. dbt/cli/main.py +2660 -0
  57. dbt/cli/option_types.py +121 -0
  58. dbt/cli/options.py +80 -0
  59. dbt/cli/params.py +844 -0
  60. dbt/cli/requires.py +490 -0
  61. dbt/cli/resolvers.py +60 -0
  62. dbt/cli/types.py +40 -0
  63. dbt/clients/__init__.py +0 -0
  64. dbt/clients/checked_load.py +83 -0
  65. dbt/clients/git.py +164 -0
  66. dbt/clients/jinja.py +206 -0
  67. dbt/clients/jinja_static.py +245 -0
  68. dbt/clients/registry.py +192 -0
  69. dbt/clients/yaml_helper.py +68 -0
  70. dbt/compilation.py +876 -0
  71. dbt/compute/__init__.py +14 -0
  72. dbt/compute/engines/__init__.py +12 -0
  73. dbt/compute/engines/spark_engine.py +642 -0
  74. dbt/compute/federated_executor.py +1080 -0
  75. dbt/compute/filter_pushdown.py +273 -0
  76. dbt/compute/jar_provisioning.py +273 -0
  77. dbt/compute/java_compat.py +689 -0
  78. dbt/compute/jdbc_utils.py +1252 -0
  79. dbt/compute/metadata/__init__.py +63 -0
  80. dbt/compute/metadata/adapters_registry.py +370 -0
  81. dbt/compute/metadata/catalog_store.py +1036 -0
  82. dbt/compute/metadata/registry.py +674 -0
  83. dbt/compute/metadata/store.py +1020 -0
  84. dbt/compute/smart_selector.py +377 -0
  85. dbt/compute/spark_logger.py +272 -0
  86. dbt/compute/strategies/__init__.py +55 -0
  87. dbt/compute/strategies/base.py +165 -0
  88. dbt/compute/strategies/dataproc.py +207 -0
  89. dbt/compute/strategies/emr.py +203 -0
  90. dbt/compute/strategies/local.py +472 -0
  91. dbt/compute/strategies/standalone.py +262 -0
  92. dbt/config/__init__.py +4 -0
  93. dbt/config/catalogs.py +94 -0
  94. dbt/config/compute.py +513 -0
  95. dbt/config/dvt_profile.py +408 -0
  96. dbt/config/profile.py +422 -0
  97. dbt/config/project.py +888 -0
  98. dbt/config/project_utils.py +48 -0
  99. dbt/config/renderer.py +231 -0
  100. dbt/config/runtime.py +564 -0
  101. dbt/config/selectors.py +208 -0
  102. dbt/config/utils.py +77 -0
  103. dbt/constants.py +28 -0
  104. dbt/context/__init__.py +0 -0
  105. dbt/context/base.py +745 -0
  106. dbt/context/configured.py +135 -0
  107. dbt/context/context_config.py +382 -0
  108. dbt/context/docs.py +82 -0
  109. dbt/context/exceptions_jinja.py +178 -0
  110. dbt/context/macro_resolver.py +195 -0
  111. dbt/context/macros.py +171 -0
  112. dbt/context/manifest.py +72 -0
  113. dbt/context/providers.py +2249 -0
  114. dbt/context/query_header.py +13 -0
  115. dbt/context/secret.py +58 -0
  116. dbt/context/target.py +74 -0
  117. dbt/contracts/__init__.py +0 -0
  118. dbt/contracts/files.py +413 -0
  119. dbt/contracts/graph/__init__.py +0 -0
  120. dbt/contracts/graph/manifest.py +1904 -0
  121. dbt/contracts/graph/metrics.py +97 -0
  122. dbt/contracts/graph/model_config.py +70 -0
  123. dbt/contracts/graph/node_args.py +42 -0
  124. dbt/contracts/graph/nodes.py +1806 -0
  125. dbt/contracts/graph/semantic_manifest.py +232 -0
  126. dbt/contracts/graph/unparsed.py +811 -0
  127. dbt/contracts/project.py +419 -0
  128. dbt/contracts/results.py +53 -0
  129. dbt/contracts/selection.py +23 -0
  130. dbt/contracts/sql.py +85 -0
  131. dbt/contracts/state.py +68 -0
  132. dbt/contracts/util.py +46 -0
  133. dbt/deprecations.py +348 -0
  134. dbt/deps/__init__.py +0 -0
  135. dbt/deps/base.py +152 -0
  136. dbt/deps/git.py +195 -0
  137. dbt/deps/local.py +79 -0
  138. dbt/deps/registry.py +130 -0
  139. dbt/deps/resolver.py +149 -0
  140. dbt/deps/tarball.py +120 -0
  141. dbt/docs/source/_ext/dbt_click.py +119 -0
  142. dbt/docs/source/conf.py +32 -0
  143. dbt/env_vars.py +64 -0
  144. dbt/event_time/event_time.py +40 -0
  145. dbt/event_time/sample_window.py +60 -0
  146. dbt/events/__init__.py +15 -0
  147. dbt/events/base_types.py +36 -0
  148. dbt/events/core_types_pb2.py +2 -0
  149. dbt/events/logging.py +108 -0
  150. dbt/events/types.py +2516 -0
  151. dbt/exceptions.py +1486 -0
  152. dbt/flags.py +89 -0
  153. dbt/graph/__init__.py +11 -0
  154. dbt/graph/cli.py +249 -0
  155. dbt/graph/graph.py +172 -0
  156. dbt/graph/queue.py +214 -0
  157. dbt/graph/selector.py +374 -0
  158. dbt/graph/selector_methods.py +975 -0
  159. dbt/graph/selector_spec.py +222 -0
  160. dbt/graph/thread_pool.py +18 -0
  161. dbt/hooks.py +21 -0
  162. dbt/include/README.md +49 -0
  163. dbt/include/__init__.py +3 -0
  164. dbt/include/data/adapters_registry.duckdb +0 -0
  165. dbt/include/data/build_comprehensive_registry.py +1254 -0
  166. dbt/include/data/build_registry.py +242 -0
  167. dbt/include/data/csv/adapter_queries.csv +33 -0
  168. dbt/include/data/csv/syntax_rules.csv +9 -0
  169. dbt/include/data/csv/type_mappings_bigquery.csv +28 -0
  170. dbt/include/data/csv/type_mappings_databricks.csv +30 -0
  171. dbt/include/data/csv/type_mappings_mysql.csv +40 -0
  172. dbt/include/data/csv/type_mappings_oracle.csv +30 -0
  173. dbt/include/data/csv/type_mappings_postgres.csv +56 -0
  174. dbt/include/data/csv/type_mappings_redshift.csv +33 -0
  175. dbt/include/data/csv/type_mappings_snowflake.csv +38 -0
  176. dbt/include/data/csv/type_mappings_sqlserver.csv +35 -0
  177. dbt/include/dvt_starter_project/README.md +15 -0
  178. dbt/include/dvt_starter_project/__init__.py +3 -0
  179. dbt/include/dvt_starter_project/analyses/PLACEHOLDER +0 -0
  180. dbt/include/dvt_starter_project/dvt_project.yml +39 -0
  181. dbt/include/dvt_starter_project/logs/PLACEHOLDER +0 -0
  182. dbt/include/dvt_starter_project/macros/PLACEHOLDER +0 -0
  183. dbt/include/dvt_starter_project/models/example/my_first_dbt_model.sql +27 -0
  184. dbt/include/dvt_starter_project/models/example/my_second_dbt_model.sql +6 -0
  185. dbt/include/dvt_starter_project/models/example/schema.yml +21 -0
  186. dbt/include/dvt_starter_project/seeds/PLACEHOLDER +0 -0
  187. dbt/include/dvt_starter_project/snapshots/PLACEHOLDER +0 -0
  188. dbt/include/dvt_starter_project/tests/PLACEHOLDER +0 -0
  189. dbt/internal_deprecations.py +26 -0
  190. dbt/jsonschemas/__init__.py +3 -0
  191. dbt/jsonschemas/jsonschemas.py +309 -0
  192. dbt/jsonschemas/project/0.0.110.json +4717 -0
  193. dbt/jsonschemas/project/0.0.85.json +2015 -0
  194. dbt/jsonschemas/resources/0.0.110.json +2636 -0
  195. dbt/jsonschemas/resources/0.0.85.json +2536 -0
  196. dbt/jsonschemas/resources/latest.json +6773 -0
  197. dbt/links.py +4 -0
  198. dbt/materializations/__init__.py +0 -0
  199. dbt/materializations/incremental/__init__.py +0 -0
  200. dbt/materializations/incremental/microbatch.py +236 -0
  201. dbt/mp_context.py +8 -0
  202. dbt/node_types.py +37 -0
  203. dbt/parser/__init__.py +23 -0
  204. dbt/parser/analysis.py +21 -0
  205. dbt/parser/base.py +548 -0
  206. dbt/parser/common.py +266 -0
  207. dbt/parser/docs.py +52 -0
  208. dbt/parser/fixtures.py +51 -0
  209. dbt/parser/functions.py +30 -0
  210. dbt/parser/generic_test.py +100 -0
  211. dbt/parser/generic_test_builders.py +333 -0
  212. dbt/parser/hooks.py +122 -0
  213. dbt/parser/macros.py +137 -0
  214. dbt/parser/manifest.py +2208 -0
  215. dbt/parser/models.py +573 -0
  216. dbt/parser/partial.py +1178 -0
  217. dbt/parser/read_files.py +445 -0
  218. dbt/parser/schema_generic_tests.py +422 -0
  219. dbt/parser/schema_renderer.py +111 -0
  220. dbt/parser/schema_yaml_readers.py +935 -0
  221. dbt/parser/schemas.py +1466 -0
  222. dbt/parser/search.py +149 -0
  223. dbt/parser/seeds.py +28 -0
  224. dbt/parser/singular_test.py +20 -0
  225. dbt/parser/snapshots.py +44 -0
  226. dbt/parser/sources.py +558 -0
  227. dbt/parser/sql.py +62 -0
  228. dbt/parser/unit_tests.py +621 -0
  229. dbt/plugins/__init__.py +20 -0
  230. dbt/plugins/contracts.py +9 -0
  231. dbt/plugins/exceptions.py +2 -0
  232. dbt/plugins/manager.py +163 -0
  233. dbt/plugins/manifest.py +21 -0
  234. dbt/profiler.py +20 -0
  235. dbt/py.typed +1 -0
  236. dbt/query_analyzer.py +410 -0
  237. dbt/runners/__init__.py +2 -0
  238. dbt/runners/exposure_runner.py +7 -0
  239. dbt/runners/no_op_runner.py +45 -0
  240. dbt/runners/saved_query_runner.py +7 -0
  241. dbt/selected_resources.py +8 -0
  242. dbt/task/__init__.py +0 -0
  243. dbt/task/base.py +506 -0
  244. dbt/task/build.py +197 -0
  245. dbt/task/clean.py +56 -0
  246. dbt/task/clone.py +161 -0
  247. dbt/task/compile.py +150 -0
  248. dbt/task/compute.py +458 -0
  249. dbt/task/debug.py +513 -0
  250. dbt/task/deps.py +280 -0
  251. dbt/task/docs/__init__.py +3 -0
  252. dbt/task/docs/api/__init__.py +23 -0
  253. dbt/task/docs/api/catalog.py +204 -0
  254. dbt/task/docs/api/lineage.py +234 -0
  255. dbt/task/docs/api/profile.py +204 -0
  256. dbt/task/docs/api/spark.py +186 -0
  257. dbt/task/docs/generate.py +1002 -0
  258. dbt/task/docs/index.html +250 -0
  259. dbt/task/docs/serve.py +174 -0
  260. dbt/task/dvt_output.py +509 -0
  261. dbt/task/dvt_run.py +282 -0
  262. dbt/task/dvt_seed.py +806 -0
  263. dbt/task/freshness.py +322 -0
  264. dbt/task/function.py +121 -0
  265. dbt/task/group_lookup.py +46 -0
  266. dbt/task/init.py +1022 -0
  267. dbt/task/java.py +316 -0
  268. dbt/task/list.py +236 -0
  269. dbt/task/metadata.py +804 -0
  270. dbt/task/migrate.py +714 -0
  271. dbt/task/printer.py +175 -0
  272. dbt/task/profile.py +1489 -0
  273. dbt/task/profile_serve.py +662 -0
  274. dbt/task/retract.py +441 -0
  275. dbt/task/retry.py +175 -0
  276. dbt/task/run.py +1647 -0
  277. dbt/task/run_operation.py +141 -0
  278. dbt/task/runnable.py +758 -0
  279. dbt/task/seed.py +103 -0
  280. dbt/task/show.py +149 -0
  281. dbt/task/snapshot.py +56 -0
  282. dbt/task/spark.py +414 -0
  283. dbt/task/sql.py +110 -0
  284. dbt/task/target_sync.py +814 -0
  285. dbt/task/test.py +464 -0
  286. dbt/tests/fixtures/__init__.py +1 -0
  287. dbt/tests/fixtures/project.py +620 -0
  288. dbt/tests/util.py +651 -0
  289. dbt/tracking.py +529 -0
  290. dbt/utils/__init__.py +3 -0
  291. dbt/utils/artifact_upload.py +151 -0
  292. dbt/utils/utils.py +408 -0
  293. dbt/version.py +271 -0
  294. dvt_cli/__init__.py +158 -0
  295. dvt_core-0.59.0a51.dist-info/METADATA +288 -0
  296. dvt_core-0.59.0a51.dist-info/RECORD +299 -0
  297. dvt_core-0.59.0a51.dist-info/WHEEL +5 -0
  298. dvt_core-0.59.0a51.dist-info/entry_points.txt +2 -0
  299. dvt_core-0.59.0a51.dist-info/top_level.txt +2 -0
@@ -0,0 +1,814 @@
1
+ """
2
+ Target Sync Task
3
+
4
+ Handles DVT target synchronization:
5
+ - Scans profiles.yml connections to detect required adapter types
6
+ - Reports connections found and their types
7
+ - Shows adapter install instructions (manual pip/uv install)
8
+ - Resolves JDBC JARs with transitive dependencies via Maven POM
9
+ - Downloads all JARs to project .dvt/jdbc_jars/ directory
10
+ - Configures spark.jars to use pre-downloaded JARs (no Spark download at runtime)
11
+ - Removes unused adapters and JARs (with --clean flag)
12
+
13
+ v0.5.91: Smart sync based on profiles.yml connections
14
+ v0.5.93: Actual JDBC JAR download to project directory
15
+ v0.5.94: Show install instructions instead of auto-installing adapters
16
+ v0.5.95: Hybrid JAR resolution - DVT downloads with transitive deps, spark.jars config
17
+ """
18
+
19
+ import os
20
+ import subprocess
21
+ import sys
22
+ import urllib.request
23
+ import xml.etree.ElementTree as ET
24
+ from pathlib import Path
25
+ from typing import Dict, List, Optional, Set, Tuple
26
+
27
+ from dbt.config.compute import ComputeRegistry
28
+ from dbt_common.exceptions import DbtRuntimeError
29
+
30
+
31
+ def get_dvt_dir() -> Path:
32
+ """Get the DVT configuration directory (~/.dvt/)."""
33
+ dvt_dir = Path.home() / ".dvt"
34
+ dvt_dir.mkdir(parents=True, exist_ok=True)
35
+ return dvt_dir
36
+
37
+
38
+ # Maven repository URL
39
+ MAVEN_REPO = "https://repo1.maven.org/maven2"
40
+
41
+
42
+ # Mapping of adapter type to dbt adapter package name
43
+ ADAPTER_PACKAGE_MAPPING = {
44
+ "postgres": "dbt-postgres",
45
+ "snowflake": "dbt-snowflake",
46
+ "bigquery": "dbt-bigquery",
47
+ "redshift": "dbt-redshift",
48
+ "spark": "dbt-spark",
49
+ "databricks": "dbt-databricks",
50
+ "trino": "dbt-trino",
51
+ "duckdb": "dbt-duckdb",
52
+ "mysql": "dbt-mysql",
53
+ "sqlserver": "dbt-sqlserver",
54
+ "synapse": "dbt-synapse",
55
+ "fabric": "dbt-fabric",
56
+ "oracle": "dbt-oracle",
57
+ "teradata": "dbt-teradata",
58
+ "clickhouse": "dbt-clickhouse",
59
+ "greenplum": "dbt-greenplum",
60
+ "vertica": "dbt-vertica",
61
+ "sqlite": "dbt-sqlite",
62
+ "mariadb": "dbt-mysql", # Uses MySQL adapter
63
+ "exasol": "dbt-exasol",
64
+ "db2": "dbt-db2",
65
+ "athena": "dbt-athena-community",
66
+ "presto": "dbt-presto",
67
+ "hive": "dbt-hive",
68
+ "impala": "dbt-impala",
69
+ "singlestore": "dbt-singlestore",
70
+ "firebolt": "dbt-firebolt",
71
+ "starrocks": "dbt-starrocks",
72
+ "doris": "dbt-doris",
73
+ "materialize": "dbt-materialize",
74
+ "rockset": "dbt-rockset",
75
+ "questdb": "dbt-questdb",
76
+ "neo4j": "dbt-neo4j",
77
+ "timescaledb": "dbt-postgres", # Uses PostgreSQL adapter
78
+ }
79
+
80
+ # Mapping of adapter type to JDBC Maven coordinates (ONE JAR per adapter)
81
+ # These are pure JDBC drivers that Spark uses for spark.read.jdbc()
82
+ # NOTE: All versions verified against Maven Central as of Dec 2025
83
+ ADAPTER_JDBC_MAPPING = {
84
+ # Official dbt-labs adapters - JDBC drivers only
85
+ "postgres": "org.postgresql:postgresql:42.7.4",
86
+ "snowflake": "net.snowflake:snowflake-jdbc:3.16.1",
87
+ "bigquery": "com.google.cloud.bigdataoss:gcs-connector:hadoop3-2.2.22", # GCS connector for BQ
88
+ "redshift": "com.amazon.redshift:redshift-jdbc42:2.1.0.32",
89
+ "spark": "", # Native, no JDBC needed
90
+ "databricks": "com.databricks:databricks-jdbc:2.6.36",
91
+ "trino": "io.trino:trino-jdbc:443",
92
+ "duckdb": "org.duckdb:duckdb_jdbc:1.1.3",
93
+ # Community adapters - JDBC drivers only (verified on Maven)
94
+ "mysql": "com.mysql:mysql-connector-j:9.1.0",
95
+ "sqlserver": "com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11",
96
+ "synapse": "com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11",
97
+ "fabric": "com.microsoft.sqlserver:mssql-jdbc:12.8.1.jre11",
98
+ "oracle": "com.oracle.database.jdbc:ojdbc11:23.6.0.24.10",
99
+ "teradata": "com.teradata.jdbc:terajdbc:20.00.00.20",
100
+ "clickhouse": "com.clickhouse:clickhouse-jdbc:0.6.5",
101
+ "greenplum": "org.postgresql:postgresql:42.7.4", # PostgreSQL compatible
102
+ "vertica": "com.vertica.jdbc:vertica-jdbc:24.3.0-0",
103
+ "sqlite": "org.xerial:sqlite-jdbc:3.47.1.0",
104
+ "mariadb": "org.mariadb.jdbc:mariadb-java-client:3.4.1",
105
+ "exasol": "com.exasol:exasol-jdbc:24.2.0",
106
+ "db2": "com.ibm.db2:jcc:11.5.9.0",
107
+ "presto": "io.prestosql:presto-jdbc:350",
108
+ "hive": "org.apache.hive:hive-jdbc:3.1.3",
109
+ "singlestore": "com.singlestore:singlestore-jdbc-client:1.2.9",
110
+ "starrocks": "com.mysql:mysql-connector-j:9.1.0", # MySQL wire protocol
111
+ "doris": "com.mysql:mysql-connector-j:9.1.0", # MySQL wire protocol
112
+ "materialize": "org.postgresql:postgresql:42.7.4", # PostgreSQL wire protocol
113
+ "neo4j": "org.neo4j:neo4j-jdbc-driver:4.0.10",
114
+ "timescaledb": "org.postgresql:postgresql:42.7.4", # PostgreSQL extension
115
+ "questdb": "org.postgresql:postgresql:42.7.4", # PostgreSQL wire protocol
116
+ # Adapters without Maven JDBC drivers (require manual JAR download):
117
+ # athena, impala, firebolt, rockset - use respective vendor download pages
118
+ }
119
+
120
+
121
+ class TargetSyncTask:
122
+ """Task for synchronizing adapters and JARs based on profiles.yml connections."""
123
+
124
+ def __init__(
125
+ self,
126
+ project_dir: Optional[str] = None,
127
+ profiles_dir: Optional[str] = None,
128
+ profile_name: Optional[str] = None,
129
+ ):
130
+ """
131
+ Initialize TargetSyncTask.
132
+
133
+ :param project_dir: Path to project root directory
134
+ :param profiles_dir: Path to profiles directory (defaults to ~/.dvt/)
135
+ :param profile_name: Profile name to sync (defaults to project profile)
136
+ """
137
+ self.project_dir = project_dir or str(Path.cwd())
138
+ self.profiles_dir = profiles_dir or str(get_dvt_dir())
139
+ self.profile_name = profile_name
140
+ self.compute_registry = ComputeRegistry(self.project_dir)
141
+
142
+ def _get_profile_name(self) -> Optional[str]:
143
+ """Get the profile name from project or explicit parameter."""
144
+ if self.profile_name:
145
+ return self.profile_name
146
+
147
+ # Try to read from dvt_project.yml first (DVT), then dbt_project.yml (legacy)
148
+ project_files = [
149
+ Path(self.project_dir) / "dvt_project.yml",
150
+ Path(self.project_dir) / "dbt_project.yml",
151
+ ]
152
+
153
+ for project_file in project_files:
154
+ if project_file.exists():
155
+ try:
156
+ from dbt.clients.yaml_helper import load_yaml_text
157
+
158
+ content = project_file.read_text()
159
+ data = load_yaml_text(content)
160
+ if data and "profile" in data:
161
+ return data["profile"]
162
+ except Exception:
163
+ pass
164
+
165
+ return None
166
+
167
+ def _load_profiles(self) -> Dict:
168
+ """Load profiles.yml and return the data."""
169
+ profiles_path = Path(self.profiles_dir) / "profiles.yml"
170
+ if not profiles_path.exists():
171
+ raise DbtRuntimeError(
172
+ f"profiles.yml not found at {profiles_path}\n"
173
+ f"Create it with: dvt init <project_name>"
174
+ )
175
+
176
+ try:
177
+ from dbt.clients.yaml_helper import load_yaml_text
178
+
179
+ content = profiles_path.read_text()
180
+ return load_yaml_text(content) or {}
181
+ except Exception as e:
182
+ raise DbtRuntimeError(f"Failed to load profiles.yml: {e}") from e
183
+
184
+ def get_connections_info(self) -> Dict[str, Dict]:
185
+ """
186
+ Scan profiles.yml and return detailed info about connections.
187
+
188
+ :returns: Dict mapping connection name to {type, profile}
189
+ """
190
+ profiles = self._load_profiles()
191
+ connections = {}
192
+
193
+ profile_name = self._get_profile_name()
194
+
195
+ if profile_name and profile_name in profiles:
196
+ # Scan only the specified profile
197
+ profile_data = profiles[profile_name]
198
+ outputs = profile_data.get("outputs", {})
199
+ for target_name, target_config in outputs.items():
200
+ adapter_type = target_config.get("type")
201
+ if adapter_type:
202
+ connections[target_name] = {
203
+ "type": adapter_type,
204
+ "profile": profile_name,
205
+ }
206
+ elif profile_name:
207
+ # v0.59.0a18: Profile specified but not found - don't fall back to all profiles
208
+ raise DbtRuntimeError(
209
+ f"Profile '{profile_name}' not found in profiles.yml.\n"
210
+ f"Available profiles: {list(profiles.keys())}\n"
211
+ f"Create it with: dvt init"
212
+ )
213
+ # v0.59.0a18: No profile detected (not in a project) - return empty
214
+ # Don't scan all profiles as that breaks multi-project setups
215
+
216
+ return connections
217
+
218
+ def get_required_adapter_types(self) -> Set[str]:
219
+ """
220
+ Scan profiles.yml and return the set of adapter types needed.
221
+
222
+ :returns: Set of adapter type names (e.g., {'postgres', 'snowflake'})
223
+ """
224
+ connections = self.get_connections_info()
225
+ return {info["type"] for info in connections.values()}
226
+
227
+ def get_installed_adapters(self) -> Set[str]:
228
+ """
229
+ Detect which dbt adapters are currently installed.
230
+
231
+ :returns: Set of installed adapter type names
232
+ """
233
+ import importlib.util
234
+
235
+ installed = set()
236
+
237
+ adapter_modules = {
238
+ "postgres": "dbt.adapters.postgres",
239
+ "snowflake": "dbt.adapters.snowflake",
240
+ "bigquery": "dbt.adapters.bigquery",
241
+ "redshift": "dbt.adapters.redshift",
242
+ "spark": "dbt.adapters.spark",
243
+ "databricks": "dbt.adapters.databricks",
244
+ "trino": "dbt.adapters.trino",
245
+ "duckdb": "dbt.adapters.duckdb",
246
+ "mysql": "dbt.adapters.mysql",
247
+ "sqlserver": "dbt.adapters.sqlserver",
248
+ "synapse": "dbt.adapters.synapse",
249
+ "fabric": "dbt.adapters.fabric",
250
+ "oracle": "dbt.adapters.oracle",
251
+ "teradata": "dbt.adapters.teradata",
252
+ "clickhouse": "dbt.adapters.clickhouse",
253
+ "greenplum": "dbt.adapters.greenplum",
254
+ "vertica": "dbt.adapters.vertica",
255
+ "sqlite": "dbt.adapters.sqlite",
256
+ "mariadb": "dbt.adapters.mysql", # Uses MySQL adapter
257
+ "exasol": "dbt.adapters.exasol",
258
+ "athena": "dbt.adapters.athena",
259
+ "hive": "dbt.adapters.hive",
260
+ "impala": "dbt.adapters.impala",
261
+ "singlestore": "dbt.adapters.singlestore",
262
+ "firebolt": "dbt.adapters.firebolt",
263
+ "starrocks": "dbt.adapters.starrocks",
264
+ "doris": "dbt.adapters.doris",
265
+ "materialize": "dbt.adapters.materialize",
266
+ "rockset": "dbt.adapters.rockset",
267
+ }
268
+
269
+ for adapter_type, module_name in adapter_modules.items():
270
+ spec = importlib.util.find_spec(module_name)
271
+ if spec is not None:
272
+ installed.add(adapter_type)
273
+
274
+ return installed
275
+
276
+ def install_adapters(
277
+ self, adapter_types: Set[str], verbose: bool = True
278
+ ) -> Tuple[List[str], List[str]]:
279
+ """
280
+ Install dbt adapters for the given adapter types.
281
+
282
+ :param adapter_types: Set of adapter type names to install
283
+ :param verbose: Print progress messages
284
+ :returns: Tuple of (installed packages, failed packages)
285
+ """
286
+ installed = []
287
+ failed = []
288
+
289
+ for adapter_type in adapter_types:
290
+ package = ADAPTER_PACKAGE_MAPPING.get(adapter_type)
291
+ if not package:
292
+ if verbose:
293
+ print(f" ⚠ Unknown adapter type: {adapter_type}")
294
+ continue
295
+
296
+ if verbose:
297
+ print(f" Installing {package}...")
298
+
299
+ try:
300
+ # Use pip to install the adapter
301
+ result = subprocess.run(
302
+ [sys.executable, "-m", "pip", "install", package, "--quiet"],
303
+ capture_output=True,
304
+ text=True,
305
+ )
306
+ if result.returncode == 0:
307
+ installed.append(package)
308
+ if verbose:
309
+ print(f" ✓ {package} installed")
310
+ else:
311
+ failed.append(package)
312
+ if verbose:
313
+ print(f" ✗ Failed to install {package}")
314
+ if result.stderr:
315
+ print(f" {result.stderr[:200]}")
316
+ except Exception as e:
317
+ failed.append(package)
318
+ if verbose:
319
+ print(f" ✗ Error installing {package}: {e}")
320
+
321
+ # DVT v0.59.0a42: Clean up dbt-core if it was pulled in as transitive dependency
322
+ if installed:
323
+ self._cleanup_dbt_core_conflict(verbose)
324
+
325
+ return installed, failed
326
+
327
+ def _cleanup_dbt_core_conflict(self, verbose: bool = True) -> None:
328
+ """
329
+ Reinstall dvt-core to restore files overwritten by dbt-core.
330
+
331
+ dbt adapters (dbt-databricks, dbt-snowflake, etc.) depend on dbt-core.
332
+ When installed, dbt-core overwrites dvt-core's files in the shared 'dbt'
333
+ namespace. This method reinstalls dvt-core to restore its files.
334
+
335
+ Note: We keep dbt-core installed because adapters need its metadata
336
+ (e.g., dbt-databricks calls metadata.version("dbt-core")).
337
+
338
+ DVT v0.59.0a42: Automatic cleanup after adapter installation.
339
+ """
340
+ try:
341
+ # Check if dbt-core is installed
342
+ result = subprocess.run(
343
+ [sys.executable, "-m", "pip", "show", "dbt-core"],
344
+ capture_output=True,
345
+ text=True,
346
+ )
347
+ if result.returncode != 0:
348
+ # dbt-core not installed, nothing to clean up
349
+ return
350
+
351
+ if verbose:
352
+ print("\n 🔧 Restoring dvt-core files...")
353
+ print(" (dbt adapters install dbt-core which overwrites dvt-core files)")
354
+
355
+ # Reinstall dvt-core to restore overwritten files
356
+ # Keep dbt-core installed - adapters need its metadata
357
+ subprocess.run(
358
+ [sys.executable, "-m", "pip", "install", "--reinstall", "--no-deps",
359
+ "dvt-core", "--quiet"],
360
+ capture_output=True,
361
+ text=True,
362
+ )
363
+ if verbose:
364
+ print(" ✓ Restored dvt-core files")
365
+
366
+ except Exception as e:
367
+ if verbose:
368
+ print(f" ⚠ Cleanup warning: {e}")
369
+
370
+ def uninstall_adapters(
371
+ self, adapter_types: Set[str], verbose: bool = True
372
+ ) -> Tuple[List[str], List[str]]:
373
+ """
374
+ Uninstall dbt adapters for the given adapter types.
375
+
376
+ :param adapter_types: Set of adapter type names to uninstall
377
+ :param verbose: Print progress messages
378
+ :returns: Tuple of (uninstalled packages, failed packages)
379
+ """
380
+ uninstalled = []
381
+ failed = []
382
+
383
+ for adapter_type in adapter_types:
384
+ package = ADAPTER_PACKAGE_MAPPING.get(adapter_type)
385
+ if not package:
386
+ continue
387
+
388
+ if verbose:
389
+ print(f" Removing {package}...")
390
+
391
+ try:
392
+ result = subprocess.run(
393
+ [sys.executable, "-m", "pip", "uninstall", package, "-y", "--quiet"],
394
+ capture_output=True,
395
+ text=True,
396
+ )
397
+ if result.returncode == 0:
398
+ uninstalled.append(package)
399
+ if verbose:
400
+ print(f" ✓ {package} removed")
401
+ else:
402
+ failed.append(package)
403
+ if verbose:
404
+ print(f" ✗ Failed to remove {package}")
405
+ except Exception as e:
406
+ failed.append(package)
407
+ if verbose:
408
+ print(f" ✗ Error removing {package}: {e}")
409
+
410
+ return uninstalled, failed
411
+
412
+ def _maven_coord_to_url(self, coord: str) -> Tuple[str, str]:
413
+ """
414
+ Convert Maven coordinate to download URL and JAR filename.
415
+
416
+ :param coord: Maven coordinate (e.g., 'org.postgresql:postgresql:42.7.4')
417
+ :returns: Tuple of (download_url, jar_filename)
418
+ """
419
+ parts = coord.split(":")
420
+ if len(parts) < 3:
421
+ raise ValueError(f"Invalid Maven coordinate: {coord}")
422
+
423
+ group_id = parts[0]
424
+ artifact_id = parts[1]
425
+ version = parts[2]
426
+
427
+ # Convert group.id to group/id path
428
+ group_path = group_id.replace(".", "/")
429
+
430
+ # Build URL
431
+ jar_name = f"{artifact_id}-{version}.jar"
432
+ url = f"{MAVEN_REPO}/{group_path}/{artifact_id}/{version}/{jar_name}"
433
+
434
+ return url, jar_name
435
+
436
+ def _resolve_transitive_deps(
437
+ self, coord: str, resolved: Optional[Set[str]] = None, depth: int = 0
438
+ ) -> Set[str]:
439
+ """
440
+ Resolve transitive dependencies for a Maven coordinate by parsing POM file.
441
+
442
+ :param coord: Maven coordinate (e.g., 'org.postgresql:postgresql:42.7.4')
443
+ :param resolved: Set of already resolved coordinates (to avoid cycles)
444
+ :param depth: Current recursion depth (max 3 to avoid deep trees)
445
+ :returns: Set of all coordinates (including transitive deps)
446
+ """
447
+ if resolved is None:
448
+ resolved = set()
449
+
450
+ # Parse coordinate
451
+ parts = coord.split(":")
452
+ if len(parts) < 3:
453
+ return resolved
454
+
455
+ group_id, artifact_id, version = parts[0], parts[1], parts[2]
456
+
457
+ # Avoid cycles and limit depth
458
+ if coord in resolved or depth > 3:
459
+ return resolved
460
+
461
+ resolved.add(coord)
462
+
463
+ # Skip transitive resolution for known self-contained JDBC drivers
464
+ # Most JDBC drivers bundle their dependencies or have minimal deps
465
+ self_contained_drivers = {
466
+ "postgresql", "snowflake-jdbc", "mysql-connector-j", "mssql-jdbc",
467
+ "ojdbc11", "terajdbc", "clickhouse-jdbc", "sqlite-jdbc",
468
+ "mariadb-java-client", "exasol-jdbc", "jcc", "trino-jdbc",
469
+ "presto-jdbc", "duckdb_jdbc", "databricks-jdbc", "redshift-jdbc42",
470
+ "singlestore-jdbc-client", "neo4j-jdbc-driver", "vertica-jdbc"
471
+ }
472
+ if artifact_id in self_contained_drivers:
473
+ return resolved
474
+
475
+ # Try to fetch and parse POM for transitive deps
476
+ try:
477
+ group_path = group_id.replace(".", "/")
478
+ pom_url = f"{MAVEN_REPO}/{group_path}/{artifact_id}/{version}/{artifact_id}-{version}.pom"
479
+
480
+ request = urllib.request.Request(
481
+ pom_url,
482
+ headers={"User-Agent": "DVT-Core/0.5.95"}
483
+ )
484
+
485
+ with urllib.request.urlopen(request, timeout=10) as response:
486
+ pom_content = response.read().decode("utf-8")
487
+
488
+ # Parse POM XML
489
+ root = ET.fromstring(pom_content)
490
+ ns = {"m": "http://maven.apache.org/POM/4.0.0"}
491
+
492
+ # Find dependencies
493
+ for dep in root.findall(".//m:dependency", ns):
494
+ dep_group = dep.find("m:groupId", ns)
495
+ dep_artifact = dep.find("m:artifactId", ns)
496
+ dep_version = dep.find("m:version", ns)
497
+ dep_scope = dep.find("m:scope", ns)
498
+ dep_optional = dep.find("m:optional", ns)
499
+
500
+ # Skip test, provided, and optional dependencies
501
+ if dep_scope is not None and dep_scope.text in ("test", "provided"):
502
+ continue
503
+ if dep_optional is not None and dep_optional.text == "true":
504
+ continue
505
+
506
+ if dep_group is not None and dep_artifact is not None and dep_version is not None:
507
+ dep_coord = f"{dep_group.text}:{dep_artifact.text}:{dep_version.text}"
508
+ # Recursively resolve (limited depth)
509
+ self._resolve_transitive_deps(dep_coord, resolved, depth + 1)
510
+
511
+ except Exception:
512
+ # If POM parsing fails, just return current resolved set
513
+ pass
514
+
515
+ return resolved
516
+
517
+ def _download_jar(self, url: str, dest_path: Path, verbose: bool = True) -> bool:
518
+ """
519
+ Download a JAR file from URL.
520
+
521
+ :param url: URL to download from
522
+ :param dest_path: Destination path
523
+ :param verbose: Print progress messages
524
+ :returns: True if successful
525
+ """
526
+ try:
527
+ if verbose:
528
+ print(f" Downloading {dest_path.name}...")
529
+
530
+ # Create request with user agent
531
+ request = urllib.request.Request(
532
+ url,
533
+ headers={"User-Agent": "DVT-Core/0.5.95"}
534
+ )
535
+
536
+ with urllib.request.urlopen(request, timeout=60) as response:
537
+ with open(dest_path, "wb") as f:
538
+ f.write(response.read())
539
+
540
+ if verbose:
541
+ size_mb = dest_path.stat().st_size / (1024 * 1024)
542
+ print(f" ✓ Downloaded ({size_mb:.1f} MB)")
543
+ return True
544
+
545
+ except Exception as e:
546
+ if verbose:
547
+ print(f" ✗ Failed: {e}")
548
+ return False
549
+
550
+ def download_jdbc_jars(
551
+ self, adapter_types: Set[str], verbose: bool = True
552
+ ) -> Tuple[List[str], List[str]]:
553
+ """
554
+ Download JDBC JARs to project .dvt/jdbc_jars/ directory.
555
+
556
+ v0.5.95: Hybrid approach - resolves transitive dependencies via Maven POM,
557
+ downloads all JARs to local cache, then uses spark.jars for fast startup.
558
+
559
+ :param adapter_types: Set of adapter type names
560
+ :param verbose: Print progress messages
561
+ :returns: Tuple of (downloaded jars, failed jars)
562
+ """
563
+ # Ensure jdbc_jars directory exists
564
+ jdbc_jars_dir = Path(self.project_dir) / ".dvt" / "jdbc_jars"
565
+ jdbc_jars_dir.mkdir(parents=True, exist_ok=True)
566
+
567
+ downloaded = []
568
+ failed = []
569
+
570
+ # Build list of required JAR coordinates (direct dependencies)
571
+ direct_coords = set()
572
+ for adapter_type in adapter_types:
573
+ jars = ADAPTER_JDBC_MAPPING.get(adapter_type, "")
574
+ if jars:
575
+ for jar in jars.split(","):
576
+ jar = jar.strip()
577
+ if jar:
578
+ direct_coords.add(jar)
579
+
580
+ if not direct_coords:
581
+ if verbose:
582
+ print("\n No JDBC JARs needed for these adapters")
583
+ return downloaded, failed
584
+
585
+ if verbose:
586
+ print(f"\n Resolving JDBC dependencies...")
587
+ print(f" Direct dependencies: {len(direct_coords)}")
588
+
589
+ # Resolve transitive dependencies for all direct coords
590
+ all_coords = set()
591
+ for coord in direct_coords:
592
+ resolved = self._resolve_transitive_deps(coord)
593
+ all_coords.update(resolved)
594
+
595
+ if verbose:
596
+ transitive_count = len(all_coords) - len(direct_coords)
597
+ if transitive_count > 0:
598
+ print(f" Transitive dependencies: {transitive_count}")
599
+ print(f" Total JARs to download: {len(all_coords)}")
600
+ print(f"\n Downloading to {jdbc_jars_dir}/")
601
+
602
+ for coord in sorted(all_coords):
603
+ try:
604
+ url, jar_name = self._maven_coord_to_url(coord)
605
+ dest_path = jdbc_jars_dir / jar_name
606
+
607
+ # Skip if already downloaded
608
+ if dest_path.exists():
609
+ if verbose:
610
+ print(f" {jar_name} (cached)")
611
+ downloaded.append(jar_name)
612
+ continue
613
+
614
+ # Download the JAR
615
+ if self._download_jar(url, dest_path, verbose):
616
+ downloaded.append(jar_name)
617
+ else:
618
+ failed.append(jar_name)
619
+
620
+ except ValueError as e:
621
+ if verbose:
622
+ print(f" ⚠ Skipping {coord}: {e}")
623
+ continue
624
+ except Exception as e:
625
+ if verbose:
626
+ print(f" ✗ Error with {coord}: {e}")
627
+ failed.append(coord)
628
+
629
+ return downloaded, failed
630
+
631
+ def update_jdbc_jars(self, adapter_types: Set[str], verbose: bool = True) -> bool:
632
+ """
633
+ Report JDBC JAR status (JARs discovered at runtime by Spark).
634
+
635
+ v0.5.96: No longer stores spark.jars in config - JARs are discovered at runtime.
636
+ This enables project folder portability (move folder → JARs still work).
637
+
638
+ The LocalStrategy._get_jdbc_jars() method discovers JARs from current project
639
+ directory at runtime: <project>/.dvt/jdbc_jars/*.jar
640
+
641
+ :param adapter_types: Set of adapter type names
642
+ :param verbose: Print progress messages
643
+ :returns: True if JARs found
644
+ """
645
+ # Get the jdbc_jars directory
646
+ jdbc_jars_dir = Path(self.project_dir) / ".dvt" / "jdbc_jars"
647
+
648
+ # Find all downloaded JAR files
649
+ jar_paths = []
650
+ if jdbc_jars_dir.exists():
651
+ jar_paths = sorted(jdbc_jars_dir.glob("*.jar"))
652
+
653
+ if verbose:
654
+ if jar_paths:
655
+ print(f"\n JDBC JARs downloaded ({len(jar_paths)}):")
656
+ for jar_path in jar_paths:
657
+ print(f" - {jar_path.name}")
658
+ print(f"\n ✓ JARs stored in: {jdbc_jars_dir}")
659
+ print(" (Spark discovers JARs at runtime - portable across folder moves)")
660
+ else:
661
+ print("\n No JDBC JARs downloaded")
662
+
663
+ # v0.5.96: Remove spark.jars from config if present (old absolute path config)
664
+ # JARs are now discovered at runtime from project directory
665
+ spark_local = self.compute_registry.get("spark-local")
666
+ if spark_local:
667
+ modified = False
668
+ if "spark.jars" in spark_local.config:
669
+ spark_local.config.pop("spark.jars", None)
670
+ modified = True
671
+ if "spark.jars.packages" in spark_local.config:
672
+ spark_local.config.pop("spark.jars.packages", None)
673
+ modified = True
674
+ if modified:
675
+ self.compute_registry._save()
676
+ if verbose:
677
+ print("\n ✓ Cleaned up old spark.jars config (now uses runtime discovery)")
678
+
679
+ return bool(jar_paths)
680
+
681
+ def sync(self, verbose: bool = True, clean: bool = False, dry_run: bool = False) -> bool:
682
+ """
683
+ Synchronize adapters and JARs based on profiles.yml.
684
+
685
+ :param verbose: Print progress messages
686
+ :param clean: If True, remove adapters not needed by profiles.yml
687
+ :param dry_run: If True, only report what would be done without making changes
688
+ :returns: True if sync successful
689
+ """
690
+ if verbose:
691
+ print("\nDVT Target Sync")
692
+ print("=" * 60)
693
+
694
+ # Get connection info from profiles.yml
695
+ try:
696
+ connections = self.get_connections_info()
697
+ required = self.get_required_adapter_types()
698
+ except DbtRuntimeError as e:
699
+ print(f"✗ Error: {e}")
700
+ return False
701
+
702
+ # Report connections found
703
+ if verbose:
704
+ profile_name = self._get_profile_name()
705
+ if profile_name:
706
+ print(f"Profile: {profile_name}")
707
+ print(f"\nConnections found: {len(connections)}")
708
+ print("-" * 40)
709
+
710
+ # Group connections by type
711
+ by_type: Dict[str, List[str]] = {}
712
+ for conn_name, info in connections.items():
713
+ adapter_type = info["type"]
714
+ if adapter_type not in by_type:
715
+ by_type[adapter_type] = []
716
+ by_type[adapter_type].append(conn_name)
717
+
718
+ for adapter_type in sorted(by_type.keys()):
719
+ conn_names = by_type[adapter_type]
720
+ package = ADAPTER_PACKAGE_MAPPING.get(adapter_type, "unknown")
721
+ print(f"\n {adapter_type} ({len(conn_names)} connection(s)):")
722
+ for conn_name in conn_names:
723
+ print(f" - {conn_name}")
724
+ print(f" Package: {package}")
725
+
726
+ if not required:
727
+ print("\n⚠ No connections found in profiles.yml")
728
+ print(" Add connections to ~/.dvt/profiles.yml first")
729
+ return False
730
+
731
+ # Get currently installed adapters
732
+ installed = self.get_installed_adapters()
733
+
734
+ # Determine what to install and uninstall
735
+ to_install = required - installed
736
+ to_uninstall = installed - required if clean else set()
737
+
738
+ # Report what will be installed
739
+ if verbose:
740
+ print("\n" + "-" * 40)
741
+ print("\nAdapter Status:")
742
+
743
+ if to_install:
744
+ print(f"\n To install ({len(to_install)}):")
745
+ for adapter_type in sorted(to_install):
746
+ package = ADAPTER_PACKAGE_MAPPING.get(adapter_type, "unknown")
747
+ print(f" - {adapter_type}: pip install {package}")
748
+ else:
749
+ print("\n ✓ All required adapters already installed")
750
+
751
+ if to_uninstall:
752
+ print(f"\n To remove ({len(to_uninstall)}):")
753
+ for adapter_type in sorted(to_uninstall):
754
+ package = ADAPTER_PACKAGE_MAPPING.get(adapter_type, "unknown")
755
+ print(f" - {adapter_type}: pip uninstall {package}")
756
+
757
+ # Report adapters installed but not used (if not cleaning)
758
+ unused = installed - required
759
+ if unused and not clean:
760
+ print(f"\n Installed but not used ({len(unused)}):")
761
+ for adapter_type in sorted(unused):
762
+ package = ADAPTER_PACKAGE_MAPPING.get(adapter_type, "unknown")
763
+ print(f" - {adapter_type} ({package})")
764
+ print(" (use --clean to remove unused adapters)")
765
+
766
+ # If dry run, stop here
767
+ if dry_run:
768
+ if verbose:
769
+ print("\n" + "=" * 60)
770
+ print("Dry run complete. No changes made.")
771
+ return True
772
+
773
+ # Show install instructions for missing adapters (don't auto-install)
774
+ if to_install:
775
+ if verbose:
776
+ print(f"\n" + "-" * 40)
777
+ print(f"\nMissing Adapters ({len(to_install)}):")
778
+ print(" Install manually with pip or uv:\n")
779
+ for adapter_type in sorted(to_install):
780
+ package = ADAPTER_PACKAGE_MAPPING.get(adapter_type, "unknown")
781
+ print(f" pip install {package}")
782
+ print(f" # or: uv pip install {package}\n")
783
+
784
+ # Show uninstall instructions for unused adapters (only if clean=True)
785
+ if to_uninstall:
786
+ if verbose:
787
+ print(f"\n" + "-" * 40)
788
+ print(f"\nUnused Adapters ({len(to_uninstall)}):")
789
+ print(" Uninstall manually with pip or uv:\n")
790
+ for adapter_type in sorted(to_uninstall):
791
+ package = ADAPTER_PACKAGE_MAPPING.get(adapter_type, "unknown")
792
+ print(f" pip uninstall {package}")
793
+ print(f" # or: uv pip uninstall {package}\n")
794
+
795
+ # Download JDBC JARs to project directory
796
+ if verbose:
797
+ print("\n" + "-" * 40)
798
+ print("\nDownloading JDBC JARs...")
799
+ downloaded_jars, failed_jars = self.download_jdbc_jars(required, verbose)
800
+ if failed_jars and verbose:
801
+ print(f"\n ⚠ {len(failed_jars)} JAR(s) failed to download")
802
+
803
+ # Update JDBC JARs config in spark-local
804
+ if verbose:
805
+ print("\n" + "-" * 40)
806
+ print("\nUpdating JDBC configuration...")
807
+ self.update_jdbc_jars(required, verbose)
808
+
809
+ if verbose:
810
+ print("\n" + "=" * 60)
811
+ print("✓ Sync complete")
812
+ print("\nYou can now run: dvt run")
813
+
814
+ return True