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