@@ -132,13 +132,8 @@ def parse_source(self, target: UnpatchedSourceDefinition) -> SourceDefinition:
132132 source_description = source .description or ""
133133
134134 quoting = source .quoting .merged (table .quoting )
135- # path = block.path.original_file_path
136- table_meta = table .meta or {}
137- source_meta = source .meta or {}
138- meta = {** source_meta , ** table_meta }
139-
140- # make sure we don't do duplicate tags from source + table
141- tags = sorted (set (itertools .chain (source .tags , table .tags )))
135+ # Retain original source meta prior to merge with table meta
136+ source_meta = {** source .meta , ** source .config .get ("meta" , {})}
142137
143138 config = self ._generate_source_config (
144139 target = target ,
@@ -176,15 +171,15 @@ def parse_source(self, target: UnpatchedSourceDefinition) -> SourceDefinition:
176171 source_name = source .name ,
177172 source_description = source_description ,
178173 source_meta = source_meta ,
179- meta = meta ,
174+ meta = config . meta ,
180175 loader = source .loader ,
181176 loaded_at_field = config .loaded_at_field ,
182177 loaded_at_query = config .loaded_at_query ,
183178 freshness = config .freshness ,
184179 quoting = quoting ,
185180 resource_type = NodeType .Source ,
186181 fqn = target .fqn ,
187- tags = tags ,
182+ tags = config . tags ,
188183 config = config ,
189184 unrendered_config = unrendered_config ,
190185 )
@@ -313,6 +308,13 @@ def _generate_source_config(self, target: UnpatchedSourceDefinition, rendered: b
313308 precedence_configs ["loaded_at_field" ] = precedence_loaded_at_field
314309 precedence_configs ["loaded_at_query" ] = precedence_loaded_at_query
315310
311+ # Handle merges across source, table, and config for meta and tags
312+ precedence_meta = self .calculate_meta_from_raw_target (target )
313+ precedence_configs ["meta" ] = precedence_meta
314+
315+ precedence_tags = self .calculate_tags_from_raw_target (target )
316+ precedence_configs ["tags" ] = precedence_tags
317+
316318 # Because freshness is a "object" config, the freshness from the dbt_project.yml and the freshness
317319 # from the schema file _won't_ get merged by this process. The result will be that the freshness will
318320 # come from the schema file if provided, and if not, it'll fall back to the dbt_project.yml freshness.
@@ -472,6 +474,34 @@ def calculate_loaded_at_field_query_from_raw_target(
472474
473475 return loaded_at_field , loaded_at_query
474476
477+ def calculate_meta_from_raw_target (self , target : UnpatchedSourceDefinition ) -> Dict [str , Any ]:
478+ source_meta = target .source .meta or {}
479+ source_config_meta = target .source .config .get ("meta" , {})
480+ source_config_meta = source_config_meta if isinstance (source_config_meta , dict ) else {}
481+
482+ table_meta = target .table .meta or {}
483+ table_config_meta = target .table .config .get ("meta" , {})
484+ table_config_meta = table_config_meta if isinstance (table_config_meta , dict ) else {}
485+
486+ return {** source_meta , ** source_config_meta , ** table_meta , ** table_config_meta }
487+
488+ def calculate_tags_from_raw_target (self , target : UnpatchedSourceDefinition ) -> List [str ]:
489+ source_tags = target .source .tags or []
490+ source_config_tags = target .source .config .get ("tags" , [])
491+ source_config_tags = (
492+ source_config_tags if isinstance (source_config_tags , list ) else [source_config_tags ]
493+ )
494+
495+ table_tags = target .table .tags or []
496+ table_config_tags = target .table .config .get ("tags" , [])
497+ table_config_tags = (
498+ table_config_tags if isinstance (table_config_tags , list ) else [table_config_tags ]
499+ )
500+
501+ return sorted (
502+ set (itertools .chain (source_tags , source_config_tags , table_tags , table_config_tags ))
503+ )
504+
475505
476506def merge_freshness_time_thresholds (
477507 base : Optional [Time ], update : Optional [Time ]
0 commit comments