From bf99c21bdf91867fcb6d1f6a6f1d69680f373e7b Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Fri, 27 Feb 2026 15:16:11 +1100 Subject: [PATCH] updates --- README.md | 2 - .../databricks_account/credentials.iql | 9 +- src/commands/base.rs | 28 +- src/commands/common_args.rs | 35 +- src/commands/init.rs | 8 - src/core/utils.rs | 6 +- src/globals.rs | 18 - src/resource/exports.rs | 290 --------- src/resource/mod.rs | 29 - src/resource/operations.rs | 561 ------------------ src/resource/queries.rs | 339 ----------- src/template/mod.rs | 28 - src/utils/display.rs | 24 - src/utils/server.rs | 3 - 14 files changed, 10 insertions(+), 1370 deletions(-) delete mode 100644 src/resource/exports.rs delete mode 100644 src/resource/operations.rs delete mode 100644 src/resource/queries.rs diff --git a/README.md b/README.md index 0dc5d6a..ea890d6 100644 --- a/README.md +++ b/README.md @@ -69,5 +69,3 @@ examples/databricks/serverless dev \ -e DATABRICKS_ACCOUNT_ID=${DATABRICKS_ACCOUNT_ID} \ -e DATABRICKS_AWS_ACCOUNT_ID=${DATABRICKS_AWS_ACCOUNT_ID} \ --dry-run - -C:\LocalGitRepos\stackql\stackql-deploy-rs\examples\databricks\serverless\stackql_manifest.yml \ No newline at end of file diff --git a/examples/databricks/serverless/resources/databricks_account/credentials.iql b/examples/databricks/serverless/resources/databricks_account/credentials.iql index 569d133..039fa88 100644 --- a/examples/databricks/serverless/resources/databricks_account/credentials.iql +++ b/examples/databricks/serverless/resources/databricks_account/credentials.iql @@ -1,5 +1,5 @@ /*+ exists */ -SELECT count(*) as count --should use list +SELECT count(*) as count FROM databricks_account.provisioning.credentials WHERE account_id = '{{ account_id }}' AND credentials_name = '{{ credentials_name }}' @@ -18,16 +18,17 @@ SELECT ; /*+ statecheck, retries=5, retry_delay=10 */ -SELECT count(*) as count --should use list +SELECT count(*) as count FROM databricks_account.provisioning.credentials WHERE -credentials_name = '{{ credentials_name }}' AND --where are the '' -aws_credentials = '{{ aws_credentials }}' AND --where are the '' +credentials_name = '{{ credentials_name }}' AND +aws_credentials = '{{ aws_credentials.sts_role.role_arn }}' AND account_id = '{{ account_id }}'; -- {"sts_role":{"external_id":"ebfcc5a9-9d49-4c93-b651-b3ee6cf1c9ce","role_arn":"arn:aws:iam::824532806693:role/stackql-serverless-dev-role"}} + /*+ exports */ SELECT -- account_id, -- dont get account_id back from exports, but do get credentials_id back, which is needed for updates and deletes credentials_name, diff --git a/src/commands/base.rs b/src/commands/base.rs index 89f8813..b947dbf 100644 --- a/src/commands/base.rs +++ b/src/commands/base.rs @@ -14,7 +14,7 @@ use log::{debug, error, info}; use pgwire_lite::PgwireLite; use crate::core::config::{ - get_full_context, get_resource_type, render_globals, render_string_value, + get_full_context, render_globals, render_string_value, }; use crate::core::env::load_env_vars; use crate::core::templating::{self, ParsedQuery}; @@ -24,7 +24,6 @@ use crate::core::utils::{ }; use crate::resource::manifest::{Manifest, Resource}; use crate::template::engine::TemplateEngine; -// display imports available for future use /// Core state for all command operations, equivalent to Python's StackQLBase. pub struct CommandRunner { @@ -85,12 +84,6 @@ impl CommandRunner { ) } - /// Get resource type string, validated. - #[allow(dead_code)] - pub fn get_resource_type(&self, resource: &Resource) -> String { - get_resource_type(resource).to_string() - } - /// Evaluate a resource's `if` condition. Returns true if the resource should be processed. pub fn evaluate_condition( &self, @@ -812,22 +805,3 @@ fn evaluate_simple_condition(condition: &str) -> Option { None } -/// Helper to get export names as strings from YAML values. -#[allow(dead_code)] -pub fn get_export_names(exports: &[serde_yaml::Value]) -> Vec { - exports - .iter() - .filter_map(|e| { - if let Some(s) = e.as_str() { - Some(s.to_string()) - } else if let Some(map) = e.as_mapping() { - // For dict exports, get the value (the lookup key) - map.values() - .next() - .and_then(|v| v.as_str().map(|s| s.to_string())) - } else { - None - } - }) - .collect() -} diff --git a/src/commands/common_args.rs b/src/commands/common_args.rs index 3cb36eb..342c0ec 100644 --- a/src/commands/common_args.rs +++ b/src/commands/common_args.rs @@ -5,7 +5,7 @@ //! This module defines common command-line arguments that can be reused across //! different commands in the application. -use clap::{value_parser, Arg, ArgAction, ArgMatches}; +use clap::{value_parser, Arg, ArgAction}; use std::str::FromStr; /// Possible actions to take on failure @@ -99,36 +99,3 @@ pub fn on_failure() -> Arg { .default_value("error") } -/// Structure to hold common command arguments -#[derive(Debug)] -#[allow(dead_code)] -pub struct CommonCommandArgs<'a> { - /// Directory containing stack configuration - pub stack_dir: &'a str, - /// Environment to operate on - pub stack_env: &'a str, - /// Logging level - pub log_level: &'a str, - /// Environment file path - pub env_file: &'a str, - /// Whether to run in dry-run mode - pub dry_run: bool, - /// Whether to show queries - pub show_queries: bool, - /// What to do on failure - pub on_failure: &'a FailureAction, -} - -/// Create CommonCommandArgs from ArgMatches -#[allow(dead_code)] -pub fn args_from_matches(matches: &ArgMatches) -> CommonCommandArgs<'_> { - CommonCommandArgs { - stack_dir: matches.get_one::("stack_dir").unwrap(), - stack_env: matches.get_one::("stack_env").unwrap(), - log_level: matches.get_one::("log-level").unwrap(), - env_file: matches.get_one::("env-file").unwrap(), - dry_run: matches.get_flag("dry-run"), - show_queries: matches.get_flag("show-queries"), - on_failure: matches.get_one::("on-failure").unwrap(), - } -} diff --git a/src/commands/init.rs b/src/commands/init.rs index 8239d61..5df150e 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -40,14 +40,6 @@ enum TemplateSource { } impl TemplateSource { - #[allow(dead_code)] - fn provider_or_path(&self) -> &str { - match self { - TemplateSource::Embedded(provider) => provider, - TemplateSource::Custom(path) => path, - } - } - fn get_sample_res_name(&self) -> &str { match self { TemplateSource::Embedded(provider) => match provider.as_str() { diff --git a/src/core/utils.rs b/src/core/utils.rs index dfa098f..b29fb86 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -486,15 +486,15 @@ pub fn export_vars( // --- resource-scoped key (immutable: only written if not already set) --- let scoped_key = format!("{}.{}", resource_name, key); - if !global_context.contains_key(&scoped_key) { + global_context.entry(scoped_key.clone()).or_insert_with(|| { info!( "set {} [{}] to [{}] in exports", if is_protected { "protected variable" } else { "variable" }, scoped_key, display_value, ); - global_context.insert(scoped_key, value.clone()); - } + value.clone() + }); // --- global (unscoped) key (can be overridden by later resources) --- info!( diff --git a/src/globals.rs b/src/globals.rs index 83b3a1a..00f56b5 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -119,21 +119,3 @@ pub fn server_port() -> u16 { .unwrap_or(DEFAULT_SERVER_PORT) } -/// Retrieves the configured global connection string. -/// -/// The connection string is generated during initialization via `init_globals`. -/// If not initialized, it returns an empty string. -/// -/// # Returns -/// - `&'static str` - The configured connection string or an empty string if not initialized. -/// -/// # Example -/// ```rust -/// use crate::globals::{init_globals, connection_string}; -/// init_globals("localhost".to_string(), 5444); -/// println!("Connection String: {}", connection_string()); -/// ``` -#[allow(dead_code)] -pub fn connection_string() -> &'static str { - STACKQL_CONNECTION_STRING.get().map_or("", |s| s.as_str()) -} diff --git a/src/resource/exports.rs b/src/resource/exports.rs deleted file mode 100644 index b410c09..0000000 --- a/src/resource/exports.rs +++ /dev/null @@ -1,290 +0,0 @@ -// resource/exports.rs - -//! # Resource Exports Module -//! -//! Handles exporting variables from resources. -//! Exports are used to share data between resources, such as IDs or attributes -//! that are needed for dependent resources. -//! -//! This module provides functionality for processing exports, including -//! masking protected values and updating the context with exported values. - -use std::collections::HashMap; -use std::error::Error; -use std::fmt; - -use colored::*; - -use crate::resource::manifest::Resource; -use crate::template::context::Context; - -/// Errors that can occur during export operations. -#[derive(Debug)] -pub enum ExportError { - /// Missing required export - MissingExport(String), - - /// Invalid export format - InvalidFormat(String), - - /// Export processing failed - ProcessingFailed(String), -} - -impl fmt::Display for ExportError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ExportError::MissingExport(name) => write!(f, "Missing required export: {}", name), - ExportError::InvalidFormat(msg) => write!(f, "Invalid export format: {}", msg), - ExportError::ProcessingFailed(msg) => write!(f, "Export processing failed: {}", msg), - } - } -} - -impl Error for ExportError {} - -/// Type alias for export operation results -pub type ExportResult = Result; - -/// Represents the result of processing exports. -#[derive(Debug, Clone)] -pub struct ExportOutput { - /// Exported values - pub values: HashMap, - - /// Protected values that were exported (keys only) - pub protected: Vec, -} - -/// Processes exports from a query result. -/// -/// # Arguments -/// * `resource` - The resource being processed -/// * `row` - Row of data from query result -/// * `columns` - Column definitions from query result -/// * `dry_run` - Whether this is a dry run -/// -/// # Returns -/// A map of export names to values. -pub fn process_raw_exports( - resource: &Resource, - row: Option<&Vec>, - columns: &[String], - dry_run: bool, -) -> ExportResult { - let mut exported = HashMap::new(); - let protected = resource.protected.clone(); - - if dry_run { - // For dry run, just use placeholder values - for export_name in &resource.exports { - exported.insert(export_name.clone(), "".to_string()); - } - } else if let Some(row_values) = row { - // Check if we have values to export - if row_values.len() != columns.len() { - return Err(ExportError::InvalidFormat( - "Column count mismatch in export query result".to_string(), - )); - } - - // Extract values for each requested export - for export_name in &resource.exports { - // Find the column index for this export - if let Some(idx) = columns.iter().position(|c| c == export_name) { - if idx < row_values.len() { - let value = row_values[idx].clone(); - exported.insert(export_name.clone(), value); - } else { - return Err(ExportError::MissingExport(format!( - "Export '{}' column index out of bounds", - export_name - ))); - } - } else { - return Err(ExportError::MissingExport(format!( - "Export '{}' not found in query result", - export_name - ))); - } - } - } else { - // No row data - return Err(ExportError::ProcessingFailed( - "No row data for exports".to_string(), - )); - } - - Ok(ExportOutput { - values: exported, - protected, - }) -} - -/// Updates a context with exported values. -/// -/// # Arguments -/// * `context` - The context to update -/// * `exports` - The export output to apply -/// * `show_values` - Whether to print the values being exported -/// -/// # Returns -/// Nothing, but updates the context in place. -pub fn apply_exports_to_context(context: &mut Context, exports: &ExportOutput, show_values: bool) { - for (name, value) in &exports.values { - if exports.protected.contains(name) { - // Mask protected values in output - if show_values { - let mask = "*".repeat(value.len()); - println!( - " 🔒 Set protected variable [{}] to [{}] in exports", - name, mask - ); - } - } else { - // Show regular exports - if show_values { - println!(" 📤 Set [{}] to [{}] in exports", name, value); - } - } - - // Add to context - context.add_variable(name.clone(), value.clone()); - } -} - -/// Processes exports for all resources in a stack. -/// -/// Useful for commands like teardown that need to process all exports -/// before starting operations. -/// -/// # Arguments -/// * `resources` - Resources to process -/// * `context` - Context to update with exports -/// * `client` - Database client -/// * `dry_run` - Whether this is a dry run -/// -/// # Returns -/// Success or error -pub fn collect_all_exports( - resources: &Vec, - context: &mut Context, - client: &mut postgres::Client, - dry_run: bool, -) -> ExportResult<()> { - let _ = client; - let _ = dry_run; - - println!("Collecting exports for all resources..."); - - for resource in resources { - // Skip if not a resource type or has no exports - let resource_type = resource["type"].as_str().unwrap_or("resource"); - if resource_type == "script" || resource_type == "command" { - continue; - } - - if !resource["exports"].is_sequence() - || resource["exports"].as_sequence().unwrap().is_empty() - { - continue; - } - - // Get resource name - let resource_name = match resource["name"].as_str() { - Some(name) => name, - None => { - eprintln!("Error: Missing 'name' for resource"); - continue; - } - }; - - println!( - " {} Collecting exports for {}", - "đŸ“Ļ".bright_magenta(), - resource_name - ); - - // This part would require refactoring or additional methods to properly handle - // resource loading and processing exports. In a full implementation, we would have: - // - // 1. Load the resource from the manifest - // 2. Load its queries - // 3. Render and execute the exports query - // 4. Process the results and update the context - - // For now, we'll simulate a simplified version - // In a real implementation, this would use the proper loading functions - let fake_export_values = HashMap::new(); // Would be actual values in real implementation - let fake_protected = Vec::new(); - - let fake_exports = ExportOutput { - values: fake_export_values, - protected: fake_protected, - }; - - apply_exports_to_context(context, &fake_exports, false); - } - - Ok(()) -} - -/// Unit tests for export functionality. -#[cfg(test)] -mod tests { - use super::*; - use crate::resource::manifest::Resource; - - #[test] - fn test_process_raw_exports() { - // Create a test resource with exports - let resource = Resource { - name: "test-resource".to_string(), - r#type: "resource".to_string(), - file: None, - props: Vec::new(), - exports: vec!["id".to_string(), "name".to_string()], - protected: vec!["id".to_string()], - description: "".to_string(), - r#if: None, - }; - - // Test with a row of data - let columns = vec!["id".to_string(), "name".to_string()]; - let row = vec!["123".to_string(), "test".to_string()]; - - let result = process_raw_exports(&resource, Some(&row), &columns, false).unwrap(); - - assert_eq!(result.values.len(), 2); - assert_eq!(result.values.get("id").unwrap(), "123"); - assert_eq!(result.values.get("name").unwrap(), "test"); - assert_eq!(result.protected.len(), 1); - assert!(result.protected.contains(&"id".to_string())); - - // Test dry run - let dry_result = process_raw_exports(&resource, None, &columns, true).unwrap(); - - assert_eq!(dry_result.values.len(), 2); - assert_eq!(dry_result.values.get("id").unwrap(), ""); - assert_eq!(dry_result.values.get("name").unwrap(), ""); - } - - #[test] - fn test_apply_exports_to_context() { - let mut context = Context::new(); - - let mut values = HashMap::new(); - values.insert("id".to_string(), "123".to_string()); - values.insert("name".to_string(), "test".to_string()); - - let exports = ExportOutput { - values, - protected: vec!["id".to_string()], - }; - - apply_exports_to_context(&mut context, &exports, false); - - assert_eq!(context.get_variable("id").unwrap(), "123"); - assert_eq!(context.get_variable("name").unwrap(), "test"); - } -} diff --git a/src/resource/mod.rs b/src/resource/mod.rs index 3ba596e..7e0bdcb 100644 --- a/src/resource/mod.rs +++ b/src/resource/mod.rs @@ -8,34 +8,5 @@ //! Resources are the fundamental building blocks of a stack, and this module //! provides the tools needed to load, manipulate, and process them. -// pub mod exports; pub mod manifest; pub mod validation; -// pub mod operations; -// pub mod queries; - -// /// Creates a combined error type for resource operations. -// #[derive(thiserror::Error, Debug)] -// pub enum ResourceError { -// #[error("Manifest error: {0}")] -// Manifest(#[from] manifest::ManifestError), - -// #[error("Operation error: {0}")] -// Operation(#[from] operations::OperationError), - -// #[error("Query error: {0}")] -// Query(#[from] queries::QueryError), - -// #[error("Export error: {0}")] -// Export(#[from] exports::ExportError), - -// #[error("I/O error: {0}")] -// Io(#[from] std::io::Error), - -// #[allow(dead_code)] -// #[error("Other error: {0}")] -// Other(String), -// } - -// /// Type alias for resource operation results -// pub type _Result = std::result::Result; diff --git a/src/resource/operations.rs b/src/resource/operations.rs deleted file mode 100644 index 469dd15..0000000 --- a/src/resource/operations.rs +++ /dev/null @@ -1,561 +0,0 @@ -// resource/operations.rs - -//! # Resource Operations Module -//! -//! Provides functionality for performing operations on resources. -//! This includes creating, updating, and deleting resources, as well as -//! checking their existence and state. -//! -//! Operations are performed by executing SQL queries against a StackQL server. - -use std::collections::HashMap; -use std::error::Error; -use std::fmt; - -use colored::*; -use postgres::Client; - -use crate::resource::manifest::Resource; -use crate::resource::queries::QueryType; -use crate::template::context::Context; -use crate::template::engine::TemplateEngine; -use crate::utils::query::{execute_query, QueryResult}; - -/// Errors that can occur during resource operations. -#[derive(Debug)] -pub enum OperationError { - /// Query execution failed - QueryError(String), - - /// Resource validation failed - ValidationError(String), - - /// Missing required query - MissingQuery(String), - - /// Operation not supported for resource type - UnsupportedOperation(String), - - /// State check failed after operation - StateCheckFailed(String), -} - -impl fmt::Display for OperationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - OperationError::QueryError(msg) => write!(f, "Query error: {}", msg), - OperationError::ValidationError(msg) => write!(f, "Validation error: {}", msg), - OperationError::MissingQuery(msg) => write!(f, "Missing query: {}", msg), - OperationError::UnsupportedOperation(msg) => { - write!(f, "Unsupported operation: {}", msg) - } - OperationError::StateCheckFailed(msg) => write!(f, "State check failed: {}", msg), - } - } -} - -impl Error for OperationError {} - -/// Type alias for operation results -pub type OperationResult = Result; - -/// Result of a resource existence check. -#[derive(Debug, PartialEq)] -pub enum ExistenceStatus { - /// Resource exists - Exists, - - /// Resource does not exist - NotExists, - - /// Could not determine if resource exists - Unknown, -} - -/// Result of a resource state check. -#[derive(Debug, PartialEq)] -pub enum StateStatus { - /// Resource is in the correct state - Correct, - - /// Resource is not in the correct state - Incorrect, - - /// Could not determine resource state - Unknown, -} - -/// Handles resource operations. -pub struct ResourceOperator<'a> { - /// Database client for query execution - client: &'a mut Client, - - /// Template engine for rendering queries - engine: TemplateEngine, - - /// Whether to run in dry-run mode - dry_run: bool, - - /// Whether to show queries - show_queries: bool, -} - -impl<'a> ResourceOperator<'a> { - /// Creates a new ResourceOperator. - pub fn new(client: &'a mut Client, dry_run: bool, show_queries: bool) -> Self { - Self { - client, - engine: TemplateEngine::new(), - dry_run, - show_queries, - } - } - - /// Checks if a resource exists. - pub fn check_exists( - &mut self, - resource: &Resource, - queries: &HashMap, - context: &Context, - ) -> OperationResult { - // Try exists query first, then fall back to preflight (for backward compatibility), then statecheck - let exists_query = if let Some(query) = queries.get(&QueryType::Exists) { - query - } else if let Some(query) = queries.get(&QueryType::Preflight) { - query - } else if let Some(query) = queries.get(&QueryType::StateCheck) { - query - } else { - println!( - " {} No exists check configured for [{}]", - "â„šī¸".bright_blue(), - resource.name - ); - return Ok(ExistenceStatus::Unknown); - }; - - let rendered_query = self - .engine - .render(exists_query, context.get_variables()) - .map_err(|e| OperationError::QueryError(e.to_string()))?; - - if self.dry_run { - println!( - " {} Dry run exists check for [{}]:", - "🔎".bright_cyan(), - resource.name - ); - if self.show_queries { - println!("{}", rendered_query); - } - return Ok(ExistenceStatus::NotExists); // Assume it doesn't exist in dry run - } - - println!( - " {} Running exists check for [{}]", - "🔎".bright_cyan(), - resource.name - ); - if self.show_queries { - println!("{}", rendered_query); - } - - match execute_query(&rendered_query, self.client) { - Ok(result) => match result { - QueryResult::Data { columns, rows, .. } => { - if rows.is_empty() || columns.is_empty() { - return Ok(ExistenceStatus::NotExists); - } - - // Check for "count" column with value 1 - let count_col_idx = columns.iter().position(|c| c.name == "count"); - if let Some(idx) = count_col_idx { - if let Some(row) = rows.first() { - if let Some(count) = row.values.get(idx) { - if count == "1" { - return Ok(ExistenceStatus::Exists); - } else { - return Ok(ExistenceStatus::NotExists); - } - } - } - } - - Ok(ExistenceStatus::NotExists) - } - _ => Ok(ExistenceStatus::NotExists), - }, - Err(e) => Err(OperationError::QueryError(format!( - "Exists check failed: {}", - e - ))), - } - } - - /// Checks if a resource is in the correct state. - pub fn check_state( - &mut self, - resource: &Resource, - queries: &HashMap, - context: &Context, - ) -> OperationResult { - let statecheck_query = if let Some(query) = queries.get(&QueryType::StateCheck) { - query - } else if let Some(query) = queries.get(&QueryType::PostDeploy) { - query - } else { - println!( - " {} State check not configured for [{}]", - "â„šī¸".bright_blue(), - resource.name - ); - return Ok(StateStatus::Unknown); - }; - - let rendered_query = self - .engine - .render(statecheck_query, context.get_variables()) - .map_err(|e| OperationError::QueryError(e.to_string()))?; - - if self.dry_run { - println!( - " {} Dry run state check for [{}]:", - "🔎".bright_cyan(), - resource.name - ); - if self.show_queries { - println!("{}", rendered_query); - } - return Ok(StateStatus::Correct); // Assume correct state in dry run - } - - println!( - " {} Running state check for [{}]", - "🔎".bright_cyan(), - resource.name - ); - if self.show_queries { - println!("{}", rendered_query); - } - - match execute_query(&rendered_query, self.client) { - Ok(result) => match result { - QueryResult::Data { columns, rows, .. } => { - if rows.is_empty() || columns.is_empty() { - return Ok(StateStatus::Incorrect); - } - - // Check for "count" column with value 1 - let count_col_idx = columns.iter().position(|c| c.name == "count"); - if let Some(idx) = count_col_idx { - if let Some(row) = rows.first() { - if let Some(count) = row.values.get(idx) { - if count == "1" { - println!( - " {} [{}] is in the desired state", - "👍".green(), - resource.name - ); - return Ok(StateStatus::Correct); - } else { - println!( - " {} [{}] is not in the desired state", - "👎".yellow(), - resource.name - ); - return Ok(StateStatus::Incorrect); - } - } - } - } - - println!( - " {} Could not determine state for [{}]", - "âš ī¸".yellow(), - resource.name - ); - Ok(StateStatus::Unknown) - } - _ => { - println!( - " {} Unexpected result type from state check", - "âš ī¸".yellow() - ); - Ok(StateStatus::Unknown) - } - }, - Err(e) => Err(OperationError::QueryError(format!( - "State check failed: {}", - e - ))), - } - } - - /// Creates a new resource. - pub fn create_resource( - &mut self, - resource: &Resource, - queries: &HashMap, - context: &Context, - ) -> OperationResult { - // Try createorupdate query first, then fall back to create - let create_query = if let Some(query) = queries.get(&QueryType::CreateOrUpdate) { - query - } else if let Some(query) = queries.get(&QueryType::Create) { - query - } else { - return Err(OperationError::MissingQuery(format!( - "No create or createorupdate query for resource '{}'", - resource.name - ))); - }; - - let rendered_query = self - .engine - .render(create_query, context.get_variables()) - .map_err(|e| OperationError::QueryError(e.to_string()))?; - - if self.dry_run { - println!( - " {} Dry run create for [{}]:", - "🚧".yellow(), - resource.name - ); - if self.show_queries { - println!("{}", rendered_query); - } - return Ok(true); // Pretend success in dry run - } - - println!( - " {} [{}] does not exist, creating...", - "🚧".yellow(), - resource.name - ); - if self.show_queries { - println!("{}", rendered_query); - } - - match execute_query(&rendered_query, self.client) { - Ok(_) => { - println!(" {} Resource created successfully", "✓".green()); - Ok(true) - } - Err(e) => Err(OperationError::QueryError(format!( - "Create operation failed: {}", - e - ))), - } - } - - /// Updates an existing resource. - pub fn update_resource( - &mut self, - resource: &Resource, - queries: &HashMap, - context: &Context, - ) -> OperationResult { - let update_query = if let Some(query) = queries.get(&QueryType::Update) { - query - } else { - println!( - " {} Update query not configured for [{}], skipping update", - "â„šī¸".bright_blue(), - resource.name - ); - return Ok(false); - }; - - let rendered_query = self - .engine - .render(update_query, context.get_variables()) - .map_err(|e| OperationError::QueryError(e.to_string()))?; - - if self.dry_run { - println!( - " {} Dry run update for [{}]:", - "🚧".yellow(), - resource.name - ); - if self.show_queries { - println!("{}", rendered_query); - } - return Ok(true); // Pretend success in dry run - } - - println!(" {} Updating [{}]...", "🔧".yellow(), resource.name); - if self.show_queries { - println!("{}", rendered_query); - } - - match execute_query(&rendered_query, self.client) { - Ok(_) => { - println!(" {} Resource updated successfully", "✓".green()); - Ok(true) - } - Err(e) => Err(OperationError::QueryError(format!( - "Update operation failed: {}", - e - ))), - } - } - - /// Deletes a resource. - pub fn delete_resource( - &mut self, - resource: &Resource, - queries: &HashMap, - context: &Context, - ) -> OperationResult { - let delete_query = if let Some(query) = queries.get(&QueryType::Delete) { - query - } else { - return Err(OperationError::MissingQuery(format!( - "No delete query for resource '{}'", - resource.name - ))); - }; - - let rendered_query = self - .engine - .render(delete_query, context.get_variables()) - .map_err(|e| OperationError::QueryError(e.to_string()))?; - - if self.dry_run { - println!( - " {} Dry run delete for [{}]:", - "🚧".yellow(), - resource.name - ); - if self.show_queries { - println!("{}", rendered_query); - } - return Ok(true); // Pretend success in dry run - } - - println!(" {} Deleting [{}]...", "🚧".yellow(), resource.name); - if self.show_queries { - println!("{}", rendered_query); - } - - match execute_query(&rendered_query, self.client) { - Ok(_) => { - println!(" {} Resource deleted successfully", "✓".green()); - Ok(true) - } - Err(e) => Err(OperationError::QueryError(format!( - "Delete operation failed: {}", - e - ))), - } - } - - /// Processes exports from a resource. - pub fn process_exports( - &mut self, - resource: &Resource, - queries: &HashMap, - context: &mut Context, - ) -> OperationResult> { - let exports_query = if let Some(query) = queries.get(&QueryType::Exports) { - query - } else { - println!( - " {} No exports query for [{}]", - "â„šī¸".bright_blue(), - resource.name - ); - return Ok(HashMap::new()); - }; - - let rendered_query = self - .engine - .render(exports_query, context.get_variables()) - .map_err(|e| OperationError::QueryError(e.to_string()))?; - - let mut exported_values = HashMap::new(); - - if self.dry_run { - println!( - " {} Dry run exports for [{}]:", - "đŸ“Ļ".bright_magenta(), - resource.name - ); - if self.show_queries { - println!("{}", rendered_query); - } - - // Simulate exports in dry run - for export in &resource.exports { - let value = "".to_string(); - context - .get_variables_mut() - .insert(export.clone(), value.clone()); - exported_values.insert(export.clone(), value); - println!(" 📤 Set [{}] to [] in exports", export); - } - - return Ok(exported_values); - } - - println!( - " {} Exporting variables for [{}]", - "đŸ“Ļ".bright_magenta(), - resource.name - ); - if self.show_queries { - println!("{}", rendered_query); - } - - match execute_query(&rendered_query, self.client) { - Ok(result) => match result { - QueryResult::Data { columns, rows, .. } => { - if rows.is_empty() { - return Err(OperationError::QueryError( - "Exports query returned no rows".to_string(), - )); - } - - let row = &rows[0]; // Typically exports query returns one row - - for (i, col) in columns.iter().enumerate() { - if i < row.values.len() && resource.exports.contains(&col.name) { - let value = row.values[i].clone(); - - if resource.protected.contains(&col.name) { - let mask = "*".repeat(value.len()); - println!( - " 🔒 Set protected variable [{}] to [{}] in exports", - col.name, mask - ); - } else { - println!(" 📤 Set [{}] to [{}] in exports", col.name, value); - } - - context - .get_variables_mut() - .insert(col.name.clone(), value.clone()); - exported_values.insert(col.name.clone(), value); - } - } - - Ok(exported_values) - } - _ => Err(OperationError::QueryError( - "Unexpected result from exports query".to_string(), - )), - }, - Err(e) => Err(OperationError::QueryError(format!( - "Exports query failed: {}", - e - ))), - } - } -} - -/// Unit tests for resource operations. -#[cfg(test)] -mod tests { - // These would be added in a real implementation to test the operations - // with a mock database client -} diff --git a/src/resource/queries.rs b/src/resource/queries.rs deleted file mode 100644 index 0768bda..0000000 --- a/src/resource/queries.rs +++ /dev/null @@ -1,339 +0,0 @@ -// resource/queries.rs - -//! # Resource Queries Module -//! -//! Handles parsing and managing queries for resources. -//! Queries are stored in .iql files and include various types like -//! exists, create, update, delete, and statecheck. -//! -//! This module provides functionality for loading query files, parsing queries, -//! and working with query options. - -use std::collections::HashMap; -use std::fs; -use std::path::Path; -use std::str::FromStr; - -use thiserror::Error; - -/// Errors that can occur when working with queries. -#[derive(Error, Debug)] -pub enum QueryError { - #[error("Failed to read query file: {0}")] - FileReadError(#[from] std::io::Error), - - #[error("Invalid query format: {0}")] - InvalidFormat(String), - - #[error("Missing query: {0}")] - MissingQuery(String), - - #[error("Invalid query type: {0}")] - InvalidType(String), -} - -/// Type alias for query results -pub type QueryResult = Result; - -/// Types of queries that can be defined in a resource file. -#[derive(Debug, PartialEq, Eq, Hash, Clone)] -pub enum QueryType { - /// Check if a resource exists - Exists, - - /// Preflight check (alias for Exists for backward compatibility) - Preflight, - - /// Create a new resource - Create, - - /// Update an existing resource - Update, - - /// Create or update a resource (idempotent operation) - CreateOrUpdate, - - /// Check if a resource is in the correct state - StateCheck, - - /// Post-deployment check (alias for StateCheck for backward compatibility) - PostDeploy, - - /// Export variables from a resource - Exports, - - /// Delete a resource - Delete, - - /// Execute a command - Command, -} - -impl FromStr for QueryType { - type Err = QueryError; - - fn from_str(s: &str) -> Result { - match s.trim().to_lowercase().as_str() { - "exists" => Ok(QueryType::Exists), - "preflight" => Ok(QueryType::Preflight), - "create" => Ok(QueryType::Create), - "update" => Ok(QueryType::Update), - "createorupdate" => Ok(QueryType::CreateOrUpdate), - "statecheck" => Ok(QueryType::StateCheck), - "postdeploy" => Ok(QueryType::PostDeploy), - "exports" => Ok(QueryType::Exports), - "delete" => Ok(QueryType::Delete), - "command" => Ok(QueryType::Command), - _ => Err(QueryError::InvalidType(format!( - "Unknown query type: {}", - s - ))), - } - } -} - -/// Options for a query. -#[derive(Debug, Clone)] -pub struct QueryOptions { - /// Number of times to retry the query - pub retries: u32, - - /// Delay between retries in seconds - pub retry_delay: u32, - - /// Number of times to retry after deletion - pub postdelete_retries: u32, - - /// Delay between post-deletion retries in seconds - pub postdelete_retry_delay: u32, -} - -impl Default for QueryOptions { - fn default() -> Self { - Self { - retries: 1, - retry_delay: 0, - postdelete_retries: 10, - postdelete_retry_delay: 5, - } - } -} - -/// Represents a query with its options. -#[derive(Debug, Clone)] -pub struct Query { - /// Type of query - pub query_type: QueryType, - - /// SQL query text - pub sql: String, - - /// Options for the query - pub options: QueryOptions, -} - -/// Loads queries from a file. -pub fn load_queries_from_file(path: &Path) -> QueryResult> { - let content = fs::read_to_string(path)?; - parse_queries_from_content(&content) -} - -/// Parses queries from content. -pub fn parse_queries_from_content(content: &str) -> QueryResult> { - let mut queries = HashMap::new(); - let mut current_query_type: Option = None; - let mut current_options = QueryOptions::default(); - let mut current_query = String::new(); - - let lines: Vec<&str> = content.lines().collect(); - let mut i = 0; - - while i < lines.len() { - let line = lines[i].trim(); - - // Check for query anchor - if line.starts_with("/*+") && line.contains("*/") { - // Store previous query if exists - if let Some(query_type) = current_query_type.take() { - if !current_query.is_empty() { - queries.insert( - query_type.clone(), - Query { - query_type, - sql: current_query.trim().to_string(), - options: current_options, - }, - ); - current_query = String::new(); - current_options = QueryOptions::default(); - } - } - - // Extract new anchor - let start = line.find("/*+").unwrap() + 3; - let end = line.find("*/").unwrap(); - let anchor_with_options = &line[start..end].trim(); - - // Handle options (like retries=5) - let parts: Vec<&str> = anchor_with_options.split(',').collect(); - if let Ok(query_type) = QueryType::from_str(parts[0].trim()) { - current_query_type = Some(query_type); - - // Parse options - for part in &parts[1..] { - let option_parts: Vec<&str> = part.split('=').collect(); - if option_parts.len() == 2 { - let option_name = option_parts[0].trim(); - let option_value = option_parts[1].trim(); - - if let Ok(value) = option_value.parse::() { - match option_name { - "retries" => current_options.retries = value, - "retry_delay" => current_options.retry_delay = value, - "postdelete_retries" => current_options.postdelete_retries = value, - "postdelete_retry_delay" => { - current_options.postdelete_retry_delay = value - } - _ => {} // Ignore unknown options - } - } - } - } - } else { - current_query_type = None; - } - } else if let Some(_) = current_query_type { - // Accumulate query content - current_query.push_str(line); - current_query.push('\n'); - } - - i += 1; - } - - // Store last query if exists - if let Some(query_type) = current_query_type { - if !current_query.is_empty() { - queries.insert( - query_type.clone(), - Query { - query_type, - sql: current_query.trim().to_string(), - options: current_options, - }, - ); - } - } - - Ok(queries) -} - -/// Gets all queries as a simple map from query type to SQL string. -pub fn get_queries_as_map(queries: &HashMap) -> HashMap { - queries - .iter() - .map(|(k, v)| (k.clone(), v.sql.clone())) - .collect() -} - -/// Unit tests for query functionality. -#[cfg(test)] -mod tests { - use super::*; - use std::io::Write; - use tempfile::NamedTempFile; - - fn create_test_query_file() -> NamedTempFile { - let mut file = NamedTempFile::new().unwrap(); - - writeln!(file, "/*+ exists */").unwrap(); - writeln!(file, "SELECT COUNT(*) as count FROM aws.ec2.vpc_tags").unwrap(); - writeln!(file, "WHERE region = '{{ region }}';").unwrap(); - writeln!(file).unwrap(); - writeln!(file, "/*+ create, retries=3, retry_delay=5 */").unwrap(); - writeln!(file, "INSERT INTO aws.ec2.vpcs (").unwrap(); - writeln!(file, " CidrBlock,").unwrap(); - writeln!(file, " region").unwrap(); - writeln!(file, ")").unwrap(); - writeln!(file, "SELECT ").unwrap(); - writeln!(file, " '{{ vpc_cidr_block }}',").unwrap(); - writeln!(file, " '{{ region }}';").unwrap(); - - file - } - - #[test] - fn test_parse_queries() { - let file = create_test_query_file(); - let content = fs::read_to_string(file.path()).unwrap(); - - let queries = parse_queries_from_content(&content).unwrap(); - - assert_eq!(queries.len(), 2); - assert!(queries.contains_key(&QueryType::Exists)); - assert!(queries.contains_key(&QueryType::Create)); - - let create_query = queries.get(&QueryType::Create).unwrap(); - assert_eq!(create_query.options.retries, 3); - assert_eq!(create_query.options.retry_delay, 5); - } - - #[test] - fn test_query_type_from_str() { - assert_eq!(QueryType::from_str("exists").unwrap(), QueryType::Exists); - assert_eq!(QueryType::from_str("create").unwrap(), QueryType::Create); - assert_eq!( - QueryType::from_str("createorupdate").unwrap(), - QueryType::CreateOrUpdate - ); - assert_eq!( - QueryType::from_str("statecheck").unwrap(), - QueryType::StateCheck - ); - assert_eq!(QueryType::from_str("exports").unwrap(), QueryType::Exports); - assert_eq!(QueryType::from_str("delete").unwrap(), QueryType::Delete); - - // Case insensitive - assert_eq!(QueryType::from_str("EXISTS").unwrap(), QueryType::Exists); - assert_eq!(QueryType::from_str("Create").unwrap(), QueryType::Create); - - // With spaces - assert_eq!(QueryType::from_str(" exists ").unwrap(), QueryType::Exists); - - // Invalid - assert!(QueryType::from_str("invalid").is_err()); - } - - #[test] - fn test_get_queries_as_map() { - let mut queries = HashMap::new(); - queries.insert( - QueryType::Exists, - Query { - query_type: QueryType::Exists, - sql: "SELECT COUNT(*) FROM table".to_string(), - options: QueryOptions::default(), - }, - ); - queries.insert( - QueryType::Create, - Query { - query_type: QueryType::Create, - sql: "INSERT INTO table VALUES (1)".to_string(), - options: QueryOptions::default(), - }, - ); - - let map = get_queries_as_map(&queries); - - assert_eq!(map.len(), 2); - assert_eq!( - map.get(&QueryType::Exists).unwrap(), - "SELECT COUNT(*) FROM table" - ); - assert_eq!( - map.get(&QueryType::Create).unwrap(), - "INSERT INTO table VALUES (1)" - ); - } -} diff --git a/src/template/mod.rs b/src/template/mod.rs index a51242d..0340ddb 100644 --- a/src/template/mod.rs +++ b/src/template/mod.rs @@ -11,31 +11,3 @@ pub mod context; pub mod engine; - -// Re-export commonly used types, avoid naming conflicts by using aliases -pub use context::ContextError; -pub use engine::TemplateError as EngineTemplateError; - -/// Creates a combined error type for template operations. -#[derive(thiserror::Error, Debug)] -pub enum TemplateError { - #[error("Engine error: {0}")] - Engine(#[from] EngineTemplateError), - - #[error("Context error: {0}")] - Context(#[from] ContextError), - - #[error("Other error: {0}")] - Other(String), // Keep this if you intend to handle generic errors -} - -// Type alias for template operation results -pub type _TemplateResult = std::result::Result; - -// If you don't plan to use `Other`, you can suppress the warning like this: -#[allow(dead_code)] -impl TemplateError { - pub fn other(msg: &str) -> Self { - TemplateError::Other(msg.to_string()) - } -} diff --git a/src/utils/display.rs b/src/utils/display.rs index 21a8db3..7e303c4 100644 --- a/src/utils/display.rs +++ b/src/utils/display.rs @@ -6,12 +6,8 @@ //! including Unicode-styled message boxes and color-coded output for errors, success messages, and informational messages. //! It leverages the `colored` crate for styling and `unicode_width` crate for handling Unicode text width. -use log::debug; use unicode_width::UnicodeWidthStr; -use crate::commands::common_args::CommonCommandArgs; -use clap::ArgMatches; - /// Border color options for Unicode boxes, matching Python's BorderColor enum. #[derive(Debug, Clone, Copy)] pub enum BorderColor { @@ -95,23 +91,3 @@ macro_rules! print_success { }}; } -/// Log common command arguments at debug level -#[allow(dead_code)] -pub fn log_common_command_args(args: &CommonCommandArgs, matches: &ArgMatches) { - debug!("Stack Directory: {}", args.stack_dir); - debug!("Stack Environment: {}", args.stack_env); - debug!("Log Level: {}", args.log_level); - debug!("Environment File: {}", args.env_file); - - // Log environment variables if present - if let Some(vars) = matches.get_many::("env") { - debug!("Environment Variables:"); - for var in vars { - debug!(" - {}", var); - } - } - - debug!("Dry Run: {}", args.dry_run); - debug!("Show Queries: {}", args.show_queries); - debug!("On Failure: {:?}", args.on_failure); -} diff --git a/src/utils/server.rs b/src/utils/server.rs index 97c50f2..30333e5 100644 --- a/src/utils/server.rs +++ b/src/utils/server.rs @@ -36,11 +36,8 @@ use std::process::{Command as ProcessCommand, Stdio}; use std::thread; use std::time::Duration; -// use clap::error; use log::{error, info, warn}; -// use colored::*; - use crate::app::{DEFAULT_LOG_FILE, LOCAL_SERVER_ADDRESSES}; use crate::globals::{server_host, server_port}; use crate::utils::binary::get_binary_path;