nl.nlsw.SQLite.psm1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# __ _ ____ _ _ _ _ ____ ____ ____ ____ ____ ___ _ _ ____ ____ ____
# | \| |=== |/\| |___ | |--- |=== ==== [__] |--- | |/\| |--| |--< |===
#
# @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 *