gSpanner.psm1

#!/usr/bin/env pwsh
<#
   Copyright 2020 Google LLC
 
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
 
        https://www.apache.org/licenses/LICENSE-2.0
 
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
 
Demo:
    $p = '<your projectid>'
    $ic = Get-gSpannerInstanceConfig -ProjectId $p | ? name -like *regional-europe-west4
    $i = Add-gSpannerInstance -Id spanner -Config $ic
    $db = Add-gSpannerDatabase -Instance $i -Id db
    Get-Process | Add-gSpannerItems -Database $db
    Get-gSpannerSchema -Database $db
    $s = New-gSpannerSession -Database $db
    Invoke-gSpannerSql -Session $s -SingleUse -Sql 'select * from System_Diagnostics_Process' |
        Select-Object -Expand resultSet | Out-GridView
    Invoke-gSpannerSql -Session $s -SingleUse -Sql "select processname from System_Diagnostics_Process where workingset > $(200mb)" |
        Select-Object -Expand resuktSet | Export-Csv t.csv
    Remove-gSpannerInstance -Instance $i
 
#>

Function Set-gSpannerAliases {
    Set-Alias -Scope Global -Force -Name asdb -Value Add-gSpannerDatabase
    Set-Alias -Scope Global -Force -Name asi  -Value Add-gSpannerItems
    Set-Alias -Scope Global -Force -Name ast  -Value Add-gSpannerTable
    Set-Alias -Scope Global -Force -Name gsdb -Value Get-gSpannerDatabase
    Set-Alias -Scope Global -Force -Name gsdo -Value Get-gSpannerDatabaseOperation
    Set-Alias -Scope Global -Force -Name gsi  -Value Get-gSpannerInstance
    Set-Alias -Scope Global -Force -Name gsic -Value Get-gSpannerInstanceConfig
    Set-Alias -Scope Global -Force -Name gsio -Value Get-gSpannerInstanceOperation
    Set-Alias -Scope Global -Force -Name gss  -Value Get-gSpannerSession
    Set-Alias -Scope Global -Force -Name gssc -Value Get-gSpannerSchema
    Set-Alias -Scope Global -Force -Name gst  -Value Get-gSpannerTable
    Set-Alias -Scope Global -Force -Name isb  -Value Invoke-gSpannerBatchDml
    Set-Alias -Scope Global -Force -Name iss  -Value Invoke-gSpannerSql
    Set-Alias -Scope Global -Force -Name nss  -Value New-gSpannerSession
    Set-Alias -Scope Global -Force -Name nstr -Value New-gSpannerTransaction
    Set-Alias -Scope Global -Force -Name pstr -Value Publish-gSpannerTransaction
    Set-Alias -Scope Global -Force -Name rsdb -Value Remove-gSpannerDatabase
    Set-Alias -Scope Global -Force -Name rsdo -Value Remove-gSpannerDatabaseOperation
    Set-Alias -Scope Global -Force -name rsi -Value Remove-gSpannerInstance
    Set-Alias -Scope Global -Force -name rsio -Value Remove-gSpannerInstanceOperation
    Set-Alias -Scope Global -Force -Name rss  -Value Remove-gSpannerSession
    Set-Alias -Scope Global -Force -Name rst  -Value Remove-gSpannerTable
    Set-Alias -Scope Global -Force -Name ssdp -Value Set-gSpannerDefaultProjectId
    Set-Alias -Scope Global -Force -Name sss  -Value Start-gSpannerStreamingSql
    Set-Alias -Scope Global -Force -Name sssh -Value Set-gSpannerSchema
    Set-Alias -Scope Global -Force -Name ustr -Value Undo-gSpannerTransaction
}
Set-gSpannerAliases
Function Set-gSpannerDefaultProjectId {
    Param(
        $ProjectId
    )
    $Global:PSDefaultParameterValues['*-gSpanner*:ProjectId'] = $ProjectId
}

$version1 = 'https://spanner.googleapis.com/v1'
Add-Type -TypeDefinition @"
    public enum SpannerTransactionMode {
        readWrite,
        readOnly,
        partitionedDml
    }
"@

