SqlTemplate.psm1

<#
.Synopsis
    Processes a SQL Embedded PowerShell template (https://github.com/panasenco/sqltemplate).
.Description
    SqlTemplate is a templating tool based on Embedded PowerShell that aims to resolve the following frequent SQL pain
    points:
     * Reusing subqueries and CTEs across multiple queries
     * Writing queries meant to run on different SQL platforms
     * Generating many similar columns
    Read the guide and browse the source code at https://github.com/panasenco/sqltemplate
.Parameter Binding
    Hashtable containing value bindings to pass on to Invoke-EpsTemplate. There are some reserved bindings:
     - Server: The type of server to compile the query for. The server binding allows the use of platform-agnostic
           cmdlets like New-Concat, New-StringAgg, New-ToDate, and more in your EPS template files.
     - Body: The body of the nested template for wrapper templates.
     - ChildPath: The path to the nested template file for wrapper templates.
     - Basename: The nested template's name (e.g. basename of ChildPath) for wrapper templates.
.Parameter Path
    The path to the .eps1.sql template file to apply (does not modify the file, just goes to stdout).
    NOTE The file is only processed with EPS templating if the path ends in .eps1.sql!
.Parameter Template
    The string template to apply.
.Parameter Wrapper
    Array of names of standard wrapper templates (in the Wrappers directory of the SqlTemplate module) to apply, in
    order from innermost to outermost.
.Example
    Reusing subqueries and CTEs across multiple queries
    ---------------------------------------------------
    In the worlds of reporting and data warehousing, situations arise where creating views is more trouble than it's worth:
     * You may want to be able to run the same complex query on multiple different servers. Keeping those views in sync
       would become a synchronization nightmare.
     * You may have requirements to redo QC and end-user validation for each change to a reporting view, no matter how
       small. Then each change to a view that 10 reports depend on would trigger 10 rounds of QC.
     * You may want to create a report that you can send to a colleague at a different institution who's using the same
       type of system.
     * You may not have the security access to create views on the server you want to query.
    This is equivalent to static linking in the software development world.
 
    With SqlTemplate, reusing subqueries and CTEs is very easy. Suppose you have the following 3 files:
 
    **subquery1.eps1.sql**
    ```
    SELECT 'This is the first subquery' AS var1
    ```
 
    **subquery2.eps1.sql**
    ```
    SELECT 'This is the second subquery' AS var2
    ```
 
 
    **subquery3.eps1.sql**
    ```
    SELECT 'This is the third subquery' AS var3
    ```
 
    Then you can create the following file that can use them without having to copy-and-paste their contents:
 
    **mainquery.eps1.sql**
    ```
    WITH <%= Invoke-SqlTemplate -Path .\subquery1.eps1.sql -Wrapper 'CTE' %>,
    <%= Invoke-SqlTemplate -Path .\subquery2.eps1.sql -Wrapper 'CTE' %>
    SELECT
      subquery1.var1,
      subquery2.var2,
      subquery3.var3
    FROM subquery1
    LEFT JOIN subquery2 ON 2=2
    LEFT JOIN <%= Invoke-SqlTemplate -Path .\subquery3.eps1.sql -Wrapper 'Inline' %> ON 3=3
    ```
    Note that the two special wrappers 'CTE' and 'Inline' above allow you to easily include CTEs and subqueries without
    having to worry about the indentation and formatting:
 
    Invoking the template shows that it successfully included the subqueries:
    ```
    > Invoke-SqlTemplate -Path .\mainquery.eps1.sql
    WITH subquery1 AS (
      SELECT 'This is the first subquery' AS var1
    ),
    subquery2 AS (
      SELECT 'This is the second subquery' AS var2
    )
    SELECT
      subquery1.var1,
      subquery2.var2,
      subquery3.var3
    FROM subquery1
    LEFT JOIN subquery2 ON 2=2
    LEFT JOIN (
      SELECT 'This is the third subquery' AS var3
    ) subquery3 ON 3=3
    ```
.Example
    Writing queries meant to run on different SQL platforms
    -------------------------------------------------------
    In a data warehousing setting, you will frequently want to run the same query on data in the source system as well as
    data in the data warehouse to identify possible ETL issues. SqlTemplate allows you to write one file that will
    'compile' to the syntax of a particular SQL implementation.
 
    Here's an example that shows how to generate a substring command on Oracle and SQL Server:
    ```
    > @{Server='ORA'; Substring="'def'"} | Invoke-SqlTemplate -Template "'abcdefghijk'" -Wrapper 'SubstringIndex'
    INSTR('abcdefghijk', 'def')
    > @{Server='SS13'; Substring="'def'"} | Invoke-SqlTemplate -Template "'abcdefghijk'" -Wrapper 'SubstringIndex'
    CHARINDEX('def', 'abcdefghijk')
    ```
 
    Wrappers can be nested. Suppose you want to select the above substring as a single row. Single-row selection is
    implemented differently in [Oracle and SQL Server](https://stackoverflow.com/a/35254602/12981893):
    ```
    > @{Server='ORA'; Substring="'def'"} | Invoke-SqlTemplate -Template "'abcdefghijk'" -Wrapper 'SubstringIndex','SelectSingle'
    SELECT INSTR('abcdefghijk', 'def') FROM dual
    > @{Server='SS13'; Substring="'def'"} | Invoke-SqlTemplate -Template "'abcdefghijk'" -Wrapper 'SubstringIndex','SelectSingle'
    SELECT CHARINDEX('def', 'abcdefghijk')
    ```
 
    Finally, wrappers can of course be included as parts of larger queries using standard
    [EPS templating](https://github.com/straightdave/eps). Suppose you have a file example.eps1.sql (the extension is
    important - Invoke-SqlTemplate won't attempt template processing without it) with the following contents:
 
    **example.eps1.sql**
    ```
    SELECT
      Customers.key,
      <%= ($Binding + @{ToFormat='YYYYMMDD'}) | Invoke-SqlTemplate -Template 'Purchases.date' `
          -Wrapper 'DateToString','StringToInt' %> AS PurchaseDateKey,
      <%= ($Binding +
              @{Length=($Binding + @{Substring="' '"}) |
                  Invoke-SqlTemplate -Template 'Purchases.fullname' -Wrapper 'SubstringIndex'
              }) | Invoke-SqlTemplate -Template 'Purchases.fullname' -Wrapper 'Substring'
      %> AS PurchaseCode
    FROM Purchases
    LEFT JOIN Customers ON Purchases.customer_key = Customers.key
    ```
    Note that the special variable $Binding above is a copy of the hash table that was piped to the top-level call to
    Invoke-SqlTemplate. It must be explicitly passed to sub-templates to allow the variables to propagate.
 
    This one template file can produce different queries for different server implementations:
    ```
    > @{Server='ORA'} | Invoke-SqlTemplate -Path .\example.eps1.sql
    SELECT
      Customers.key,
      TO_NUMBER(TO_CHAR(Purchases.date, 'YYYYMMDD')) AS PurchaseDateKey,
      SUBSTR(Purchases.fullname, 1, INSTR(Purchases.fullname, ' ')) AS PurchaseCode
    FROM Purchases
    LEFT JOIN Customers ON Purchases.customer_key = Customers.key
    ```
    ```
    > @{Server='SS13'} | Invoke-SqlTemplate -Path .\example.eps1.sql
    SELECT
      Customers.key,
      CAST(CONVERT(char(8), Purchases.date, 112) AS int) AS PurchaseDateKey,
      SUBSTRING(Purchases.fullname, 1, CHARINDEX(' ', Purchases.fullname)) AS PurchaseCode
    FROM Purchases
    LEFT JOIN Customers ON Purchases.customer_key = Customers.key
    ```
.Example
    Generating many similar columns
    -------------------------------
    Pivot tables are evil, incomprehensible, and impossible to optimize. With SqlTemplate you can take full advantage of
    [Embedded PowerShell](https://github.com/straightdave/eps) and generate view columns using scripting. For example:
 
    ```
    > @{Columns=@('a','b','c')} | Invoke-SqlTemplate -Template 'SELECT <% $Columns | Each { %><%= $_ %> AS <%= $_ %>, <% } %>4 AS x'
    SELECT a AS a, b AS b, c AS c, 4 AS x
    ```
#>

function Invoke-SqlTemplate {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline=$true)]
        [Hashtable] $Binding = @{},
        [string] $Path,
        [string] $Template,
        [string[]] $Wrapper
    )
    
    if (-not $Path) {
        # Invoke the template
        $Body = $Binding.Clone() | Invoke-EpsTemplate -Template $Template
    } elseif ($Path -match '.*\.eps1\.sql\s*$') {
        # Invoke the template only if the extension is .eps1.sql
        $Body = $Binding.Clone() | Invoke-EpsTemplate -Path $Path
    } else {
        # Return the raw file contents if no name and the file extension is not .eps1.sql
        $Body = Get-Content -Raw -Path $Path
    }
    
    # Trim trailing whitespace
    $Body = $Body -replace '\s*$'
    
    if ($Wrapper) {
        # Create a clean copy of the binding
        $BindingCopy = $Binding.Clone()
        if ($Path -and -not $Binding.Basename) {
            $BindingCopy.Add('Basename', ((Get-Item -Path $Path).BaseName -split '\.')[0])
        }
        $BindingCopy.Remove('Body')
        $BindingCopy.Remove('ChildPath')
        $BindingCopy.Add('ChildPath', $Path)
        # Apply the wrappers in order from innermost to outermost
        $ModuleFileList = Get-Item (Get-Module -Name SqlTemplate).FileList
        foreach ($WrapperName in $Wrapper) {
            $Body = ($BindingCopy + @{Body=$Body}) |
                Invoke-SqlTemplate -Path ($ModuleFileList | where {$_.Basename -eq "$WrapperName.eps1"})
        }
    }
    
    $Body
}