From 79a2d7cb04e7b67b2fdbea8524af2ea6cd6595a1 Mon Sep 17 00:00:00 2001 From: dal Date: Tue, 6 May 2025 12:15:34 -0600 Subject: [PATCH] ok some changes to locations for semantic models --- cli/cli/src/commands/deploy/deploy.rs | 245 +++++++++++++++----------- cli/cli/src/commands/generate.rs | 4 +- cli/cli/src/commands/init.rs | 125 ++++++++++--- cli/cli/src/utils/config.rs | 4 +- 4 files changed, 246 insertions(+), 132 deletions(-) diff --git a/cli/cli/src/commands/deploy/deploy.rs b/cli/cli/src/commands/deploy/deploy.rs index 0897badd6..d3f6249f5 100644 --- a/cli/cli/src/commands/deploy/deploy.rs +++ b/cli/cli/src/commands/deploy/deploy.rs @@ -223,7 +223,6 @@ fn check_excluded_tags( exclude_tags: Some(exclude_tags.to_vec()), model_paths: None, projects: None, - semantic_models_file: None, }; let manager = ExclusionManager::new(&temp_config)?; @@ -390,103 +389,124 @@ pub async fn deploy(path: Option<&str>, dry_run: bool, recursive: bool) -> Resul } }; - // Determine the base directory for resolving relative paths (where buster.yml is or would be) let effective_buster_config_dir = BusterConfig::base_dir(&buster_config_load_dir.join("buster.yml")).unwrap_or(buster_config_load_dir.clone()); let mut deploy_requests_final: Vec = Vec::new(); let mut model_mappings_final: Vec = Vec::new(); + let mut processed_models_from_spec = false; - // --- PRIMARY PATH: Use semantic_models_file from BusterConfig if available --- + // --- PRIMARY PATH: Iterate through projects and use semantic_models_file if available --- if let Some(ref cfg) = buster_config { - if let Some(ref semantic_models_file_str) = cfg.semantic_models_file { - println!("ℹ️ Using semantic_models_file from buster.yml: {}", semantic_models_file_str.cyan()); - let semantic_spec_path = effective_buster_config_dir.join(semantic_models_file_str); + if let Some(ref projects) = cfg.projects { + for project_ctx in projects { + if let Some(ref semantic_models_file_str) = project_ctx.semantic_models_file { + println!( + "ℹ️ Using semantic_models_file for project '{}': {}", + project_ctx.identifier().cyan(), + semantic_models_file_str.cyan() + ); + let semantic_spec_path = effective_buster_config_dir.join(semantic_models_file_str); - if !semantic_spec_path.exists() { - return Err(anyhow!("Specified semantic_models_file not found: {}", semantic_spec_path.display())); - } - - progress.current_file = semantic_spec_path.to_string_lossy().into_owned(); - progress.status = "Loading main semantic layer specification...".to_string(); - progress.log_progress(); - - let spec = match parse_semantic_layer_spec(&semantic_spec_path) { - Ok(s) => s, - Err(e) => { - progress.log_error(&format!("Failed to parse semantic layer spec: {}", e)); - result.failures.push((progress.current_file.clone(), "spec_level".to_string(), vec![e.to_string()])); - // Decide if to bail out or proceed to fallback - // For now, let's assume if semantic_models_file is specified and fails, we stop. - progress.log_summary(&result); - return Err(anyhow!("Failed to process specified semantic_models_file.")); - } - }; - progress.total_files = spec.models.len(); - - // Resolve configurations for all models in the spec - // For a single spec file, the "project context" is effectively the global one or the first one defined. - let primary_project_context = cfg.projects.as_ref().and_then(|p| p.first()); - let models_with_context: Vec<(Model, Option<&ProjectContext>)> = spec.models.into_iter() - .map(|m| (m, primary_project_context)) - .collect(); - - let resolved_models = match resolve_model_configurations(models_with_context, cfg) { - Ok(models) => models, - Err(e) => { - progress.log_error(&format!("Configuration resolution failed for spec: {}", e)); - result.failures.push((progress.current_file.clone(), "spec_config_resolution".to_string(), vec![e.to_string()])); - progress.log_summary(&result); - return Err(anyhow!("Configuration resolution failed for models in semantic_models_file.")); - } - }; - - for model in resolved_models { - progress.processed += 1; - progress.current_file = format!("{} (from {})", model.name, semantic_spec_path.file_name().unwrap_or_default().to_string_lossy()); - progress.status = format!("Processing model '{}'", model.name); - progress.log_progress(); - - let sql_content = match get_sql_content_for_model(&model, &effective_buster_config_dir, &semantic_spec_path) { - Ok(content) => content, - Err(e) => { - progress.log_error(&format!("Failed to get SQL for model {}: {}", model.name, e)); - result.failures.push((progress.current_file.clone(),model.name.clone(),vec![e.to_string()])); - continue; + if !semantic_spec_path.exists() { + // Log error for this specific project and continue to next or fallback + let error_msg = format!("Specified semantic_models_file not found for project '{}': {}", project_ctx.identifier(), semantic_spec_path.display()); + eprintln!("❌ {}", error_msg.red()); + result.failures.push(( + semantic_spec_path.to_string_lossy().into_owned(), + format!("project_{}", project_ctx.identifier()), + vec![format!("File not found: {}", semantic_spec_path.display())] + )); + continue; // Continue to the next project or fallback if this was the last one } - }; - - model_mappings_final.push(ModelMapping { - file: semantic_spec_path.file_name().unwrap_or_default().to_string_lossy().into_owned(), // Use spec filename - model_name: model.name.clone() - }); - deploy_requests_final.push(to_deploy_request(&model, sql_content)); - progress.log_success(); - } - } else { - println!("ℹ️ No semantic_models_file specified in buster.yml. Falling back to scanning for individual .yml files."); - deploy_individual_yml_files( - buster_config.as_ref(), - &effective_buster_config_dir, - recursive, - &mut progress, - &mut result, - &mut deploy_requests_final, - &mut model_mappings_final - ).await?; + progress.current_file = semantic_spec_path.to_string_lossy().into_owned(); + progress.status = format!("Loading semantic layer specification for project '{}'...", project_ctx.identifier()); + progress.log_progress(); + + let spec = match parse_semantic_layer_spec(&semantic_spec_path) { + Ok(s) => s, + Err(e) => { + progress.log_error(&format!("Failed to parse semantic layer spec for project '{}': {}", project_ctx.identifier(), e)); + result.failures.push(( + progress.current_file.clone(), + format!("project_{}_spec_level", project_ctx.identifier()), + vec![e.to_string()] + )); + continue; // Continue to the next project or fallback + } + }; + progress.total_files += spec.models.len(); // Accumulate total files + processed_models_from_spec = true; + + // Resolve configurations for all models in the spec using the current project_ctx + let models_with_context: Vec<(Model, Option<&ProjectContext>)> = spec.models.into_iter() + .map(|m| (m, Some(project_ctx))) + .collect(); + + let resolved_models = match resolve_model_configurations(models_with_context, cfg) { // cfg is the global BusterConfig + Ok(models) => models, + Err(e) => { + progress.log_error(&format!("Configuration resolution failed for spec in project '{}': {}", project_ctx.identifier(), e)); + result.failures.push(( + progress.current_file.clone(), + format!("project_{}_config_resolution", project_ctx.identifier()), + vec![e.to_string()] + )); + continue; // Continue to the next project or fallback + } + }; + + for model in resolved_models { + progress.processed += 1; + progress.current_file = format!("{} (from {} in project '{}')", model.name, semantic_spec_path.file_name().unwrap_or_default().to_string_lossy(), project_ctx.identifier()); + progress.status = format!("Processing model '{}'", model.name); + progress.log_progress(); + + let sql_content = match get_sql_content_for_model(&model, &effective_buster_config_dir, &semantic_spec_path) { + Ok(content) => content, + Err(e) => { + progress.log_error(&format!("Failed to get SQL for model {}: {}", model.name, e)); + result.failures.push((progress.current_file.clone(),model.name.clone(),vec![e.to_string()])); + continue; + } + }; + + model_mappings_final.push(ModelMapping { + file: semantic_spec_path.file_name().unwrap_or_default().to_string_lossy().into_owned(), + model_name: model.name.clone() + }); + deploy_requests_final.push(to_deploy_request(&model, sql_content)); + progress.log_success(); + } + } + } } - } else { - // No BusterConfig loaded at all - println!("ℹ️ No buster.yml loaded. Scanning current/target directory for individual .yml files."); + } + + // --- FALLBACK or ADDITIONAL: Scan for individual .yml files --- + // This runs if no semantic_models_file was processed from any project, + // or to supplement if specific logic allows (currently, it runs if processed_models_from_spec is false). + if !processed_models_from_spec { + if buster_config.as_ref().map_or(false, |cfg| cfg.projects.as_ref().map_or(false, |p| p.iter().any(|pc| pc.semantic_models_file.is_some()))) { + // This case means semantic_models_file was specified in some project but all failed to load/process. + println!("⚠️ A semantic_models_file was specified in buster.yml project(s) but failed to process. Now attempting to scan for individual .yml files."); + } else if buster_config.is_some() { + println!("ℹ️ No semantic_models_file specified in any project in buster.yml. Falling back to scanning for individual .yml files."); + } else { + println!("ℹ️ No buster.yml loaded. Scanning current/target directory for individual .yml files."); + } + deploy_individual_yml_files( - None, - &buster_config_load_dir, // Use the initial load directory as base if no config + buster_config.as_ref(), + &effective_buster_config_dir, // Use effective_buster_config_dir as the base for resolving model_paths recursive, &mut progress, &mut result, &mut deploy_requests_final, &mut model_mappings_final ).await?; + } else { + println!("ℹ️ Processed models from semantic_models_file(s) specified in buster.yml. Skipping scan for individual .yml files."); } @@ -538,34 +558,61 @@ async fn deploy_individual_yml_files( ExclusionManager::empty() }; - let yml_files_to_process = if let Some(cfg) = buster_config { - let effective_paths = cfg.resolve_effective_model_paths(base_search_dir); - if !effective_paths.is_empty() { + // Collect all files to process, associating them with their project context if found. + let mut files_to_process_with_context: Vec<(PathBuf, Option<&ProjectContext>)> = Vec::new(); + + if let Some(cfg) = buster_config { + let effective_paths_with_contexts = cfg.resolve_effective_model_paths(base_search_dir); + if !effective_paths_with_contexts.is_empty() { println!("ℹ️ Using effective model paths for individual .yml scan:"); - // ... (logging paths) ... - let mut all_files = Vec::new(); - for (path, _project_ctx) in effective_paths { + for (path, project_ctx_opt) in effective_paths_with_contexts { + // Log the path and its associated project context identifier if available + let context_identifier = project_ctx_opt.map_or_else(|| "Global/Default".to_string(), |ctx| ctx.identifier()); + println!(" - Path: {}, Context: {}", path.display(), context_identifier.dimmed()); + if path.is_dir() { - all_files.extend(find_yml_files(&path, recursive, &exclusion_manager, Some(progress))?); + match find_yml_files(&path, recursive, &exclusion_manager, Some(progress)) { + Ok(files_in_dir) => { + for f in files_in_dir { + files_to_process_with_context.push((f, project_ctx_opt)); + } + }, + Err(e) => eprintln!("Error finding YML files in {}: {}", path.display(), format!("{}", e).red()), + } } else if path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("yml") { if path.file_name().and_then(|name| name.to_str()) != Some("buster.yml") { - all_files.push(path); + files_to_process_with_context.push((path.clone(), project_ctx_opt)); } } } - all_files } else { - find_yml_files(base_search_dir, recursive, &exclusion_manager, Some(progress))? + // No effective paths from config, scan base_search_dir with no specific project context. + match find_yml_files(base_search_dir, recursive, &exclusion_manager, Some(progress)) { + Ok(files_in_dir) => { + for f in files_in_dir { + files_to_process_with_context.push((f, None)); + } + }, + Err(e) => eprintln!("Error finding YML files in {}: {}", base_search_dir.display(), format!("{}", e).red()), + } } } else { - find_yml_files(base_search_dir, recursive, &exclusion_manager, Some(progress))? + // No buster_config at all, scan base_search_dir with no project context. + match find_yml_files(base_search_dir, recursive, &exclusion_manager, Some(progress)) { + Ok(files_in_dir) => { + for f in files_in_dir { + files_to_process_with_context.push((f, None)); + } + }, + Err(e) => eprintln!("Error finding YML files in {}: {}", base_search_dir.display(), format!("{}", e).red()), + } }; - println!("Found {} individual model .yml files to process.", yml_files_to_process.len()); - progress.total_files = yml_files_to_process.len(); // Reset total files for this phase + println!("Found {} individual model .yml files to process.", files_to_process_with_context.len()); + progress.total_files = files_to_process_with_context.len(); // Reset total files for this phase progress.processed = 0; // Reset processed for this phase - for yml_path in yml_files_to_process { + for (yml_path, project_ctx_opt) in files_to_process_with_context { progress.processed += 1; progress.current_file = yml_path.strip_prefix(base_search_dir).unwrap_or(&yml_path).to_string_lossy().into_owned(); progress.status = "Loading individual model file...".to_string(); @@ -580,12 +627,8 @@ async fn deploy_individual_yml_files( } }; - let project_context_opt = if let Some(cfg) = buster_config { - yml_path.parent().and_then(|p| cfg.get_context_for_path(&yml_path, p)) - } else { None }; - let models_with_context: Vec<(Model, Option<&ProjectContext>)> = parsed_models.into_iter() - .map(|m| (m, project_context_opt)) + .map(|m| (m, project_ctx_opt)) .collect(); let resolved_models = match resolve_model_configurations(models_with_context, buster_config.unwrap_or(&BusterConfig::default())) { @@ -836,6 +879,7 @@ models: exclude_tags: None, model_paths: None, name: Some("Test Project".to_string()), + semantic_models_file: None, }; let global_config = BusterConfig { @@ -846,7 +890,6 @@ models: exclude_tags: None, model_paths: None, projects: None, - semantic_models_file: None, }; let models_with_context = vec![ diff --git a/cli/cli/src/commands/generate.rs b/cli/cli/src/commands/generate.rs index 4d62d873b..92766b34c 100644 --- a/cli/cli/src/commands/generate.rs +++ b/cli/cli/src/commands/generate.rs @@ -46,8 +46,8 @@ pub async fn generate_semantic_models_command( // 3. Determine target semantic YAML file path let semantic_models_file_path_str = match target_semantic_file_arg { Some(path_str) => path_str, - None => match buster_config.semantic_models_file { - Some(path_str) => path_str, + None => match buster_config.projects.as_ref().and_then(|projects| projects.first()) { + Some(project) => project.semantic_models_file.clone().unwrap_or_else(|| "models.yml".to_string()), None => { return Err(anyhow!( "No target semantic model file specified and 'semantic_models_file' not set in buster.yml. \nPlease use the --output-file option or configure buster.yml via 'buster init'." diff --git a/cli/cli/src/commands/init.rs b/cli/cli/src/commands/init.rs index 958baf41a..47a1fd0bc 100644 --- a/cli/cli/src/commands/init.rs +++ b/cli/cli/src/commands/init.rs @@ -490,30 +490,58 @@ pub async fn init(destination_path: Option<&str>) -> Result<()> { .with_default(true) .prompt()? { + // Default directory for semantic models: + // Try to use the first model_path from the first project context, if available. + let default_semantic_models_dir = current_buster_config.projects.as_ref() + .and_then(|projs| projs.first()) + .and_then(|proj| proj.model_paths.as_ref()) + .and_then(|paths| paths.first()) + .map(|p| Path::new(p).parent().unwrap_or_else(|| Path::new(p)).to_string_lossy().into_owned()) // Use parent of first model path, or the path itself + .unwrap_or_else(|| "./buster_semantic_models".to_string()); + + let semantic_models_dir_str = Text::new("Enter directory for generated semantic model YAML files:") - .with_default("./buster_semantic_models") - .with_help_message("Example: ./semantic_layer or ./models_config") + .with_default(&default_semantic_models_dir) + .with_help_message("Example: ./semantic_layer or ./models") .prompt()?; let semantic_models_filename_str = Text::new("Enter filename for the main semantic models YAML file:") - .with_default("models.yml") + .with_default("models.yml") // Keep models.yml as a common default name .with_help_message("Example: main_spec.yml or buster_models.yml") .prompt()?; let semantic_output_path = PathBuf::from(&semantic_models_dir_str).join(&semantic_models_filename_str); - // Ensure path is relative to buster.yml location for portability + + // Ensure the output directory exists + if let Some(parent_dir) = semantic_output_path.parent() { + fs::create_dir_all(parent_dir).map_err(|e| { + anyhow!("Failed to create directory for semantic models YAML '{}': {}", parent_dir.display(), e) + })?; + println!("{} {}", "✓".green(), format!("Ensured directory exists: {}", parent_dir.display()).dimmed()); + } + let relative_semantic_path = match pathdiff::diff_paths(&semantic_output_path, &dest_path) { Some(p) => p.to_string_lossy().into_owned(), None => { - // If paths are on different prefixes (e.g. Windows C: vs D:), or one is not prefix of other. - // Default to the absolute path in this rare case, or use semantic_output_path directly. eprintln!("{}", "Could not determine relative path for semantic models file. Using absolute path.".yellow()); semantic_output_path.to_string_lossy().into_owned() } }; - current_buster_config.semantic_models_file = Some(relative_semantic_path); + // Store in the first project context + if let Some(projects) = current_buster_config.projects.as_mut() { + if let Some(first_project) = projects.first_mut() { + first_project.semantic_models_file = Some(relative_semantic_path.clone()); + } else { + // This case should ideally not happen if create_buster_config_file always creates a project + eprintln!("{}", "Warning: No project contexts found in buster.yml to store semantic_models_file path.".yellow()); + // Optionally, create a default project here if necessary, or rely on create_buster_config_file to have done its job + } + } else { + eprintln!("{}", "Warning: 'projects' array is None in buster.yml. Cannot store semantic_models_file path.".yellow()); + } + current_buster_config.save(&config_path).map_err(|e| anyhow!("Failed to save buster.yml with semantic model path: {}", e))?; - println!("{} {} {}", "✓".green(), "Updated buster.yml with semantic_models_file path:".green(), current_buster_config.semantic_models_file.as_ref().unwrap().cyan()); + println!("{} {} {}", "✓".green(), "Updated buster.yml with semantic_models_file path in the first project:".green(), relative_semantic_path.cyan()); generate_semantic_models_from_dbt_catalog(¤t_buster_config, &config_path, &dest_path).await?; } @@ -527,14 +555,35 @@ async fn generate_semantic_models_flow(buster_config: &mut BusterConfig, config_ let default_dir = "./buster_semantic_models"; let default_file = "models.yml"; + // Try to get defaults from the first project context's semantic_models_file + let (initial_dir, initial_file) = buster_config.projects.as_ref() + .and_then(|projs| projs.first()) + .and_then(|proj| proj.semantic_models_file.as_ref()) + .map(|p_str| { + let pth = Path::new(p_str); + let dir = pth.parent().and_then(|pp| pp.to_str()).unwrap_or(default_dir); + let file = pth.file_name().and_then(|f| f.to_str()).unwrap_or(default_file); + (dir.to_string(), file.to_string()) + }) + .unwrap_or((default_dir.to_string(), default_file.to_string())); + let semantic_models_dir_str = Text::new("Enter directory for generated semantic model YAML files:") - .with_default(buster_config.semantic_models_file.as_ref().and_then(|p| Path::new(p).parent().and_then(|pp| pp.to_str())).unwrap_or(default_dir)) + .with_default(&initial_dir) .prompt()?; let semantic_models_filename_str = Text::new("Enter filename for the main semantic models YAML file:") - .with_default(buster_config.semantic_models_file.as_ref().and_then(|p| Path::new(p).file_name().and_then(|f| f.to_str())).unwrap_or(default_file)) + .with_default(&initial_file) .prompt()?; let semantic_output_path = PathBuf::from(&semantic_models_dir_str).join(&semantic_models_filename_str); + + // Ensure the output directory exists + if let Some(parent_dir) = semantic_output_path.parent() { + fs::create_dir_all(parent_dir).map_err(|e| { + anyhow!("Failed to create directory for semantic models YAML '{}': {}", parent_dir.display(), e) + })?; + println!("{} {}", "✓".green(), format!("Ensured directory exists: {}", parent_dir.display()).dimmed()); + } + let relative_semantic_path = match pathdiff::diff_paths(&semantic_output_path, buster_config_dir) { Some(p) => p.to_string_lossy().into_owned(), None => { @@ -543,9 +592,19 @@ async fn generate_semantic_models_flow(buster_config: &mut BusterConfig, config_ } }; - buster_config.semantic_models_file = Some(relative_semantic_path); + // Store in the first project context + if let Some(projects) = buster_config.projects.as_mut() { + if let Some(first_project) = projects.first_mut() { + first_project.semantic_models_file = Some(relative_semantic_path.clone()); + } else { + eprintln!("{}", "Warning: No project contexts found in buster.yml to update semantic_models_file path.".yellow()); + } + } else { + eprintln!("{}", "Warning: 'projects' array is None in buster.yml. Cannot update semantic_models_file path.".yellow()); + } + buster_config.save(config_path).map_err(|e| anyhow!("Failed to save buster.yml with semantic model path: {}", e))?; - println!("{} {} {}", "✓".green(), "Updated buster.yml with semantic_models_file path:".green(), buster_config.semantic_models_file.as_ref().unwrap().cyan()); + println!("{} {} {}", "✓".green(), "Updated buster.yml with semantic_models_file path in the first project:".green(), relative_semantic_path.cyan()); generate_semantic_models_from_dbt_catalog(buster_config, config_path, buster_config_dir).await } @@ -554,11 +613,19 @@ async fn generate_semantic_models_flow(buster_config: &mut BusterConfig, config_ // Placeholder for the main logic function async fn generate_semantic_models_from_dbt_catalog( buster_config: &BusterConfig, - config_path: &Path, // Path to buster.yml + _config_path: &Path, // Path to buster.yml (config_path is not directly used for choosing semantic_models_file anymore) buster_config_dir: &Path, // Directory containing buster.yml, assumed dbt project root ) -> Result<()> { println!("{}", "Starting semantic model generation from dbt catalog...".dimmed()); + // Get semantic_models_file from the first project context + let semantic_output_path_str = buster_config.projects.as_ref() + .and_then(|projs| projs.first()) + .and_then(|proj| proj.semantic_models_file.as_ref()) + .ok_or_else(|| anyhow!("Semantic models file path not set in any project context within BusterConfig. This should have been prompted."))?; + + let semantic_output_path = buster_config_dir.join(semantic_output_path_str); + let dbt_project_path = buster_config_dir; let catalog_json_path = dbt_project_path.join("target").join("catalog.json"); @@ -645,15 +712,15 @@ async fn generate_semantic_models_from_dbt_catalog( let mut yaml_models: Vec = Vec::new(); let primary_project_context = buster_config.projects.as_ref().and_then(|p| p.first()); + + // These defaults are now primarily for the model properties themselves if not set in dbt, + // data_source_name should come from the project context more directly. let default_data_source_name = primary_project_context - .and_then(|pc| pc.data_source_name.as_ref()) - .or(buster_config.data_source_name.as_ref()); + .and_then(|pc| pc.data_source_name.as_ref()); let default_database = primary_project_context - .and_then(|pc| pc.database.as_ref()) - .or(buster_config.database.as_ref()); + .and_then(|pc| pc.database.as_ref()); let default_schema = primary_project_context - .and_then(|pc| pc.schema.as_ref()) - .or(buster_config.schema.as_ref()); + .and_then(|pc| pc.schema.as_ref()); for (_node_id, node) in dbt_catalog.nodes.iter().filter(|(_id, n)| n.resource_type == "model") { let original_file_path_abs = buster_config_dir.join(&node.original_file_path); @@ -718,15 +785,17 @@ async fn generate_semantic_models_from_dbt_catalog( } let semantic_spec = YamlSemanticLayerSpec { models: yaml_models }; - let yaml_output_path_str = buster_config - .semantic_models_file - .as_ref() - .ok_or_else(|| anyhow!("Semantic models file path not set in BusterConfig"))?; - let semantic_output_path = buster_config_dir.join(yaml_output_path_str); + // The semantic_output_path is already determined above using project context's semantic_models_file + // let yaml_output_path_str = buster_config + // .semantic_models_file // This top-level field is removed + // .as_ref() + // .ok_or_else(|| anyhow!("Semantic models file path not set in BusterConfig"))?; + // let semantic_output_path = buster_config_dir.join(yaml_output_path_str); + if let Some(parent_dir) = semantic_output_path.parent() { fs::create_dir_all(parent_dir).map_err(|e| { - anyhow!("Failed to create directory for semantic models YAML: {}", e) + anyhow!("Failed to create directory for semantic models YAML '{}': {}", parent_dir.display(), e) })?; } @@ -868,18 +937,19 @@ fn create_buster_config_file( model_paths: model_paths_vec, exclude_files: None, exclude_tags: None, + semantic_models_file: None, // Initialized as None, will be set later if user opts in }); } let config = BusterConfig { - data_source_name: None, + data_source_name: None, // Top-level fields are for fallback or specific global settings not tied to projects schema: None, database: None, exclude_files: None, exclude_tags: None, model_paths: None, // This top-level field is superseded by 'projects' projects: Some(project_contexts), - semantic_models_file: None, + // semantic_models_file: None, // Removed from top-level }; config.save(path)?; @@ -957,6 +1027,7 @@ fn build_contexts_recursive( model_paths: if model_globs_for_context.is_empty() { None } else { Some(model_globs_for_context) }, exclude_files: None, exclude_tags: None, + semantic_models_file: None, // Initialized as None for contexts derived from dbt_project.yml }); println!("Generated project context: {} (Schema: {}, DB: {})", context_name.cyan(), diff --git a/cli/cli/src/utils/config.rs b/cli/cli/src/utils/config.rs index e12d3cf08..c556aaa40 100644 --- a/cli/cli/src/utils/config.rs +++ b/cli/cli/src/utils/config.rs @@ -22,6 +22,8 @@ pub struct ProjectContext { pub model_paths: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, // Optional name for the project + #[serde(skip_serializing_if = "Option::is_none")] + pub semantic_models_file: Option, // Path to the semantic layer YAML for this project } impl ProjectContext { @@ -81,8 +83,6 @@ pub struct BusterConfig { // --- New multi-project structure --- #[serde(default, skip_serializing_if = "Option::is_none")] // Allows files without 'projects' key to parse and skips serializing if None pub projects: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub semantic_models_file: Option, // Path to the generated semantic layer YAML file } impl BusterConfig {