dvt-core 0.58.6__cp311-cp311-macosx_10_9_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. dbt/__init__.py +7 -0
  2. dbt/_pydantic_shim.py +26 -0
  3. dbt/artifacts/__init__.py +0 -0
  4. dbt/artifacts/exceptions/__init__.py +1 -0
  5. dbt/artifacts/exceptions/schemas.py +31 -0
  6. dbt/artifacts/resources/__init__.py +116 -0
  7. dbt/artifacts/resources/base.py +67 -0
  8. dbt/artifacts/resources/types.py +93 -0
  9. dbt/artifacts/resources/v1/analysis.py +10 -0
  10. dbt/artifacts/resources/v1/catalog.py +23 -0
  11. dbt/artifacts/resources/v1/components.py +274 -0
  12. dbt/artifacts/resources/v1/config.py +277 -0
  13. dbt/artifacts/resources/v1/documentation.py +11 -0
  14. dbt/artifacts/resources/v1/exposure.py +51 -0
  15. dbt/artifacts/resources/v1/function.py +52 -0
  16. dbt/artifacts/resources/v1/generic_test.py +31 -0
  17. dbt/artifacts/resources/v1/group.py +21 -0
  18. dbt/artifacts/resources/v1/hook.py +11 -0
  19. dbt/artifacts/resources/v1/macro.py +29 -0
  20. dbt/artifacts/resources/v1/metric.py +172 -0
  21. dbt/artifacts/resources/v1/model.py +145 -0
  22. dbt/artifacts/resources/v1/owner.py +10 -0
  23. dbt/artifacts/resources/v1/saved_query.py +111 -0
  24. dbt/artifacts/resources/v1/seed.py +41 -0
  25. dbt/artifacts/resources/v1/semantic_layer_components.py +72 -0
  26. dbt/artifacts/resources/v1/semantic_model.py +314 -0
  27. dbt/artifacts/resources/v1/singular_test.py +14 -0
  28. dbt/artifacts/resources/v1/snapshot.py +91 -0
  29. dbt/artifacts/resources/v1/source_definition.py +84 -0
  30. dbt/artifacts/resources/v1/sql_operation.py +10 -0
  31. dbt/artifacts/resources/v1/unit_test_definition.py +77 -0
  32. dbt/artifacts/schemas/__init__.py +0 -0
  33. dbt/artifacts/schemas/base.py +191 -0
  34. dbt/artifacts/schemas/batch_results.py +24 -0
  35. dbt/artifacts/schemas/catalog/__init__.py +11 -0
  36. dbt/artifacts/schemas/catalog/v1/__init__.py +0 -0
  37. dbt/artifacts/schemas/catalog/v1/catalog.py +59 -0
  38. dbt/artifacts/schemas/freshness/__init__.py +1 -0
  39. dbt/artifacts/schemas/freshness/v3/__init__.py +0 -0
  40. dbt/artifacts/schemas/freshness/v3/freshness.py +158 -0
  41. dbt/artifacts/schemas/manifest/__init__.py +2 -0
  42. dbt/artifacts/schemas/manifest/v12/__init__.py +0 -0
  43. dbt/artifacts/schemas/manifest/v12/manifest.py +211 -0
  44. dbt/artifacts/schemas/results.py +147 -0
  45. dbt/artifacts/schemas/run/__init__.py +2 -0
  46. dbt/artifacts/schemas/run/v5/__init__.py +0 -0
  47. dbt/artifacts/schemas/run/v5/run.py +184 -0
  48. dbt/artifacts/schemas/upgrades/__init__.py +4 -0
  49. dbt/artifacts/schemas/upgrades/upgrade_manifest.py +174 -0
  50. dbt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py +2 -0
  51. dbt/artifacts/utils/validation.py +153 -0
  52. dbt/cli/__init__.py +1 -0
  53. dbt/cli/context.py +17 -0
  54. dbt/cli/exceptions.py +57 -0
  55. dbt/cli/flags.py +560 -0
  56. dbt/cli/main.py +2403 -0
  57. dbt/cli/option_types.py +121 -0
  58. dbt/cli/options.py +80 -0
  59. dbt/cli/params.py +844 -0
  60. dbt/cli/requires.py +490 -0
  61. dbt/cli/resolvers.py +50 -0
  62. dbt/cli/types.py +40 -0
  63. dbt/clients/__init__.py +0 -0
  64. dbt/clients/checked_load.py +83 -0
  65. dbt/clients/git.py +164 -0
  66. dbt/clients/jinja.py +206 -0
  67. dbt/clients/jinja_static.py +245 -0
  68. dbt/clients/registry.py +192 -0
  69. dbt/clients/yaml_helper.py +68 -0
  70. dbt/compilation.py +876 -0
  71. dbt/compute/__init__.py +14 -0
  72. dbt/compute/engines/__init__.py +12 -0
  73. dbt/compute/engines/spark_engine.cpython-311-darwin.so +0 -0
  74. dbt/compute/engines/spark_engine.py +642 -0
  75. dbt/compute/federated_executor.cpython-311-darwin.so +0 -0
  76. dbt/compute/federated_executor.py +1080 -0
  77. dbt/compute/filter_pushdown.cpython-311-darwin.so +0 -0
  78. dbt/compute/filter_pushdown.py +273 -0
  79. dbt/compute/jar_provisioning.cpython-311-darwin.so +0 -0
  80. dbt/compute/jar_provisioning.py +255 -0
  81. dbt/compute/java_compat.cpython-311-darwin.so +0 -0
  82. dbt/compute/java_compat.py +689 -0
  83. dbt/compute/jdbc_utils.cpython-311-darwin.so +0 -0
  84. dbt/compute/jdbc_utils.py +678 -0
  85. dbt/compute/metadata/__init__.py +40 -0
  86. dbt/compute/metadata/adapters_registry.cpython-311-darwin.so +0 -0
  87. dbt/compute/metadata/adapters_registry.py +370 -0
  88. dbt/compute/metadata/registry.cpython-311-darwin.so +0 -0
  89. dbt/compute/metadata/registry.py +674 -0
  90. dbt/compute/metadata/store.cpython-311-darwin.so +0 -0
  91. dbt/compute/metadata/store.py +1499 -0
  92. dbt/compute/smart_selector.cpython-311-darwin.so +0 -0
  93. dbt/compute/smart_selector.py +377 -0
  94. dbt/compute/strategies/__init__.py +55 -0
  95. dbt/compute/strategies/base.cpython-311-darwin.so +0 -0
  96. dbt/compute/strategies/base.py +165 -0
  97. dbt/compute/strategies/dataproc.cpython-311-darwin.so +0 -0
  98. dbt/compute/strategies/dataproc.py +207 -0
  99. dbt/compute/strategies/emr.cpython-311-darwin.so +0 -0
  100. dbt/compute/strategies/emr.py +203 -0
  101. dbt/compute/strategies/local.cpython-311-darwin.so +0 -0
  102. dbt/compute/strategies/local.py +443 -0
  103. dbt/compute/strategies/standalone.cpython-311-darwin.so +0 -0
  104. dbt/compute/strategies/standalone.py +262 -0
  105. dbt/config/__init__.py +4 -0
  106. dbt/config/catalogs.py +94 -0
  107. dbt/config/compute.cpython-311-darwin.so +0 -0
  108. dbt/config/compute.py +513 -0
  109. dbt/config/dvt_profile.cpython-311-darwin.so +0 -0
  110. dbt/config/dvt_profile.py +342 -0
  111. dbt/config/profile.py +422 -0
  112. dbt/config/project.py +873 -0
  113. dbt/config/project_utils.py +28 -0
  114. dbt/config/renderer.py +231 -0
  115. dbt/config/runtime.py +553 -0
  116. dbt/config/selectors.py +208 -0
  117. dbt/config/utils.py +77 -0
  118. dbt/constants.py +28 -0
  119. dbt/context/__init__.py +0 -0
  120. dbt/context/base.py +745 -0
  121. dbt/context/configured.py +135 -0
  122. dbt/context/context_config.py +382 -0
  123. dbt/context/docs.py +82 -0
  124. dbt/context/exceptions_jinja.py +178 -0
  125. dbt/context/macro_resolver.py +195 -0
  126. dbt/context/macros.py +171 -0
  127. dbt/context/manifest.py +72 -0
  128. dbt/context/providers.py +2249 -0
  129. dbt/context/query_header.py +13 -0
  130. dbt/context/secret.py +58 -0
  131. dbt/context/target.py +74 -0
  132. dbt/contracts/__init__.py +0 -0
  133. dbt/contracts/files.py +413 -0
  134. dbt/contracts/graph/__init__.py +0 -0
  135. dbt/contracts/graph/manifest.py +1904 -0
  136. dbt/contracts/graph/metrics.py +97 -0
  137. dbt/contracts/graph/model_config.py +70 -0
  138. dbt/contracts/graph/node_args.py +42 -0
  139. dbt/contracts/graph/nodes.py +1806 -0
  140. dbt/contracts/graph/semantic_manifest.py +232 -0
  141. dbt/contracts/graph/unparsed.py +811 -0
  142. dbt/contracts/project.py +417 -0
  143. dbt/contracts/results.py +53 -0
  144. dbt/contracts/selection.py +23 -0
  145. dbt/contracts/sql.py +85 -0
  146. dbt/contracts/state.py +68 -0
  147. dbt/contracts/util.py +46 -0
  148. dbt/deprecations.py +348 -0
  149. dbt/deps/__init__.py +0 -0
  150. dbt/deps/base.py +152 -0
  151. dbt/deps/git.py +195 -0
  152. dbt/deps/local.py +79 -0
  153. dbt/deps/registry.py +130 -0
  154. dbt/deps/resolver.py +149 -0
  155. dbt/deps/tarball.py +120 -0
  156. dbt/docs/source/_ext/dbt_click.py +119 -0
  157. dbt/docs/source/conf.py +32 -0
  158. dbt/env_vars.py +64 -0
  159. dbt/event_time/event_time.py +40 -0
  160. dbt/event_time/sample_window.py +60 -0
  161. dbt/events/__init__.py +15 -0
  162. dbt/events/base_types.py +36 -0
  163. dbt/events/core_types_pb2.py +2 -0
  164. dbt/events/logging.py +108 -0
  165. dbt/events/types.py +2516 -0
  166. dbt/exceptions.py +1486 -0
  167. dbt/flags.py +89 -0
  168. dbt/graph/__init__.py +11 -0
  169. dbt/graph/cli.py +249 -0
  170. dbt/graph/graph.py +172 -0
  171. dbt/graph/queue.py +214 -0
  172. dbt/graph/selector.py +374 -0
  173. dbt/graph/selector_methods.py +975 -0
  174. dbt/graph/selector_spec.py +222 -0
  175. dbt/graph/thread_pool.py +18 -0
  176. dbt/hooks.py +21 -0
  177. dbt/include/README.md +49 -0
  178. dbt/include/__init__.py +3 -0
  179. dbt/include/data/adapters_registry.duckdb +0 -0
  180. dbt/include/data/build_registry.py +242 -0
  181. dbt/include/data/csv/adapter_queries.csv +33 -0
  182. dbt/include/data/csv/syntax_rules.csv +9 -0
  183. dbt/include/data/csv/type_mappings_bigquery.csv +28 -0
  184. dbt/include/data/csv/type_mappings_databricks.csv +30 -0
  185. dbt/include/data/csv/type_mappings_mysql.csv +40 -0
  186. dbt/include/data/csv/type_mappings_oracle.csv +30 -0
  187. dbt/include/data/csv/type_mappings_postgres.csv +56 -0
  188. dbt/include/data/csv/type_mappings_redshift.csv +33 -0
  189. dbt/include/data/csv/type_mappings_snowflake.csv +38 -0
  190. dbt/include/data/csv/type_mappings_sqlserver.csv +35 -0
  191. dbt/include/starter_project/.gitignore +4 -0
  192. dbt/include/starter_project/README.md +15 -0
  193. dbt/include/starter_project/__init__.py +3 -0
  194. dbt/include/starter_project/analyses/.gitkeep +0 -0
  195. dbt/include/starter_project/dbt_project.yml +36 -0
  196. dbt/include/starter_project/macros/.gitkeep +0 -0
  197. dbt/include/starter_project/models/example/my_first_dbt_model.sql +27 -0
  198. dbt/include/starter_project/models/example/my_second_dbt_model.sql +6 -0
  199. dbt/include/starter_project/models/example/schema.yml +21 -0
  200. dbt/include/starter_project/seeds/.gitkeep +0 -0
  201. dbt/include/starter_project/snapshots/.gitkeep +0 -0
  202. dbt/include/starter_project/tests/.gitkeep +0 -0
  203. dbt/internal_deprecations.py +26 -0
  204. dbt/jsonschemas/__init__.py +3 -0
  205. dbt/jsonschemas/jsonschemas.py +309 -0
  206. dbt/jsonschemas/project/0.0.110.json +4717 -0
  207. dbt/jsonschemas/project/0.0.85.json +2015 -0
  208. dbt/jsonschemas/resources/0.0.110.json +2636 -0
  209. dbt/jsonschemas/resources/0.0.85.json +2536 -0
  210. dbt/jsonschemas/resources/latest.json +6773 -0
  211. dbt/links.py +4 -0
  212. dbt/materializations/__init__.py +0 -0
  213. dbt/materializations/incremental/__init__.py +0 -0
  214. dbt/materializations/incremental/microbatch.py +236 -0
  215. dbt/mp_context.py +8 -0
  216. dbt/node_types.py +37 -0
  217. dbt/parser/__init__.py +23 -0
  218. dbt/parser/analysis.py +21 -0
  219. dbt/parser/base.py +548 -0
  220. dbt/parser/common.py +266 -0
  221. dbt/parser/docs.py +52 -0
  222. dbt/parser/fixtures.py +51 -0
  223. dbt/parser/functions.py +30 -0
  224. dbt/parser/generic_test.py +100 -0
  225. dbt/parser/generic_test_builders.py +333 -0
  226. dbt/parser/hooks.py +118 -0
  227. dbt/parser/macros.py +137 -0
  228. dbt/parser/manifest.py +2204 -0
  229. dbt/parser/models.py +573 -0
  230. dbt/parser/partial.py +1178 -0
  231. dbt/parser/read_files.py +445 -0
  232. dbt/parser/schema_generic_tests.py +422 -0
  233. dbt/parser/schema_renderer.py +111 -0
  234. dbt/parser/schema_yaml_readers.py +935 -0
  235. dbt/parser/schemas.py +1466 -0
  236. dbt/parser/search.py +149 -0
  237. dbt/parser/seeds.py +28 -0
  238. dbt/parser/singular_test.py +20 -0
  239. dbt/parser/snapshots.py +44 -0
  240. dbt/parser/sources.py +558 -0
  241. dbt/parser/sql.py +62 -0
  242. dbt/parser/unit_tests.py +621 -0
  243. dbt/plugins/__init__.py +20 -0
  244. dbt/plugins/contracts.py +9 -0
  245. dbt/plugins/exceptions.py +2 -0
  246. dbt/plugins/manager.py +163 -0
  247. dbt/plugins/manifest.py +21 -0
  248. dbt/profiler.py +20 -0
  249. dbt/py.typed +1 -0
  250. dbt/query_analyzer.cpython-311-darwin.so +0 -0
  251. dbt/query_analyzer.py +410 -0
  252. dbt/runners/__init__.py +2 -0
  253. dbt/runners/exposure_runner.py +7 -0
  254. dbt/runners/no_op_runner.py +45 -0
  255. dbt/runners/saved_query_runner.py +7 -0
  256. dbt/selected_resources.py +8 -0
  257. dbt/task/__init__.py +0 -0
  258. dbt/task/base.py +503 -0
  259. dbt/task/build.py +197 -0
  260. dbt/task/clean.py +56 -0
  261. dbt/task/clone.py +161 -0
  262. dbt/task/compile.py +150 -0
  263. dbt/task/compute.cpython-311-darwin.so +0 -0
  264. dbt/task/compute.py +458 -0
  265. dbt/task/debug.py +505 -0
  266. dbt/task/deps.py +280 -0
  267. dbt/task/docs/__init__.py +3 -0
  268. dbt/task/docs/api/__init__.py +23 -0
  269. dbt/task/docs/api/catalog.cpython-311-darwin.so +0 -0
  270. dbt/task/docs/api/catalog.py +204 -0
  271. dbt/task/docs/api/lineage.cpython-311-darwin.so +0 -0
  272. dbt/task/docs/api/lineage.py +234 -0
  273. dbt/task/docs/api/profile.cpython-311-darwin.so +0 -0
  274. dbt/task/docs/api/profile.py +204 -0
  275. dbt/task/docs/api/spark.cpython-311-darwin.so +0 -0
  276. dbt/task/docs/api/spark.py +186 -0
  277. dbt/task/docs/generate.py +947 -0
  278. dbt/task/docs/index.html +250 -0
  279. dbt/task/docs/serve.cpython-311-darwin.so +0 -0
  280. dbt/task/docs/serve.py +174 -0
  281. dbt/task/dvt_output.py +362 -0
  282. dbt/task/dvt_run.py +204 -0
  283. dbt/task/freshness.py +322 -0
  284. dbt/task/function.py +121 -0
  285. dbt/task/group_lookup.py +46 -0
  286. dbt/task/init.cpython-311-darwin.so +0 -0
  287. dbt/task/init.py +604 -0
  288. dbt/task/java.cpython-311-darwin.so +0 -0
  289. dbt/task/java.py +316 -0
  290. dbt/task/list.py +236 -0
  291. dbt/task/metadata.cpython-311-darwin.so +0 -0
  292. dbt/task/metadata.py +804 -0
  293. dbt/task/printer.py +175 -0
  294. dbt/task/profile.cpython-311-darwin.so +0 -0
  295. dbt/task/profile.py +1307 -0
  296. dbt/task/profile_serve.py +615 -0
  297. dbt/task/retract.py +438 -0
  298. dbt/task/retry.py +175 -0
  299. dbt/task/run.py +1387 -0
  300. dbt/task/run_operation.py +141 -0
  301. dbt/task/runnable.py +758 -0
  302. dbt/task/seed.py +103 -0
  303. dbt/task/show.py +149 -0
  304. dbt/task/snapshot.py +56 -0
  305. dbt/task/spark.cpython-311-darwin.so +0 -0
  306. dbt/task/spark.py +414 -0
  307. dbt/task/sql.py +110 -0
  308. dbt/task/target_sync.cpython-311-darwin.so +0 -0
  309. dbt/task/target_sync.py +766 -0
  310. dbt/task/test.py +464 -0
  311. dbt/tests/fixtures/__init__.py +1 -0
  312. dbt/tests/fixtures/project.py +620 -0
  313. dbt/tests/util.py +651 -0
  314. dbt/tracking.py +529 -0
  315. dbt/utils/__init__.py +3 -0
  316. dbt/utils/artifact_upload.py +151 -0
  317. dbt/utils/utils.py +408 -0
  318. dbt/version.py +270 -0
  319. dvt_cli/__init__.py +72 -0
  320. dvt_core-0.58.6.dist-info/METADATA +288 -0
  321. dvt_core-0.58.6.dist-info/RECORD +324 -0
  322. dvt_core-0.58.6.dist-info/WHEEL +5 -0
  323. dvt_core-0.58.6.dist-info/entry_points.txt +2 -0
  324. dvt_core-0.58.6.dist-info/top_level.txt +2 -0
