nl.nlsw.SQLite.psm1

# __ _ ____ _ _ _ _ ____ ____ ____ ____ ____ ___ _ _ ____ ____ ____
# | \| |=== |/\| |___ | |--- |=== ==== [__] |--- | |/\| |--| |--< |===
#
# @file nl.nlsw.SQLite.psm1
# @date 2022-09-08
#requires -version 5

class SQLite {
    static [string] $PackageName = "Stub.System.Data.SQLite.Core.NetStandard"

    # static constructor
    static SQLite() {
        # run this only once
        [SQLite]::Install()
    }

    # Function with dummy behavior that can be called to trigger
    # the one-time class construction.
    static [void] Check() {
    }

    # Make sure the System.Data.SQLite .NET Standard 2.0 library is loaded
    # @see https://stackoverflow.com/questions/39257572/loading-assemblies-from-nuget-packages
    # @see https://stackoverflow.com/questions/69118045/sqlkata-with-sqlite-minimal-example-powershell/69126680
    static [void] Install() {
        # First check if the NuGet package provider is available
        $nugetPP = Get-PackageProvider "NuGet" -ErrorAction SilentlyContinue
        if (!$nugetPP) {
            write-host ("{0,16} {1}" -f "installing","NuGet Package Provider for CurrentUser")
            # install the NuGet package provider for the current user
            Install-PackageProvider "NuGet" -verbose -Scope CurrentUser
            # register the NuGet package source
            Register-PackageSource -ProviderName NuGet -Name nuget.org -Location https://www.nuget.org/api/v2
        }
        # Second, check the presence of the SQLite package
        $sqlite = Get-Package $([SQLite]::PackageName) -ErrorAction SilentlyContinue
        if (!$sqlite) {
            write-host ("{0,16} {1}" -f "installing",[SQLite]::PackageName)
            # install the package
            Install-Package $([SQLite]::PackageName) -ProviderName "NuGet" -Scope CurrentUser
            # get the installed package
            $sqlite = Get-Package $([SQLite]::PackageName)
            # make the platform-specific InterOp dll available for Win32 and Win64
            if ([System.Environment]::OSVersion.Platform -eq $([System.PlatformID]::Win32NT)) {
                $sqliteNupkg = get-item $sqlite.Source
                foreach ($platform in "64","86") {
                    $destFolder = [System.IO.DirectoryInfo]::new((Join-Path $($sqliteNupkg.DirectoryName) "lib/netstandard2.0/x$($platform)"))
                    if (!$destFolder.Exists) {
                        # make sure the target folder exists
                        $destFolder = New-Item $destFolder -Force -ItemType Directory
                    }
                    $destFile = [System.IO.FileInfo]::new((Join-Path $destFolder.FullName "SQLite.Interop.dll"))
                    if (!$destFile.Exists) {
                        # copy the InterOp.dll to the location that the managed dll will look for
                        $interop = get-item (Join-Path $($sqliteNupkg.DirectoryName) "runtimes/win-x$($platform)/native/SQLite.Interop.dll")
                        Copy-Item $interop.FullName $destFile.FullName
                        $destFile.Refresh()
                        write-host ("{0,16} {1}" -f "copied",$destFile)
                    }
                }
            }
            else {
                throw [InvalidOperationException]::new(("please install the System.Data.SQLite package manually on operating system {0}" -f $env:OS))
            }
        }
        # Get the NetStandard2.0 dll
        $sqliteNupkg = get-item $sqlite.Source
        $sqlitedll = get-item (Join-Path $sqliteNupkg.DirectoryName "lib/netstandard2.0/System.Data.SQLite.dll")
        Add-Type -Path $sqlitedll
    }
}

<#
.SYNOPSIS
 Get an SQLite database content as System.Data.DataSet.
 
.DESCRIPTION
 Get data from an SQLite database, using the System.Data.SQLite .NET library.
 
.PARAMETER Path
 The file name of the SQLite file(s) to process. May contain wildcards,
 and may be input via the pipeline.
 
.PARAMETER TableName
 The name of the table to get. When left empty (default), all tables are returned.
 
.INPUTS
 System.String
 
.OUTPUTS
 System.Data.DataSet
 
.LINK
 https://system.data.sqlite.org/
 
.NOTES
 This function requires the System.Data.SQLite .NET assembly.
 It will be automatically installed if not present already.
 This installation requires the NuGet Package Provider, which
 in turn will also be installed if not present already.
#>

