CoginitiScript: Generating SQL Dynamically
CoginitiScript includes a powerful preprocessor that acts like a template engine, allowing you to generate SQL dynamically using interpolated expressions, loops, and conditionals. This guide shows you how to build flexible, reusable SQL queries that adapt to different data requirements.
What You'll Learn
- How to use CoginitiScript's preprocessor for dynamic SQL
- How to create pivot tables with dynamic columns
- How to use loops and conditionals in SQL generation
- How to work with data-driven query construction
- Best practices for maintainable dynamic queries
Prerequisites
- Basic understanding of CoginitiScript syntax
- Familiarity with SQL and pivot operations
- Understanding of template concepts
The Problem: Static SQL Limitations
Consider a common scenario where you need to create pivot tables. You have a scores table with player scores by date:
CREATE TABLE scores AS
SELECT 'John' AS name, '2012-01-01' AS score_date, 3 AS score UNION ALL
SELECT 'John' AS name, '2012-01-01' AS score_date, 2 AS score UNION ALL
SELECT 'John' AS name, '2012-01-02' AS score_date, 4 AS score UNION ALL
SELECT 'Bill' AS name, '2012-01-01' AS score_date, 2 AS score UNION ALL
SELECT 'Bill' AS name, '2012-01-02' AS score_date, 3 AS score UNION ALL
SELECT 'Mila' AS name, '2012-01-01' AS score_date, 2 AS score UNION ALL
SELECT 'Mila' AS name, '2012-01-02' AS score_date, 0 AS score UNION ALL
SELECT 'Brian' AS name, '2012-01-01' AS score_date, 2.5 AS score UNION ALL
SELECT 'Brian' AS name, '2012-01-02' AS score_date, 2 AS score;
Sample Data:
| name | score_date | score |
|---|---|---|
| John | 2012-01-01 | 3 |
| John | 2012-01-01 | 2 |
| John | 2012-01-02 | 4 |
| Bill | 2012-01-01 | 2 |
| Bill | 2012-01-02 | 3 |
| Mila | 2012-01-01 | 2 |
| Mila | 2012-01-02 | 0 |
| Brian | 2012-01-01 | 2.5 |
| Brian | 2012-01-02 | 2 |
You want to pivot this data to show scores by date:
Desired Output:
| name | 2012-01-01 | 2012-01-02 |
|---|---|---|
| Bill | 2 | 3 |
| Brian | 2.5 | 2 |
| John | 5 | 4 |
| Mila | 2 | 0 |
Traditional Static Approach
The conventional SQL approach requires hardcoding each date:
SELECT
name,
SUM(CASE WHEN s.score_date = '2012-01-01' THEN s.score END) AS "2012-01-01",
SUM(CASE WHEN s.score_date = '2012-01-02' THEN s.score END) AS "2012-01-02"
FROM
scores s
GROUP BY
name
ORDER BY
name;
Problems with this approach:
- Manual column creation: Each date requires a separate CASE expression
- Code duplication: Similar patterns repeated for each column
- Maintenance burden: Adding new dates requires modifying the query
- Inflexibility: Cannot easily adapt to different date ranges
The Solution: Dynamic SQL Generation
Step 1: Create a Dynamic Pivot Block
Use CoginitiScript's preprocessor to generate columns dynamically:
#+src sql scoreData(dates)
#+begin
SELECT
name,
#+for d : dates separator "," do
SUM(CASE WHEN s.score_date = '{{"{{ d }}"}}' THEN s.score END) AS "{{"{{ d }}"}}"
#+end
FROM
scores s
GROUP BY
name
ORDER BY
name;
#+end
Key elements:
#+for d : dates: Loops through the dates arrayseparator ",": Adds commas between generated columns{{"{{ d }}"}}: Interpolates each date value- Template generates one CASE expression per date
Step 2: Call the Dynamic Block
Use the block with a list of dates:
SELECT * FROM {{"{{ scoreData(dates=[\"2012-01-01\", \"2012-01-02\"]) }}"}};
Step 3: See the Generated SQL
CoginitiScript expands the template to produce:
SELECT
name,
SUM(CASE WHEN s.score_date = '2012-01-01' THEN s.score END) AS "2012-01-01",
SUM(CASE WHEN s.score_date = '2012-01-02' THEN s.score END) AS "2012-01-02"
FROM
scores s
GROUP BY
name
ORDER BY
name;
Advanced Technique: Data-Driven Generation
Using the Iterator Function
Instead of hardcoding dates, generate them from data using the iterator() function:
#+src sql scoreDataFromQuery(dateSource)
#+begin
SELECT
name,
#+for d : iterator(dateSource()) separator "," do
SUM(CASE WHEN s.score_date = '{{"{{ d[\"report_date\"] }}"}}' THEN s.score END) AS "{{"{{ d[\"report_date\"] }}"}}"
#+end
FROM
scores s
GROUP BY
name
ORDER BY
name;
#+end
#+src sql GetDates()
#+begin
SELECT '2012-01-01' AS report_date
UNION ALL
SELECT '2012-01-02' AS report_date;
#+end
Call with Dynamic Date Source
SELECT * FROM {{"{{ scoreDataFromQuery(dateSource=GetDates) }}"}};
How it works:
GetDates()returns a recordset with date valuesiterator(dateSource())converts the recordset to an iterable collection- Loop processes each row, accessing the
report_datecolumn - Dynamic columns are generated based on actual data
More Dynamic SQL Patterns
Conditional Column Generation
Generate columns based on conditions:
#+src sql salesReport(includeQuarterly, includeYearly)
#+begin
SELECT
product_name,
SUM(revenue) as total_revenue,
#+if includeQuarterly then
SUM(CASE WHEN EXTRACT(QUARTER FROM sale_date) = 1 THEN revenue END) as q1_revenue,
SUM(CASE WHEN EXTRACT(QUARTER FROM sale_date) = 2 THEN revenue END) as q2_revenue,
SUM(CASE WHEN EXTRACT(QUARTER FROM sale_date) = 3 THEN revenue END) as q3_revenue,
SUM(CASE WHEN EXTRACT(QUARTER FROM sale_date) = 4 THEN revenue END) as q4_revenue,
#+end
#+if includeYearly then
SUM(CASE WHEN EXTRACT(YEAR FROM sale_date) = 2023 THEN revenue END) as revenue_2023,
SUM(CASE WHEN EXTRACT(YEAR FROM sale_date) = 2024 THEN revenue END) as revenue_2024
#+end
FROM sales
GROUP BY product_name;
#+end
Usage:
-- Include both quarterly and yearly breakdowns
SELECT * FROM {{"{{ salesReport(includeQuarterly=true, includeYearly=true) }}"}};
-- Only quarterly breakdown
SELECT * FROM {{"{{ salesReport(includeQuarterly=true, includeYearly=false) }}"}};
Dynamic WHERE Clauses
Build flexible filtering conditions:
#+src sql customerAnalysis(regions, minRevenue, includeInactive)
#+begin
SELECT
customer_id,
name,
region,
total_revenue,
status
FROM customers
WHERE 1=1
#+if regions != null && length(regions) > 0 then
AND region IN (
#+for r : regions separator "," do
'{{"{{ r }}"}}'
#+end
)
#+end
#+if minRevenue != null then
AND total_revenue >= {{"{{ minRevenue }}"}}
#+end
#+if !includeInactive then
AND status = 'active'
#+end
ORDER BY total_revenue DESC;
#+end
Usage:
-- All active customers in specific regions with minimum revenue
SELECT * FROM {{"{{ customerAnalysis(
regions=[\"North\", \"South\"],
minRevenue=10000,
includeInactive=false
) }}"}};
Dynamic Aggregation Levels
Create reports with different grouping levels:
#+src sql salesSummary(groupBy, metrics)
#+begin
SELECT
#+for field : groupBy separator "," do
{{"{{ field }}"}}
#+end,
#+for metric : metrics separator "," do
#+if metric == "revenue" then
SUM(revenue) as total_revenue
#+else if metric == "quantity" then
SUM(quantity) as total_quantity
#+else if metric == "orders" then
COUNT(DISTINCT order_id) as total_orders
#+else if metric == "avg_order_value" then
AVG(revenue) as avg_order_value
#+end
#+end
FROM sales
#+if length(groupBy) > 0 then
GROUP BY
#+for field : groupBy separator "," do
{{"{{ field }}"}}
#+end
#+end;
#+end
Usage:
-- Group by region and product, show revenue and order count
SELECT * FROM {{"{{ salesSummary(
groupBy=[\"region\", \"product_category\"],
metrics=[\"revenue\", \"orders\"]
) }}"}};
Best Practices
1. Use Constants for Reusability
Define commonly used values as constants:
#+const
standardDateColumns = ["created_date", "updated_date", "deleted_date"];
commonMetrics = ["count", "sum", "avg", "min", "max"];
#+end
#+src sql auditReport(tables, dateColumns, metrics)
#+begin
-- Use constants when no specific values are provided
#+const actualDateColumns = dateColumns != null ? dateColumns : standardDateColumns;
#+const actualMetrics = metrics != null ? metrics : commonMetrics;
-- Generate report using the resolved constants
SELECT table_name,
#+for metric : actualMetrics separator "," do
{{"{{ metric }}"}}(record_count) as {{"{{ metric }}"}}_records
#+end
FROM table_stats;
#+end
2. Validate Parameters
Add parameter validation to prevent errors:
#+src sql pivotData(dimensions, measures, filters)
#+begin
#+if dimensions == null || length(dimensions) == 0 then
-- Error: At least one dimension is required
SELECT 'Error: No dimensions specified' as error_message;
#+else if measures == null || length(measures) == 0 then
-- Error: At least one measure is required
SELECT 'Error: No measures specified' as error_message;
#+else
-- Generate the actual pivot query
SELECT
#+for dim : dimensions separator "," do
{{"{{ dim }}"}}
#+end,
#+for measure : measures separator "," do
SUM({{"{{ measure }}"}}) as total_{{"{{ measure }}"}}
#+end
FROM fact_table
WHERE 1=1
#+if filters != null then
#+for filter : filters do
AND {{"{{ filter }}"}}
#+end
#+end
GROUP BY
#+for dim : dimensions separator "," do
{{"{{ dim }}"}}
#+end;
#+end
#+end
3. Document Template Logic
Add clear documentation for complex templates:
#+src sql dynamicCrossTab(rowDimension, colDimension, measure, aggregation)
#+meta {
:doc "Creates a dynamic cross-tabulation report.
Parameters:
- rowDimension: Column name for row grouping
- colDimension: Column name that becomes pivot columns
- measure: Numeric column to aggregate
- aggregation: Type of aggregation (SUM, AVG, COUNT, etc.)
Example:
{{ dynamicCrossTab(
rowDimension=\"product_category\",
colDimension=\"region\",
measure=\"revenue\",
aggregation=\"SUM\"
) }}
Generates pivot table with product categories as rows,
regions as columns, and revenue sums as values."
}
#+begin
-- Implementation here
#+end
4. Test Template Expansion
Always test your templates with different parameter combinations:
-- Test with minimal parameters
SELECT * FROM {{"{{ scoreData(dates=[\"2012-01-01\"]) }}"}};
-- Test with multiple parameters
SELECT * FROM {{"{{ scoreData(dates=[\"2012-01-01\", \"2012-01-02\", \"2012-01-03\"]) }}"}};
-- Test edge cases
SELECT * FROM {{"{{ scoreData(dates=[]) }}"}}; -- Empty array
5. Use Meaningful Variable Names
Choose descriptive names for loop variables and parameters:
-- Good: Clear variable names
#+for dateValue : reportDates separator "," do
SUM(CASE WHEN order_date = '{{"{{ dateValue }}"}}' THEN amount END) as "{{"{{ dateValue }}"}}"
#+end
-- Avoid: Unclear variable names
#+for d : dates separator "," do
SUM(CASE WHEN order_date = '{{"{{ d }}"}}' THEN amount END) as "{{"{{ d }}"}}"
#+end
Advanced Patterns
Nested Loops for Complex Structures
Generate multi-level pivot tables:
#+src sql multiLevelPivot(rowDimensions, colDimensions, measure)
#+begin
SELECT
#+for rowDim : rowDimensions separator "," do
{{"{{ rowDim }}"}}
#+end,
#+for colDim : colDimensions do
#+for colValue : iterator(getDistinctValues(colDim)) separator "," do
SUM(CASE WHEN {{"{{ colDim }}"}} = '{{"{{ colValue[\"value\"] }}"}}'
THEN {{"{{ measure }}"}} END) as "{{"{{ colDim }}"}}__{{"{{ colValue[\"value\"] }}"}}"
#+end
#+end
FROM fact_table
GROUP BY
#+for rowDim : rowDimensions separator "," do
{{"{{ rowDim }}"}}
#+end;
#+end
Conditional Template Logic
Build templates that adapt to different database platforms:
#+src sql platformSpecificQuery(platform, tableName, columns)
#+begin
SELECT
#+for col : columns separator "," do
#+if platform == "snowflake" then
UPPER({{"{{ col }}"}}) as {{"{{ col }}"}}_upper
#+else if platform == "bigquery" then
LOWER({{"{{ col }}"}}) as {{"{{ col }}"}}_lower
#+else
{{"{{ col }}"}}
#+end
#+end
FROM {{"{{ tableName }}"}}
#+if platform == "snowflake" then
LIMIT 1000
#+else if platform == "bigquery" then
LIMIT 500
#+end;
#+end
Troubleshooting
Common Issues and Solutions
Issue: Loop generates invalid SQL syntax
-- Problem: Missing separator creates invalid SQL
#+for col : columns do
{{"{{ col }}"}}
#+end
-- Solution: Add separator
#+for col : columns separator "," do
{{"{{ col }}"}}
#+end
Issue: Template produces empty results
-- Problem: Empty array or null parameters
#+for item : emptyArray do
{{"{{ item }}"}}
#+end
-- Solution: Add conditional check
#+if items != null && length(items) > 0 then
#+for item : items separator "," do
{{"{{ item }}"}}
#+end
#+else
1 as placeholder_column
#+end
Issue: Complex expressions break template parsing
-- Problem: Complex interpolation
{{"{{ someFunction(param1, param2).property }}"}}
-- Solution: Use intermediate variables
#+const result = someFunction(param1, param2);
{{"{{ result.property }}"}}
Debugging Tips
- Start simple: Begin with basic templates and add complexity gradually
- Test expansion: Use simple queries to verify template expansion
- Check syntax: Ensure generated SQL is syntactically correct
- Use logging: Add comments to track template execution
- Validate parameters: Check for null or empty values
Next Steps
- Explore CoginitiScript Reference for complete preprocessor documentation
- Learn about macros for reusable code snippets
- Check out constants and expressions for advanced features
Summary
Dynamic SQL generation with CoginitiScript enables you to:
- Create flexible, parameterized queries
- Eliminate code duplication in pivot operations
- Build data-driven report structures
- Adapt queries to different requirements automatically
By leveraging CoginitiScript's preprocessor features—loops, conditionals, and interpolation—you can create powerful, maintainable SQL generation patterns that scale with your data needs.