$PsDefaultParameterValues['*-gOAuth*:ProjectId'] = 'powershell'
Function Invoke-gSpannerLogin {
    [CmdletBinding()]
    Param()
    Invoke-gOAuthLogin -Verbose:$VerbosePreference
    [void](Get-gOAuthAccessToken -Refresh -Verbose:$VerbosePreference)
}
Function Get-gSpannerInstanceConfig {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]
        $ProjectId
    )
    (Invoke-gOAuthRestMethod -Uri "$version1/projects/$ProjectId/instanceConfigs" -Verbose:$VerbosePreference).instanceConfigs
}
Function Get-gSpannerInstance {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]
        $ProjectId,
        $Id,
        [switch]
        $Default
    )
    $Id ?
    (Invoke-gOAuthRestMethod  -Uri "$version1/projects/$ProjectId/instances/$Id" -Verbose:$VerbosePreference) :
    (Invoke-gOAuthRestMethod  -Uri "$version1/projects/$ProjectId/instances" -Verbose:$VerbosePreference).instances |
    % {
        If ( $Default.IsPresent ) {
            $Global:PSDefaultParameterValues['*-gSpanner*:Instance'] = $_
        }
        $_
    }

}
Function Add-gSpannerInstance {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]
        $Id,
        [Parameter(Mandatory)]
        $Config,
        [string]
        $DisplayName = $Id,
        [Parameter()]
        [int]
        $NodeCount = 1
    )
    $projectId = ($Config.name -split '/')[1]
    $b = @{
        instanceId = $Id;
        instance   = @{
            config      = $Config.name;
            nodeCount   = $NodeCount;
            displayName = $DisplayName
        }
    }
    $o = Invoke-gOAuthRestMethod -Uri "$version1/projects/$projectId/instances" -Body $b -Verbose:$VerbosePreference
    While ( -not $o.done) {
        Start-Sleep -Milliseconds 30
        $o = Get-gSpannerInstanceOperation -Operation $o -Verbose:$VerbosePreference
        If ( $VerbosePreference ) {
            $o | ConvertTo-Json | Write-Verbose -Verbose:$VerbosePreference
        }
    }
    If ( $o.error.code) {
        throw $o.error
    }
    $o.response
}
Function Remove-gSpannerInstance {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        $Instance
    )
    Process {
        $i = $_ ?? $Instance
        if ($PSCmdlet.ShouldProcess(('Spanner instance {0} from project' -f $i.name), "remove")) {
            $r = Invoke-gOAuthRestMethod -Uri "$version1/$($i.name)" -Method DELETE -Verbose:$VerbosePreference
            If ( $VerbosePreference ) {
                $r | ConvertTo-Json -Depth 99 | Write-Verbose -Verbose:$VerbosePreference
            }
        }
        Else {
            'Remove Spanner instance {0} from project' -f $db.name | Write-Information
        }
    }
}
Function Get-gSpannerDatabase {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $Instance,
        $Id,
        [switch]
        $Default
    )
    $Id ?
    (Invoke-gOAuthRestMethod -Uri "$version1/$($Instance.name)/databases/$Id" -Verbose:$VerbosePreference) :
    (Invoke-gOAuthRestMethod -Uri "$version1/$($Instance.name)/databases" -Verbose:$VerbosePreference).databases |
    % {
        If ( $Default.IsPresent ) {
            $Global:PSDefaultParameterValues['*-gSpanner*:Database'] = $_
        }
        $_
    }
}
Function Add-gSpannerDatabase {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $Instance,
        [Parameter(Mandatory)]
        $Id
    )
    $b = @{
        createStatement = 'CREATE DATABASE `{0}`' -f $Id;
        extraStatements = @()
    }
    $o = Invoke-gOAuthRestMethod  -Uri "$version1/$($Instance.name)/databases" -Body $b -Verbose:$VerbosePreference
    If ( $VerbosePreference ) {
        $o | ConvertTo-Json | Write-Verbose -Verbose:$VerbosePreference
    }
    While ( -not $o.done) {
        Start-Sleep -Milliseconds 30
        $o = Get-gSpannerDatabaseOperation -Operation $o -Verbose:$VerbosePreference
        If ( $VerbosePreference ) {
            $o | ConvertTo-Json | Write-Verbose -Verbose:$VerbosePreference
        }
    }
    If ( $o.error.code) {
        throw $o.error
    }
    $o.response
}
Function Get-gSpannerDatabaseDdl {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        $Database
    )
    Process {
        $db = $_ ?? $Database
        Invoke-gOAuthRestMethod  -Uri "$version1/$($db.name)/ddl" -Verbose:$VerbosePreference
    }
}
Function Remove-gSpannerDatabase {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        $Database
    )
    Process {
        $db = $_ ?? $Database
        if ($PSCmdlet.ShouldProcess(('database {0} from Spanner instance' -f $db.name), "remove")) {
            Invoke-gOAuthRestMethod -Uri "$version1/$($db.name)" -Method DELETE -Verbose:$VerbosePreference
        }
        Else { 
            'Remove database {0} from Spanner instance' -f $db.name | Write-Information
        }
    }
}
Function Get-gSpannerTable {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $Database,
        $Like,
        [scriptblock]
        $Filter = { $_.TABLE_SCHEMA -eq '' }
    )
    $s = New-gSpannerSession -Database $Database -Verbose:$VerbosePreference
    try {
        $sql = $Pattern ?
        'select * from INFORMATION_SCHEMA.TABLES where TABLE_NAME like "{0}"' -f $Like :
        'select * from INFORMATION_SCHEMA.TABLES'
        Invoke-gSpannerSql -Session $s -SingleUse -Sql $sql -Verbose:$VerbosePreference |
        Select-Object -ExpandProperty resultSet |
        Where-Object $Filter |
        Select-Object -ExpandProperty TABLE_NAME | % {
            [PSCustomObject]@{
                name     = $_;
                database = $Database
            }
        }
    }
    finally {
        Remove-gSpannerSession -Session $s
    }
}
Function Remove-gSpannerTable {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        $Table
    )
    Begin {
        $tables = @()
    }
    Process {
        $tables += $_ ?? $Table
    }
    End {
        If ( $tables.Count -gt 0 ) {
            $tables |
            Sort-Object -Property database |
            Group-Object -Property database | % {
                $statements = @()
                $db = $null
                $_.Group | % {
                    if ($PSCmdlet.ShouldProcess(('table {0} from database' -f $_.name), "drop")) {
                        $statements += 'DROP TABLE `{0}`' -f $_.name
                        $db = $_.database
                    }
                    else {
                        'Drop table {0} from database' -f $_ | Write-Information
                    }
                }
                If ( $statements.Count -gt 0 ) {
                    $statements | Set-gSpannerSchema -Database $db -Verbose:$VerbosePreference
                }
            }
        }
    }
}
Function Get-gSpannerSchema {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $Database
    )
    (Invoke-gOAuthRestMethod -Uri "$version1/$($Database.name)/ddl").statements
}
Function New-gSpannerSession {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $Database,
        [switch]
        $Default
    )
    Invoke-gOAuthRestMethod -Uri "$version1/$($Database.name)/sessions" -Body @{ } -Verbose:$VerbosePreference |
    % {
        If ( $Default.IsPresent) {
            $Global:PSDefaultParameterValues['*-gSpanner*:Session'] = $_
        }
        $_
    }
}
Function Remove-gSpannerSession {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline, Position = 1)]
        $Session
    )
    Begin { }
    Process {
        $s = $_ ?? $Session
        If ( $s ) {
            [void](Invoke-gOAuthRestMethod -Uri "$version1/$($s.name)" -Method DELETE -Verbose:$VerbosePreference)
        }
    }
    End { }
}
Function Get-gSpannerSession {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $Database,
        [switch]
        $Default
    )
    (Invoke-gOAuthRestMethod -Uri "$version1/$($Database.name)/sessions" -Verbose:$VerbosePreference).sessions |
    % {
        If ( $Default.IsPresent) {
            $Global:PSDefaultParameterValues['*-gSpanner*:Session'] = $_
        }
        $_
    }
}
Function Get-gSpannerInstanceOperation {
    [CmdletBinding()]
    param (
        #[Parameter(Mandatory)]
        $Operation,
        $Instance
    )
    $Operation ?
    (Invoke-gOAuthRestMethod  -Uri "$version1/$($Operation.name)" -Verbose:$VerbosePreference) : 
    (Invoke-gOAuthRestMethod  -Uri "$version1/$($Instance.name)/operations" -Verbose:$VerbosePreference).operations
}
Function Remove-gSpannerInstanceOperation {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        $Operation
    )
    Process {
        $o = $_ ?? $Operation
        If ( $o ) {
            [void](Invoke-gOAuthRestMethod -Uri "$version1/$($o.name)" -Method DELETE -Verbose:$VerbosePreference)
        }
    }
}
Function Get-gSpannerDatabaseOperation {
    [CmdletBinding()]
    param (
        #[Parameter(Mandatory)]
        $Operation,
        $Database,
        $Instance
    )
    $Operation ?
    (Invoke-gOAuthRestMethod  -Uri "$version1/$($Operation.name)" -Verbose:$VerbosePreference) :
    $Database ?
    (Invoke-gOAuthRestMethod  -Uri "$version1/$($Database.name)/operations" -Verbose:$VerbosePreference).operations :
    (Invoke-gOAuthRestMethod  -Uri "$version1/$($Instance.name)/databaseOperations" -Verbose:$VerbosePreference).operations
}
Function Remove-gSpannerDatabaseOperation {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        $Operation
    )
    Process {
        $o = $_ ?? $Operation
        If ( $o ) {
            Try {
                [void](Invoke-gOAuthRestMethod -Uri "$version1/$($o.name)" -Method DELETE -Verbose:$VerbosePreference)
            }
            Catch {
                $m = $_.ErrorDetails.Message | ConvertFrom-Json
                If ( 'UNIMPLEMENTED' -eq $m.error.status ) {
                    $m.error.message | Write-Warning
                }
                Else {
                    Throw
                }
            }
        }
    }
}
Function Set-gSpannerSchema {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]
        $Statements,
        [Parameter(Mandatory)]
        $Database
    )
    Begin {
        $s = @()
    }
    Process {
        $s += [string]($_ ?? $Statements)
    }
    End {
        If ( $s.Count -eq 0 ) {
            throw 'No statements have been given...'
        }
        $operation = 'id_' + [System.Guid]::NewGuid().ToString().Replace('-', '')
        $body = @{
            statements  = $s;
            operationId = $operation
        }
        $o = Invoke-gOAuthRestMethod -Uri "$version1/$($Database.name)/ddl" -Method PATCH -Body $body -Verbose:$VerbosePreference
        If ( $VerbosePreference ) {
            $o | ConvertTo-Json | Write-Verbose -Verbose:$VerbosePreference
        }
        While ( -not $o.done ) {
            Start-Sleep -Milliseconds 30
            $o = Get-gSpannerInstanceOperation -Operation $o -Verbose:$VerbosePreference
            If ( $VerbosePreference ) {
                $o | ConvertTo-Json | Write-Verbose -Verbose:$VerbosePreference
            }
        }
        If ( $o.error.code) {
            throw $o.error
        }
        $o
    }
}
Function Invoke-gSpannerBatchDml {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [string[]]
        $Statements,
        [Parameter(Mandatory)]
        $Session,
        $Transaction,
        [int64]
        $SeqNo = 1,
        [switch]
        $Commit,
        [SpannerTransactionMode]
        $TransactionMode = [SpannerTransactionMode]::readWrite,
        $RetryCount = 3
    )
    Begin {
        $s = @()
    }
    Process {
        $s += $_ ?? $Statements
    }
    End {
        If ($VerbosePreference) {
            $s | Write-Verbose -Verbose:$VerbosePreference
        }
        $tp = @{ }
        foreach ( $p in $PSBoundParameters.GetEnumerator() ) {
            $v = switch ( $p.Key ) {
                'Statements' { }
                'Session' { }
                'SeqNo' { }
                'Commit' { }
                'RetryCount' { }
                default { $p }
            }
            If ( $v ) {
                $tp.Add($v.Key, $v.Value)
            }
        }
        $to = Get-gSpannerTransactionOptions @tp
        $body = [PSCustomObject]@{
            transaction = [pscustomobject]@{
                $to.key = $to.options
            }
            statements  = @( $s | % { @{ sql = [string]$_ } } );
            seqno       = $SeqNo.ToString()
        }
        $o = Invoke-gOAuthRestMethod -Uri "$version1/$($Session.Name):executeBatchDml" -Body $body -Verbose:$VerbosePreference
        If ( $VerbosePreference ) {
            $o | ConvertTo-Json -Depth 99 | Write-Verbose -Verbose:$VerbosePreference
        }
        If ( $o.status.code ) {
            throw $o.status
        }
        $r = [PSCustomObject]@{
            executeBatchDml = $o;
            commit          = $null;
        }
        If ( $Commit.IsPresent ) {
            $r.executeBatchDml.resultSets | Select-Object -First 1 | % {
                $r.commit = Publish-gSpannerTransaction -Session $Session -Transaction ($_.metadata.transaction ?? $Transaction ) -RetryCount $RetryCount
            }
            If ( $VerbosePreference ) {
                $r.commit | ConvertTo-Json -Depth 99 | Write-Verbose -Verbose:$VerbosePreference
            }
        }
        If ( $VerbosePreference ) {
            $r | ConvertTo-Json -Depth 99 | Write-Verbose -Verbose:$VerbosePreference
        }
        $r
    }
}
Function Get-gSpannerTransactionOptions {
    [CmdletBinding()]
    param (
        $Transaction,
        [SpannerTransactionMode]
        $TransactionMode = [SpannerTransactionMode]::readOnly,
        [switch]
        $ReturnReadTimestamp,
        [switch]
        $Strong,
        [datetime]
        $MinReadTimestamp,
        [timespan]
        $MaxStaleness,
        [datetime]
        $ReadTimestamp,
        [timespan]
        $ExactStaleness,
        [switch]
        $SingleUse
    )
    If ( $VerbosePreference ) {
        $PSBoundParameters | ConvertTo-Json -Depth 99 | Write-Verbose -Verbose:$VerbosePreference
    }
    $b = If ( $Transaction ) { 
        [PSCustomObject]@{
            key     = 'id'
            options = $transaction.id
        }
    }
    else {
        switch ($TransactionMode.ToString()) {
            'readWrite' {
                [PSCustomObject]@{
                    key     = 'begin'
                    options = [PSCustomObject]@{
                        readWrite = [PSCustomObject]@{ }
                    }
                }
                break
            }
            'readOnly' {
                $o = [PSCustomObject]@{
                    readOnly = [PSCustomObject]@{ }
                }
                $r = $o.readOnly
                $b = If ( $SingleUse.IsPresent -or ( -not ($PSBoundParameters.ContainsKey('TransactionMode') ))) {
                    [PSCustomObject]@{
                        key     = 'singleUse'
                        options = $o
                    }
                }
                Else {
                    [PSCustomObject]@{
                        key     = 'begin'
                        options = $o
                    }
                }
                If ( $ReturnReadTimestamp) {
                    Add-Member -InputObject $r -MemberType NoteProperty -Name returnReadTimestamp -Value $true
                }
                If ( $Strong ) {
                    Add-Member -InputObject $r -MemberType NoteProperty -Name strong -Value $true
                }
                If ( $MinReadTimestamp ) {
                    Add-Member -InputObject $r -MemberType NoteProperty -Name minReadTimestamp -Value ('{0:o}' -f $MinReadTimestamp.ToUniversalTime())
                }
                If ( $MaxStaleness ) {
                    Add-Member -InputObject $r -MemberType NoteProperty -Name maxStaleness -Value ('{0}s' -f ( $MaxStaleness.Ticks / 10000000 ))
                }
                If ( $ReadTimestamp ) {
                    Add-Member -InputObject $r -MemberType NoteProperty -Name readTimestamp -Value ('{0:o}' -f $ReadTimestamp.ToUniversalTime())
                }
                If ( $ExactStaleness ) {
                    Add-Member -InputObject $r -MemberType NoteProperty -Name exactStaleness -Value ('{0}s' -f ( $ExactStaleness.Ticks / 10000000 ))
                }
                $b
                break
            }
            'partitionedDml' {
                [PSCustomObject]@{
                    key     = 'begin'
                    options = [PSCustomObject]@{
                        partitionedDml = [PSCustomObject]@{ } 
                    }
                };
                break
            }
        }
    }
    If ( $VerbosePreference ) {
        $b | ConvertTo-Json | Write-Verbose -Verbose:$VerbosePreference
    }
    $b
}
Function New-gSpannerTransaction {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [SpannerTransactionMode]
        $TransactionMode,
        [Parameter(Mandatory)]
        $Session,
        [switch]
        $ReturnReadTimestamp,
        [switch]
        $Strong,
        [datetime]
        $MinReadTimestamp,
        [timespan]
        $MaxStaleness,
        [datetime]
        $ReadTimestamp,
        [timespan]
        $ExactStaleness,
        [switch]
        $SingleUse,
        [switch]
        $Default
    )
    $tp = @{ }
    foreach ( $p in $PSBoundParameters.GetEnumerator() ) {
        $v = switch ( $p.Key ) {
            'Session' { }
            'Default' { }
            default { $p }
        }
        If ( $v ) {
            $tp.Add($v.Key, $v.Value)
        }
    }
    $to = Get-gSpannerTransactionOptions @tp
    $b = [pscustomobject]@{
        options = $to.options
    }
    Invoke-gOAuthRestMethod -Uri "$version1/$($Session.Name):beginTransaction" -Body $b -Verbose:$VerbosePreference |
    % {
        If ( $Default.IsPresent ) {
            $Global:PSDefaultParameterValues['*-gSpanner*:Transaction'] = $_
        }
        $_
    }
}
Function Start-gSpannerStreamingSql {
    [CmdletBinding(DefaultParameterSetName = 'ReadOnly')]
    param (
        [Parameter(ValueFromPipeline, Mandatory, Position = 1)]
        [string]
        $Sql,
        [Parameter(Mandatory)]
        $Session,
        [int]
        $SeqNo = 1,
        [Parameter(ParameterSetName = 'Custom')]
        $Transaction,
        [switch]
        $Commit,
        [Parameter(ParameterSetName = 'SingleUse')]
        [switch]
        $SingleUse,
        [Parameter(ParameterSetName = 'ReadWrite')]
        [Parameter(ParameterSetName = 'ReadOnly')]
        [Parameter(ParameterSetName = 'SingleUse')]
        [SpannerTransactionMode]
        $TransactionMode = [SpannerTransactionMode]::readOnly,
        [Parameter(ParameterSetName = 'ReadOnly')]
        [Parameter(ParameterSetName = 'SingleUse')]
        [switch]
        $ReturnReadTimestamp,
        [Parameter(ParameterSetName = 'ReadOnly')]
        [Parameter(ParameterSetName = 'SingleUse')]
        [switch]
        $Strong,
        [Parameter(ParameterSetName = 'ReadOnly')]
        [Parameter(ParameterSetName = 'SingleUse')]
        [datetime]
        $MinReadTimestamp,
        [Parameter(ParameterSetName = 'SingleUse')]
        [timespan]
        $MaxStaleness,
        [Parameter(ParameterSetName = 'ReadOnly')]
        [Parameter(ParameterSetName = 'SingleUse')]
        [datetime]
        $ReadTimestamp,
        [Parameter(ParameterSetName = 'ReadOnly')]
        [Parameter(ParameterSetName = 'SingleUse')]
        [timespan]
        $ExactStaleness
    )
    $tp = @{ }
    foreach ( $p in $PSBoundParameters.GetEnumerator() ) {
        $v = switch ( $p.Key ) {
            'Sql' { }
            'Session' { }
            'SeqNo' { }
            'Commit' { }
            default { $p }
        }
        If ( $v ) {
            $tp.Add($v.Key, $v.Value)
        }
    }
    $to = Get-gSpannerTransactionOptions @tp
    $body = @{
        transaction = [pscustomobject]@{
            $to.key = $to.options
        }
        sql         = $Sql;
        seqno       = $SeqNo.ToString();
        queryMode   = 'PROFILE'
    }
    $o = Invoke-gOAuthRestMethod -Uri "$version1/$($Session.Name):executeStreamingSql" -Body $body -Verbose:$VerbosePreference
    If ( $VerbosePreference) {
        $o | ConvertTo-Json | Write-Verbose -Verbose:$VerbosePreference
    }
    $r = [PSCustomObject]@{
        executeStreamingSql = $o
        transaction         = $o.metadata.transaction ?? $Transaction
    }
    If ( $Commit ) {
        $o = Publish-gSpannerTransaction -Session $Session -Transaction $r.transaction -Verbose:$VerbosePreference
        Add-Member -InputObject $r -MemberType NoteProperty -Name commit -Value $o
    }
    If ( $VerbosePreference ) {
        $r | ConvertTo-Json -Depth 99 | Write-Verbose -Verbose:$VerbosePreference
    }
    Get-gSpannerResultSet -ResultSet $r.executeStreamingSql -Streaming
}
Function Invoke-gSpannerSql {
    [CmdletBinding(DefaultParameterSetName = 'ReadOnly')]
    param (
        [Parameter(ValueFromPipeline, Mandatory, Position = 1)]
        [string]
        $Sql,
        [Parameter(Mandatory)]
        $Session,
        [int]
        $SeqNo = 1,
        [Parameter(ParameterSetName = 'Custom')]
        $Transaction,
        [switch]
        $Commit,
        [Parameter(ParameterSetName = 'SingleUse')]
        [switch]
        $SingleUse,
        [Parameter(ParameterSetName = 'ReadWrite')]
        [Parameter(ParameterSetName = 'ReadOnly')]
        [Parameter(ParameterSetName = 'SingleUse')]
        [SpannerTransactionMode]
        $TransactionMode = [SpannerTransactionMode]::readOnly,
        [Parameter(ParameterSetName = 'ReadOnly')]
        [Parameter(ParameterSetName = 'SingleUse')]
        [switch]
        $ReturnReadTimestamp,
        [Parameter(ParameterSetName = 'ReadOnly')]
        [Parameter(ParameterSetName = 'SingleUse')]
        [switch]
        $Strong,
        [Parameter(ParameterSetName = 'ReadOnly')]
        [Parameter(ParameterSetName = 'SingleUse')]
        [datetime]
        $MinReadTimestamp,
        [Parameter(ParameterSetName = 'SingleUse')]
        [timespan]
        $MaxStaleness,
        [Parameter(ParameterSetName = 'ReadOnly')]
        [Parameter(ParameterSetName = 'SingleUse')]
        [datetime]
        $ReadTimestamp,
        [Parameter(ParameterSetName = 'ReadOnly')]
        [Parameter(ParameterSetName = 'SingleUse')]
        [timespan]
        $ExactStaleness,
        [switch]
        $ExpandResultset
    )
    Begin {
        $tp = @{ }
        foreach ( $p in $PSBoundParameters.GetEnumerator() ) {
            $v = switch ( $p.Key ) {
                'Sql' { }
                'Session' { }
                'SeqNo' { }
                'Commit' { }
                'ExpandResultset' { }
                default { $p }
            }
            If ( $v ) {
                $tp.Add($v.Key, $v.Value)
            }
        }
        $to = Get-gSpannerTransactionOptions @tp
    }
    Process {
        $s = $_ ?? $Sql;
        $s | Write-Verbose -Verbose:$VerbosePreference
        $body = [pscustomobject]@{
            transaction = [pscustomobject]@{
                $to.key = $to.options
            }
            sql         = $s.PSObject.BaseObject;
            seqno       = $SeqNo.ToString();
            queryMode   = 'PROFILE'
        }
        If ( $VerbosePreference ) {
            $body | ConvertTo-Json -Depth 1 | Write-Verbose -Verbose:$VerbosePreference
        }
        $o = Invoke-gOAuthRestMethod -Uri "$version1/$($Session.Name):executeSql" -Body $body -Verbose:$VerbosePreference
        $r = [pscustomobject]@{
            executeSql  = $o;
            transaction = $o.metadata.transaction ?? $Transaction
            commit      = $null;
        } |
        Add-Member -Name resultSet -MemberType ScriptProperty -Value { @( Get-gSpannerResultSet -ResultSet $this.executeSql ) } -PassThru
        If ( $Commit.IsPresent ) {
            $r.commit = Publish-gSpannerTransaction -Session $Session -Transaction $r.transaction -Verbose:$VerbosePreference
            If ( $VerbosePreference ) {
                $r.commit | ConvertTo-Json -Depth 99 | Write-Verbose -Verbose:$VerbosePreference
            }
        }
        If ( $ExpandResultset.IsPresent ) {
            $r | Select-Object -ExpandProperty resultSet
        }
        Else {
            $r
        }
    }
    End {
    }
}
Function Get-gSpannerColumnHeader {
    Param(
        [Parameter(Mandatory)]
        $ResultSet,
        [Parameter(Mandatory)]
        [int]
        $Index
    )
    ( $ResultSet.metadata.rowType.fields ? $ResultSet.metadata.rowType.fields[$Index].name : $null ) ?? 'Column{0:00#}' -f $Index
}
Function Get-gSpannerColumn {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline, Mandatory)]
        $Field
    )
    Process {
        $f = $_ ?? $Field
        $t = switch ( $f.type.code ) {
            'BYTES' {
                [System.Byte[]]
            }
            'DATE' {
                [System.DateTime]
            }
            'BOOL' {
                [System.Boolean]
            }
            'FLOAT64' {
                [System.Double]
            }
            'INT64' {
                [System.Int64]
            }
            'TIMESTAMP' {
                [System.DateTime]
            }
            'STRING' {
                [System.String]
            }
            default {
                [System.String]
            }
        }
        '{0} {1}: {2}' -f $f.name, $f.type.code, $t | Write-Verbose -Verbose:$VerbosePreference
        [PSCustomObject]@{
            Name = $f.name ?? 'Column{0:00#}';
            Type = $t
        }
    }
}