function Get-SQLiteDataSet {
    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'TableName', Justification="false positive")]
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline = $true)]
        [SupportsWildcards()]
        [string]$Path,

        [Parameter(Mandatory=$false, Position=1)]
        [Alias("table")]
        [string]$TableName
    )
    begin {
        [SQLite]::Check()

        function GetDataTable {
            param([System.Data.SQLite.SQLiteConnection]$con, [string]$sql)
            $table = [System.Data.DataTable]::new()
            try {
                $cmd = [System.Data.SQLite.SQLiteCommand]::new($sql, $con);
                $reader = $cmd.ExecuteReader();
                $table.Load($reader);
            }
            finally {
                if ($cmd) {
                    $cmd.Dispose()
                }
            }
            return ,$table
        }
    }
    process {
        $Path | get-item | where-object { $_ -is [System.IO.FileInfo] } | foreach-object {
            $file = $_
            $dataset = [System.Data.DataSet]::new()
            write-verbose ("{0,16} {1}" -f "reading",$file.FullName)
            try {
                # https://social.technet.microsoft.com/wiki/contents/articles/30562.powershell-accessing-sqlite-databases.aspx
                # https://stackoverflow.com/questions/20256043/is-there-easy-method-to-read-all-tables-from-sqlite-database-to-dataset-object
                $con = [System.Data.SQLite.SQLiteConnection]::new(("Data Source={0}" -f $file.FullName))
                $con.Open()
                if ($TableName) {
                    $table = GetDataTable $con ("SELECT * FROM '{0}'" -f $TableName)
                    $dataset.Tables.Add($table)
                    write-verbose ("{0,16} {1} ({2} rows)" -f "table",$TableName,$table.Rows.Count)
                }
                else {
                    # read the names of the tables
                    $namesTable = GetDataTable $con "SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY 1"
                    $names = [System.Collections.Generic.List[string]]::new();
                    foreach ($row in $namesTable.Rows) {
                        $tableName = $row.ItemArray[0].ToString()
                        $names.Add($tableName);
                        $table = GetDataTable $con ("SELECT * FROM '{0}'" -f $tableName)
                        $dataset.Tables.Add($table)
                        write-verbose ("{0,16} {1} ({2} rows)" -f $names.Count,$tableName,$table.Rows.Count)
                    }
                }
            }
            #catch {
            # # something went wrong ??
            # Write-Error $_.Exception.Message
            #}
            finally {
                if ($con) {
                    $con.Close();
                }
            }

            # Return the results as a set
            return ,$dataset
        }
    }
    end {
    }
}

<#
.SYNOPSIS
 Execute one or more SQL commands on an SQLite database.
 
.DESCRIPTION
 Edit an SQLite database by executing one or more SQL commands.
 
 This functions uses the System.Data.SQLite .NET library.
 
.PARAMETER Path
 The file name of the SQLite database file to process.
 
.PARAMETER Command
 The SQL command to invoke. May be pipelined.
 
.PARAMETER DataSet
 The dataset to store the results in.
 
.PARAMETER NonQuery
 Return no result data of the executed commands.
 
.PARAMETER SingleTransaction
 Automatically will execute the commands in a single transaction.
 
.INPUTS
 System.String
 System.String[]
 
.OUTPUTS
 System.Data.DataRow
 System.Data.DataTable
 System.Data.DataSet
 
.LINK
 https://system.data.sqlite.org/
 https://zetcode.com/csharp/sqlite/
 
.NOTES
 This function requires the System.Data.SQLite .NET assembly.
 It will be automatically installed if not present already.
 This installation requires the NuGet Package Provider, which
 in turn will also be installed if not present already.
#>

function Invoke-SQLiteCommand {
    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'NonQuery', Justification="false positive")]
    param(
        [Parameter(Mandatory=$true, Position=0)]
        [Alias("database")]
        [string]$Path,

        [Parameter(Mandatory=$true, Position=1, ValueFromPipeline = $true)]
        [Alias("sql")]
        [string[]]$Command,

        [Parameter(Mandatory=$false, Position=2)]
        [System.Data.DataSet]$DataSet,

        [Parameter(Mandatory=$false)]
        [switch]$NonQuery,

        [Parameter(Mandatory=$false)]
        [switch]$SingleTransaction
    )
    begin {
        [SQLite]::Check()

        $file = Get-Item $Path
        $con = [System.Data.SQLite.SQLiteConnection]::new(("Data Source={0}" -f $file.FullName))
        $con.Open()
        $cmd = [System.Data.SQLite.SQLiteCommand]::new($con);
        write-verbose ("{0,16} {1}" -f "opening",$file.FullName)
        #$dataset = [System.Data.DataSet]::new()
        $transaction = if ($SingleTransaction) { $con.BeginTransaction() } else { $null }
    }
    process {
        try {
            $Command | where-object { $_ -is [string] } | foreach-object {
                $cmd.CommandText = $_
                write-verbose ("{0,16} {1}" -f "executing",$cmd.CommandText)
                if ($NonQuery) {
                    $numberOfRows = $cmd.ExecuteNonQuery();
                    write-verbose ("{0,16} rows updated" -f $numberOfRows)
                }
                else {
                    $reader = $cmd.ExecuteReader();
                    $table = [System.Data.DataTable]::new()
                    $table.Load($reader)
                    if ($DataSet) {
                        $DataSet.Tables.Add($table)
                    }
                    else {
                        $table
                    }
                }
            }
        }
        catch {
            if ($transaction) {
                $transaction.Dispose()
                $transaction = $null
            }
            throw
        }
        if ($cmd) {
            $cmd.Dispose()
            $cmd = $null
        }
        if ($con) {
            write-verbose ("{0,16} {1}" -f "closing",$file.FullName)
            $con.Close();
            $con.Dispose();
            $con = $null
        }
        if ($DataSet) {
            $DataSet.Dispose()
            $DataSet = $null
        }
    }
    end {
        if ($transaction) {
            write-verbose ("{0,16} {1}" -f "committing",$file.FullName)
            $transaction.Commit()
        }
        if ($cmd) {
            $cmd.Dispose()
        }
        if ($con) {
            write-verbose ("{0,16} {1}" -f "closing",$file.FullName)
            $con.Close();
            $con.Dispose();
        }
        if ($DataSet) {
            Write-Output $DataSet -NoEnumerate
        }
    }
}

Export-ModuleMember -Function *