@@ -0,0 +1,947 @@
1
+ import os
2
+ import shutil
3
+ from dataclasses import replace
4
+ from datetime import datetime, timezone
5
+ from itertools import chain
6
+ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
7
+
8
+ import agate
9
+
10
+ import dbt.compilation
11
+ import dbt.exceptions
12
+ import dbt.utils
13
+ import dbt_common.utils.formatting
14
+ from dbt.adapters.events.types import (
15
+ BuildingCatalog,
16
+ CannotGenerateDocs,
17
+ CatalogWritten,
18
+ WriteCatalogFailure,
19
+ )
20
+ from dbt.adapters.factory import get_adapter
21
+ from dbt.artifacts.schemas.catalog import (
22
+ CatalogArtifact,
23
+ CatalogKey,
24
+ CatalogResults,
25
+ CatalogTable,
26
+ ColumnMetadata,
27
+ PrimitiveDict,
28
+ StatsDict,
29
+ StatsItem,
30
+ TableMetadata,
31
+ )
32
+ from dbt.artifacts.schemas.results import NodeStatus
33
+ from dbt.constants import CATALOG_FILENAME, MANIFEST_FILE_NAME
34
+ from dbt.context.providers import generate_runtime_macro_context
35
+ from dbt.contracts.graph.manifest import Manifest
36
+ from dbt.contracts.graph.nodes import ResultNode
37
+ from dbt.events.types import ArtifactWritten
38
+ from dbt.exceptions import AmbiguousCatalogMatchError
39
+ from dbt.graph import ResourceTypeSelector
40
+ from dbt.graph.graph import UniqueId
41
+ from dbt.node_types import EXECUTABLE_NODE_TYPES, NodeType
42
+ from dbt.parser.manifest import write_manifest
43
+ from dbt.task.compile import CompileTask
44
+ from dbt.task.docs import DOCS_INDEX_FILE_PATH
45
+ from dbt.utils.artifact_upload import add_artifact_produced
46
+ from dbt_common.clients.system import load_file_contents
47
+ from dbt_common.dataclass_schema import ValidationError
48
+ from dbt_common.events.functions import fire_event
49
+ from dbt_common.exceptions import DbtInternalError
50
+
51
+
52
+ def get_stripped_prefix(source: Dict[str, Any], prefix: str) -> Dict[str, Any]:
53
+ """Go through the source, extracting every key/value pair where the key starts
54
+ with the given prefix.
55
+ """
56
+ cut = len(prefix)
57
+ return {k[cut:]: v for k, v in source.items() if k.startswith(prefix)}
58
+
59
+
60
+ def build_catalog_table(data, adapter_type: Optional[str] = None) -> CatalogTable:
61
+ # build the new table's metadata + stats
62
+ metadata = TableMetadata.from_dict(get_stripped_prefix(data, "table_"))
63
+ stats = format_stats(get_stripped_prefix(data, "stats:"))
64
+
65
+ # DVT v0.4.3: Add adapter type metadata for visualization
66
+ # This enables adapter logos and connection badges in dbt docs
67
+ if adapter_type:
68
+ # Add adapter type to metadata comment for catalog display
69
+ comment_text = metadata.comment or ""
70
+ if comment_text and not comment_text.endswith(' '):
71
+ comment_text += " "
72
+ metadata = replace(
73
+ metadata,
74
+ comment=f"{comment_text}[adapter:{adapter_type}]"
75
+ )
76
+
77
+ return CatalogTable(
78
+ metadata=metadata,
79
+ stats=stats,
80
+ columns={},
81
+ )
82
+
83
+
84
+ # keys are database name, schema name, table name
85
+ class Catalog(Dict[CatalogKey, CatalogTable]):
86
+ def __init__(self, columns: List[PrimitiveDict]) -> None:
87
+ super().__init__()
88
+ for col in columns:
89
+ self.add_column(col)
90
+
91
+ def get_table(self, data: PrimitiveDict, adapter_type: Optional[str] = None) -> CatalogTable:
92
+ database = data.get("table_database")
93
+ if database is None:
94
+ dkey: Optional[str] = None
95
+ else:
96
+ dkey = str(database)
97
+
98
+ try:
99
+ key = CatalogKey(
100
+ dkey,
101
+ str(data["table_schema"]),
102
+ str(data["table_name"]),
103
+ )
104
+ except KeyError as exc:
105
+ raise dbt_common.exceptions.CompilationError(
106
+ "Catalog information missing required key {} (got {})".format(exc, data)
107
+ )
108
+ table: CatalogTable
109
+ if key in self:
110
+ table = self[key]
111
+ else:
112
+ table = build_catalog_table(data, adapter_type)
113
+ self[key] = table
114
+ return table
115
+
116
+ def add_column(self, data: PrimitiveDict):
117
+ table = self.get_table(data)
118
+ column_data = get_stripped_prefix(data, "column_")
119
+ # the index should really never be that big so it's ok to end up
120
+ # serializing this to JSON (2^53 is the max safe value there)
121
+ column_data["index"] = int(column_data["index"])
122
+
123
+ column = ColumnMetadata.from_dict(column_data)
124
+ table.columns[column.name] = column
125
+
126
+ def make_unique_id_map(
127
+ self, manifest: Manifest, selected_node_ids: Optional[Set[UniqueId]] = None
128
+ ) -> Tuple[Dict[str, CatalogTable], Dict[str, CatalogTable]]:
129
+ """
130
+ Create mappings between CatalogKeys and CatalogTables for nodes and sources, filtered by selected_node_ids.
131
+
132
+ By default, selected_node_ids is None and all nodes and sources defined in the manifest are included in the mappings.
133
+ """
134
+ nodes: Dict[str, CatalogTable] = {}
135
+ sources: Dict[str, CatalogTable] = {}
136
+
137
+ node_map, source_map = get_unique_id_mapping(manifest)
138
+ table: CatalogTable
139
+ for table in self.values():
140
+ key = table.key()
141
+ if key in node_map:
142
+ unique_id = node_map[key]
143
+ if selected_node_ids is None or unique_id in selected_node_ids:
144
+ # DVT v0.4.3: Add comprehensive adapter and connection metadata for nodes
145
+ node = manifest.nodes.get(unique_id)
146
+ connection_name = None
147
+ adapter_type = None
148
+ compute_engine = None
149
+
150
+ if node:
151
+ # Get target connection name
152
+ if hasattr(node.config, 'target') and node.config.target:
153
+ connection_name = node.config.target
154
+
155
+ # Get compute engine if specified
156
+ if hasattr(node.config, 'compute') and node.config.compute:
157
+ compute_engine = node.config.compute
158
+
159
+ # Build metadata tags for catalog display
160
+ comment_text = table.metadata.comment or ""
161
+ tags = []
162
+
163
+ if connection_name:
164
+ tags.append(f"target:{connection_name}")
165
+ if compute_engine:
166
+ tags.append(f"compute:{compute_engine}")
167
+
168
+ if tags:
169
+ if comment_text and not comment_text.endswith(' '):
170
+ comment_text += " "
171
+ comment_text += f"[{' | '.join(tags)}]"
172
+
173
+ # Create updated metadata with enriched info
174
+ updated_metadata = replace(
175
+ table.metadata,
176
+ comment=comment_text if tags else table.metadata.comment
177
+ )
178
+ nodes[unique_id] = replace(table, unique_id=unique_id, metadata=updated_metadata)
179
+
180
+ unique_ids = source_map.get(table.key(), set())
181
+ for unique_id in unique_ids:
182
+ if unique_id in sources:
183
+ raise AmbiguousCatalogMatchError(
184
+ unique_id,
185
+ sources[unique_id].to_dict(omit_none=True),
186
+ table.to_dict(omit_none=True),
187
+ )
188
+ elif selected_node_ids is None or unique_id in selected_node_ids:
189
+ # DVT v0.4.3: Add comprehensive adapter and connection metadata for sources
190
+ source = manifest.sources.get(unique_id)
191
+ connection_name = None
192
+ adapter_type = None
193
+
194
+ if source:
195
+ # Get connection name for source
196
+ if hasattr(source, 'connection') and source.connection:
197
+ connection_name = source.connection
198
+
199
+ # Try to determine adapter type from connection
200
+ # Check if we can get adapter info from manifest's profile
201
+ if connection_name:
202
+ # Sources store connection name, we need to map it to adapter type
203
+ # This requires access to the RuntimeConfig which has the profile info
204
+ # For now, we'll add just the connection tag and let dbt docs UI handle it
205
+ pass
206
+
207
+ # Build metadata tags for catalog display
208
+ comment_text = table.metadata.comment or ""
209
+ tags = []
210
+
211
+ if connection_name:
212
+ tags.append(f"source:{connection_name}")
213
+
214
+ if tags:
215
+ if comment_text and not comment_text.endswith(' '):
216
+ comment_text += " "
217
+ comment_text += f"[{' | '.join(tags)}]"
218
+
219
+ # Create updated metadata with enriched info
220
+ updated_metadata = replace(
221
+ table.metadata,
222
+ comment=comment_text if tags else table.metadata.comment
223
+ )
224
+ sources[unique_id] = replace(table, unique_id=unique_id, metadata=updated_metadata)
225
+ return nodes, sources
226
+
227
+
228
+ def format_stats(stats: PrimitiveDict) -> StatsDict:
229
+ """Given a dictionary following this layout:
230
+
231
+ {
232
+ 'encoded:label': 'Encoded',
233
+ 'encoded:value': 'Yes',
234
+ 'encoded:description': 'Indicates if the column is encoded',
235
+ 'encoded:include': True,
236
+
237
+ 'size:label': 'Size',
238
+ 'size:value': 128,
239
+ 'size:description': 'Size of the table in MB',
240
+ 'size:include': True,
241
+ }
242
+
243
+ format_stats will convert the dict into a StatsDict with keys of 'encoded'
244
+ and 'size'.
245
+ """
246
+ stats_collector: StatsDict = {}
247
+
248
+ base_keys = {k.split(":")[0] for k in stats}
249
+ for key in base_keys:
250
+ dct: PrimitiveDict = {"id": key}
251
+ for subkey in ("label", "value", "description", "include"):
252
+ dct[subkey] = stats["{}:{}".format(key, subkey)]
253
+
254
+ try:
255
+ stats_item = StatsItem.from_dict(dct)
256
+ except ValidationError:
257
+ continue
258
+ if stats_item.include:
259
+ stats_collector[key] = stats_item
260
+
261
+ # we always have a 'has_stats' field, it's never included
262
+ has_stats = StatsItem(
263
+ id="has_stats",
264
+ label="Has Stats?",
265
+ value=len(stats_collector) > 0,
266
+ description="Indicates whether there are statistics for this table",
267
+ include=False,
268
+ )
269
+ stats_collector["has_stats"] = has_stats
270
+ return stats_collector
271
+
272
+
273
+ def mapping_key(node: ResultNode) -> CatalogKey:
274
+ dkey = dbt_common.utils.formatting.lowercase(node.database)
275
+ return CatalogKey(dkey, node.schema.lower(), node.identifier.lower())
276
+
277
+
278
+ def get_unique_id_mapping(
279
+ manifest: Manifest,
280
+ ) -> Tuple[Dict[CatalogKey, str], Dict[CatalogKey, Set[str]]]:
281
+ # A single relation could have multiple unique IDs pointing to it if a
282
+ # source were also a node.
283
+ node_map: Dict[CatalogKey, str] = {}
284
+ source_map: Dict[CatalogKey, Set[str]] = {}
285
+ for unique_id, node in manifest.nodes.items():
286
+ key = mapping_key(node)
287
+ node_map[key] = unique_id
288
+
289
+ for unique_id, source in manifest.sources.items():
290
+ key = mapping_key(source)
291
+ if key not in source_map:
292
+ source_map[key] = set()
293
+ source_map[key].add(unique_id)
294
+ return node_map, source_map
295
+
296
+
297
+ class GenerateTask(CompileTask):
298
+ def run(self) -> CatalogArtifact:
299
+ compile_results = None
300
+ if self.args.compile:
301
+ compile_results = CompileTask.run(self)
302
+ if any(r.status == NodeStatus.Error for r in compile_results):
303
+ fire_event(CannotGenerateDocs())
304
+ return CatalogArtifact.from_results(
305
+ nodes={},
306
+ sources={},
307
+ generated_at=datetime.now(timezone.utc).replace(tzinfo=None),
308
+ errors=None,
309
+ compile_results=compile_results,
310
+ )
311
+
312
+ shutil.copyfile(
313
+ DOCS_INDEX_FILE_PATH, os.path.join(self.config.project_target_path, "index.html")
314
+ )
315
+
316
+ for asset_path in self.config.asset_paths:
317
+ to_asset_path = os.path.join(self.config.project_target_path, asset_path)
318
+
319
+ if os.path.exists(to_asset_path):
320
+ shutil.rmtree(to_asset_path)
321
+
322
+ from_asset_path = os.path.join(self.config.project_root, asset_path)
323
+
324
+ if os.path.exists(from_asset_path):
325
+ shutil.copytree(from_asset_path, to_asset_path)
326
+
327
+ if self.manifest is None:
328
+ raise DbtInternalError("self.manifest was None in run!")
329
+
330
+ selected_node_ids: Optional[Set[UniqueId]] = None
331
+ if self.args.empty_catalog:
332
+ catalog_table: agate.Table = agate.Table([])
333
+ exceptions: List[Exception] = []
334
+ selected_node_ids = set()
335
+ else:
336
+ # DVT v0.4.4: Multi-adapter catalog generation
337
+ # Group catalogable nodes by their connection/adapter to avoid cross-db errors
338
+ fire_event(BuildingCatalog())
339
+
340
+ # Get selected nodes if applicable
341
+ relations = None
342
+ if self.job_queue is not None:
343
+ selected_node_ids = self.job_queue.get_selected_nodes()
344
+ selected_nodes = self._get_nodes_from_ids(self.manifest, selected_node_ids)
345
+
346
+ # Source selection is handled separately
347
+ selected_source_ids = self._get_selected_source_ids()
348
+ selected_source_nodes = self._get_nodes_from_ids(
349
+ self.manifest, selected_source_ids
350
+ )
351
+ selected_node_ids.update(selected_source_ids)
352
+ selected_nodes.extend(selected_source_nodes)
353
+
354
+ # Group all catalogable nodes by their connection/adapter
355
+ catalogable_nodes = chain(
356
+ [
357
+ node
358
+ for node in self.manifest.nodes.values()
359
+ if (node.is_relational and not node.is_ephemeral_model)
360
+ ],
361
+ self.manifest.sources.values(),
362
+ )
363
+
364
+ # Group nodes by connection name
365
+ from collections import defaultdict
366
+ from dbt.contracts.graph.nodes import SourceDefinition
367
+
368
+ nodes_by_connection: Dict[str, List] = defaultdict(list)
369
+ for node in catalogable_nodes:
370
+ # Determine which connection/adapter this node uses
371
+ if isinstance(node, SourceDefinition):
372
+ # Sources use their 'connection' field or meta.connection
373
+ connection_name = (
374
+ node.connection or
375
+ (node.meta.get('connection') if node.meta else None) or
376
+ self.config.target_name
377
+ )
378
+ elif hasattr(node, 'config') and hasattr(node.config, 'target') and node.config.target:
379
+ # Models use config.target override
380
+ connection_name = node.config.target
381
+ else:
382
+ # Default to target connection
383
+ connection_name = self.config.target_name
384
+
385
+ nodes_by_connection[connection_name].append(node)
386
+
387
+ # Query catalog for each connection with its appropriate adapter
388
+ all_catalog_tables: List[agate.Table] = []
389
+ exceptions: List[Exception] = []
390
+
391
+ for connection_name, nodes in nodes_by_connection.items():
392
+ try:
393
+ # Get adapter for this connection
394
+ adapter = self.config.get_adapter(connection_name)
395
+
396
+ # DVT v0.4.7: Set macro resolver and context generator for adapter
397
+ adapter.set_macro_resolver(self.manifest)
398
+ adapter.set_macro_context_generator(generate_runtime_macro_context)
399
+
400
+ with adapter.connection_named(f"generate_catalog_{connection_name}"):
401
+ # Build relations set for this connection if we have selected nodes
402
+ connection_relations = None
403
+ if self.job_queue is not None and selected_node_ids:
404
+ connection_relations = {
405
+ adapter.Relation.create_from(adapter.config, node)
406
+ for node in nodes
407
+ if node.unique_id in selected_node_ids
408
+ }
409
+
410
+ # Get schemas used by this connection's nodes
411
+ connection_schemas = set()
412
+ for node in nodes:
413
+ if hasattr(node, 'schema') and node.schema:
414
+ if hasattr(node, 'database') and node.database:
415
+ connection_schemas.add((node.database, node.schema))
416
+
417
+ # Query catalog for this connection's nodes
418
+ catalog_table_part, connection_exceptions = adapter.get_filtered_catalog(
419
+ nodes, connection_schemas, connection_relations
420
+ )
421
+
422
+ all_catalog_tables.append(catalog_table_part)
423
+
424
+ # DVT v0.4.7: Filter out "not implemented" errors from Snowflake/other adapters
425
+ # that don't support catalog generation
426
+ filtered_exceptions = [
427
+ e for e in connection_exceptions
428
+ if not ("not implemented" in str(e).lower() and
429
+ isinstance(e, dbt.exceptions.CompilationError))
430
+ ]
431
+ exceptions.extend(filtered_exceptions)
432
+
433
+ except dbt.exceptions.CompilationError as e:
434
+ # DVT v0.4.9: Universal fallback for adapters without get_catalog_relations
435
+ if "not implemented" in str(e).lower():
436
+ try:
437
+ # Try INFORMATION_SCHEMA fallback (works for most SQL databases)
438
+ catalog_table_part = self._get_catalog_via_information_schema(
439
+ adapter, connection_name, connection_schemas
440
+ )
441
+ if catalog_table_part and len(catalog_table_part) > 0:
442
+ all_catalog_tables.append(catalog_table_part)
443
+ fire_event(
444
+ BuildingCatalog() # Log success
445
+ )
446
+ except Exception as fallback_ex:
447
+ # DVT v0.4.9: Log fallback errors for debugging
448
+ import traceback
449
+ fire_event(
450
+ CannotGenerateDocs(
451
+ msg=f"INFORMATION_SCHEMA fallback failed for '{connection_name}': {str(fallback_ex)}\n{traceback.format_exc()}"
452
+ )
453
+ )
454
+ else:
455
+ # Other compilation errors should be reported
456
+ exceptions.append(e)
457
+ except Exception as e:
458
+ # Log error but continue with other connections
459
+ exceptions.append(e)
460
+
461
+ # Merge all catalog tables into one
462
+ if all_catalog_tables:
463
+ # Merge by concatenating rows from all tables
464
+ if len(all_catalog_tables) == 1:
465
+ catalog_table = all_catalog_tables[0]
466
+ else:
467
+ # Combine all tables - they should have the same columns
468
+ catalog_table = agate.Table.merge(all_catalog_tables)
469
+ else:
470
+ catalog_table = agate.Table([])
471
+
472
+ catalog_data: List[PrimitiveDict] = [
473
+ dict(zip(catalog_table.column_names, map(dbt.utils._coerce_decimal, row)))
474
+ for row in catalog_table
475
+ ]
476
+
477
+ catalog = Catalog(catalog_data)
478
+
479
+ errors: Optional[List[str]] = None
480
+ if exceptions:
481
+ errors = [str(e) for e in exceptions]
482
+
483
+ nodes, sources = catalog.make_unique_id_map(self.manifest, selected_node_ids)
484
+ results = self.get_catalog_results(
485
+ nodes=nodes,
486
+ sources=sources,
487
+ generated_at=datetime.now(timezone.utc).replace(tzinfo=None),
488
+ compile_results=compile_results,
489
+ errors=errors,
490
+ )
491
+
492
+ catalog_path = os.path.join(self.config.project_target_path, CATALOG_FILENAME)
493
+ results.write(catalog_path)
494
+ add_artifact_produced(catalog_path)
495
+ fire_event(
496
+ ArtifactWritten(artifact_type=results.__class__.__name__, artifact_path=catalog_path)
497
+ )
498
+
499
+ if self.args.compile:
500
+ write_manifest(self.manifest, self.config.project_target_path)
501
+
502
+ if self.args.static:
503
+
504
+ # Read manifest.json and catalog.json
505
+ read_manifest_data = load_file_contents(
506
+ os.path.join(self.config.project_target_path, MANIFEST_FILE_NAME)
507
+ )
508
+ read_catalog_data = load_file_contents(catalog_path)
509
+
510
+ # Create new static index file contents
511
+ index_data = load_file_contents(DOCS_INDEX_FILE_PATH)
512
+ index_data = index_data.replace('"MANIFEST.JSON INLINE DATA"', read_manifest_data)
513
+ index_data = index_data.replace('"CATALOG.JSON INLINE DATA"', read_catalog_data)
514
+
515
+ # Write out the new index file
516
+ static_index_path = os.path.join(self.config.project_target_path, "static_index.html")
517
+ with open(static_index_path, "wb") as static_index_file:
518
+ static_index_file.write(bytes(index_data, "utf8"))
519
+
520
+ if exceptions:
521
+ fire_event(WriteCatalogFailure(num_exceptions=len(exceptions)))
522
+ fire_event(CatalogWritten(path=os.path.abspath(catalog_path)))
523
+
524
+ # DVT v0.56.0: Write enriched catalog to metadata_store.duckdb
525
+ self._write_catalog_to_duckdb(nodes, sources)
526
+ self._write_lineage_to_duckdb()
527
+
528
+ return results
529
+
530
+ def get_node_selector(self) -> ResourceTypeSelector:
531
+ if self.manifest is None or self.graph is None:
532
+ raise DbtInternalError("manifest and graph must be set to perform node selection")
533
+ return ResourceTypeSelector(
534
+ graph=self.graph,
535
+ manifest=self.manifest,
536
+ previous_state=self.previous_state,
537
+ resource_types=EXECUTABLE_NODE_TYPES,
538
+ include_empty_nodes=True,
539
+ )
540
+
541
+ def get_catalog_results(
542
+ self,
543
+ nodes: Dict[str, CatalogTable],
544
+ sources: Dict[str, CatalogTable],
545
+ generated_at: datetime,
546
+ compile_results: Optional[Any],
547
+ errors: Optional[List[str]],
548
+ ) -> CatalogArtifact:
549
+ return CatalogArtifact.from_results(
550
+ generated_at=generated_at,
551
+ nodes=nodes,
552
+ sources=sources,
553
+ compile_results=compile_results,
554
+ errors=errors,
555
+ )
556
+
557
+ @classmethod
558
+ def interpret_results(self, results: Optional[CatalogResults]) -> bool:
559
+ if results is None:
560
+ return False
561
+ if results.errors:
562
+ return False
563
+ compile_results = results._compile_results
564
+ if compile_results is None:
565
+ return True
566
+
567
+ return super().interpret_results(compile_results)
568
+
569
+ @staticmethod
570
+ def _get_nodes_from_ids(manifest: Manifest, node_ids: Iterable[str]) -> List[ResultNode]:
571
+ selected: List[ResultNode] = []
572
+ for unique_id in node_ids:
573
+ if unique_id in manifest.nodes:
574
+ node = manifest.nodes[unique_id]
575
+ if node.is_relational and not node.is_ephemeral_model:
576
+ selected.append(node)
577
+ elif unique_id in manifest.sources:
578
+ source = manifest.sources[unique_id]
579
+ selected.append(source)
580
+ return selected
581
+
582
+ def _get_selected_source_ids(self) -> Set[UniqueId]:
583
+ if self.manifest is None or self.graph is None:
584
+ raise DbtInternalError("manifest and graph must be set to perform node selection")
585
+
586
+ source_selector = ResourceTypeSelector(
587
+ graph=self.graph,
588
+ manifest=self.manifest,
589
+ previous_state=self.previous_state,
590
+ resource_types=[NodeType.Source],
591
+ )
592
+
593
+ return source_selector.get_graph_queue(self.get_selection_spec()).get_selected_nodes()
594
+
595
+ def _get_catalog_via_information_schema(
596
+ self, adapter, connection_name: str, schemas: Set[Tuple[str, str]]
597
+ ) -> agate.Table:
598
+ """
599
+ DVT v0.4.8: Universal fallback for catalog generation using INFORMATION_SCHEMA.
600
+
601
+ Works for most SQL databases (Postgres, MySQL, Snowflake, Redshift, BigQuery, SQL Server).
602
+ Falls back gracefully for databases without INFORMATION_SCHEMA (Oracle, DB2).
603
+
604
+ :param adapter: Database adapter
605
+ :param connection_name: Connection name for logging
606
+ :param schemas: Set of (database, schema) tuples to query
607
+ :return: agate.Table with catalog data
608
+ """
609
+ if not schemas:
610
+ return agate.Table([])
611
+
612
+ # Build WHERE clause for schemas
613
+ schema_conditions = []
614
+ for database, schema in schemas:
615
+ # Most databases only need schema filter, some need database too
616
+ schema_conditions.append(f"table_schema = '{schema}'")
617
+
618
+ where_clause = " OR ".join(schema_conditions)
619
+
620
+ # Universal INFORMATION_SCHEMA query (works for most SQL databases)
621
+ query = f"""
622
+ SELECT
623
+ table_catalog as table_database,
624
+ table_schema,
625
+ table_name,
626
+ column_name,
627
+ data_type,
628
+ ordinal_position as column_index
629
+ FROM information_schema.columns
630
+ WHERE {where_clause}
631
+ ORDER BY table_schema, table_name, ordinal_position
632
+ """
633
+
634
+ try:
635
+ # Execute query using adapter's connection
636
+ _, result = adapter.execute(query, auto_begin=False, fetch=True)
637
+
638
+ # Convert to agate.Table format expected by catalog
639
+ if result and len(result) > 0:
640
+ # Transform result into catalog format
641
+ catalog_data = []
642
+ for row in result:
643
+ catalog_data.append({
644
+ 'table_database': row[0],
645
+ 'table_schema': row[1],
646
+ 'table_name': row[2],
647
+ 'column_name': row[3],
648
+ 'column_type': row[4],
649
+ 'column_index': row[5]
650
+ })
651
+
652
+ # Create agate.Table with proper column types
653
+ return agate.Table(catalog_data)
654
+ else:
655
+ return agate.Table([])
656
+
657
+ except Exception as e:
658
+ # Fallback failed - database might not support INFORMATION_SCHEMA
659
+ # (e.g., Oracle, DB2, or permission issues)
660
+ fire_event(
661
+ CannotGenerateDocs(
662
+ msg=f"INFORMATION_SCHEMA fallback failed for '{connection_name}': {str(e)}"
663
+ )
664
+ )
665
+ return agate.Table([])
666
+
667
+ # =========================================================================
668
+ # DVT v0.56.0: DuckDB Catalog and Lineage Storage
669
+ # =========================================================================
670
+
671
+ def _write_catalog_to_duckdb(
672
+ self,
673
+ nodes: Dict[str, CatalogTable],
674
+ sources: Dict[str, CatalogTable],
675
+ ) -> None:
676
+ """
677
+ Write enriched catalog to metadata_store.duckdb.
678
+
679
+ DVT v0.56.0: Stores catalog nodes with connection info, adapter type,
680
+ and visual enrichment (icons, colors) for enhanced docs serve.
681
+ """
682
+ try:
683
+ import json
684
+ from pathlib import Path
685
+ from dbt.compute.metadata import ProjectMetadataStore, CatalogNode
686
+
687
+ project_root = Path(self.config.project_root)
688
+ store = ProjectMetadataStore(project_root)
689
+ store.initialize()
690
+
691
+ # Clear existing catalog data
692
+ store.clear_catalog_nodes()
693
+
694
+ # Adapter icon mapping
695
+ adapter_icons = {
696
+ 'postgres': 'postgresql',
697
+ 'snowflake': 'snowflake',
698
+ 'bigquery': 'bigquery',
699
+ 'redshift': 'redshift',
700
+ 'databricks': 'databricks',
701
+ 'spark': 'spark',
702
+ 'duckdb': 'duckdb',
703
+ 'mysql': 'mysql',
704
+ 'sqlserver': 'sqlserver',
705
+ 'oracle': 'oracle',
706
+ }
707
+
708
+ # Connection color mapping (for visual distinction)
709
+ connection_colors = [
710
+ '#3498db', # Blue
711
+ '#2ecc71', # Green
712
+ '#e74c3c', # Red
713
+ '#9b59b6', # Purple
714
+ '#f39c12', # Orange
715
+ '#1abc9c', # Teal
716
+ '#e91e63', # Pink
717
+ '#607d8b', # Blue Grey
718
+ ]
719
+ color_index = 0
720
+ connection_color_map: Dict[str, str] = {}
721
+
722
+ # Process nodes (models)
723
+ for unique_id, table in nodes.items():
724
+ node = self.manifest.nodes.get(unique_id) if self.manifest else None
725
+
726
+ # Get connection and adapter info
727
+ connection_name = "default"
728
+ adapter_type = None
729
+ materialized = None
730
+ tags = []
731
+ meta = {}
732
+
733
+ if node:
734
+ if hasattr(node.config, 'target') and node.config.target:
735
+ connection_name = node.config.target
736
+ if hasattr(node.config, 'materialized'):
737
+ materialized = node.config.materialized
738
+ if hasattr(node, 'tags'):
739
+ tags = list(node.tags)
740
+ if hasattr(node, 'meta'):
741
+ meta = dict(node.meta) if node.meta else {}
742
+
743
+ # Assign connection color
744
+ if connection_name not in connection_color_map:
745
+ connection_color_map[connection_name] = connection_colors[color_index % len(connection_colors)]
746
+ color_index += 1
747
+
748
+ # Get adapter type from connection
749
+ try:
750
+ adapter = self.config.get_adapter(connection_name)
751
+ adapter_type = adapter.type() if hasattr(adapter, 'type') else None
752
+ except Exception:
753
+ adapter_type = None
754
+
755
+ icon_type = adapter_icons.get(adapter_type, 'database') if adapter_type else 'database'
756
+
757
+ # Serialize columns
758
+ columns_data = []
759
+ for col_name, col_meta in table.columns.items():
760
+ columns_data.append({
761
+ 'name': col_name,
762
+ 'type': col_meta.type if hasattr(col_meta, 'type') else None,
763
+ 'comment': col_meta.comment if hasattr(col_meta, 'comment') else None,
764
+ })
765
+
766
+ # Get row count from stats
767
+ row_count = None
768
+ if hasattr(table, 'stats') and table.stats:
769
+ for stat_id, stat in table.stats.items():
770
+ if stat_id == 'row_count' and hasattr(stat, 'value'):
771
+ try:
772
+ row_count = int(stat.value)
773
+ except (ValueError, TypeError):
774
+ pass
775
+
776
+ catalog_node = CatalogNode(
777
+ unique_id=unique_id,
778
+ resource_type='model',
779
+ name=node.name if node else table.metadata.name,
780
+ schema_name=table.metadata.schema,
781
+ database=table.metadata.database,
782
+ connection_name=connection_name,
783
+ adapter_type=adapter_type,
784
+ description=node.description if node and hasattr(node, 'description') else None,
785
+ icon_type=icon_type,
786
+ color_hex=connection_color_map.get(connection_name),
787
+ materialized=materialized,
788
+ tags=json.dumps(tags) if tags else None,
789
+ meta=json.dumps(meta) if meta else None,
790
+ columns=json.dumps(columns_data) if columns_data else None,
791
+ row_count=row_count,
792
+ )
793
+ store.save_catalog_node(catalog_node)
794
+
795
+ # Process sources
796
+ for unique_id, table in sources.items():
797
+ source = self.manifest.sources.get(unique_id) if self.manifest else None
798
+
799
+ # Get connection and adapter info
800
+ connection_name = "default"
801
+ adapter_type = None
802
+ tags = []
803
+ meta = {}
804
+
805
+ if source:
806
+ if hasattr(source, 'connection') and source.connection:
807
+ connection_name = source.connection
808
+ elif hasattr(source, 'meta') and source.meta and source.meta.get('connection'):
809
+ connection_name = source.meta.get('connection')
810
+ if hasattr(source, 'tags'):
811
+ tags = list(source.tags)
812
+ if hasattr(source, 'meta'):
813
+ meta = dict(source.meta) if source.meta else {}
814
+
815
+ # Assign connection color
816
+ if connection_name not in connection_color_map:
817
+ connection_color_map[connection_name] = connection_colors[color_index % len(connection_colors)]
818
+ color_index += 1
819
+
820
+ # Get adapter type from connection
821
+ try:
822
+ adapter = self.config.get_adapter(connection_name)
823
+ adapter_type = adapter.type() if hasattr(adapter, 'type') else None
824
+ except Exception:
825
+ adapter_type = None
826
+
827
+ icon_type = adapter_icons.get(adapter_type, 'database') if adapter_type else 'database'
828
+
829
+ # Serialize columns
830
+ columns_data = []
831
+ for col_name, col_meta in table.columns.items():
832
+ columns_data.append({
833
+ 'name': col_name,
834
+ 'type': col_meta.type if hasattr(col_meta, 'type') else None,
835
+ 'comment': col_meta.comment if hasattr(col_meta, 'comment') else None,
836
+ })
837
+
838
+ catalog_node = CatalogNode(
839
+ unique_id=unique_id,
840
+ resource_type='source',
841
+ name=source.name if source else table.metadata.name,
842
+ schema_name=table.metadata.schema,
843
+ database=table.metadata.database,
844
+ connection_name=connection_name,
845
+ adapter_type=adapter_type,
846
+ description=source.description if source and hasattr(source, 'description') else None,
847
+ icon_type=icon_type,
848
+ color_hex=connection_color_map.get(connection_name),
849
+ tags=json.dumps(tags) if tags else None,
850
+ meta=json.dumps(meta) if meta else None,
851
+ columns=json.dumps(columns_data) if columns_data else None,
852
+ )
853
+ store.save_catalog_node(catalog_node)
854
+
855
+ store.close()
856
+ fire_event(CatalogWritten(path=str(store.db_path)))
857
+
858
+ except ImportError:
859
+ # DuckDB not installed - skip
860
+ pass
861
+ except Exception as e:
862
+ # Log but don't fail catalog generation
863
+ fire_event(
864
+ CannotGenerateDocs(msg=f"Could not write catalog to DuckDB: {str(e)}")
865
+ )
866
+
867
+ def _write_lineage_to_duckdb(self) -> None:
868
+ """
869
+ Write lineage edges to metadata_store.duckdb.
870
+
871
+ DVT v0.56.0: Stores full DAG with cross-connection indicators
872
+ for enhanced visualization in docs serve.
873
+ """
874
+ if self.manifest is None:
875
+ return
876
+
877
+ try:
878
+ from pathlib import Path
879
+ from dbt.compute.metadata import ProjectMetadataStore, LineageEdge
880
+
881
+ project_root = Path(self.config.project_root)
882
+ store = ProjectMetadataStore(project_root)
883
+ store.initialize()
884
+
885
+ # Clear existing lineage data
886
+ store.clear_lineage_edges()
887
+
888
+ # Build connection map for cross-connection detection
889
+ node_connections: Dict[str, str] = {}
890
+
891
+ # Map nodes to connections
892
+ for unique_id, node in self.manifest.nodes.items():
893
+ if hasattr(node.config, 'target') and node.config.target:
894
+ node_connections[unique_id] = node.config.target
895
+ else:
896
+ node_connections[unique_id] = self.config.target_name
897
+
898
+ # Map sources to connections
899
+ for unique_id, source in self.manifest.sources.items():
900
+ if hasattr(source, 'connection') and source.connection:
901
+ node_connections[unique_id] = source.connection
902
+ elif hasattr(source, 'meta') and source.meta and source.meta.get('connection'):
903
+ node_connections[unique_id] = source.meta.get('connection')
904
+ else:
905
+ node_connections[unique_id] = self.config.target_name
906
+
907
+ # Process dependencies
908
+ for unique_id, node in self.manifest.nodes.items():
909
+ if not hasattr(node, 'depends_on') or not node.depends_on:
910
+ continue
911
+
912
+ target_connection = node_connections.get(unique_id, self.config.target_name)
913
+
914
+ # Process node dependencies
915
+ for dep_id in node.depends_on.nodes:
916
+ source_connection = node_connections.get(dep_id, self.config.target_name)
917
+
918
+ # Determine edge type
919
+ if dep_id.startswith('source.'):
920
+ edge_type = 'source'
921
+ elif dep_id.startswith('model.'):
922
+ edge_type = 'ref'
923
+ else:
924
+ edge_type = 'depends_on'
925
+
926
+ is_cross = source_connection != target_connection
927
+
928
+ edge = LineageEdge(
929
+ source_node_id=dep_id,
930
+ target_node_id=unique_id,
931
+ edge_type=edge_type,
932
+ is_cross_connection=is_cross,
933
+ source_connection=source_connection,
934
+ target_connection=target_connection,
935
+ )
936
+ store.save_lineage_edge(edge)
937
+
938
+ store.close()
939
+
940
+ except ImportError:
941
+ # DuckDB not installed - skip
942
+ pass
943
+ except Exception as e:
944
+ # Log but don't fail
945
+ fire_event(
946
+ CannotGenerateDocs(msg=f"Could not write lineage to DuckDB: {str(e)}")
947
+ )