@@ -243,6 +243,15 @@ def _resolve_db_path(
243243_SOURCE_CALL_RE = re .compile (
244244 r"""\bsource\s*\(\s*(['"])(?P<source>[^'"]+)\1\s*,\s*(['"])(?P<table>[^'"]+)\3\s*\)"""
245245)
246+ _JINJA_IF_ELSE_RE = re .compile (
247+ r"""\{%\s*if\s+(?P<condition>.*?)\s*%\}(?P<true>.*?)\{%\s*else\s*%\}(?P<false>.*?)\{%\s*endif\s*%\}""" ,
248+ re .DOTALL ,
249+ )
250+ _JINJA_EXPR_RE = re .compile (r"""\{\{\s*(?P<expr>.*?)\s*\}\}""" , re .DOTALL )
251+ _VAR_CALL_RE = re .compile (
252+ r"""^var\s*\(\s*(['"])(?P<name>[^'"]+)\1(?:\s*,\s*(?P<default>.*?))?\s*\)$""" ,
253+ re .DOTALL ,
254+ )
246255
247256
248257def _read_dbt_source_tables (workspace : Path ) -> set [tuple [str , str ]]:
@@ -267,6 +276,7 @@ def _read_dbt_source_tables(workspace: Path) -> set[tuple[str, str]]:
267276 return set ()
268277
269278 referenced_sources = _read_referenced_source_names (workspace )
279+ render_context = _read_dbt_render_context (workspace , yaml )
270280 relations : set [tuple [str , str ]] = set ()
271281 for yaml_path in _iter_candidate_dbt_yaml_files (workspace ):
272282 try :
@@ -276,6 +286,9 @@ def _read_dbt_source_tables(workspace: Path) -> set[tuple[str, str]]:
276286 for source in _iter_dicts (_as_list (_as_dict (document ).get ("sources" ))):
277287 source_name = source .get ("name" )
278288 source_schema = source .get ("schema" ) or source_name
289+ source_external_location = _as_dict (source .get ("meta" )).get (
290+ "external_location"
291+ )
279292 for table in _iter_dicts (_as_list (source .get ("tables" ))):
280293 table_name = table .get ("name" )
281294 if not (
@@ -288,9 +301,23 @@ def _read_dbt_source_tables(workspace: Path) -> set[tuple[str, str]]:
288301 source_key = (source_name .strip ().lower (), table_name .strip ().lower ())
289302 if referenced_sources and source_key not in referenced_sources :
290303 continue
304+ table_external_location = _as_dict (table .get ("meta" )).get (
305+ "external_location"
306+ )
307+ if source_external_location or table_external_location :
308+ continue
291309 name = table .get ("identifier" ) or table_name
292310 schema = table .get ("schema" ) or source_schema
293- if not (isinstance (schema , str ) and schema .strip ()):
311+ if not (
312+ isinstance (name , str )
313+ and name .strip ()
314+ and isinstance (schema , str )
315+ and schema .strip ()
316+ ):
317+ continue
318+ name = _render_dbt_metadata_value (name , render_context )
319+ schema = _render_dbt_metadata_value (schema , render_context )
320+ if "{{" in name or "{%" in name or "{{" in schema or "{%" in schema :
294321 continue
295322 relations .add ((schema .strip ().lower (), name .strip ().lower ()))
296323 return relations
@@ -320,6 +347,141 @@ def _read_referenced_source_names(workspace: Path) -> set[tuple[str, str]]:
320347 return referenced
321348
322349
350+ def _read_dbt_render_context (workspace : Path , yaml_module : Any ) -> dict [str , Any ]:
351+ context : dict [str , Any ] = {
352+ "vars" : {},
353+ "target" : {"schema" : "main" },
354+ }
355+
356+ project_path = workspace / "dbt_project.yml"
357+ if project_path .is_file ():
358+ try :
359+ document = yaml_module .safe_load (project_path .read_text ())
360+ except Exception :
361+ document = None
362+ vars_value = _as_dict (_as_dict (document ).get ("vars" ))
363+ context ["vars" ] = _flatten_dbt_vars (vars_value )
364+
365+ target_schema = _read_profiles_target_schema (workspace , yaml_module )
366+ if target_schema :
367+ context ["target" ]["schema" ] = target_schema
368+
369+ return context
370+
371+
372+ def _flatten_dbt_vars (value : dict [str , Any ]) -> dict [str , Any ]:
373+ flattened : dict [str , Any ] = {}
374+ for key , child in value .items ():
375+ if not isinstance (key , str ):
376+ continue
377+ flattened [key ] = child
378+
379+ for child in value .values ():
380+ if not isinstance (child , dict ):
381+ continue
382+ for key , nested_child in child .items ():
383+ if isinstance (key , str ) and key not in flattened :
384+ flattened [key ] = nested_child
385+ return flattened
386+
387+
388+ def _read_profiles_target_schema (workspace : Path , yaml_module : Any ) -> str | None :
389+ for profiles_path in sorted (workspace .rglob ("profiles.yml" )):
390+ try :
391+ document = yaml_module .safe_load (profiles_path .read_text ())
392+ except Exception :
393+ continue
394+ for profile in _iter_dicts (list (_as_dict (document ).values ())):
395+ outputs = {
396+ name : out
397+ for name , out in _as_dict (profile .get ("outputs" )).items ()
398+ if isinstance (out , dict )
399+ }
400+ if not outputs :
401+ continue
402+
403+ target = profile .get ("target" )
404+ selected = None
405+ if isinstance (target , str ) and target .strip () in outputs :
406+ selected = outputs [target .strip ()]
407+ elif len (outputs ) == 1 :
408+ (selected ,) = outputs .values ()
409+
410+ schema = _as_dict (selected ).get ("schema" )
411+ if isinstance (schema , str ) and schema .strip ():
412+ return schema .strip ()
413+ return None
414+
415+
416+ def _render_dbt_metadata_value (value : str , context : dict [str , Any ]) -> str :
417+ text = value
418+ while True :
419+ match = _JINJA_IF_ELSE_RE .search (text )
420+ if match is None :
421+ break
422+ replacement = (
423+ match .group ("true" )
424+ if _eval_dbt_condition (match .group ("condition" ), context )
425+ else match .group ("false" )
426+ )
427+ text = text [: match .start ()] + replacement + text [match .end () :]
428+
429+ return _JINJA_EXPR_RE .sub (
430+ lambda match : _stringify_dbt_value (
431+ _eval_dbt_expr (match .group ("expr" ), context )
432+ ),
433+ text ,
434+ )
435+
436+
437+ def _eval_dbt_condition (expr : str , context : dict [str , Any ]) -> bool :
438+ value = _eval_dbt_expr (expr , context )
439+ if isinstance (value , str ):
440+ return value .lower () not in {"" , "0" , "false" , "none" , "null" }
441+ return bool (value )
442+
443+
444+ def _eval_dbt_expr (expr : str , context : dict [str , Any ]) -> Any :
445+ parts = [part .strip () for part in expr .split ("~" )]
446+ if len (parts ) > 1 :
447+ return "" .join (
448+ _stringify_dbt_value (_eval_dbt_expr (part , context )) for part in parts
449+ )
450+
451+ value = expr .strip ()
452+ if len (value ) >= 2 and value [0 ] == value [- 1 ] and value [0 ] in {"'" , '"' }:
453+ return value [1 :- 1 ]
454+ if value == "target.schema" :
455+ return _as_dict (context .get ("target" )).get ("schema" , "main" )
456+ if value .lower () == "true" :
457+ return True
458+ if value .lower () == "false" :
459+ return False
460+
461+ var_match = _VAR_CALL_RE .match (value )
462+ if var_match :
463+ var_name = var_match .group ("name" )
464+ vars_value = _as_dict (context .get ("vars" ))
465+ if var_name in vars_value :
466+ return vars_value [var_name ]
467+ default = var_match .group ("default" )
468+ if default is not None :
469+ return _eval_dbt_expr (default , context )
470+ return ""
471+
472+ return value
473+
474+
475+ def _stringify_dbt_value (value : Any ) -> str :
476+ if value is True :
477+ return "true"
478+ if value is False :
479+ return "false"
480+ if value is None :
481+ return ""
482+ return str (value )
483+
484+
323485def _format_relations (relations : set [tuple [str , str ]]) -> list [str ]:
324486 return [f"{ schema } .{ table } " for schema , table in relations ]
325487
0 commit comments