Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/databricks/serverless/stackql_manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ resources:
- file(aws/iam/policy_statements/iam_service_linked_role.json)
Version: '2012-10-17'
PolicyName: "{{ stack_name }}-{{ stack_env }}-policy"
- name: managed_policy_arns
value: []
- name: max_session_duration
value: 3600
- name: permissions_boundary
value: ""
- name: tags
value: []
merge:
- global_tags
exports:
- aws_iam_role_name: aws_iam_cross_account_role_name
- aws_iam_role_arn: aws_iam_cross_account_role_arn
Expand Down
60 changes: 60 additions & 0 deletions src/core/templating.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,37 @@ pub fn render_query(
"Error rendering query for [{}] [{}]: {}",
res_name, anchor, e
);

// Extract template variable references for diagnostics
let re = Regex::new(r"\{\{\s*(\w+)").unwrap();
let referenced_vars: Vec<&str> = re
.captures_iter(&processed_query)
.filter_map(|c| c.get(1).map(|m| m.as_str()))
.collect();
let missing: Vec<&&str> = referenced_vars
.iter()
.filter(|v| !ctx.contains_key(**v))
.collect();

if !missing.is_empty() {
error!(
"Missing variables in context for [{}] [{}]: {:?}",
res_name, anchor, missing
);
error!(
"Hint: ensure these properties are defined in the manifest for resource [{}], \
or that the .iql template only references variables provided by the manifest.",
res_name
);
}

debug!(
"[{}] [{}] available context keys: {:?}",
res_name,
anchor,
ctx.keys().collect::<Vec<_>>()
);

process::exit(1);
}
}
Expand Down Expand Up @@ -383,6 +414,35 @@ pub fn render_inline_template(
"Error rendering inline template for [{}]: {}",
resource_name, e
);

let re = Regex::new(r"\{\{\s*(\w+)").unwrap();
let referenced_vars: Vec<&str> = re
.captures_iter(&processed)
.filter_map(|c| c.get(1).map(|m| m.as_str()))
.collect();
let missing: Vec<&&str> = referenced_vars
.iter()
.filter(|v| !temp_context.contains_key(**v))
.collect();

if !missing.is_empty() {
error!(
"Missing variables in context for [{}]: {:?}",
resource_name, missing
);
error!(
"Hint: ensure these properties are defined in the manifest for resource [{}], \
or that the inline SQL only references variables provided by the manifest.",
resource_name
);
}

debug!(
"[{}] available context keys: {:?}",
resource_name,
temp_context.keys().collect::<Vec<_>>()
);

process::exit(1);
}
}
Expand Down
59 changes: 56 additions & 3 deletions src/template/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//! `generate_patch_document`, `sql_list`, `sql_escape`.

use std::collections::HashMap;
use std::error::Error as StdError;

use base64::Engine as Base64Engine;
use serde_json::Value as JsonValue;
Expand Down Expand Up @@ -122,7 +123,7 @@ impl TemplateEngine {
register_custom_filters(&mut tera);

tera.add_raw_template(template_name, template)
.map_err(|e| TemplateError::SyntaxError(e.to_string()))?;
.map_err(|e| TemplateError::SyntaxError(full_error_chain(&e)))?;

let mut tera_context = TeraContext::new();
for (key, value) in context {
Expand All @@ -133,11 +134,30 @@ impl TemplateEngine {
let uuid_val = uuid::Uuid::new_v4().to_string();
tera_context.insert("uuid", &uuid_val);

tera.render(template_name, &tera_context)
.map_err(|e| TemplateError::RenderError(e.to_string()))
tera.render(template_name, &tera_context).map_err(|e| {
let full_msg = full_error_chain(&e);
if full_msg.contains("not found in context") {
TemplateError::VariableNotFound(full_msg)
} else {
TemplateError::RenderError(full_msg)
}
})
}
}

/// Walk the full error source chain and concatenate all messages.
/// Tera's top-level `Display` often only shows "Failed to render 'name'" while
/// the root cause (e.g., missing variable) is buried in `source()`.
fn full_error_chain(err: &dyn StdError) -> String {
let mut parts = vec![err.to_string()];
let mut current = err.source();
while let Some(cause) = current {
parts.push(cause.to_string());
current = cause.source();
}
parts.join(": ")
}

/// Register all custom Jinja2 filters matching the Python implementation.
fn register_custom_filters(tera: &mut Tera) {
tera.register_filter("from_json", filter_from_json);
Expand Down Expand Up @@ -371,4 +391,37 @@ mod tests {
let result = engine.render("JSON: {{ json }}", &context).unwrap();
assert_eq!(result, r#"JSON: {"key": "value"}"#);
}

#[test]
fn test_render_with_filters_missing_var_shows_name() {
let engine = TemplateEngine::new();
let context = HashMap::new();

let result = engine.render_with_filters("test_tpl", "Hello {{ missing_var }}!", &context);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("missing_var"),
"Error should mention the missing variable name, got: {}",
err_msg
);
}

#[test]
fn test_render_with_filters_missing_var_is_variable_not_found() {
let engine = TemplateEngine::new();
let context = HashMap::new();

let result = engine.render_with_filters("test_tpl", "{{ no_such_var }}", &context);
match result {
Err(TemplateError::VariableNotFound(msg)) => {
assert!(
msg.contains("no_such_var"),
"VariableNotFound error should contain variable name, got: {}",
msg
);
}
other => panic!("Expected VariableNotFound error, got: {:?}", other),
}
}
}