Function Get-gSpannerResultSet {
    Param (
        $ResultSet,
        [switch]
        $Streaming
    )
    Begin {
        $fields = @($ResultSet.metadata.rowType.fields | Get-gSpannerColumn)
    }
    Process {
        If ( $Streaming ) {
            $i = 0
            $r = New-Object PSCustomObject
            $ResultSet.values | % {
                $f = $fields[$i]
                $n = $f.Name -f $i
                $v = $f.Type.FullName -eq 'System.Byte[]' ? [System.Convert]::FromBase64String($_) : $_
                '{0} is {1}: {2}' -f $n, $f.Type, $v | Write-Verbose -Verbose:$VerbosePreference
                Add-Member -InputObject $r -MemberType NoteProperty -Name $n -Value ($v -as $f.Type)
                $i = ++$i % $ResultSet.metadata.rowType.fields.Length
                If ( $i -eq 0) {
                    $r
                    $r = New-Object PSCustomObject
                }
            }
        }
        Else {
            $ResultSet.rows | % {
                $i = 0
                $r = New-Object PSCustomObject
                $_ | % {
                    $f = $fields[$i]
                    $n = $f.Name -f $i
                    $v = $f.Type.FullName -eq 'System.Byte[]' ? [System.Convert]::FromBase64String($_) : $_
                    '{0} is {1}: {2}' -f $n, $f.Type, $v | Write-Verbose -Verbose:$VerbosePreference
                    Add-Member -InputObject $r -MemberType NoteProperty -Name $n -Value ( $v -as $f.Type  )
                    $i += 1
                }
                $r
            }
        }
    }
    End {
    }
}
Function Publish-gSpannerTransaction {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $Session,
        [Parameter(Mandatory)]
        $Transaction,
        $RetryCount = 3
    )
    $body = @{
        transactionId = $Transaction.id
    }
    $retry = $false
    do {
        Try {
            Invoke-gOAuthRestMethod -Uri "$version1/$($Session.Name):commit" -Body $body -Verbose:$VerbosePreference
        }
        catch {
            If ( $RetryCount -lt 1) {
                throw
            }
            $json = $_.ErrorDetails.Message
            $message = $json | ConvertFrom-Json
            If ( $message.error.status -eq 'ABORTED') {
                $message.error.details |
                ? { $_.'@type' -eq 'type.googleapis.com/google.rpc.RetryInfo' } |
                Select-Object -First 1 | % {
                    try {
                        $delay = [timespan]::FromSeconds([double]::parse($_.retryDelay.TrimEnd('s')))
                    }
                    catch {
                        If ( $VerbosePreference) {
                            'Unable to parse retry delay {0}. Exception: {1}' -f $_.retryDelay, $json | Write-Warning
                        }
                        throw
                    }
                    $Retry = $RetryCount-- -gt 0
                    if (-not $Retry ) {
                        If ( $VerbosePreference) {
                            'No more retries, abandon publish transaction: {0}' -f $json | Write-Warning
                        }
                        throw
                    }
                    If ( $VerbosePreference) {
                        'Retry in {0} milliseconds to publish transaction: {1}' -f $delay.TotalMilliseconds, $json | Write-Warning
                    }
                    Start-Sleep -Milliseconds $delay.TotalMilliseconds
                }
            }
            Else {
                If ( $VerbosePreference) {
                    'Abandon publish transaction: {0}' -f $json | Write-Warning
                }
                throw
            }
        }
    } while ( $retry )
}
Function Undo-gSpannerTransaction {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        $Transaction,
        [Parameter(Mandatory)]
        $Session
    )
    $body = @{
        transactionId = $Transaction.id
    }
    [void](Invoke-gOAuthRestMethod -Uri "$version1/$($Session.Name):rollback" -Body $body -Verbose:$VerbosePreference)
}
Function Get-gSpannerDataType {
    [OutputType([string])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [String]
        $DataType
    )
    try {
        $dt = [type]$DataType
    }
    catch {
        $dt = 'System.Management.Automation.' + $DataType
    }
    '{0} became {1}' -f $DataType, $dt.FullName | Write-Verbose -Verbose:$VerbosePreference
    switch ( $dt.FullName ) {
        'System.String' {
            'STRING(MAX)'
        }
        'System.DateTime' {
            'TIMESTAMP'
        }
        'System.TimeSpan' {
            'INT64'
        }
        'System.INT64' {
            'INT64'
        }
        'System.INT32' {
            'INT64'
        }
        'System.INT16' {
            'INT64'
        }
        'System.Boolean' {
            'BOOL'
        }
        default {
            'STRING(MAX)'
        }
    }
}
Function Get-gSpannerValue {
    [OutputType([string])]
    [CmdletBinding()]
    param (
        [object]
        $Value,
        [switch]
        $JSON
    )
    if ( $null -ne $Value  ) {
        switch ( $Value.GetType().ToString()) {
            'System.DateTime' {
                If ( $JSON ) {
                    [Math]::Floor(1000 * (Get-Date -Date $Value -UFormat %s))
                }
                Else {
                    $v = $Value.Kind -eq [System.DateTimeKind]::Unspecified ? [Datetime]::SpecifyKind($Value, [System.DateTimeKind]::Utc) : $Value
                    "TIMESTAMP '{0:o}'" -f $v.ToUniversalTime()
                }
            }
            'System.TimeSpan' {
                $Value.Ticks
            }
            'System.String' {
                If ( $JSON ) {
                    "'{0}'" -f [System.Web.HttpUtility]::JavaScriptStringEncode([System.Web.HttpUtility]::JavaScriptStringEncode($Value))
                }
                Else {
                    "'{0}'" -f [System.Web.HttpUtility]::JavaScriptStringEncode($Value)
                }
            }
            'System.Boolean' {
                $Value.ToString().ToLowerInvariant()
            }
            'System.Int64' {
                $Value
            }
            'System.Int32' {
                $Value
            }
            'System.Int16' {
                $Value
            }
            'System.Management.Automation.PSCustomObject' {
                "'{0}'" -f [System.Web.HttpUtility]::JavaScriptStringEncode(($Value | ConvertTo-Json))
            }
            default {
                If ( $JSON ) {
                    #"'{0}'" -f [System.Web.HttpUtility]::JavaScriptStringEncode([System.Web.HttpUtility]::JavaScriptStringEncode($Value))
                    $null
                }
                Else {
                    "'{0}'" -f [System.Web.HttpUtility]::JavaScriptStringEncode($Value)
                }
            }
        }
    }
    else {
        'null'
    }
}
Function Get-gSpannerColumns {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [object]
        $Value
    )
    Begin {
        $v = @()
    }
    Process {
        $v += $Value
    }
    End {
        $v |
        Get-Member -MemberType Properties |
        ? {
            switch ($_.MemberType) {
                'Property' { $true }
                'NoteProperty' { $true }
                'ScriptProperty' { $true }
                default { $false }
            }
        } |
        Sort-Object -Property TypeName, Name -Unique |
        ? {
            If ( $_.Name -match '^[a-z][a-z,0-9,_,\p{Zs}]*$' ) {
                $True
            }
            else {
                If ( $_.Name -inotlike '__*') {
                    'Name invalid, ignoring {0}.{1}' -f $_.TypeName, $_.Name | Write-Warning
                }
            }
        } |
        % {
            Add-Member -InputObject $_ -Name __ColumnHeader -MemberType NoteProperty -Value ( $_.Name.Replace(' ', '_')) -PassThru
        }
    }
}
Function Get-gSpannerColumnsFromType {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [type]
        $Type
    )
    Begin {
        $v = @()
    }
    Process {
        $v += $Type
    }
    End {
        $v |
        % {
            $_.GetMembers()
        } |
        ? {
            $_.MemberType -eq 'Property'
        } |
        Sort-Object -Property ReflectedType, Name -Unique |
        ? {
            If ( $_.Name -match '^[a-z][a-z,0-9,_,\p{Zs}]*$' ) {
                $True
            }
            else {
                If ( $_.Name -inotlike '__*') {
                    'Name invalid, ignoring {0}.{1}' -f $_.TypeName, $_.Name | Write-Warning
                }
            }
        } |
        % {
            Add-Member -InputObject $_ -Name __ColumnHeader -MemberType NoteProperty -Value ( $_.Name.Replace(' ', '_')) -Force -PassThru
        }
    }
}
Function Add-gSpannerTableFromType {
    param (
        [Parameter(ValueFromPipeline)]
        [type]
        $Type,
        [Parameter(Mandatory)]
        $Database,
        [switch]
        $JSON,
        [switch]
        $Header,
        $Key
    )
    Begin {
        $values = @()
    }
    Process {
        $values += If ( $Value ) {
            $Value
        }
        Else {
            $_
        }
    }
    End {
        $values |
        Get-gSpannerColumnsFromType |
        Group-Object -Property ReflectedType | % {
            $t = $_.Name -split '\.' | Select-Object -Last 1
            $v = If ($Header.IsPresent) {
                @(
                    '`g_Id` STRING(32)'
                    '`g_Load` TIMESTAMP'
                )
                $Key = 'g_id'
            }
            Else {
                @()
            }
            If ( $JSON ) {
                $v += '`g_Value` STRING(MAX)'
            }
            Else {
                $v += @(
                    $_.Group |
                    % {
                        '`{0}` {1}' -f $_.__ColumnHeader, (Get-gSpannerDataType -DataType $_.PropertyType )
                    }
                )
            }
            $vs = $v -join ', '
            @(
                'CREATE TABLE {0}(' -f $t
                $vs
                ') PRIMARY KEY(`{0}`)' -f $Key
            ) -join ' '
        } |
        Set-gSpannerSchema -Database $Database -Verbose:$VerbosePreference
    }
}
Function Add-gSpannerTable {
    param (
        [Parameter(ValueFromPipeline)]
        [object]
        $Value,
        [Parameter(Mandatory)]
        $Database,
        $Name,
        [switch]
        $SkipLoadHeader,
        [switch]
        $JSON,
        [string[]]
        $Key = @( 'g_Id', 'g_Load')
    )
    Begin {
        $values = @()
    }
    Process {
        $values += If ( $Value ) {
            $Value
        }
        Else {
            $_
        }
    }
    End {
        $values |
        Get-gSpannerColumns |
        Group-Object -Property TypeName | % {
            $t = $Name ?? ( $_.Name -split '\.' | Select-Object -Last 1 )
            'Table: {0}' -f $t | Write-Verbose -Verbose:$VerbosePreference
            $v = @()
            If ( -not $SkipLoadHeader.IsPresent ) {
                $v += @(
                    '`g_Id` STRING(32)'
                    '`g_Load` TIMESTAMP'
                )
            }
            If ( $JSON ) {
                $v += '`g_Value` STRING(MAX)'
            }
            Else {
                $v += @(
                    $_.Group |
                    % {
                        '`{0}` {1}' -f $_.__ColumnHeader, (Get-gSpannerDataType -DataType ( $_.Definition -Split ' ' | Select-Object -First 1 ) )
                    }
                )
            }
            $vs = $v -join ', '
            @(
                'CREATE TABLE {0}(' -f $t
                $vs
                ') PRIMARY KEY('
                @(
                    $Key | % { '`{0}`' -f $_ }
                ) -Join ','
                ')' -f $t
            ) -join ' '
        } |
        Set-gSpannerSchema -Database $Database -Verbose:$VerbosePreference
    }
}
Function Add-gSpannerItemsSelectUnionAll {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [object]
        $Value,
        [Parameter(Mandatory)]
        $Database,
        [switch]
        $SkipSchemaCreation,
        [switch]
        $JSON,
        [System.DateTime]
        $Load = [System.DateTime]::UtcNow,
        [int]
        $PageSize = 200
    )
    Begin {
        $values = @()
    }
    Process {
        $values += $_
    }
    End {
        If ( -not $SkipSchemaCreation.IsPresent ) {
            $values | Add-gSpannerTable -Database $Database -JSON:$JSON
        }
        $s = @()
        $values |
        % {
            $tn = ( $_ | Get-Member | Select-Object -First 1).TypeName
            $_ | Add-Member -MemberType NoteProperty -Name __TypeName -Value $tn -Force -PassThru
        } |
        Sort-Object -Property __TypeName |
        Group-Object __TypeName |
        % {
            $v = $_
            $members = $values | Get-gSpannerColumns | ? TypeName -eq $v.Name
            $rows = $_ | Select-Object -ExpandProperty Group
            1..[int][Math]::Ceiling(($rows | Measure-Object).Count / $PageSize) |
            % {
                $page = ([int]::Parse($_) - 1)
                $l = @(
                    'INSERT INTO {0} (' -f ( $v.Name -split '\.' | Select-Object -Last 1 )
                    @(
                        '`g_Id`'
                        '`g_Load`'
                        If ( $JSON) {
                            '`g_Value`'
                        }
                        Else {
                            $members |
                            % {
                                '`{0}`' -f $_.__ColumnHeader
                            }
                        }
                    ) -join ', '
                    ')'
                    $u = @()
                    $rows |
                    Select-Object -Skip ( $page * $PageSize ) -First $PageSize |
                    % {
                        $c = $_
                        $u += @(
                            'SELECT'
                            @(
                                Get-gSpannerValue -Value ([System.Guid]::NewGuid().ToString().Replace('-', ''))
                                Get-gSpannerValue -Value $Load
                                @(
                                    $members |
                                    % {
                                        $n = $_.Name
                                        $o = $c.$n
                                        If ( $JSON ) {
                                            If ( $o ) {
                                                $ov = Get-gSpannerValue -Value $o -JSON:$JSON
                                                If ( $ov ) {
                                                    "'{0}': {1}" -f $n, $ov
                                                }
                                            }
                                        }
                                        Else {
                                            Get-gSpannerValue -Value $o
                                        }
                                    }
                                ) -Join ', ' |
                                % {
                                    If ( $JSON ) {
                                        '"{' + $_ + '}"'
                                    }
                                    Else {
                                        $_
                                    }
                                }
                            ) -join ', '
                        ) -join ' '
                    }
                    $u -join ' UNION ALL '
                ) -join ' '
                $s += $l
            }
        }
        $session = New-gSpannerSession -Database $Database
        Invoke-gSpannerBatchDml -Session $session -Statements $s -Commit -Verbose:$VerbosePreference
    }
}
Function Add-gSpannerItems {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline)]
        [object]
        $Value,
        [Parameter(Mandatory)]
        $Database,
        [switch]
        $SkipLoadHeader,
        [switch]
        $SkipSchemaCreation,
        [switch]
        $JSON,
        [System.DateTime]
        $Load = [System.DateTime]::UtcNow,
        $TableName,
        [string[]]
        $Key = @( 'g_Id', 'g_Load'),
        [int]
        $PageSize = 200
    )
    Begin {
        $values = @()
    }
    Process {
        $values += $_
    }
    End {
        If ( -not $SkipSchemaCreation.IsPresent ) {
            $values | Add-gSpannerTable -Database $Database -JSON:$JSON -Name $TableName -Key $Key -SkipLoadHeader:$SkipLoadHeader
        }
        $s = @()
        $values |
        % {
            $tn = ( $_ | Get-Member | Select-Object -First 1).TypeName
            $_ | Add-Member -MemberType NoteProperty -Name __TypeName -Value $tn -Force -PassThru
        } |
        Sort-Object -Property __TypeName |
        Group-Object __TypeName |
        % {
            $v = $_
            $members = $values | Get-gSpannerColumns | ? TypeName -eq $v.Name
            $rows = $_ | Select-Object -ExpandProperty Group
            1..[int][Math]::Ceiling(($rows | Measure-Object).Count / $PageSize) |
            % {
                $page = ([int]::Parse($_) - 1)
                $l = @(
                    'INSERT INTO {0} (' -f ($TableName ?? (( $v.Name -split '\.' | Select-Object -Last 1 )))
                    @(
                        If ( -not $SkipLoadHeader.IsPresent ) {
                            '`g_Id`'
                            '`g_Load`'
                        }
                        If ( $JSON) {
                            '`g_Value`'
                        }
                        Else {
                            $members |
                            % {
                                '`{0}`' -f $_.__ColumnHeader
                            }
                        }
                    ) -join ', '
                    ') VALUES '
                    $u = @()
                    $rows |
                    Select-Object -Skip ( $page * $PageSize ) -First $PageSize |
                    % {
                        $c = $_
                        $u += @(
                            '('
                            @(
                                If ( -not $SkipLoadHeader.IsPresent ) {
                                    Get-gSpannerValue -Value ([System.Guid]::NewGuid().ToString().Replace('-', ''))
                                    Get-gSpannerValue -Value $Load
                                }
                                @(
                                    $members |
                                    % {
                                        $n = $_.Name
                                        $o = $c.$n
                                        If ( $JSON ) {
                                            If ( $o ) {
                                                $ov = Get-gSpannerValue -Value $o -JSON:$JSON
                                                If ( $ov ) {
                                                    "'{0}': {1}" -f $n, $ov
                                                }
                                            }
                                        }
                                        Else {
                                            Get-gSpannerValue -Value $o
                                        }
                                    }
                                ) -Join ', ' |
                                % {
                                    If ( $JSON ) {
                                        '"{' + $_ + '}"'
                                    }
                                    Else {
                                        $_
                                    }
                                }
                            ) -join ', '
                            ')'
                        ) -join ' '
                    }
                    $u -join ', '
                ) -join ' '
                $s += $l
            }
        }
        $session = New-gSpannerSession -Database $Database
        Invoke-gSpannerBatchDml -Session $session -Statements $s -TransactionMode readWrite -Commit -Verbose:$VerbosePreference
    }
}
# SIG # Begin signature block
# MIIdEQYJKoZIhvcNAQcCoIIdAjCCHP4CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUBGnxyqxnT/0RhtmnKOf/F7yL
# nBGgghiiMIIFNTCCAx2gAwIBAgIQT/hgdSzRMK1Ptmol1X/K6zANBgkqhkiG9w0B
# AQsFADAOMQwwCgYDVQQDEwNnb2QwIBcNMTYwOTA4MTUxOTE5WhgPMjA1OTExMDIy
# MjE2MzNaMA4xDDAKBgNVBAMTA2dvZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
# AgoCggIBAJKirTyUVPFLWIgo8xg/YiWwYZyKxwqJ/TdOI4sX61Xm8gzxxOxPc7H1
# mqIG0ZSZQ6hSWb/JmGBm9BVctD78dDIEMMxkIfhsZQVF3wPLwc150zFpRFXSxnMR
# ivQeULzpS7aNhVhaHxX/H0YSIPUn7MU8vbGQWozheo9gljHTfjcAsHhZ94kuPFI1
# 3p5A5TfQf4AWBb21CSniqoUlfu8iYrUebYQTAvU3Lm55uOy5lVHcwlekFq40plRG
# jIPaoZT97L2LBxu+NaP9or8SXSUhKxhHiVAgYSD2trWVVDMuwsyDRmn61ORtMQDT
# TMZ9W+HoujUiATMxNDhUawlZNM8fn6d5SirqP97jrzUpZKmnKOHzWGbdz6xjDwm8
# eEKXj770zdfOQhuxh1gxfrUzf5Wa/NwV3Sj40pHfRO0dOj3llIMldpIyo5pMb+3W
# yea7FJfBf3KeTf+SQmgS0e1d2OEHKTalkThnUVUpgES2QqmWJ0iL4RgQor7EwajN
# g5qsR1QqQX1Q8A9NycxwA/YxT7pQhropEyhddVEjEWOONL5YLH8/4tN5FgrxOMwE
# u0+q+hCphttRBFAbEjGexhTjENmNMmgTIHb9jCkIFwTkWqUv1/0g8+UzRmC5vWf9
# G9gSmPACzY/ATZRsUA+Z+EQaiCVU5je0ZMOXV4QpZJ8BxDFfjTujAgMBAAGjgYww
# gYkwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF
# MAMBAf8wHQYDVR0OBBYEFOnDw0HoRV9Yspg5xBYu0pfyshkUMBAGCSsGAQQBgjcV
# AQQDAgECMCMGCSsGAQQBgjcVAgQWBBR85TAn1w27aOhVU80oamRgW9vkajANBgkq
# hkiG9w0BAQsFAAOCAgEAPoHPAzHnLZaoQFetJbyK/iZdGCCc8DXGViMbhBsNH6rM
# mk97uunVWQ0JZ/vTbfjYFe9tlDrk6HnYholJG8RqaMHcQEST/0iReP6DFXHIMkVX
# qy/z54EMgk15NIV1qhPCrORQdz+1HauDta/S0rin1K2jndFhSTddtpr3ky1NLr2G
# JPFDP7TZqSsnZCjqoWvp+s2ETtbreg28hh+RF7/lHnesWgMqbhMbBQMeR2D/Q+5B
# L58ul1T6QNWzcrhR7LkI15AYjH8WNot9wBS7HM7cYOwhRN/vj1QdFAqX/6+/cGO7
# RqeELL12SzZibkmDsF2vGKxeANxuRMg/3RxMaBpAdnw4loSFW+lFb1rHHcvl7baM
# D60iW/zNx+6rccVL/Mvclv20FvsrbWq7gTZjjjKLEzmYcCA9260glnQAdh6coZ9d
# RbPmKFmcrHos5n8MSdk2ca/CAKLrKj1b01iYUHVG70/1Yb8tr9OaIB5SLTq23Bqf
# ztcxtKNyqV6Jv0o40kxfTUmGqyWzoBgdEzxhxHUYDTOaSLg/igSGzzOnpjWXMKUU
# DNBL/+cBXbwECtYQwMHS39bjfDrd9q8R3pmu3rAljXOCyVnjUQznecK2hpxThPCy
# EHF2OMTd9Y2X9/DwhIsHt0+F1k1TkiHbpvJeIpiVlR4cDMzHL7ebPbMyKCkrcMIw
# ggYmMIIEDqADAgECAhMiAAADPr7I+iou2FbsAAIAAAM+MA0GCSqGSIb3DQEBCwUA
# MA4xDDAKBgNVBAMTA2dvZDAeFw0xOTEyMjYyMjI5MTRaFw0zOTEyMjEyMjI5MTRa
# MBgxFjAUBgNVBAMTDURhdmlkIEt1YmVsa2EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDClmZJB+vLBNeC9nHtVnNDR0BwPYd4PY+gMAKguycRavMNfsBq
# v0X5nobE/PBH/F5wwHHXYCYw7CqHLd93xLo9xSzxeOMVZvAoxxh3MtLSW6ljLYYw
# 40azaTOd1Geio47FJFIWjNca0hBFBrp/bpNmBUiXjZTXC/fcqGzSMnfB8zhGF1NC
# m9bKwzzhQN39mvHsJCUuw4Y7WSN4eF7445tznnptAIYV91cu4gBHVGHebiNCRA9X
# s9yBPXfA5aDxteAOEdMiALt6T/iEKi5Y/3bcca2MPaoO2N4UBttpaa71QLJuJy8v
# mS7J/x69jPKAy6GxKcyPralTCpYZP+Ny3GQlAgMBAAGjggJxMIICbTA+BgkrBgEE
# AYI3FQcEMTAvBicrBgEEAYI3FQiC69dxgsC2J4GdlwiB9vE6hLrhc4EMgqDXS4ak
# szsCAWUCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwCwYDVR0PBAQDAgeAMBsGCSsG
# AQQBgjcVCgQOMAwwCgYIKwYBBQUHAwMwHQYDVR0OBBYEFFxbLr96KqbNM28qgUFO
# zlDF6OHoMB8GA1UdIwQYMBaAFOnDw0HoRV9Yspg5xBYu0pfyshkUMIHJBgNVHR8E
# gcEwgb4wgbuggbiggbWGgbJsZGFwOi8vL0NOPWdvZCxDTj1kYzUsQ049Q0RQLENO
# PVB1YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3Vy
# YXRpb24sREM9a2lya2xhbmQsREM9a3ViZWxrYSxEQz1vcmc/Y2VydGlmaWNhdGVS
# ZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNzPWNSTERpc3RyaWJ1dGlvblBv
# aW50MIHBBggrBgEFBQcBAQSBtDCBsTCBrgYIKwYBBQUHMAKGgaFsZGFwOi8vL0NO
# PWdvZCxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049U2Vydmlj
# ZXMsQ049Q29uZmlndXJhdGlvbixEQz1raXJrbGFuZCxEQz1rdWJlbGthLERDPW9y
# Zz9jQUNlcnRpZmljYXRlP2Jhc2U/b2JqZWN0Q2xhc3M9Y2VydGlmaWNhdGlvbkF1
# dGhvcml0eTAcBgNVHREEFTATgRFkYXZpZEBrdWJlbGthLmNvbTANBgkqhkiG9w0B
# AQsFAAOCAgEAd3dIoAhEkFVhNMwSOk1ANilSvTAaAX67KPwraPvC7UfNszX7hcRr
# h4I8TsZSvhtPNP6nMobnHyLFdXEJRQzY6i9FEwqdH7V+GhAdSnayuSiJIfYtExgo
# SmNdtyBhka6s8y/4VBaYbhq5bm6YQFMzH+k9+YbnQgENoJ4NumQ2KU/qY/s814G4
# yJ0lO5AlD1PZ/nnYL5JVF3e90LLWbZSYOP7xfKf+CVQpe+FtujS7EDID/s5MP1Pk
# 5geEGT0kOQxbyzjt/vRkm41bvhcQfyxu2mbAjY8MCYJ6EX4ekge2TOBsC3Z+er0i
# rCfKnbfacGNj07AdA8HiV3yLQht7KgU3/hW49J7s3mLbTxZn1Uk5U5CCSJpUw+JV
# wlV2sI/0jTb4ET8h2K3LtF7l+C3brPi/c/c7kd9vlsk3uZtmlRzTi5d/Aj/83+oq
# NVjQUd3TCUFBOXjycK06Ku8e91weLBrHpex1rf09dUE2GbWNlrxRvnUCn6KNg2WW
# EREK089DfGtPF6SAfs2GV1A7UOL3rllNbaIIg/3CUx7Wt7usmCKYnEB/tDorqGZP
# a0A1P2/BT7M4niJogBBB9eMoEtdfsFr26qb8NQR8CA4OKF84ReIjb1AR7yA4HoOU
# Oy5MiYVVVnP0jpneeKeUXNqyMBQ8f9ayz9oHdg5uPyb9IqrllD76PokwggZqMIIF
# UqADAgECAhADAZoCOv9YsWvW1ermF/BmMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNV
# BAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdp
# Y2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0EtMTAeFw0x
# NDEwMjIwMDAwMDBaFw0yNDEwMjIwMDAwMDBaMEcxCzAJBgNVBAYTAlVTMREwDwYD
# VQQKEwhEaWdpQ2VydDElMCMGA1UEAxMcRGlnaUNlcnQgVGltZXN0YW1wIFJlc3Bv
# bmRlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKNkXfx8s+CCNeDg
# 9sYq5kl1O8xu4FOpnx9kWeZ8a39rjJ1V+JLjntVaY1sCSVDZg85vZu7dy4XpX6X5
# 1Id0iEQ7Gcnl9ZGfxhQ5rCTqqEsskYnMXij0ZLZQt/USs3OWCmejvmGfrvP9Enh1
# DqZbFP1FI46GRFV9GIYFjFWHeUhG98oOjafeTl/iqLYtWQJhiGFyGGi5uHzu5uc0
# LzF3gTAfuzYBje8n4/ea8EwxZI3j6/oZh6h+z+yMDDZbesF6uHjHyQYuRhDIjegE
# YNu8c3T6Ttj+qkDxss5wRoPp2kChWTrZFQlXmVYwk/PJYczQCMxr7GJCkawCwO+k
# 8IkRj3cCAwEAAaOCAzUwggMxMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAA
# MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMIIBvwYDVR0gBIIBtjCCAbIwggGhBglg
# hkgBhv1sBwEwggGSMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j
# b20vQ1BTMIIBZAYIKwYBBQUHAgIwggFWHoIBUgBBAG4AeQAgAHUAcwBlACAAbwBm
# ACAAdABoAGkAcwAgAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAGMAbwBuAHMAdABp
# AHQAdQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEAbgBjAGUAIABvAGYAIAB0AGgAZQAg
# AEQAaQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMAUABTACAAYQBuAGQAIAB0AGgAZQAg
# AFIAZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkAIABBAGcAcgBlAGUAbQBlAG4AdAAg
# AHcAaABpAGMAaAAgAGwAaQBtAGkAdAAgAGwAaQBhAGIAaQBsAGkAdAB5ACAAYQBu
# AGQAIABhAHIAZQAgAGkAbgBjAG8AcgBwAG8AcgBhAHQAZQBkACAAaABlAHIAZQBp
# AG4AIABiAHkAIAByAGUAZgBlAHIAZQBuAGMAZQAuMAsGCWCGSAGG/WwDFTAfBgNV
# HSMEGDAWgBQVABIrE5iymQftHt+ivlcNK2cCzTAdBgNVHQ4EFgQUYVpNJLZJMp1K
# Knkag0v0HonByn0wfQYDVR0fBHYwdDA4oDagNIYyaHR0cDovL2NybDMuZGlnaWNl
# cnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEQ0EtMS5jcmwwOKA2oDSGMmh0dHA6Ly9j
# cmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRENBLTEuY3JsMHcGCCsG
# AQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t
# MEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNl
# cnRBc3N1cmVkSURDQS0xLmNydDANBgkqhkiG9w0BAQUFAAOCAQEAnSV+GzNNsiaB
# XJuGziMgD4CH5Yj//7HUaiwx7ToXGXEXzakbvFoWOQCd42yE5FpA+94GAYw3+pux
# nSR+/iCkV61bt5qwYCbqaVchXTQvH3Gwg5QZBWs1kBCge5fH9j/n4hFBpr1i2fAn
# PTgdKG86Ugnw7HBi02JLsOBzppLA044x2C/jbRcTBu7kA7YUq/OPQ6dxnSHdFMoV
# XZJB2vkPgdGZdA0mxA5/G7X1oPHGdwYoFenYk+VVFvC7Cqsc21xIJ2bIo4sKHOWV
# 2q7ELlmgYd3a822iYemKC23sEhi991VUQAOSK2vCUcIKSK+w1G7g9BQKOhvjjz3K
# r2qNe9zYRDCCBs0wggW1oAMCAQICEAb9+QOWA63qAArrPye7uhswDQYJKoZIhvcN
# AQEFBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcG
# A1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJl
# ZCBJRCBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTIxMTExMDAwMDAwMFowYjEL
# MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
# LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgQXNzdXJlZCBJRCBDQS0x
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6IItmfnKwkKVpYBzQHDS
# nlZUXKnE0kEGj8kz/E1FkVyBn+0snPgWWd+etSQVwpi5tHdJ3InECtqvy15r7a2w
# cTHrzzpADEZNk+yLejYIA6sMNP4YSYL+x8cxSIB8HqIPkg5QycaH6zY/2DDD/6b3
# +6LNb3Mj/qxWBZDwMiEWicZwiPkFl32jx0PdAug7Pe2xQaPtP77blUjE7h6z8rwM
# K5nQxl0SQoHhg26Ccz8mSxSQrllmCsSNvtLOBq6thG9IhJtPQLnxTPKvmPv2zkBd
# XPao8S+v7Iki8msYZbHBc63X8djPHgp0XEK4aH631XcKJ1Z8D2KkPzIUYJX9BwSi
# CQIDAQABo4IDejCCA3YwDgYDVR0PAQH/BAQDAgGGMDsGA1UdJQQ0MDIGCCsGAQUF
# BwMBBggrBgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCDCCAdIG
# A1UdIASCAckwggHFMIIBtAYKYIZIAYb9bAABBDCCAaQwOgYIKwYBBQUHAgEWLmh0
# dHA6Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5odG0wggFk
# BggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBz
# ACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBz
# ACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBD
# AGUAcgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBp
# AG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBo
# ACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBl
# ACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAg
# AHIAZQBmAGUAcgBlAG4AYwBlAC4wCwYJYIZIAYb9bAMVMBIGA1UdEwEB/wQIMAYB
# Af8CAQAweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k
# aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4
# oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv
# b3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dEFzc3VyZWRJRFJvb3RDQS5jcmwwHQYDVR0OBBYEFBUAEisTmLKZB+0e36K+Vw0r
# ZwLNMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEB
# BQUAA4IBAQBGUD7Jtygkpzgdtlspr1LPUukxR6tWXHvVDQtBs+/sdR90OPKyXGGi
# nJXDUOSCuSPRujqGcq04eKx1XRcXNHJHhZRW0eu7NoR3zCSl8wQZVann4+erYs37
# iy2QwsDStZS9Xk+xBdIOPRqpFFumhjFiqKgz5Js5p8T1zh14dpQlc+Qqq8+cdkvt
# X8JLFuRLcEwAiR78xXm8TBJX/l/hHrwCXaj++wc4Tw3GXZG5D2dFzdaD7eeSDY2x
# aYxP+1ngIw/Sqq4AfO6cQg7PkdcntxbuD8O9fAqg7iwIVYUiuOsYGk38KiGtSTGD
# R5V3cdyxG0tLHBCcdxTBnU8vWpUIKRAmMYID2TCCA9UCAQEwJTAOMQwwCgYDVQQD
# EwNnb2QCEyIAAAM+vsj6Ki7YVuwAAgAAAz4wCQYFKw4DAhoFAKB4MBgGCisGAQQB
# gjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYK
# KwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFIZot2Bk
# 28LYdjQCrxNrX68UJM8PMA0GCSqGSIb3DQEBAQUABIIBAMD0JmSGm+IxGOqkcyeY
# vRqx/3o/a6X2itUqSHkG7q1ZjsUlYDZjlg5YqCVK6vyJQUZK2cE6t/2+QQmxrlA9
# 3e4+PdQQ+L2RR9MP7DnOpPvcK7kUsirGmsl+rF9+J7MBF3T3vi3jAV05fNWuaa6Z
# k7O6atutv42dCKXIjKmctNO9jQFaORo/t0xx485egbD43VGgIQZ8xJAIH7zRHL37
# 1yr0PC5YUo2cl+ZxMDZnFwp75HfCww2Wcdgzy67K3pOQ88z2hgUMFwTOaKlXcRe+
# BHbHBi2XxJWE++DiUfh6Qm5kLGfaB/o9zHtXEfbw0p07yqFAlUuWV44t2ARTIVow
# CByhggIPMIICCwYJKoZIhvcNAQkGMYIB/DCCAfgCAQEwdjBiMQswCQYDVQQGEwJV
# UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
# Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBBc3N1cmVkIElEIENBLTECEAMBmgI6/1ix
# a9bV6uYX8GYwCQYFKw4DAhoFAKBdMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEw
# HAYJKoZIhvcNAQkFMQ8XDTIwMDcwOTIyMjIwOFowIwYJKoZIhvcNAQkEMRYEFJs2
# UAxYSIAkFDogWUobVLA0tz9kMA0GCSqGSIb3DQEBAQUABIIBAGTX4bJpWnqePMK3
# YwAvwjDQUjOq3Rt7YPxZrJmIT92ig9MHyJdV9X8NWTPSk2G/FUZJ+/LtM/GdHm7P
# 1oNjL3DWEVCptG08Rhtu0ITw8k5ySDMmzWQgnL9thYdGFR0CQfcV7o9dxdhBvUFA
# N6tCF/zKWo/PX/nDeQI5zvsjlLF8xaW0lOsh9pr3M0SZjBQ27W3SVNh15DCA20Dj
# EKRoaZxSlgkiTKkIYP9rrbWczqEcSUMB+stf8sNLjDWnyymF4FLUliNC1VjsQeOf
# k6U/G103TsIAzK57OjB4hRVKf/jvQjgpjHugc1wd8p3pvYRomtnwrYT7S52j+qbx
# vdRNeaw=
# SIG # End signature block