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
dbt/cli/main.py ADDED
@@ -0,0 +1,2660 @@
1
+ import functools
2
+ from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
3
+ from copy import copy
4
+ from dataclasses import dataclass
5
+ import re
6
+ import time
7
+ from typing import Callable, List, Optional, Union
8
+
9
+ import click
10
+ from click.exceptions import BadOptionUsage
11
+ from click.exceptions import Exit as ClickExit
12
+ from click.exceptions import NoSuchOption, UsageError
13
+
14
+ from dbt.adapters.factory import register_adapter
15
+ from dbt.artifacts.schemas.catalog import CatalogArtifact
16
+ from dbt.artifacts.schemas.run import RunExecutionResult
17
+ from dbt.cli import params as p
18
+ from dbt.cli import requires
19
+ from dbt.cli.exceptions import DbtInternalException, DbtUsageException
20
+ from dbt.cli.requires import setup_manifest
21
+ from dbt.contracts.graph.manifest import Manifest
22
+ from dbt.mp_context import get_mp_context
23
+ from dbt_common.events.base_types import EventMsg
24
+
25
+
26
+ @dataclass
27
+ class dbtRunnerResult:
28
+ """Contains the result of an invocation of the dbtRunner"""
29
+
30
+ success: bool
31
+
32
+ exception: Optional[BaseException] = None
33
+ result: Union[
34
+ bool, # debug
35
+ CatalogArtifact, # docs generate
36
+ List[str], # list/ls
37
+ Manifest, # parse
38
+ None, # clean, deps, init, source
39
+ RunExecutionResult, # build, compile, run, seed, snapshot, test, run-operation
40
+ ] = None
41
+
42
+
43
+ # Programmatic invocation
44
+ class dbtRunner:
45
+ def __init__(
46
+ self,
47
+ manifest: Optional[Manifest] = None,
48
+ callbacks: Optional[List[Callable[[EventMsg], None]]] = None,
49
+ ) -> None:
50
+ self.manifest = manifest
51
+
52
+ if callbacks is None:
53
+ callbacks = []
54
+ self.callbacks = callbacks
55
+
56
+ def invoke(self, args: List[str], **kwargs) -> dbtRunnerResult:
57
+ try:
58
+ dbt_ctx = cli.make_context(cli.name, args.copy())
59
+ dbt_ctx.obj = {
60
+ "manifest": self.manifest,
61
+ "callbacks": self.callbacks,
62
+ "dbt_runner_command_args": args,
63
+ }
64
+
65
+ for key, value in kwargs.items():
66
+ dbt_ctx.params[key] = value
67
+ # Hack to set parameter source to custom string
68
+ dbt_ctx.set_parameter_source(key, "kwargs") # type: ignore
69
+
70
+ result, success = cli.invoke(dbt_ctx)
71
+ return dbtRunnerResult(
72
+ result=result,
73
+ success=success,
74
+ )
75
+ except requires.ResultExit as e:
76
+ return dbtRunnerResult(
77
+ result=e.result,
78
+ success=False,
79
+ )
80
+ except requires.ExceptionExit as e:
81
+ return dbtRunnerResult(
82
+ exception=e.exception,
83
+ success=False,
84
+ )
85
+ except (BadOptionUsage, NoSuchOption, UsageError) as e:
86
+ return dbtRunnerResult(
87
+ exception=DbtUsageException(e.message),
88
+ success=False,
89
+ )
90
+ except ClickExit as e:
91
+ if e.exit_code == 0:
92
+ return dbtRunnerResult(success=True)
93
+ return dbtRunnerResult(
94
+ exception=DbtInternalException(f"unhandled exit code {e.exit_code}"),
95
+ success=False,
96
+ )
97
+ except BaseException as e:
98
+ return dbtRunnerResult(
99
+ exception=e,
100
+ success=False,
101
+ )
102
+
103
+
104
+ # approach from https://github.com/pallets/click/issues/108#issuecomment-280489786
105
+ def global_flags(func):
106
+ @p.cache_selected_only
107
+ @p.debug
108
+ @p.defer
109
+ @p.deprecated_defer
110
+ @p.defer_state
111
+ @p.deprecated_favor_state
112
+ @p.deprecated_print
113
+ @p.deprecated_state
114
+ @p.fail_fast
115
+ @p.favor_state
116
+ @p.indirect_selection
117
+ @p.log_cache_events
118
+ @p.log_file_max_bytes
119
+ @p.log_format
120
+ @p.log_format_file
121
+ @p.log_level
122
+ @p.log_level_file
123
+ @p.log_path
124
+ @p.macro_debugging
125
+ @p.partial_parse
126
+ @p.partial_parse_file_path
127
+ @p.partial_parse_file_diff
128
+ @p.populate_cache
129
+ @p.print
130
+ @p.printer_width
131
+ @p.profile
132
+ @p.quiet
133
+ @p.record_timing_info
134
+ @p.send_anonymous_usage_stats
135
+ @p.single_threaded
136
+ @p.show_all_deprecations
137
+ @p.state
138
+ @p.static_parser
139
+ @p.target
140
+ @p.target_compute
141
+ @p.use_colors
142
+ @p.use_colors_file
143
+ @p.use_experimental_parser
144
+ @p.version
145
+ @p.version_check
146
+ @p.warn_error
147
+ @p.warn_error_options
148
+ @p.write_json
149
+ @p.use_fast_test_edges
150
+ @p.upload_artifacts
151
+ @functools.wraps(func)
152
+ def wrapper(*args, **kwargs):
153
+ return func(*args, **kwargs)
154
+
155
+ return wrapper
156
+
157
+
158
+ # dbt
159
+ @click.group(
160
+ context_settings={"help_option_names": ["-h", "--help"]},
161
+ invoke_without_command=True,
162
+ no_args_is_help=True,
163
+ epilog="Specify one of these sub-commands and you can find more help from there.",
164
+ )
165
+ @click.pass_context
166
+ @global_flags
167
+ @p.show_resource_report
168
+ def cli(ctx, **kwargs):
169
+ """An ELT tool for managing your SQL transformations and data models.
170
+ For more documentation on these commands, visit: docs.getdbt.com
171
+ """
172
+
173
+
174
+ # dbt build
175
+ @cli.command("build")
176
+ @click.pass_context
177
+ @global_flags
178
+ @p.empty
179
+ @p.event_time_start
180
+ @p.event_time_end
181
+ @p.exclude
182
+ @p.export_saved_queries
183
+ @p.full_refresh
184
+ @p.deprecated_include_saved_query
185
+ @p.profiles_dir
186
+ @p.project_dir
187
+ @p.resource_type
188
+ @p.exclude_resource_type
189
+ @p.sample
190
+ @p.select
191
+ @p.selector
192
+ @p.show
193
+ @p.store_failures
194
+ @p.target_path
195
+ @p.threads
196
+ @p.vars
197
+ @requires.postflight
198
+ @requires.preflight
199
+ @requires.profile
200
+ @requires.project
201
+ @requires.catalogs
202
+ @requires.runtime_config
203
+ @requires.manifest
204
+ def build(ctx, **kwargs):
205
+ """Run all seeds, models, snapshots, and tests in DAG order"""
206
+ from dbt.task.build import BuildTask
207
+
208
+ task = BuildTask(
209
+ ctx.obj["flags"],
210
+ ctx.obj["runtime_config"],
211
+ ctx.obj["manifest"],
212
+ )
213
+
214
+ results = task.run()
215
+ success = task.interpret_results(results)
216
+ return results, success
217
+
218
+
219
+ # dbt clean
220
+ @cli.command("clean")
221
+ @click.pass_context
222
+ @global_flags
223
+ @p.clean_project_files_only
224
+ @p.profiles_dir
225
+ @p.project_dir
226
+ @p.target_path
227
+ @p.vars
228
+ @requires.postflight
229
+ @requires.preflight
230
+ @requires.unset_profile
231
+ @requires.project
232
+ def clean(ctx, **kwargs):
233
+ """Delete all folders in the clean-targets list (usually the dbt_packages and target directories.)"""
234
+ from dbt.task.clean import CleanTask
235
+
236
+ with CleanTask(ctx.obj["flags"], ctx.obj["project"]) as task:
237
+ results = task.run()
238
+ success = task.interpret_results(results)
239
+ return results, success
240
+
241
+
242
+ # dbt docs
243
+ @cli.group()
244
+ @click.pass_context
245
+ @global_flags
246
+ def docs(ctx, **kwargs):
247
+ """Generate or serve the documentation website for your project"""
248
+
249
+
250
+ # dbt docs generate
251
+ @docs.command("generate")
252
+ @click.pass_context
253
+ @global_flags
254
+ @p.compile_docs
255
+ @p.exclude
256
+ @p.profiles_dir
257
+ @p.project_dir
258
+ @p.select
259
+ @p.selector
260
+ @p.empty_catalog
261
+ @p.static
262
+ @p.target_path
263
+ @p.threads
264
+ @p.vars
265
+ @requires.postflight
266
+ @requires.preflight
267
+ @requires.profile
268
+ @requires.project
269
+ @requires.runtime_config
270
+ @requires.manifest(write=False)
271
+ def docs_generate(ctx, **kwargs):
272
+ """Generate the documentation website for your project"""
273
+ from dbt.task.docs.generate import GenerateTask
274
+
275
+ task = GenerateTask(
276
+ ctx.obj["flags"],
277
+ ctx.obj["runtime_config"],
278
+ ctx.obj["manifest"],
279
+ )
280
+
281
+ results = task.run()
282
+ success = task.interpret_results(results)
283
+ return results, success
284
+
285
+
286
+ # dbt docs serve
287
+ @docs.command("serve")
288
+ @click.pass_context
289
+ @global_flags
290
+ @p.browser
291
+ @p.host
292
+ @p.port
293
+ @p.profiles_dir
294
+ @p.project_dir
295
+ @p.target_path
296
+ @p.vars
297
+ @requires.postflight
298
+ @requires.preflight
299
+ @requires.profile
300
+ @requires.project
301
+ @requires.runtime_config
302
+ def docs_serve(ctx, **kwargs):
303
+ """Serve the documentation website for your project"""
304
+ from dbt.task.docs.serve import ServeTask
305
+
306
+ task = ServeTask(
307
+ ctx.obj["flags"],
308
+ ctx.obj["runtime_config"],
309
+ )
310
+
311
+ results = task.run()
312
+ success = task.interpret_results(results)
313
+ return results, success
314
+
315
+
316
+ # dbt compile
317
+ @cli.command("compile")
318
+ @click.pass_context
319
+ @global_flags
320
+ @p.exclude
321
+ @p.full_refresh
322
+ @p.show_output_format
323
+ @p.introspect
324
+ @p.profiles_dir
325
+ @p.project_dir
326
+ @p.empty
327
+ @p.select
328
+ @p.selector
329
+ @p.inline
330
+ @p.compile_inject_ephemeral_ctes
331
+ @p.target_path
332
+ @p.threads
333
+ @p.vars
334
+ @requires.postflight
335
+ @requires.preflight
336
+ @requires.profile
337
+ @requires.project
338
+ @requires.runtime_config
339
+ @requires.manifest
340
+ def compile(ctx, **kwargs):
341
+ """Generates executable SQL from source, model, test, and analysis files. Compiled SQL files are written to the
342
+ target/ directory."""
343
+ from dbt.task.compile import CompileTask
344
+
345
+ task = CompileTask(
346
+ ctx.obj["flags"],
347
+ ctx.obj["runtime_config"],
348
+ ctx.obj["manifest"],
349
+ )
350
+
351
+ results = task.run()
352
+ success = task.interpret_results(results)
353
+ return results, success
354
+
355
+
356
+ # dbt show
357
+ @cli.command("show")
358
+ @click.pass_context
359
+ @global_flags
360
+ @p.exclude
361
+ @p.full_refresh
362
+ @p.show_output_format
363
+ @p.show_limit
364
+ @p.introspect
365
+ @p.profiles_dir
366
+ @p.project_dir
367
+ @p.select
368
+ @p.selector
369
+ @p.inline
370
+ @p.inline_direct
371
+ @p.target_path
372
+ @p.threads
373
+ @p.vars
374
+ @requires.postflight
375
+ @requires.preflight
376
+ @requires.profile
377
+ @requires.project
378
+ @requires.runtime_config
379
+ def show(ctx, **kwargs):
380
+ """Generates executable SQL for a named resource or inline query, runs that SQL, and returns a preview of the
381
+ results. Does not materialize anything to the warehouse."""
382
+ from dbt.task.show import ShowTask, ShowTaskDirect
383
+
384
+ if ctx.obj["flags"].inline_direct:
385
+ # Issue the inline query directly, with no templating. Does not require
386
+ # loading the manifest.
387
+ register_adapter(ctx.obj["runtime_config"], get_mp_context())
388
+ task = ShowTaskDirect(
389
+ ctx.obj["flags"],
390
+ ctx.obj["runtime_config"],
391
+ )
392
+ else:
393
+ setup_manifest(ctx)
394
+ task = ShowTask(
395
+ ctx.obj["flags"],
396
+ ctx.obj["runtime_config"],
397
+ ctx.obj["manifest"],
398
+ )
399
+
400
+ results = task.run()
401
+ success = task.interpret_results(results)
402
+ return results, success
403
+
404
+
405
+ # dbt debug
406
+ @cli.command("debug")
407
+ @click.pass_context
408
+ @global_flags
409
+ @p.debug_connection
410
+ @p.config_dir
411
+ @p.profiles_dir_exists_false
412
+ @p.project_dir
413
+ @p.vars
414
+ @requires.postflight
415
+ @requires.preflight
416
+ def debug(ctx, **kwargs):
417
+ """Show information on the current dbt environment and check dependencies, then test the database connection. Not to be confused with the --debug option which increases verbosity."""
418
+ from dbt.task.debug import DebugTask
419
+
420
+ task = DebugTask(
421
+ ctx.obj["flags"],
422
+ )
423
+
424
+ results = task.run()
425
+ success = task.interpret_results(results)
426
+ return results, success
427
+
428
+
429
+ # dbt deps
430
+ @cli.command("deps")
431
+ @click.pass_context
432
+ @global_flags
433
+ @p.profiles_dir_exists_false
434
+ @p.project_dir
435
+ @p.vars
436
+ @p.source
437
+ @p.lock
438
+ @p.upgrade
439
+ @p.add_package
440
+ @requires.postflight
441
+ @requires.preflight
442
+ @requires.unset_profile
443
+ @requires.project
444
+ def deps(ctx, **kwargs):
445
+ """Install dbt packages specified.
446
+ In the following case, a new `package-lock.yml` will be generated and the packages are installed:
447
+ - user updated the packages.yml
448
+ - user specify the flag --update, which means for packages that are specified as a
449
+ range, dbt-core will try to install the newer version
450
+ Otherwise, deps will use `package-lock.yml` as source of truth to install packages.
451
+
452
+ There is a way to add new packages by providing an `--add-package` flag to deps command
453
+ which will allow user to specify a package they want to add in the format of packagename@version.
454
+ """
455
+ from dbt.task.deps import DepsTask
456
+
457
+ flags = ctx.obj["flags"]
458
+ if flags.ADD_PACKAGE:
459
+ if not flags.ADD_PACKAGE["version"] and flags.SOURCE != "local":
460
+ raise BadOptionUsage(
461
+ message=f"Version is required in --add-package when a package when source is {flags.SOURCE}",
462
+ option_name="--add-package",
463
+ )
464
+ with DepsTask(flags, ctx.obj["project"]) as task:
465
+ results = task.run()
466
+ success = task.interpret_results(results)
467
+ return results, success
468
+
469
+
470
+ # dbt init
471
+ @cli.command("init")
472
+ @click.pass_context
473
+ @global_flags
474
+ # for backwards compatibility, accept 'project_name' as an optional positional argument
475
+ @click.argument("project_name", required=False)
476
+ @p.profiles_dir_exists_false
477
+ @p.project_dir
478
+ @p.skip_profile_setup
479
+ @p.vars
480
+ @requires.postflight
481
+ @requires.preflight
482
+ def init(ctx, **kwargs):
483
+ """Initialize a new dbt project."""
484
+ from dbt.task.init import InitTask
485
+
486
+ with InitTask(ctx.obj["flags"]) as task:
487
+ results = task.run()
488
+ success = task.interpret_results(results)
489
+ return results, success
490
+
491
+
492
+ # dbt list
493
+ @cli.command("list")
494
+ @click.pass_context
495
+ @global_flags
496
+ @p.exclude
497
+ @p.models
498
+ @p.output
499
+ @p.output_keys
500
+ @p.profiles_dir
501
+ @p.project_dir
502
+ @p.resource_type
503
+ @p.exclude_resource_type
504
+ @p.raw_select
505
+ @p.selector
506
+ @p.target_path
507
+ @p.vars
508
+ @requires.postflight
509
+ @requires.preflight
510
+ @requires.profile
511
+ @requires.project
512
+ @requires.runtime_config
513
+ @requires.manifest
514
+ def list(ctx, **kwargs):
515
+ """List the resources in your project"""
516
+ from dbt.task.list import ListTask
517
+
518
+ task = ListTask(
519
+ ctx.obj["flags"],
520
+ ctx.obj["runtime_config"],
521
+ ctx.obj["manifest"],
522
+ )
523
+
524
+ results = task.run()
525
+ success = task.interpret_results(results)
526
+ return results, success
527
+
528
+
529
+ # Alias "list" to "ls"
530
+ ls = copy(cli.commands["list"])
531
+ ls.hidden = True
532
+ cli.add_command(ls, "ls")
533
+
534
+
535
+ # dbt parse
536
+ @cli.command("parse")
537
+ @click.pass_context
538
+ @global_flags
539
+ @p.profiles_dir
540
+ @p.project_dir
541
+ @p.target_path
542
+ @p.threads
543
+ @p.vars
544
+ @requires.postflight
545
+ @requires.preflight
546
+ @requires.profile
547
+ @requires.project
548
+ @requires.catalogs
549
+ @requires.runtime_config
550
+ @requires.manifest(write_perf_info=True)
551
+ def parse(ctx, **kwargs):
552
+ """Parses the project and provides information on performance"""
553
+ # manifest generation and writing happens in @requires.manifest
554
+ return ctx.obj["manifest"], True
555
+
556
+
557
+ # dbt run
558
+ @cli.command("run")
559
+ @click.pass_context
560
+ @global_flags
561
+ @p.exclude
562
+ @p.full_refresh
563
+ @p.profiles_dir
564
+ @p.project_dir
565
+ @p.empty
566
+ @p.event_time_start
567
+ @p.event_time_end
568
+ @p.sample
569
+ @p.select
570
+ @p.selector
571
+ @p.target_path
572
+ @p.threads
573
+ @p.vars
574
+ @requires.postflight
575
+ @requires.preflight
576
+ @requires.profile
577
+ @requires.project
578
+ @requires.catalogs
579
+ @requires.runtime_config
580
+ @requires.manifest
581
+ def run(ctx, **kwargs):
582
+ """Compile SQL and execute against the current target database.
583
+
584
+ DVT enhances dbt run with:
585
+ - Rich progress display with spinner and progress bar
586
+ - Per-model execution path display (PUSHDOWN vs FEDERATION)
587
+ - Beautiful summary panel with pass/fail/skip counts
588
+
589
+ DVT Compute Rules (automatically applied):
590
+ - Same-target models use adapter pushdown (native SQL)
591
+ - Cross-target models use Spark federation
592
+ - Compute selection: default < model config < CLI --target-compute
593
+ - Target selection: default < model config < CLI --target
594
+ """
595
+ from dbt.task.dvt_run import create_dvt_run_task
596
+
597
+ task = create_dvt_run_task(
598
+ ctx.obj["flags"],
599
+ ctx.obj["runtime_config"],
600
+ ctx.obj["manifest"],
601
+ )
602
+
603
+ results = task.run()
604
+ success = task.interpret_results(results)
605
+ return results, success
606
+
607
+
608
+ # dbt retry
609
+ @cli.command("retry")
610
+ @click.pass_context
611
+ @global_flags
612
+ @p.project_dir
613
+ @p.profiles_dir
614
+ @p.vars
615
+ @p.target_path
616
+ @p.threads
617
+ @p.full_refresh
618
+ @requires.postflight
619
+ @requires.preflight
620
+ @requires.profile
621
+ @requires.project
622
+ @requires.runtime_config
623
+ def retry(ctx, **kwargs):
624
+ """Retry the nodes that failed in the previous run."""
625
+ from dbt.task.retry import RetryTask
626
+
627
+ # Retry will parse manifest inside the task after we consolidate the flags
628
+ task = RetryTask(
629
+ ctx.obj["flags"],
630
+ ctx.obj["runtime_config"],
631
+ )
632
+
633
+ results = task.run()
634
+ success = task.interpret_results(results)
635
+ return results, success
636
+
637
+
638
+ # dbt clone
639
+ @cli.command("clone")
640
+ @click.pass_context
641
+ @global_flags
642
+ @p.exclude
643
+ @p.full_refresh
644
+ @p.profiles_dir
645
+ @p.project_dir
646
+ @p.resource_type
647
+ @p.exclude_resource_type
648
+ @p.select
649
+ @p.selector
650
+ @p.target_path
651
+ @p.threads
652
+ @p.vars
653
+ @requires.preflight
654
+ @requires.profile
655
+ @requires.project
656
+ @requires.runtime_config
657
+ @requires.manifest
658
+ @requires.postflight
659
+ def clone(ctx, **kwargs):
660
+ """Create clones of selected nodes based on their location in the manifest provided to --state."""
661
+ from dbt.task.clone import CloneTask
662
+
663
+ task = CloneTask(
664
+ ctx.obj["flags"],
665
+ ctx.obj["runtime_config"],
666
+ ctx.obj["manifest"],
667
+ )
668
+
669
+ results = task.run()
670
+ success = task.interpret_results(results)
671
+ return results, success
672
+
673
+
674
+ # dbt run operation
675
+ @cli.command("run-operation")
676
+ @click.pass_context
677
+ @global_flags
678
+ @click.argument("macro")
679
+ @p.args
680
+ @p.profiles_dir
681
+ @p.project_dir
682
+ @p.target_path
683
+ @p.threads
684
+ @p.vars
685
+ @requires.postflight
686
+ @requires.preflight
687
+ @requires.profile
688
+ @requires.project
689
+ @requires.runtime_config
690
+ @requires.manifest
691
+ def run_operation(ctx, **kwargs):
692
+ """Run the named macro with any supplied arguments."""
693
+ from dbt.task.run_operation import RunOperationTask
694
+
695
+ task = RunOperationTask(
696
+ ctx.obj["flags"],
697
+ ctx.obj["runtime_config"],
698
+ ctx.obj["manifest"],
699
+ )
700
+
701
+ results = task.run()
702
+ success = task.interpret_results(results)
703
+ return results, success
704
+
705
+
706
+ # dbt seed (DVT: Spark-powered with pattern transformations)
707
+ @cli.command("seed")
708
+ @click.pass_context
709
+ @global_flags
710
+ @p.exclude
711
+ @p.full_refresh
712
+ @p.profiles_dir
713
+ @p.project_dir
714
+ @p.select
715
+ @p.selector
716
+ @p.show
717
+ @p.target_path
718
+ @p.threads
719
+ @p.vars
720
+ @requires.postflight
721
+ @requires.preflight
722
+ @requires.profile
723
+ @requires.project
724
+ @requires.catalogs
725
+ @requires.runtime_config
726
+ @requires.manifest
727
+ def seed(ctx, **kwargs):
728
+ """Load data from csv files into your data warehouse.
729
+
730
+ DVT Enhancement: Uses Spark compute engine with automatic pattern-based
731
+ type transformations (e.g., "1.25%" → 0.0125 DOUBLE).
732
+
733
+ Use --target to specify the database target and --target-compute to
734
+ override the Spark compute engine from computes.yml.
735
+ """
736
+ from dbt.task.dvt_seed import DVTSeedTask
737
+
738
+ task = DVTSeedTask(
739
+ ctx.obj["flags"],
740
+ ctx.obj["runtime_config"],
741
+ ctx.obj["manifest"],
742
+ )
743
+ results = task.run()
744
+ success = task.interpret_results(results)
745
+ return results, success
746
+
747
+
748
+ # dbt snapshot
749
+ @cli.command("snapshot")
750
+ @click.pass_context
751
+ @global_flags
752
+ @p.empty
753
+ @p.exclude
754
+ @p.profiles_dir
755
+ @p.project_dir
756
+ @p.select
757
+ @p.selector
758
+ @p.target_path
759
+ @p.threads
760
+ @p.vars
761
+ @requires.postflight
762
+ @requires.preflight
763
+ @requires.profile
764
+ @requires.project
765
+ @requires.catalogs
766
+ @requires.runtime_config
767
+ @requires.manifest
768
+ def snapshot(ctx, **kwargs):
769
+ """Execute snapshots defined in your project"""
770
+ from dbt.task.snapshot import SnapshotTask
771
+
772
+ task = SnapshotTask(
773
+ ctx.obj["flags"],
774
+ ctx.obj["runtime_config"],
775
+ ctx.obj["manifest"],
776
+ )
777
+
778
+ results = task.run()
779
+ success = task.interpret_results(results)
780
+ return results, success
781
+
782
+
783
+ # dbt source
784
+ @cli.group()
785
+ @click.pass_context
786
+ @global_flags
787
+ def source(ctx, **kwargs):
788
+ """Manage your project's sources"""
789
+
790
+
791
+ # dbt source freshness
792
+ @source.command("freshness")
793
+ @click.pass_context
794
+ @global_flags
795
+ @p.exclude
796
+ @p.output_path # TODO: Is this ok to re-use? We have three different output params, how much can we consolidate?
797
+ @p.profiles_dir
798
+ @p.project_dir
799
+ @p.select
800
+ @p.selector
801
+ @p.target_path
802
+ @p.threads
803
+ @p.vars
804
+ @requires.postflight
805
+ @requires.preflight
806
+ @requires.profile
807
+ @requires.project
808
+ @requires.runtime_config
809
+ @requires.manifest
810
+ def freshness(ctx, **kwargs):
811
+ """check the current freshness of the project's sources"""
812
+ from dbt.task.freshness import FreshnessTask
813
+
814
+ task = FreshnessTask(
815
+ ctx.obj["flags"],
816
+ ctx.obj["runtime_config"],
817
+ ctx.obj["manifest"],
818
+ )
819
+
820
+ results = task.run()
821
+ success = task.interpret_results(results)
822
+ return results, success
823
+
824
+
825
+ # Alias "source freshness" to "snapshot-freshness"
826
+ snapshot_freshness = copy(cli.commands["source"].commands["freshness"]) # type: ignore
827
+ snapshot_freshness.hidden = True
828
+ cli.commands["source"].add_command(snapshot_freshness, "snapshot-freshness") # type: ignore
829
+
830
+
831
+ # dbt test
832
+ @cli.command("test")
833
+ @click.pass_context
834
+ @global_flags
835
+ @p.exclude
836
+ @p.resource_type
837
+ @p.exclude_resource_type
838
+ @p.profiles_dir
839
+ @p.project_dir
840
+ @p.select
841
+ @p.selector
842
+ @p.store_failures
843
+ @p.target_path
844
+ @p.threads
845
+ @p.vars
846
+ @requires.postflight
847
+ @requires.preflight
848
+ @requires.profile
849
+ @requires.project
850
+ @requires.runtime_config
851
+ @requires.manifest
852
+ def test(ctx, **kwargs):
853
+ """Runs tests on data in deployed models. Run this after `dbt run`"""
854
+ from dbt.task.test import TestTask
855
+
856
+ task = TestTask(
857
+ ctx.obj["flags"],
858
+ ctx.obj["runtime_config"],
859
+ ctx.obj["manifest"],
860
+ )
861
+
862
+ results = task.run()
863
+ success = task.interpret_results(results)
864
+ return results, success
865
+
866
+
867
+ # =============================================================================
868
+ # DVT Metadata Command Group (v0.57.0 - replaces dvt snap)
869
+ # =============================================================================
870
+
871
+ @cli.group("metadata")
872
+ def metadata():
873
+ """Manage DVT project metadata.
874
+
875
+ The metadata store captures type information for sources and models,
876
+ enabling accurate type mapping across federated queries.
877
+
878
+ Commands:
879
+ reset Clear all metadata from the store
880
+ snapshot Capture metadata for sources and models
881
+ export Display metadata in CLI
882
+ export-csv Export metadata to CSV file
883
+ export-json Export metadata to JSON file
884
+ """
885
+ pass
886
+
887
+
888
+ @metadata.command("reset")
889
+ @click.pass_context
890
+ @p.project_dir
891
+ def metadata_reset(ctx, project_dir, **kwargs):
892
+ """Clear all metadata from the store.
893
+
894
+ Removes all captured metadata including:
895
+ - Source column metadata
896
+ - Model column metadata
897
+ - Row counts
898
+ - Profile results
899
+
900
+ Example:
901
+ dvt metadata reset
902
+ """
903
+ from dbt.task.metadata import MetadataTask
904
+
905
+ class Args:
906
+ def __init__(self):
907
+ self.subcommand = 'reset'
908
+ self.project_dir = project_dir
909
+
910
+ task = MetadataTask(Args())
911
+ success, _ = task.run()
912
+ return None, success
913
+
914
+
915
+ @metadata.command("snapshot")
916
+ @click.pass_context
917
+ @p.project_dir
918
+ def metadata_snapshot(ctx, project_dir, **kwargs):
919
+ """Capture metadata for sources and models.
920
+
921
+ Reads source definitions (sources.yml) and model definitions (schema.yml)
922
+ from your project and captures column metadata into .dvt/metadata_store.duckdb.
923
+
924
+ This metadata is used by DVT to:
925
+ - Map adapter types to Spark types for federated queries
926
+ - Optimize query planning with schema information
927
+ - Generate correct Spark DDL for table creation
928
+
929
+ Examples:
930
+ dvt metadata snapshot
931
+
932
+ Note: This is automatically run on first 'dvt run' and on 'dvt run --full-refresh'.
933
+ """
934
+ from dbt.task.metadata import MetadataTask
935
+
936
+ class Args:
937
+ def __init__(self):
938
+ self.subcommand = 'snapshot'
939
+ self.project_dir = project_dir
940
+
941
+ task = MetadataTask(Args())
942
+ success, _ = task.run()
943
+ return None, success
944
+
945
+
946
+ @metadata.command("export")
947
+ @click.pass_context
948
+ @p.project_dir
949
+ def metadata_export(ctx, project_dir, **kwargs):
950
+ """Display metadata in CLI.
951
+
952
+ Shows a Rich-formatted table of all captured metadata including:
953
+ - Source/Model type
954
+ - Table names
955
+ - Column counts
956
+ - Last updated timestamp
957
+
958
+ Example:
959
+ dvt metadata export
960
+ """
961
+ from dbt.task.metadata import MetadataTask
962
+
963
+ class Args:
964
+ def __init__(self):
965
+ self.subcommand = 'export'
966
+ self.project_dir = project_dir
967
+
968
+ task = MetadataTask(Args())
969
+ success, _ = task.run()
970
+ return None, success
971
+
972
+
973
+ @metadata.command("export-csv")
974
+ @click.argument("filename", default="metadata.csv")
975
+ @click.pass_context
976
+ @p.project_dir
977
+ def metadata_export_csv(ctx, filename, project_dir, **kwargs):
978
+ """Export metadata to CSV file.
979
+
980
+ Exports all column metadata to a CSV file with columns:
981
+ type, source_name, table_name, column_name, adapter_type,
982
+ spark_type, is_nullable, is_primary_key, ordinal_position, last_refreshed
983
+
984
+ Examples:
985
+ dvt metadata export-csv # Creates metadata.csv
986
+ dvt metadata export-csv my_export.csv # Custom filename
987
+ """
988
+ from dbt.task.metadata import MetadataTask
989
+
990
+ class Args:
991
+ def __init__(self):
992
+ self.subcommand = 'export-csv'
993
+ self.project_dir = project_dir
994
+ self.filename = filename
995
+
996
+ task = MetadataTask(Args())
997
+ success, _ = task.run()
998
+ return None, success
999
+
1000
+
1001
+ @metadata.command("export-json")
1002
+ @click.argument("filename", default="metadata.json")
1003
+ @click.pass_context
1004
+ @p.project_dir
1005
+ def metadata_export_json(ctx, filename, project_dir, **kwargs):
1006
+ """Export metadata to JSON file.
1007
+
1008
+ Exports all metadata to a JSON file with structured format:
1009
+ - sources: grouped by source name with tables and columns
1010
+ - models: grouped by model name with columns
1011
+
1012
+ Examples:
1013
+ dvt metadata export-json # Creates metadata.json
1014
+ dvt metadata export-json my_export.json # Custom filename
1015
+ """
1016
+ from dbt.task.metadata import MetadataTask
1017
+
1018
+ class Args:
1019
+ def __init__(self):
1020
+ self.subcommand = 'export-json'
1021
+ self.project_dir = project_dir
1022
+ self.filename = filename
1023
+
1024
+ task = MetadataTask(Args())
1025
+ success, _ = task.run()
1026
+ return None, success
1027
+
1028
+
1029
+ # DVT profile command group (v0.58.0) - profiling + web UI
1030
+ @cli.group("profile")
1031
+ @click.pass_context
1032
+ @global_flags
1033
+ def profile(ctx, **kwargs):
1034
+ """Profile data sources and models, or serve the profile viewer.
1035
+
1036
+ \b
1037
+ Commands:
1038
+ run - Run data profiling (default)
1039
+ serve - Start profile viewer web UI
1040
+
1041
+ \b
1042
+ Examples:
1043
+ dvt profile run # Profile all sources
1044
+ dvt profile run --sample 10000 # Sample 10K rows
1045
+ dvt profile run --sample 10% # Sample 10% of rows
1046
+ dvt profile serve # Start web UI at http://localhost:8580
1047
+ """
1048
+ pass
1049
+
1050
+
1051
+ @profile.command("run")
1052
+ @click.pass_context
1053
+ @global_flags
1054
+ @p.exclude
1055
+ @p.profiles_dir
1056
+ @p.project_dir
1057
+ @p.select
1058
+ @p.selector
1059
+ @p.profile_sample
1060
+ @p.threads
1061
+ @p.vars
1062
+ @requires.postflight
1063
+ @requires.preflight
1064
+ @requires.profile
1065
+ @requires.project
1066
+ @requires.runtime_config
1067
+ @requires.manifest
1068
+ def profile_run(ctx, **kwargs):
1069
+ """Run data profiling on sources and models.
1070
+
1071
+ Works like 'dvt run' with DAG-based execution:
1072
+ - Respects --select and --exclude selectors
1073
+ - Respects --target and --target-compute overrides
1074
+ - Follows DVT compute rules (pushdown when possible)
1075
+
1076
+ \b
1077
+ Sampling (--sample):
1078
+ - Row count: --sample 10000 (profile 10K rows)
1079
+ - Percentage: --sample 10% (profile 10% of rows)
1080
+ - Default: All rows (no sampling)
1081
+
1082
+ \b
1083
+ Examples:
1084
+ dvt profile run # Profile all sources (all rows)
1085
+ dvt profile run --select "source:*" # Profile all sources
1086
+ dvt profile run --sample 10000 # Sample 10K rows per table
1087
+ dvt profile run --sample 10% # Sample 10% of each table
1088
+
1089
+ Results are saved to: .dvt/metadata_store.duckdb
1090
+ """
1091
+ from dbt.task.profile import ProfileTask
1092
+
1093
+ task = ProfileTask(
1094
+ ctx.obj["flags"],
1095
+ ctx.obj["runtime_config"],
1096
+ ctx.obj["manifest"],
1097
+ )
1098
+
1099
+ results = task.run()
1100
+ success = task.interpret_results(results)
1101
+ return results, success
1102
+
1103
+
1104
+ @profile.command("serve")
1105
+ @click.option("--port", default=8580, type=int, help="Port number for the profile viewer server")
1106
+ @click.option("--host", default="localhost", help="Host address to bind the server")
1107
+ @click.option("--no-browser", "no_browser", is_flag=True, default=False, help="Don't auto-open browser")
1108
+ @click.option("--project-dir", default=".", help="Project directory (defaults to current directory)")
1109
+ def profile_serve(port, host, no_browser, project_dir, **kwargs):
1110
+ """Start the profile viewer web UI.
1111
+
1112
+ Opens an interactive web interface to explore profiling results
1113
+ stored in .dvt/metadata_store.duckdb.
1114
+
1115
+ \b
1116
+ Features:
1117
+ - Summary statistics (tables, columns, sources, models)
1118
+ - Table browser with column details
1119
+ - Type mappings (adapter types to Spark types)
1120
+ - Beautiful dark theme UI
1121
+
1122
+ \b
1123
+ Examples:
1124
+ dvt profile serve # Start on http://localhost:8580
1125
+ dvt profile serve --port 9000 # Custom port
1126
+ dvt profile serve --no-browser # Don't auto-open browser
1127
+
1128
+ Note: Run 'dvt profile run' first to capture profiling data.
1129
+ """
1130
+ from pathlib import Path
1131
+ from dbt.task.profile_serve import serve_profile_ui
1132
+
1133
+ project_path = Path(project_dir)
1134
+ success = serve_profile_ui(
1135
+ project_dir=project_path,
1136
+ port=port,
1137
+ host=host,
1138
+ open_browser=not no_browser,
1139
+ )
1140
+ return None, success
1141
+
1142
+
1143
+ # DVT retract command - drop materialized models (v0.58.0)
1144
+ @cli.command("retract")
1145
+ @click.pass_context
1146
+ @global_flags
1147
+ @p.dry_run
1148
+ @p.exclude
1149
+ @p.profiles_dir
1150
+ @p.project_dir
1151
+ @p.select
1152
+ @p.selector
1153
+ @p.target_path
1154
+ @p.threads
1155
+ @p.vars
1156
+ @requires.postflight
1157
+ @requires.preflight
1158
+ @requires.profile
1159
+ @requires.project
1160
+ @requires.runtime_config
1161
+ @requires.manifest
1162
+ def retract(ctx, **kwargs):
1163
+ """Drop all materialized models from target databases.
1164
+
1165
+ Removes tables and views created by DVT from the target databases.
1166
+ Sources are never dropped. Use this to clean up a project or reset state.
1167
+
1168
+ \b
1169
+ WARNING: This command permanently deletes data. Use --dry-run first!
1170
+
1171
+ \b
1172
+ Examples:
1173
+ dvt retract --dry-run # Preview what would be dropped
1174
+ dvt retract # Drop all materialized models
1175
+ dvt retract --select "dim_*" # Drop matching models only
1176
+ dvt retract --exclude "fact_*" # Keep matching models
1177
+ dvt retract --target prod # Drop from specific target
1178
+ """
1179
+ from dbt.task.retract import RetractTask
1180
+
1181
+ task = RetractTask(
1182
+ ctx.obj["flags"],
1183
+ ctx.obj["runtime_config"],
1184
+ ctx.obj["manifest"],
1185
+ )
1186
+
1187
+ results = task.run()
1188
+ success = task.interpret_results(results)
1189
+ return results, success
1190
+
1191
+
1192
+ # DVT compute commands
1193
+ @cli.group()
1194
+ @click.pass_context
1195
+ @p.version
1196
+ def compute(ctx, **kwargs):
1197
+ """Manage DVT compute engines for multi-source federation.
1198
+
1199
+ Compute engines are configured in .dvt/computes.yml.
1200
+
1201
+ \b
1202
+ Commands:
1203
+ list - List all configured compute engines
1204
+ test - Test a compute engine's connectivity
1205
+ edit - Open computes.yml in your editor
1206
+ validate - Validate computes.yml syntax
1207
+
1208
+ \b
1209
+ Examples:
1210
+ dvt compute list # List all engines
1211
+ dvt compute test spark-local # Test local Spark
1212
+ dvt compute edit # Edit configuration
1213
+ """
1214
+
1215
+
1216
+ @compute.command("list")
1217
+ @click.pass_context
1218
+ @p.project_dir
1219
+ def compute_list(ctx, **kwargs):
1220
+ """List all configured compute engines.
1221
+
1222
+ Shows all compute engines defined in computes.yml with their
1223
+ platform type and description.
1224
+
1225
+ Examples:
1226
+ dvt compute list # List all compute engines
1227
+ """
1228
+ from dbt.task.compute import ComputeTask
1229
+
1230
+ task = ComputeTask(project_dir=kwargs.get("project_dir"))
1231
+ success = task.list_computes()
1232
+ return None, success
1233
+
1234
+
1235
+ @compute.command("test")
1236
+ @click.pass_context
1237
+ @click.argument("compute_name", required=True)
1238
+ @p.project_dir
1239
+ def compute_test(ctx, compute_name, **kwargs):
1240
+ """Test a compute engine's connectivity.
1241
+
1242
+ Tests the specified compute engine and shows its status.
1243
+ Use 'dvt compute list' to see all available compute engines.
1244
+
1245
+ Shows rich status symbols:
1246
+ ✅ Connected/Available
1247
+ ❌ Error/Not available
1248
+ ⚠️ Warning (missing optional dependency)
1249
+
1250
+ Examples:
1251
+ dvt compute test spark-local # Test local Spark
1252
+ dvt compute test databricks-prod # Test Databricks connectivity
1253
+ dvt compute test spark-docker # Test Docker Spark cluster
1254
+ """
1255
+ from dbt.task.compute import ComputeTask
1256
+
1257
+ task = ComputeTask(project_dir=kwargs.get("project_dir"))
1258
+ success = task.test_single_compute(compute_name)
1259
+ return None, success
1260
+
1261
+
1262
+ @compute.command("edit")
1263
+ @click.pass_context
1264
+ @p.project_dir
1265
+ def compute_edit(ctx, **kwargs):
1266
+ """Open computes.yml in your editor.
1267
+
1268
+ Opens the compute configuration file in your preferred editor.
1269
+ Uses EDITOR environment variable, or falls back to common editors
1270
+ (code, nano, vim, vi, notepad).
1271
+
1272
+ The file contains comprehensive commented samples for:
1273
+ - Local Spark (default)
1274
+ - Databricks (SQL Warehouse and Interactive Cluster)
1275
+ - AWS EMR
1276
+ - GCP Dataproc
1277
+ - Standalone Spark clusters
1278
+
1279
+ After editing, run 'dvt compute validate' to check syntax.
1280
+
1281
+ Examples:
1282
+ dvt compute edit # Open in default editor
1283
+ EDITOR=nano dvt compute edit # Use specific editor
1284
+ """
1285
+ from dbt.task.compute import ComputeTask
1286
+
1287
+ task = ComputeTask(project_dir=kwargs.get("project_dir"))
1288
+ success = task.edit_config()
1289
+ return None, success
1290
+
1291
+
1292
+ @compute.command("validate")
1293
+ @click.pass_context
1294
+ @p.project_dir
1295
+ def compute_validate(ctx, **kwargs):
1296
+ """Validate computes.yml syntax and configuration.
1297
+
1298
+ Checks the compute configuration file for:
1299
+ - Valid YAML syntax
1300
+ - Required fields (target_compute, type)
1301
+ - Valid compute engine references
1302
+ - Platform-specific configuration
1303
+
1304
+ Examples:
1305
+ dvt compute validate # Validate configuration
1306
+ """
1307
+ from dbt.task.compute import ComputeTask
1308
+
1309
+ task = ComputeTask(project_dir=kwargs.get("project_dir"))
1310
+ is_valid = task.validate_config()
1311
+ return None, is_valid
1312
+
1313
+
1314
+
1315
+
1316
+ # DVT migrate command - migrate from dbt to DVT or import dbt projects
1317
+ @cli.command("migrate")
1318
+ @click.pass_context
1319
+ @click.argument("source_path", required=False, type=click.Path(exists=True))
1320
+ @click.option("--profiles", "migrate_profiles", is_flag=True, help="Migrate profiles.yml only")
1321
+ @click.option("--project", "migrate_project", is_flag=True, help="Migrate dbt_project.yml only")
1322
+ @click.option("--full", "migrate_full", is_flag=True, help="Full migration (profiles + project)")
1323
+ @click.option("--dry-run", is_flag=True, help="Show what would be migrated without making changes")
1324
+ @p.profiles_dir
1325
+ @p.project_dir
1326
+ def migrate(ctx, source_path, migrate_profiles, migrate_project, migrate_full, dry_run, **kwargs):
1327
+ """Migrate or import dbt configuration into DVT.
1328
+
1329
+ \b
1330
+ Mode A - Convert dbt project to DVT (run from dbt project directory):
1331
+ dvt migrate # Auto-detect and migrate
1332
+ dvt migrate --profiles # Migrate profiles.yml only
1333
+ dvt migrate --project # Migrate dbt_project.yml only
1334
+ dvt migrate --full # Full migration
1335
+
1336
+ \b
1337
+ Mode B - Import dbt project INTO DVT (run from DVT project directory):
1338
+ dvt migrate /path/to/dbt_project # Import dbt project
1339
+ # Models copied to: models/<project_name>/
1340
+ # Targets merged into DVT profile
1341
+
1342
+ \b
1343
+ Options:
1344
+ dvt migrate --dry-run # Preview what would happen
1345
+ """
1346
+ from dbt.task.migrate import MigrateTask
1347
+
1348
+ task = MigrateTask(
1349
+ source_path=source_path,
1350
+ profiles_only=migrate_profiles,
1351
+ project_only=migrate_project,
1352
+ full=migrate_full,
1353
+ dry_run=dry_run,
1354
+ project_dir=kwargs.get("project_dir"),
1355
+ )
1356
+ success = task.run()
1357
+ return None, success
1358
+
1359
+
1360
+ # DVT target (connection) management commands
1361
+ @cli.group()
1362
+ @click.pass_context
1363
+ @p.version
1364
+ def target(ctx, **kwargs):
1365
+ """Manage connection targets in profiles.yml.
1366
+
1367
+ \b
1368
+ Commands:
1369
+ list - List available targets
1370
+ test - Test connection to one or all targets
1371
+ add - Add a new target to a profile
1372
+ remove - Remove a target from a profile
1373
+ sync - Sync adapters and JDBC JARs
1374
+
1375
+ \b
1376
+ Examples:
1377
+ dvt target list # List all targets
1378
+ dvt target test # Test all connections
1379
+ dvt target test dev # Test specific target
1380
+ """
1381
+
1382
+
1383
+ @target.command("list")
1384
+ @click.option("--profile", help="Profile name to list targets from")
1385
+ @click.pass_context
1386
+ @p.profiles_dir
1387
+ @p.project_dir
1388
+ def target_list(ctx, profile, profiles_dir, project_dir, **kwargs):
1389
+ """List available targets in profiles.yml.
1390
+
1391
+ If executed from within a DVT project directory, automatically detects the profile
1392
+ from dbt_project.yml. Use --profile to override or when outside a project directory.
1393
+
1394
+ Features colored output for improved readability:
1395
+ - Cyan: Profile and target names
1396
+ - Green: Default target indicators
1397
+ - Red: Error messages
1398
+
1399
+ Examples:
1400
+ dvt target list # Auto-detect from project
1401
+ dvt target list --profile my_proj # Specific profile
1402
+ """
1403
+ from dbt.config.profile import read_profile
1404
+ from dbt.config.project_utils import get_project_profile_name
1405
+
1406
+ profiles = read_profile(profiles_dir)
1407
+
1408
+ if not profiles:
1409
+ click.echo(click.style("No profiles found in profiles.yml", fg="red"))
1410
+ ctx.exit(1)
1411
+
1412
+ # If --profile not provided, try to get from dvt_project.yml or dbt_project.yml
1413
+ if not profile:
1414
+ profile = get_project_profile_name(project_dir)
1415
+ if not profile:
1416
+ # v0.59.0a18: Don't show all profiles - that breaks multi-project setups
1417
+ # User must be in a project directory or specify --profile
1418
+ click.echo(click.style("✗ Not in a DVT project directory", fg="red"))
1419
+ click.echo("")
1420
+ click.echo("Run from within a DVT project directory, or use --profile:")
1421
+ click.echo(" dvt target list --profile <profile_name>")
1422
+ click.echo("")
1423
+ click.echo("To create a new project: dvt init <project_name>")
1424
+ ctx.exit(1)
1425
+
1426
+ # Show targets for specific profile
1427
+ if profile not in profiles:
1428
+ click.echo(click.style(f"✗ Profile '{profile}' not found", fg="red"))
1429
+ ctx.exit(1)
1430
+
1431
+ profile_data = profiles[profile]
1432
+ if profile_data is None:
1433
+ click.echo(click.style(f"✗ Profile '{profile}' is empty or invalid", fg="red"))
1434
+ ctx.exit(1)
1435
+ outputs = profile_data.get("outputs", {})
1436
+
1437
+ if not outputs:
1438
+ click.echo(click.style(f"No targets found in profile '{profile}'", fg="yellow"))
1439
+ return True, True
1440
+
1441
+ default_target = profile_data.get(
1442
+ "default_target", profile_data.get("target", "unknown")
1443
+ )
1444
+
1445
+ # Always show profile name header for context
1446
+ click.echo(click.style(f"Profile: {profile}", fg="cyan", bold=True))
1447
+ click.echo(f"Default target: {click.style(default_target, fg='green')}")
1448
+ click.echo("")
1449
+ click.echo("Available targets:")
1450
+ for target_name, target_config in outputs.items():
1451
+ default_marker = (
1452
+ click.style(" (default)", fg="green")
1453
+ if target_name == default_target
1454
+ else ""
1455
+ )
1456
+ adapter_type = target_config.get("type", "unknown")
1457
+ click.echo(
1458
+ f" {click.style(target_name, fg='cyan')} ({adapter_type}){default_marker}"
1459
+ )
1460
+
1461
+ return True, True
1462
+
1463
+
1464
+ def _get_test_query(adapter_type: str) -> str:
1465
+ """Get adapter-specific test query for connection validation.
1466
+
1467
+ Args:
1468
+ adapter_type: The adapter type (postgres, snowflake, etc.)
1469
+
1470
+ Returns:
1471
+ SQL query string for testing connectivity
1472
+ """
1473
+ test_queries = {
1474
+ # Standard SELECT 1
1475
+ "postgres": "SELECT 1",
1476
+ "mysql": "SELECT 1",
1477
+ "redshift": "SELECT 1",
1478
+ "databricks": "SELECT 1",
1479
+ "duckdb": "SELECT 1",
1480
+ "clickhouse": "SELECT 1",
1481
+ "trino": "SELECT 1",
1482
+ "presto": "SELECT 1",
1483
+ "athena": "SELECT 1",
1484
+ "spark": "SELECT 1",
1485
+ "sqlserver": "SELECT 1",
1486
+ # Snowflake has a nice version function
1487
+ "snowflake": "SELECT CURRENT_VERSION()",
1488
+ # BigQuery
1489
+ "bigquery": "SELECT 1",
1490
+ # Oracle requires FROM DUAL
1491
+ "oracle": "SELECT 1 FROM DUAL",
1492
+ # DB2 requires SYSIBM.SYSDUMMY1
1493
+ "db2": "SELECT 1 FROM SYSIBM.SYSDUMMY1",
1494
+ # Teradata
1495
+ "teradata": "SELECT 1",
1496
+ # SAP HANA requires FROM DUMMY
1497
+ "saphana": "SELECT 1 FROM DUMMY",
1498
+ # Vertica
1499
+ "vertica": "SELECT 1",
1500
+ # Exasol
1501
+ "exasol": "SELECT 1 FROM DUAL",
1502
+ # SingleStore (formerly MemSQL)
1503
+ "singlestore": "SELECT 1",
1504
+ # CockroachDB (Postgres-compatible)
1505
+ "cockroachdb": "SELECT 1",
1506
+ # TimescaleDB (Postgres-compatible)
1507
+ "timescale": "SELECT 1",
1508
+ # Greenplum (Postgres-compatible)
1509
+ "greenplum": "SELECT 1",
1510
+ }
1511
+ return test_queries.get(adapter_type, "SELECT 1")
1512
+
1513
+
1514
+ def _get_connection_error_hint(exception: Exception, adapter_type: str) -> str:
1515
+ """Provide user-friendly hints for common connection errors.
1516
+
1517
+ Args:
1518
+ exception: The exception that was raised
1519
+ adapter_type: The adapter type being tested
1520
+
1521
+ Returns:
1522
+ A helpful error message with troubleshooting hints
1523
+ """
1524
+ error_str = str(exception).lower()
1525
+
1526
+ # Common error patterns and hints
1527
+ if "timeout" in error_str or "timed out" in error_str:
1528
+ return "Connection timeout - Check network connectivity and firewall rules"
1529
+ elif "could not connect" in error_str or "connection refused" in error_str:
1530
+ return "Connection refused - Verify host and port are correct"
1531
+ elif (
1532
+ "authentication" in error_str or "password" in error_str or "login" in error_str
1533
+ ):
1534
+ return "Authentication failed - Check username and password"
1535
+ elif "database" in error_str and "does not exist" in error_str:
1536
+ return "Database not found - Verify database name"
1537
+ elif "permission" in error_str or "access denied" in error_str:
1538
+ return "Permission denied - Check user privileges"
1539
+ elif "ssl" in error_str or "certificate" in error_str:
1540
+ return "SSL/TLS error - Check SSL configuration"
1541
+ elif "no such host" in error_str or "name resolution" in error_str:
1542
+ return "Host not found - Verify hostname is correct"
1543
+
1544
+ # Adapter-specific hints
1545
+ if adapter_type == "snowflake":
1546
+ if "account" in error_str:
1547
+ return "Invalid Snowflake account - Check account identifier format"
1548
+ elif "warehouse" in error_str:
1549
+ return "Warehouse error - Verify warehouse name and status"
1550
+ elif adapter_type == "databricks":
1551
+ if "token" in error_str:
1552
+ return "Invalid token - Check Databricks access token"
1553
+ elif "cluster" in error_str:
1554
+ return "Cluster error - Verify cluster is running and accessible"
1555
+
1556
+ return "Connection failed - See error details above"
1557
+
1558
+
1559
+ def _test_single_target(profile_data: dict, target_name: str) -> tuple[bool, str]:
1560
+ """Test connection to a single target using dbt adapters (preferred) or native drivers.
1561
+
1562
+ v0.59.0a22: Uses dbt adapter first (handles env vars, credential resolution),
1563
+ falls back to native drivers only if adapter unavailable.
1564
+
1565
+ Args:
1566
+ profile_data: The profile dictionary containing outputs
1567
+ target_name: Name of the target to test
1568
+
1569
+ Returns:
1570
+ Tuple of (success: bool, message: str)
1571
+ """
1572
+ outputs = profile_data.get("outputs", {})
1573
+
1574
+ if target_name not in outputs:
1575
+ return False, f"Target '{target_name}' not found"
1576
+
1577
+ target_config = outputs[target_name]
1578
+ adapter_type = target_config.get("type", "unknown")
1579
+
1580
+ # First try dbt adapter (handles env vars, credential resolution properly)
1581
+ try:
1582
+ result = _test_via_dbt_adapter(profile_data, target_name, adapter_type)
1583
+ # If adapter test succeeded or failed with auth error, return that result
1584
+ if result[0] or "authentication" in result[1].lower() or "password" in result[1].lower():
1585
+ return result
1586
+ # If adapter not installed, fall through to native driver
1587
+ if "not installed" not in result[1].lower():
1588
+ return result
1589
+ except Exception:
1590
+ pass # Fall through to native driver
1591
+
1592
+ # Fallback to native drivers (for when dbt adapter not available)
1593
+ test_query = _get_test_query(adapter_type)
1594
+
1595
+ try:
1596
+ # =========== PostgreSQL ===========
1597
+ if adapter_type == "postgres":
1598
+ try:
1599
+ import psycopg2
1600
+ # Handle both 'password' and 'pass' keys (dbt-postgres accepts both)
1601
+ password = target_config.get("password") or target_config.get("pass")
1602
+ conn = psycopg2.connect(
1603
+ host=target_config.get("host", "localhost"),
1604
+ port=target_config.get("port", 5432),
1605
+ database=target_config.get("database", target_config.get("dbname")),
1606
+ user=target_config.get("user"),
1607
+ password=password,
1608
+ connect_timeout=10
1609
+ )
1610
+ cursor = conn.cursor()
1611
+ cursor.execute(test_query)
1612
+ cursor.fetchone()
1613
+ cursor.close()
1614
+ conn.close()
1615
+ return True, "Connection successful"
1616
+ except ImportError:
1617
+ return False, "psycopg2 not installed - Run 'pip install psycopg2-binary'"
1618
+ except Exception as e:
1619
+ return False, f"Connection failed: {str(e)}"
1620
+
1621
+ # =========== Snowflake ===========
1622
+ elif adapter_type == "snowflake":
1623
+ try:
1624
+ import snowflake.connector
1625
+ conn = snowflake.connector.connect(
1626
+ account=target_config.get("account"),
1627
+ user=target_config.get("user"),
1628
+ password=target_config.get("password"),
1629
+ database=target_config.get("database"),
1630
+ warehouse=target_config.get("warehouse"),
1631
+ schema=target_config.get("schema", "PUBLIC"),
1632
+ login_timeout=10
1633
+ )
1634
+ cursor = conn.cursor()
1635
+ cursor.execute(test_query)
1636
+ cursor.fetchone()
1637
+ cursor.close()
1638
+ conn.close()
1639
+ return True, "Connection successful"
1640
+ except ImportError:
1641
+ return False, "snowflake-connector-python not installed - Run 'pip install snowflake-connector-python'"
1642
+ except Exception as e:
1643
+ return False, f"Connection failed: {str(e)}"
1644
+
1645
+ # =========== Databricks ===========
1646
+ elif adapter_type == "databricks":
1647
+ try:
1648
+ from databricks import sql
1649
+ conn = sql.connect(
1650
+ server_hostname=target_config.get("host", "").replace("https://", ""),
1651
+ http_path=target_config.get("http_path"),
1652
+ access_token=target_config.get("token"),
1653
+ )
1654
+ cursor = conn.cursor()
1655
+ cursor.execute(test_query)
1656
+ cursor.fetchone()
1657
+ cursor.close()
1658
+ conn.close()
1659
+ return True, "Connection successful"
1660
+ except ImportError:
1661
+ return False, "databricks-sql-connector not installed - Run 'pip install databricks-sql-connector'"
1662
+ except Exception as e:
1663
+ return False, f"Connection failed: {str(e)}"
1664
+
1665
+ # =========== BigQuery ===========
1666
+ elif adapter_type == "bigquery":
1667
+ try:
1668
+ from google.cloud import bigquery
1669
+ client = bigquery.Client(project=target_config.get("project"))
1670
+ query_job = client.query(test_query)
1671
+ query_job.result(timeout=10)
1672
+ return True, "Connection successful"
1673
+ except ImportError:
1674
+ return False, "google-cloud-bigquery not installed - Run 'pip install google-cloud-bigquery'"
1675
+ except Exception as e:
1676
+ return False, f"Connection failed: {str(e)}"
1677
+
1678
+ # =========== Redshift ===========
1679
+ elif adapter_type == "redshift":
1680
+ try:
1681
+ import psycopg2
1682
+ # Handle both 'password' and 'pass' keys
1683
+ password = target_config.get("password") or target_config.get("pass")
1684
+ conn = psycopg2.connect(
1685
+ host=target_config.get("host"),
1686
+ port=target_config.get("port", 5439),
1687
+ database=target_config.get("database"),
1688
+ user=target_config.get("user"),
1689
+ password=password,
1690
+ connect_timeout=10
1691
+ )
1692
+ cursor = conn.cursor()
1693
+ cursor.execute(test_query)
1694
+ cursor.fetchone()
1695
+ cursor.close()
1696
+ conn.close()
1697
+ return True, "Connection successful"
1698
+ except ImportError:
1699
+ return False, "psycopg2 not installed - Run 'pip install psycopg2-binary'"
1700
+ except Exception as e:
1701
+ return False, f"Connection failed: {str(e)}"
1702
+
1703
+ # =========== DuckDB ===========
1704
+ elif adapter_type == "duckdb":
1705
+ try:
1706
+ import duckdb
1707
+ from pathlib import Path
1708
+ db_path = target_config.get("path", ":memory:")
1709
+ if db_path != ":memory:":
1710
+ db_path = str(Path(db_path).expanduser().resolve())
1711
+ conn = duckdb.connect(db_path)
1712
+ conn.execute(test_query).fetchone()
1713
+ conn.close()
1714
+ return True, "Connection successful"
1715
+ except ImportError:
1716
+ return False, "duckdb not installed - Run 'pip install duckdb'"
1717
+ except Exception as e:
1718
+ return False, f"Connection failed: {str(e)}"
1719
+
1720
+ # =========== MySQL ===========
1721
+ elif adapter_type == "mysql":
1722
+ try:
1723
+ import mysql.connector
1724
+ # Handle both 'password' and 'pass' keys
1725
+ password = target_config.get("password") or target_config.get("pass")
1726
+ conn = mysql.connector.connect(
1727
+ host=target_config.get("host", "localhost"),
1728
+ port=target_config.get("port", 3306),
1729
+ database=target_config.get("database", target_config.get("schema")),
1730
+ user=target_config.get("user"),
1731
+ password=password,
1732
+ connection_timeout=10
1733
+ )
1734
+ cursor = conn.cursor()
1735
+ cursor.execute(test_query)
1736
+ cursor.fetchone()
1737
+ cursor.close()
1738
+ conn.close()
1739
+ return True, "Connection successful"
1740
+ except ImportError:
1741
+ return False, "mysql-connector-python not installed - Run 'pip install mysql-connector-python'"
1742
+ except Exception as e:
1743
+ return False, f"Connection failed: {str(e)}"
1744
+
1745
+ # =========== SQL Server ===========
1746
+ elif adapter_type == "sqlserver":
1747
+ try:
1748
+ import pyodbc
1749
+ driver = target_config.get("driver", "ODBC Driver 18 for SQL Server")
1750
+ # Handle both 'password' and 'pass' keys
1751
+ password = target_config.get("password") or target_config.get("pass")
1752
+ conn_str = (
1753
+ f"DRIVER={{{driver}}};"
1754
+ f"SERVER={target_config.get('host')},{target_config.get('port', 1433)};"
1755
+ f"DATABASE={target_config.get('database')};"
1756
+ f"UID={target_config.get('user')};"
1757
+ f"PWD={password};"
1758
+ f"TrustServerCertificate=yes;"
1759
+ )
1760
+ conn = pyodbc.connect(conn_str, timeout=10)
1761
+ cursor = conn.cursor()
1762
+ cursor.execute(test_query)
1763
+ cursor.fetchone()
1764
+ cursor.close()
1765
+ conn.close()
1766
+ return True, "Connection successful"
1767
+ except ImportError:
1768
+ return False, "pyodbc not installed - Run 'pip install pyodbc'"
1769
+ except Exception as e:
1770
+ return False, f"Connection failed: {str(e)}"
1771
+
1772
+ # =========== Oracle ===========
1773
+ elif adapter_type == "oracle":
1774
+ try:
1775
+ import oracledb
1776
+ # Handle both 'password' and 'pass' keys
1777
+ password = target_config.get("password") or target_config.get("pass")
1778
+ conn = oracledb.connect(
1779
+ user=target_config.get("user"),
1780
+ password=password,
1781
+ dsn=f"{target_config.get('host')}:{target_config.get('port', 1521)}/{target_config.get('database', target_config.get('service'))}",
1782
+ )
1783
+ cursor = conn.cursor()
1784
+ cursor.execute(test_query)
1785
+ cursor.fetchone()
1786
+ cursor.close()
1787
+ conn.close()
1788
+ return True, "Connection successful"
1789
+ except ImportError:
1790
+ return False, "oracledb not installed - Run 'pip install oracledb'"
1791
+ except Exception as e:
1792
+ return False, f"Connection failed: {str(e)}"
1793
+
1794
+ # =========== ClickHouse ===========
1795
+ elif adapter_type == "clickhouse":
1796
+ try:
1797
+ import clickhouse_connect
1798
+ # Handle both 'password' and 'pass' keys
1799
+ password = target_config.get("password") or target_config.get("pass")
1800
+ client = clickhouse_connect.get_client(
1801
+ host=target_config.get("host", "localhost"),
1802
+ port=target_config.get("port", 8123),
1803
+ username=target_config.get("user"),
1804
+ password=password,
1805
+ database=target_config.get("database", "default"),
1806
+ )
1807
+ client.query(test_query)
1808
+ client.close()
1809
+ return True, "Connection successful"
1810
+ except ImportError:
1811
+ return False, "clickhouse-connect not installed - Run 'pip install clickhouse-connect'"
1812
+ except Exception as e:
1813
+ return False, f"Connection failed: {str(e)}"
1814
+
1815
+ # =========== Trino / Presto ===========
1816
+ elif adapter_type in ("trino", "presto"):
1817
+ try:
1818
+ from trino.dbapi import connect
1819
+ conn = connect(
1820
+ host=target_config.get("host"),
1821
+ port=target_config.get("port", 8080),
1822
+ user=target_config.get("user"),
1823
+ catalog=target_config.get("catalog", "hive"),
1824
+ schema=target_config.get("schema", "default"),
1825
+ )
1826
+ cursor = conn.cursor()
1827
+ cursor.execute(test_query)
1828
+ cursor.fetchone()
1829
+ cursor.close()
1830
+ conn.close()
1831
+ return True, "Connection successful"
1832
+ except ImportError:
1833
+ return False, "trino not installed - Run 'pip install trino'"
1834
+ except Exception as e:
1835
+ return False, f"Connection failed: {str(e)}"
1836
+
1837
+ # =========== Athena ===========
1838
+ elif adapter_type == "athena":
1839
+ try:
1840
+ import pyathena
1841
+ conn = pyathena.connect(
1842
+ s3_staging_dir=target_config.get("s3_staging_dir"),
1843
+ region_name=target_config.get("region_name"),
1844
+ schema_name=target_config.get("schema", target_config.get("database", "default")),
1845
+ )
1846
+ cursor = conn.cursor()
1847
+ cursor.execute(test_query)
1848
+ cursor.fetchone()
1849
+ cursor.close()
1850
+ conn.close()
1851
+ return True, "Connection successful"
1852
+ except ImportError:
1853
+ return False, "pyathena not installed - Run 'pip install pyathena'"
1854
+ except Exception as e:
1855
+ return False, f"Connection failed: {str(e)}"
1856
+
1857
+ # =========== Spark ===========
1858
+ elif adapter_type == "spark":
1859
+ try:
1860
+ from pyspark.sql import SparkSession
1861
+ spark = SparkSession.builder.appName("DVT Connection Test").getOrCreate()
1862
+ spark.sql(test_query).collect()
1863
+ spark.stop()
1864
+ return True, "Connection successful"
1865
+ except ImportError:
1866
+ return False, "pyspark not installed - Run 'pip install pyspark'"
1867
+ except Exception as e:
1868
+ return False, f"Connection failed: {str(e)}"
1869
+
1870
+ # =========== Fallback: Try dbt adapter ===========
1871
+ else:
1872
+ try:
1873
+ return _test_via_dbt_adapter(profile_data, target_name, adapter_type)
1874
+ except Exception:
1875
+ return True, f"Configuration valid (testing not available for '{adapter_type}')"
1876
+
1877
+ except Exception as e:
1878
+ return False, f"Unexpected error: {str(e)}"
1879
+
1880
+
1881
+ def _test_via_dbt_adapter(
1882
+ profile_data: dict, target_name: str, adapter_type: str
1883
+ ) -> tuple[bool, str]:
1884
+ """Test connection using dbt's adapter infrastructure.
1885
+
1886
+ This fallback method works for ANY dbt adapter that is installed.
1887
+ It creates an actual adapter instance and tests the connection,
1888
+ providing full support for all 30+ dbt adapters.
1889
+
1890
+ Args:
1891
+ profile_data: The profile dictionary
1892
+ target_name: Name of the target
1893
+ adapter_type: The adapter type
1894
+
1895
+ Returns:
1896
+ Tuple of (success: bool, message: str)
1897
+ """
1898
+ try:
1899
+ from dbt.adapters.factory import get_adapter_class_by_name
1900
+ from dbt.config.runtime import RuntimeConfig
1901
+ from dbt.flags import set_from_args
1902
+ from argparse import Namespace
1903
+ import tempfile
1904
+ import yaml
1905
+ import os
1906
+
1907
+ # Check if adapter is available
1908
+ try:
1909
+ adapter_cls = get_adapter_class_by_name(adapter_type)
1910
+ except Exception:
1911
+ return False, f"Adapter 'dbt-{adapter_type}' not installed - Run 'pip install dbt-{adapter_type}'"
1912
+
1913
+ if not adapter_cls:
1914
+ return False, f"Adapter 'dbt-{adapter_type}' not installed"
1915
+
1916
+ # Try to create a connection using the adapter
1917
+ # This requires creating a minimal runtime config
1918
+ try:
1919
+ # Create a temporary profiles.yml with just this profile/target
1920
+ with tempfile.TemporaryDirectory() as tmpdir:
1921
+ # Create minimal profiles.yml
1922
+ profiles_path = os.path.join(tmpdir, "profiles.yml")
1923
+ minimal_profile = {
1924
+ "test_profile": {
1925
+ "target": target_name,
1926
+ "outputs": {target_name: profile_data.get("outputs", {}).get(target_name, {})}
1927
+ }
1928
+ }
1929
+ with open(profiles_path, "w") as f:
1930
+ yaml.dump(minimal_profile, f)
1931
+
1932
+ # Create minimal dbt_project.yml
1933
+ project_path = os.path.join(tmpdir, "dbt_project.yml")
1934
+ with open(project_path, "w") as f:
1935
+ yaml.dump({
1936
+ "name": "connection_test",
1937
+ "version": "1.0.0",
1938
+ "profile": "test_profile",
1939
+ }, f)
1940
+
1941
+ # Try to get adapter and test connection
1942
+ # Set minimal flags
1943
+ args = Namespace(
1944
+ profiles_dir=tmpdir,
1945
+ project_dir=tmpdir,
1946
+ target=target_name,
1947
+ profile="test_profile",
1948
+ threads=1,
1949
+ vars="{}",
1950
+ )
1951
+ set_from_args(args, {})
1952
+
1953
+ # Load runtime config and get adapter
1954
+ config = RuntimeConfig.from_args(args)
1955
+ adapter = adapter_cls(config, config.get_macro_resolver())
1956
+
1957
+ # Test the connection
1958
+ with adapter.connection_named("test_connection"):
1959
+ # Connection was successful if we get here
1960
+ pass
1961
+
1962
+ adapter.cleanup_connections()
1963
+ return True, "Connection successful (via dbt adapter)"
1964
+
1965
+ except Exception as conn_error:
1966
+ # Connection test failed, but adapter is available
1967
+ error_msg = str(conn_error)
1968
+ if "authentication" in error_msg.lower() or "password" in error_msg.lower():
1969
+ return False, f"Authentication failed: {error_msg}"
1970
+ elif "connection" in error_msg.lower() or "timeout" in error_msg.lower():
1971
+ return False, f"Connection failed: {error_msg}"
1972
+ else:
1973
+ # For other errors, at least confirm adapter is installed
1974
+ return True, f"Adapter '{adapter_type}' available (connection test inconclusive: {error_msg})"
1975
+
1976
+ except ImportError as e:
1977
+ return False, f"Adapter 'dbt-{adapter_type}' not installed - Run 'pip install dbt-{adapter_type}'"
1978
+ except Exception as e:
1979
+ return False, f"Could not validate adapter: {str(e)}"
1980
+
1981
+
1982
+ def _test_target_with_timeout(
1983
+ profile_data: dict, target_name: str, timeout: int = 30
1984
+ ) -> tuple[bool, str]:
1985
+ """Test target connection with timeout protection.
1986
+
1987
+ Args:
1988
+ profile_data: The profile dictionary
1989
+ target_name: Name of the target to test
1990
+ timeout: Timeout in seconds (default 30)
1991
+
1992
+ Returns:
1993
+ Tuple of (success: bool, message: str)
1994
+ """
1995
+ with ThreadPoolExecutor(max_workers=1) as executor:
1996
+ future = executor.submit(_test_single_target, profile_data, target_name)
1997
+ try:
1998
+ success, message = future.result(timeout=timeout)
1999
+ return success, message
2000
+ except FuturesTimeoutError:
2001
+ return False, f"Connection test timed out after {timeout} seconds"
2002
+ except Exception as e:
2003
+ return False, f"Unexpected error during connection test: {str(e)}"
2004
+
2005
+
2006
+ @target.command("test")
2007
+ @click.argument("target_name", required=False, default=None)
2008
+ @click.option("--profile", help="Profile name (defaults to project profile)")
2009
+ @click.option(
2010
+ "--timeout",
2011
+ type=int,
2012
+ default=30,
2013
+ help="Connection timeout in seconds (default: 30)",
2014
+ )
2015
+ @click.pass_context
2016
+ @p.profiles_dir
2017
+ @p.project_dir
2018
+ def target_test(
2019
+ ctx, target_name, profile, timeout, profiles_dir, project_dir, **kwargs
2020
+ ):
2021
+ """Test connection to one or all targets.
2022
+
2023
+ When TARGET_NAME is provided: Tests connection to a specific target
2024
+ When TARGET_NAME is omitted: Tests all targets in the profile
2025
+
2026
+ This command now performs REAL connection testing by executing a simple query
2027
+ against the target database. It validates both configuration AND network connectivity.
2028
+
2029
+ Features colored output and proper exit codes:
2030
+ - Exit code 0: All connections succeeded
2031
+ - Exit code 1: One or more connections failed
2032
+ - Green checkmarks (✓): Success
2033
+ - Red X marks (✗): Errors
2034
+
2035
+ Examples:
2036
+ dvt target test # Test ALL targets (auto-detect profile)
2037
+ dvt target test dev # Test specific target (auto-detect profile)
2038
+ dvt target test prod --profile my_proj # Test specific target (explicit profile)
2039
+ dvt target test --profile my_proj # Test all targets in profile
2040
+ dvt target test dev --timeout 60 # Custom timeout
2041
+
2042
+ Performance:
2043
+ - Tests run with configurable timeout (default 30s)
2044
+ - Provides helpful error hints for common connection issues
2045
+ - Shows detailed connection information on success
2046
+ """
2047
+ from dbt.config.profile import read_profile
2048
+ from dbt.config.project_utils import get_project_profile_name
2049
+
2050
+ profiles = read_profile(profiles_dir)
2051
+
2052
+ # Determine which profile to use
2053
+ if not profile:
2054
+ # Try to get from dvt_project.yml or dbt_project.yml
2055
+ profile = get_project_profile_name(project_dir)
2056
+
2057
+ # v0.59.0a17: Do NOT search all profiles - that breaks multi-project setups
2058
+ # User must be in a project directory or use --profile flag
2059
+
2060
+ if not profile:
2061
+ click.echo(
2062
+ click.style(
2063
+ "✗ Error: Could not determine profile. Use --profile flag.", fg="red"
2064
+ )
2065
+ )
2066
+ ctx.exit(1)
2067
+
2068
+ if profile not in profiles:
2069
+ click.echo(click.style(f"✗ Profile '{profile}' not found", fg="red"))
2070
+ ctx.exit(1)
2071
+
2072
+ profile_data = profiles[profile]
2073
+ if profile_data is None:
2074
+ click.echo(click.style(f"✗ Profile '{profile}' is empty or invalid", fg="red"))
2075
+ ctx.exit(1)
2076
+ outputs = profile_data.get("outputs", {})
2077
+
2078
+ if not outputs:
2079
+ click.echo(click.style(f"No targets found in profile '{profile}'", fg="yellow"))
2080
+ ctx.exit(0)
2081
+
2082
+ # CASE 1: Test specific target
2083
+ if target_name:
2084
+ if target_name not in outputs:
2085
+ click.echo(
2086
+ click.style(
2087
+ f"✗ Target '{target_name}' not found in profile '{profile}'",
2088
+ fg="red",
2089
+ )
2090
+ )
2091
+ ctx.exit(1)
2092
+
2093
+ target_config = outputs[target_name]
2094
+ adapter_type = target_config.get("type", "unknown")
2095
+
2096
+ click.echo(
2097
+ f"Testing connection: {click.style(target_name, fg='cyan')} ({adapter_type})"
2098
+ )
2099
+
2100
+ # Show connection details FIRST (like dbt debug)
2101
+ if "host" in target_config:
2102
+ click.echo(f" host: {target_config['host']}")
2103
+ if "port" in target_config:
2104
+ click.echo(f" port: {target_config['port']}")
2105
+ if "account" in target_config:
2106
+ click.echo(f" account: {target_config['account']}")
2107
+ if "database" in target_config or "dbname" in target_config:
2108
+ db = target_config.get("database") or target_config.get("dbname")
2109
+ click.echo(f" database: {db}")
2110
+ if "warehouse" in target_config:
2111
+ click.echo(f" warehouse: {target_config['warehouse']}")
2112
+ if "schema" in target_config:
2113
+ click.echo(f" schema: {target_config['schema']}")
2114
+ if "project" in target_config:
2115
+ click.echo(f" project: {target_config['project']}")
2116
+
2117
+ # Test connection with timeout
2118
+ success, message = _test_target_with_timeout(profile_data, target_name, timeout)
2119
+
2120
+ # Show test result (like dbt debug)
2121
+ if success:
2122
+ click.echo(
2123
+ f" Connection test: {click.style('[OK connection ok]', fg='green')}"
2124
+ )
2125
+ ctx.exit(0)
2126
+ else:
2127
+ click.echo(f" Connection test: {click.style('[ERROR]', fg='red')}")
2128
+ click.echo(f" {message}")
2129
+ ctx.exit(1)
2130
+
2131
+ # CASE 2: Test all targets in profile
2132
+ else:
2133
+ total_targets = len(outputs)
2134
+ click.echo(
2135
+ f"Testing all connections in profile {click.style(profile, fg='cyan')}...\n"
2136
+ )
2137
+
2138
+ # Test each target with progress indicators
2139
+ passed_count = 0
2140
+ failed_count = 0
2141
+ target_index = 1
2142
+
2143
+ for tgt_name, target_config in outputs.items():
2144
+ adapter_type = target_config.get("type", "unknown")
2145
+
2146
+ # Progress indicator
2147
+ progress = click.style(f"[{target_index}/{total_targets}]", fg="yellow")
2148
+ click.echo(
2149
+ f"{progress} Testing connection: {click.style(tgt_name, fg='cyan')} ({adapter_type})"
2150
+ )
2151
+
2152
+ # Show connection details FIRST (like dbt debug)
2153
+ if "host" in target_config:
2154
+ click.echo(f" host: {target_config['host']}")
2155
+ if "port" in target_config:
2156
+ click.echo(f" port: {target_config['port']}")
2157
+ if "account" in target_config:
2158
+ click.echo(f" account: {target_config['account']}")
2159
+ if "database" in target_config or "dbname" in target_config:
2160
+ db = target_config.get("database") or target_config.get("dbname")
2161
+ click.echo(f" database: {db}")
2162
+ if "warehouse" in target_config:
2163
+ click.echo(f" warehouse: {target_config['warehouse']}")
2164
+ if "schema" in target_config:
2165
+ click.echo(f" schema: {target_config['schema']}")
2166
+ if "project" in target_config:
2167
+ click.echo(f" project: {target_config['project']}")
2168
+
2169
+ # Test connection
2170
+ success, message = _test_target_with_timeout(
2171
+ profile_data, tgt_name, timeout
2172
+ )
2173
+
2174
+ # Show test result (like dbt debug)
2175
+ if success:
2176
+ click.echo(
2177
+ f" Connection test: {click.style('[OK connection ok]', fg='green')}"
2178
+ )
2179
+ passed_count += 1
2180
+ else:
2181
+ click.echo(f" Connection test: {click.style('[ERROR]', fg='red')}")
2182
+ click.echo(f" {message}")
2183
+ failed_count += 1
2184
+
2185
+ click.echo("")
2186
+ target_index += 1
2187
+
2188
+ # Summary line
2189
+ click.echo("─" * 60)
2190
+ if failed_count == 0:
2191
+ summary = click.style(
2192
+ f"✓ All {passed_count} connection tests passed", fg="green", bold=True
2193
+ )
2194
+ click.echo(summary)
2195
+ ctx.exit(0)
2196
+ else:
2197
+ passed_str = click.style(f"{passed_count} passed", fg="green")
2198
+ failed_str = click.style(f"{failed_count} failed", fg="red")
2199
+ summary = f"✗ {passed_str}, {failed_str}"
2200
+ click.echo(summary)
2201
+ ctx.exit(1)
2202
+
2203
+
2204
+ @target.command("add")
2205
+ @click.argument("target_name")
2206
+ @click.option("--profile", help="Profile name (auto-detected from project if not specified)")
2207
+ @click.option(
2208
+ "--type",
2209
+ "adapter_type",
2210
+ required=True,
2211
+ help="Adapter type (postgres, snowflake, etc)",
2212
+ )
2213
+ @click.option("--host", help="Database host")
2214
+ @click.option("--port", type=int, help="Database port")
2215
+ @click.option("--user", help="Database user")
2216
+ @click.option("--password", help="Database password")
2217
+ @click.option("--database", help="Database name")
2218
+ @click.option("--schema", help="Default schema")
2219
+ @click.option("--threads", type=int, default=4, help="Number of threads")
2220
+ @click.option("--set-default", is_flag=True, help="Set as default target for profile")
2221
+ @click.pass_context
2222
+ @p.profiles_dir
2223
+ @p.project_dir
2224
+ def target_add(
2225
+ ctx,
2226
+ target_name,
2227
+ profile,
2228
+ adapter_type,
2229
+ host,
2230
+ port,
2231
+ user,
2232
+ password,
2233
+ database,
2234
+ schema,
2235
+ threads,
2236
+ set_default,
2237
+ profiles_dir,
2238
+ project_dir,
2239
+ **kwargs,
2240
+ ):
2241
+ """Add a new target to a profile in profiles.yml"""
2242
+ import yaml
2243
+ from pathlib import Path
2244
+ from dbt.config.project_utils import get_project_profile_name
2245
+
2246
+ # v0.59.0a18: Auto-detect profile from project if not specified
2247
+ if not profile:
2248
+ profile = get_project_profile_name(project_dir)
2249
+ if not profile:
2250
+ click.echo(click.style("✗ Not in a DVT project directory", fg="red"))
2251
+ click.echo("")
2252
+ click.echo("Run from within a DVT project directory, or use --profile:")
2253
+ click.echo(" dvt target add <target_name> --profile <profile_name> --type <type>")
2254
+ ctx.exit(1)
2255
+
2256
+ profiles_file = Path(profiles_dir) / "profiles.yml"
2257
+
2258
+ if not profiles_file.exists():
2259
+ click.echo(f"✗ profiles.yml not found at {profiles_file}")
2260
+ return False, False
2261
+
2262
+ with open(profiles_file, "r") as f:
2263
+ profiles = yaml.safe_load(f) or {}
2264
+
2265
+ if profile not in profiles:
2266
+ click.echo(f"✗ Profile '{profile}' not found in profiles.yml")
2267
+ return False, False
2268
+
2269
+ profile_data = profiles[profile]
2270
+ if profile_data is None:
2271
+ click.echo(f"✗ Profile '{profile}' is empty or invalid")
2272
+ return False, False
2273
+
2274
+ # Get or create outputs dict (standard dbt format)
2275
+ if "outputs" not in profile_data:
2276
+ profile_data["outputs"] = {}
2277
+
2278
+ outputs = profile_data["outputs"]
2279
+
2280
+ # Check if target already exists
2281
+ if target_name in outputs:
2282
+ if not click.confirm(f"Target '{target_name}' already exists. Overwrite?"):
2283
+ return False, False
2284
+
2285
+ # Build target config
2286
+ target_config = {"type": adapter_type}
2287
+
2288
+ if host:
2289
+ target_config["host"] = host
2290
+ if port:
2291
+ target_config["port"] = port
2292
+ if user:
2293
+ target_config["user"] = user
2294
+ if password:
2295
+ target_config["password"] = password
2296
+ if database:
2297
+ target_config["database"] = database
2298
+ if schema:
2299
+ target_config["schema"] = schema
2300
+ if threads:
2301
+ target_config["threads"] = threads
2302
+
2303
+ # Add target to outputs
2304
+ outputs[target_name] = target_config
2305
+
2306
+ # Set as default if requested
2307
+ if set_default:
2308
+ profile_data["target"] = target_name
2309
+
2310
+ # Write back to profiles.yml
2311
+ with open(profiles_file, "w") as f:
2312
+ yaml.dump(profiles, f, default_flow_style=False, sort_keys=False)
2313
+
2314
+ click.echo(f"✓ Added target '{target_name}' to profile '{profile}'")
2315
+ if set_default:
2316
+ click.echo(f" Set as default target")
2317
+
2318
+ return True, True
2319
+
2320
+
2321
+ @target.command("sync")
2322
+ @click.option("--profile", help="Profile name (defaults to project profile)")
2323
+ @click.option("--clean", is_flag=True, help="Remove adapters not needed by profiles.yml")
2324
+ @click.option("--dry-run", is_flag=True, help="Show what would be done without making changes")
2325
+ @click.pass_context
2326
+ @p.profiles_dir
2327
+ @p.project_dir
2328
+ def target_sync(ctx, profile, clean, dry_run, profiles_dir, project_dir, **kwargs):
2329
+ """Sync adapters and JDBC JARs based on profiles.yml connections.
2330
+
2331
+ Scans your profiles.yml to find all connection types, then:
2332
+ - Installs required dbt adapters via pip
2333
+ - Updates JDBC JARs for Spark federation
2334
+ - Optionally removes unused adapters (with --clean)
2335
+
2336
+ Examples:
2337
+
2338
+ dvt target sync # Sync for current project
2339
+ dvt target sync --profile my_project # Sync specific profile
2340
+ dvt target sync --dry-run # Show what would happen
2341
+ dvt target sync --clean # Also remove unused adapters
2342
+ """
2343
+ from dbt.task.target_sync import TargetSyncTask
2344
+
2345
+ task = TargetSyncTask(
2346
+ project_dir=project_dir,
2347
+ profiles_dir=profiles_dir,
2348
+ profile_name=profile,
2349
+ )
2350
+ success = task.sync(verbose=True, clean=clean, dry_run=dry_run)
2351
+ return None, success
2352
+
2353
+
2354
+ @target.command("remove")
2355
+ @click.argument("target_name")
2356
+ @click.option("--profile", help="Profile name (auto-detected from project if not specified)")
2357
+ @click.pass_context
2358
+ @p.profiles_dir
2359
+ @p.project_dir
2360
+ def target_remove(ctx, target_name, profile, profiles_dir, project_dir, **kwargs):
2361
+ """Remove a target from a profile in profiles.yml"""
2362
+ import yaml
2363
+ from pathlib import Path
2364
+ from dbt.config.project_utils import get_project_profile_name
2365
+
2366
+ # v0.59.0a18: Auto-detect profile from project if not specified
2367
+ if not profile:
2368
+ profile = get_project_profile_name(project_dir)
2369
+ if not profile:
2370
+ click.echo(click.style("✗ Not in a DVT project directory", fg="red"))
2371
+ click.echo("")
2372
+ click.echo("Run from within a DVT project directory, or use --profile:")
2373
+ click.echo(" dvt target remove <target_name> --profile <profile_name>")
2374
+ ctx.exit(1)
2375
+
2376
+ profiles_file = Path(profiles_dir) / "profiles.yml"
2377
+
2378
+ if not profiles_file.exists():
2379
+ click.echo(f"✗ profiles.yml not found at {profiles_file}")
2380
+ return False, False
2381
+
2382
+ with open(profiles_file, "r") as f:
2383
+ profiles = yaml.safe_load(f) or {}
2384
+
2385
+ if profile not in profiles:
2386
+ click.echo(f"✗ Profile '{profile}' not found in profiles.yml")
2387
+ return False, False
2388
+
2389
+ profile_data = profiles[profile]
2390
+ if profile_data is None:
2391
+ click.echo(f"✗ Profile '{profile}' is empty or invalid")
2392
+ return False, False
2393
+ outputs = profile_data.get("outputs", {})
2394
+
2395
+ if target_name not in outputs:
2396
+ click.echo(f"✗ Target '{target_name}' not found in profile '{profile}'")
2397
+ return False, False
2398
+
2399
+ # Remove the target
2400
+ del outputs[target_name]
2401
+
2402
+ # Check if this was the default target
2403
+ default_target = profile_data.get("target")
2404
+ if default_target == target_name:
2405
+ # Set new default to first available target
2406
+ if outputs:
2407
+ new_default = list(outputs.keys())[0]
2408
+ profile_data["target"] = new_default
2409
+ click.echo(
2410
+ f" Note: '{target_name}' was the default target, changed to '{new_default}'"
2411
+ )
2412
+ else:
2413
+ click.echo(f" Warning: No targets remaining in profile '{profile}'")
2414
+
2415
+ # Write back to profiles.yml
2416
+ with open(profiles_file, "w") as f:
2417
+ yaml.dump(profiles, f, default_flow_style=False, sort_keys=False)
2418
+
2419
+ click.echo(f"✓ Removed target '{target_name}' from profile '{profile}'")
2420
+
2421
+ return True, True
2422
+
2423
+
2424
+ # DVT java commands for Java management
2425
+ @cli.group()
2426
+ @click.pass_context
2427
+ @p.version
2428
+ def java(ctx, **kwargs):
2429
+ """Manage Java installations for PySpark.
2430
+
2431
+ Java is required for Spark compute engines.
2432
+
2433
+ \b
2434
+ Compatibility:
2435
+ PySpark 4.0.x -> Java 17 or 21
2436
+ PySpark 3.5.x -> Java 8, 11, or 17
2437
+ PySpark 3.3-3.4 -> Java 8 or 11
2438
+
2439
+ \b
2440
+ Commands:
2441
+ check - Check Java compatibility with PySpark
2442
+ search - Find all Java installations
2443
+ set - Select and configure JAVA_HOME
2444
+ install - Show installation guide
2445
+
2446
+ \b
2447
+ Examples:
2448
+ dvt java check # Check compatibility
2449
+ dvt java search # Find installations
2450
+ dvt java set # Configure JAVA_HOME
2451
+ """
2452
+
2453
+
2454
+ @java.command("check")
2455
+ @click.pass_context
2456
+ def java_check(ctx, **kwargs):
2457
+ """Check Java installation and PySpark compatibility.
2458
+
2459
+ Shows current Java version, installed PySpark version, and whether
2460
+ they are compatible. Provides guidance if there's a mismatch.
2461
+
2462
+ Exit codes:
2463
+ 0 - Java and PySpark are compatible
2464
+ 1 - Java/PySpark mismatch or not found
2465
+ """
2466
+ from dbt.task.java import JavaTask
2467
+
2468
+ task = JavaTask()
2469
+ is_compatible = task.check()
2470
+ ctx.exit(0 if is_compatible else 1)
2471
+
2472
+
2473
+ @java.command("search")
2474
+ @click.pass_context
2475
+ def java_search(ctx, **kwargs):
2476
+ """Find all Java installations on the system.
2477
+
2478
+ Searches common installation locations for Java on your OS:
2479
+
2480
+ \b
2481
+ macOS: /Library/Java/JavaVirtualMachines, Homebrew, SDKMAN
2482
+ Linux: /usr/lib/jvm, /opt/java, update-alternatives, SDKMAN
2483
+ Windows: Program Files, Registry, Scoop, Chocolatey
2484
+
2485
+ Shows Java version, vendor, and compatibility with installed PySpark.
2486
+ """
2487
+ from dbt.task.java import JavaTask
2488
+
2489
+ task = JavaTask()
2490
+ installations = task.search()
2491
+ ctx.exit(0 if installations else 1)
2492
+
2493
+
2494
+ @java.command("set")
2495
+ @click.pass_context
2496
+ def java_set(ctx, **kwargs):
2497
+ """Interactively select and set JAVA_HOME.
2498
+
2499
+ Shows all found Java installations with compatibility indicators,
2500
+ lets you choose one, and updates your shell configuration file
2501
+ (.zshrc, .bashrc, etc.) to persist JAVA_HOME.
2502
+
2503
+ After setting, restart your terminal or run 'source ~/.zshrc'
2504
+ (or equivalent) for changes to take effect.
2505
+ """
2506
+ from dbt.task.java import JavaTask
2507
+
2508
+ task = JavaTask()
2509
+ success = task.set_java_home()
2510
+ ctx.exit(0 if success else 1)
2511
+
2512
+
2513
+ @java.command("install")
2514
+ @click.pass_context
2515
+ def java_install(ctx, **kwargs):
2516
+ """Show Java installation guide for your platform.
2517
+
2518
+ Provides platform-specific installation instructions based on
2519
+ your installed PySpark version. Includes options for:
2520
+
2521
+ \b
2522
+ macOS: Homebrew, SDKMAN, manual download
2523
+ Linux: apt-get, dnf, pacman, SDKMAN
2524
+ Windows: Winget, Chocolatey, Scoop, manual download
2525
+ """
2526
+ from dbt.task.java import JavaTask
2527
+
2528
+ task = JavaTask()
2529
+ task.install_guide()
2530
+ ctx.exit(0)
2531
+
2532
+
2533
+ # DVT spark commands for Spark/PySpark management
2534
+ @cli.group()
2535
+ @click.pass_context
2536
+ @p.version
2537
+ def spark(ctx, **kwargs):
2538
+ """Manage PySpark installations and cluster compatibility.
2539
+
2540
+ PySpark is used by DVT for federated query execution.
2541
+
2542
+ \b
2543
+ Commands:
2544
+ check - Check PySpark/Java status
2545
+ set-version - Install specific PySpark version
2546
+ match-cluster - Match PySpark to cluster version
2547
+ versions - Show compatibility matrix
2548
+
2549
+ \b
2550
+ Examples:
2551
+ dvt spark check # Check status
2552
+ dvt spark set-version # Install PySpark
2553
+ dvt spark match-cluster spark # Match to cluster
2554
+ dvt spark versions # Show matrix
2555
+ """
2556
+
2557
+
2558
+ @spark.command("check")
2559
+ @click.pass_context
2560
+ def spark_check(ctx, **kwargs):
2561
+ """Check PySpark installation and Java compatibility.
2562
+
2563
+ Shows:
2564
+ - Installed PySpark version and requirements
2565
+ - Current Java version
2566
+ - Compatibility status
2567
+
2568
+ Exit codes:
2569
+ 0 - PySpark installed and Java compatible
2570
+ 1 - PySpark not installed or Java incompatible
2571
+ """
2572
+ from dbt.task.spark import SparkTask
2573
+
2574
+ task = SparkTask()
2575
+ is_ok = task.check()
2576
+ ctx.exit(0 if is_ok else 1)
2577
+
2578
+
2579
+ @spark.command("set-version")
2580
+ @click.pass_context
2581
+ def spark_set_version(ctx, **kwargs):
2582
+ """Interactively select and install a PySpark version.
2583
+
2584
+ Presents available PySpark versions with their Java requirements.
2585
+ Shows compatibility indicators based on your current Java.
2586
+ Installs the selected version via pip.
2587
+
2588
+ Available versions:
2589
+ \b
2590
+ PySpark 4.0.x - Latest, requires Java 17+
2591
+ PySpark 3.5.x - Stable, Java 8/11/17
2592
+ PySpark 3.4.x - Java 8/11/17
2593
+ PySpark 3.3.x - Java 8/11
2594
+ PySpark 3.2.x - Java 8/11
2595
+
2596
+ After installing, check Java compatibility with 'dvt java check'.
2597
+ """
2598
+ from dbt.task.spark import SparkTask
2599
+
2600
+ task = SparkTask()
2601
+ success = task.set_version()
2602
+ ctx.exit(0 if success else 1)
2603
+
2604
+
2605
+ @spark.command("match-cluster")
2606
+ @click.argument("compute_name")
2607
+ @click.pass_context
2608
+ def spark_match_cluster(ctx, compute_name, **kwargs):
2609
+ """Detect cluster Spark version and check PySpark compatibility.
2610
+
2611
+ Connects to the specified compute engine from computes.yml,
2612
+ detects its Spark version, and compares with locally installed
2613
+ PySpark. Provides recommendations if versions don't match.
2614
+
2615
+ IMPORTANT: PySpark version must match the cluster's Spark version
2616
+ (same major.minor). A mismatch can cause runtime errors.
2617
+
2618
+ Arguments:
2619
+ COMPUTE_NAME: Name of compute engine in computes.yml
2620
+
2621
+ Examples:
2622
+
2623
+ \b
2624
+ dvt spark match-cluster spark-docker
2625
+ dvt spark match-cluster spark-local
2626
+ dvt spark match-cluster databricks-prod
2627
+ """
2628
+ from dbt.task.spark import SparkTask
2629
+
2630
+ task = SparkTask()
2631
+ is_match = task.match_cluster(compute_name)
2632
+ ctx.exit(0 if is_match else 1)
2633
+
2634
+
2635
+ @spark.command("versions")
2636
+ @click.pass_context
2637
+ def spark_versions(ctx, **kwargs):
2638
+ """Display PySpark/Java compatibility matrix.
2639
+
2640
+ Shows all available PySpark versions with their Java requirements,
2641
+ marks the currently installed version, and shows your current
2642
+ Java installation.
2643
+ """
2644
+ from dbt.task.spark import SparkTask
2645
+
2646
+ task = SparkTask()
2647
+ task.show_versions()
2648
+ ctx.exit(0)
2649
+
2650
+
2651
+ # Register DVT command groups with main CLI
2652
+ cli.add_command(compute)
2653
+ cli.add_command(target)
2654
+ cli.add_command(java)
2655
+ cli.add_command(spark)
2656
+
2657
+
2658
+ # Support running as a module (python -m dbt.cli.main)
2659
+ if __name__ == "__main__":
2660
+ cli()