AdoSQLiteModule.psm1
# AdoSQLiteModule.psm1 <# Module: AdoSQLiteModule Description: A PowerShell module providing ADO.NET-backed SQLite cmdlets with names similar to PSSQLite, but implemented using ADO.NET (System.Data.SQLite or Microsoft.Data.Sqlite) or sqlite3 CLI fallback. Supported PowerShell versions: 5.1+ (tested) and PowerShell 7+. Exported Cmdlets: - Invoke-AdoSQLiteQuery - Invoke-AdoSQLiteNonQuery - Invoke-AdoSQLiteBulkCopy Usage: See README included in module manifest for install/upload instructions. #> # --- Helper: Load and detect available ADO.NET provider --- function Get-AdoSqliteProvider { <# Returns a hashtable with keys: ProviderName: 'System.Data.SQLite' | 'Microsoft.Data.Sqlite' | 'sqlite3' Assembly: loaded assembly object or $null Types: hashtable mapping 'Connection','Command','Parameter' #> if ($script:__AdoSqliteProvider) { return $script:__AdoSqliteProvider } $prov = @{ ProviderName = $null; Assembly = $null; Types = @{} } $psMajor = $PSVersionTable.PSVersion.Major function TryLoad([string]$simpleName, [string[]]$paths) { try { $a = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq $simpleName } | Select-Object -First 1; if ($a) { return $a } } catch {} try { return [Reflection.Assembly]::Load($simpleName) } catch {} foreach ($p in $paths) { if ($p -and (Test-Path $p)) { try { return [Reflection.Assembly]::LoadFrom($p) } catch {} } } return $null } $candidates = if ($psMajor -le 5) { @('System.Data.SQLite','Microsoft.Data.Sqlite') } else { @('Microsoft.Data.Sqlite','System.Data.SQLite') } foreach ($c in $candidates) { if ($c -eq 'System.Data.SQLite') { $paths = @(Join-Path $PSScriptRoot 'System.Data.SQLite.dll', Join-Path (Split-Path -Parent $PSScriptRoot) 'System.Data.SQLite.dll') $asm = TryLoad -simpleName 'System.Data.SQLite' -paths $paths if ($asm) { $prov.ProviderName = 'System.Data.SQLite' $prov.Assembly = $asm $prov.Types['Connection'] = $asm.GetType('System.Data.SQLite.SQLiteConnection') $prov.Types['Command'] = $asm.GetType('System.Data.SQLite.SQLiteCommand') $prov.Types['Parameter'] = $asm.GetType('System.Data.SQLite.SQLiteParameter') $script:__AdoSqliteProvider = $prov return $prov } } elseif ($c -eq 'Microsoft.Data.Sqlite') { $paths = @(Join-Path $PSScriptRoot 'Microsoft.Data.Sqlite.dll', Join-Path (Split-Path -Parent $PSScriptRoot) 'Microsoft.Data.Sqlite.dll') $asm = TryLoad -simpleName 'Microsoft.Data.Sqlite' -paths $paths if ($asm) { $prov.ProviderName = 'Microsoft.Data.Sqlite' $prov.Assembly = $asm $prov.Types['Connection'] = $asm.GetType('Microsoft.Data.Sqlite.SqliteConnection') $prov.Types['Command'] = $asm.GetType('Microsoft.Data.Sqlite.SqliteCommand') $prov.Types['Parameter'] = $asm.GetType('Microsoft.Data.Sqlite.SqliteParameter') $script:__AdoSqliteProvider = $prov return $prov } } } # sqlite3 CLI fallback if (Get-Command sqlite3 -ErrorAction SilentlyContinue) { $prov.ProviderName = 'sqlite3' $prov.Assembly = $null $prov.Types = @{} $script:__AdoSqliteProvider = $prov return $prov } throw "AdoSQLiteModule: No SQLite ADO.NET provider or sqlite3 CLI found. Install System.Data.SQLite (PS5.1) or Microsoft.Data.Sqlite (PS7+) or ensure sqlite3 is accessible." } function New-AdoSqliteConnectionString { param( [Parameter(Mandatory)][string] $DatabaseFilePath, [string] $Additional = '' ) return "Data Source=$DatabaseFilePath;$Additional" } # --- Core query execution (returns PSObjects by default) --- function Invoke-AdoSQLiteQuery { <# .SYNOPSIS Execute a SQL query and return results. .PARAMETER Database Path to sqlite database file. .PARAMETER Query SQL text. Use named parameters like @name. .PARAMETER Queries Array of SQL statements to run sequentially. .PARAMETER Parameters Hashtable of parameterName -> value. .PARAMETER AsDataTable Return a System.Data.DataTable object. .PARAMETER AsScalar Return single scalar value from first column of first row. .PARAMETER Timeout Command timeout in seconds. #> [CmdletBinding()] param( [Parameter(Mandatory=$true,Position=0)][string] $Database, [Parameter(Position=1)][string] $Query, [Parameter()][string[]] $Queries, [Parameter()][hashtable] $Parameters, [switch] $AsDataTable, [switch] $AsScalar, [int] $Timeout = 30 ) if (-not $Database) { throw 'Invoke-AdoSQLiteQuery: -Database is required.' } $prov = Get-AdoSqliteProvider $returnMode = if ($AsDataTable) { 'DataTable' } elseif ($AsScalar) { 'Scalar' } else { 'PSObject' } function Exec([string] $sql) { if ($prov.ProviderName -eq 'sqlite3') { $out = & sqlite3 -header -csv $Database $sql 2>&1 if ($LASTEXITCODE -ne 0) { throw "sqlite3 error: $out" } if (-not $out) { return @() } $csv = $out -join "`n" $rows = $csv | ConvertFrom-Csv if ($returnMode -eq 'DataTable') { $dt = New-Object System.Data.DataTable foreach ($c in $rows[0].PSObject.Properties.Name) { [void]$dt.Columns.Add($c) } foreach ($r in $rows) { $nr = $dt.NewRow() foreach ($c in $rows[0].PSObject.Properties.Name) { $nr[$c] = $r.$c } $dt.Rows.Add($nr) } return $dt } elseif ($returnMode -eq 'Scalar') { return $rows[0].PSObject.Properties.Value[0] } else { return ,$rows } } else { $connType = $prov.Types['Connection']; $cmdType = $prov.Types['Command']; $paramType = $prov.Types['Parameter'] $cs = New-AdoSqliteConnectionString -DatabaseFilePath $Database $conn = [Activator]::CreateInstance($connType, $cs) try { $conn.Open() $cmd = [Activator]::CreateInstance($cmdType, $sql, $conn) $cmd.CommandTimeout = $Timeout if ($Parameters) { foreach ($k in $Parameters.Keys) { $pname = if ($k -like '@*') { $k } else { '@' + $k } $pval = $Parameters[$k] try { $p = [Activator]::CreateInstance($paramType, $pname, $pval) $cmd.Parameters.Add($p) | Out-Null } catch { try { $cmd.Parameters.AddWithValue($pname,$pval) | Out-Null } catch { $pp = New-Object -TypeName $paramType $pp.ParameterName = $pname; $pp.Value = $pval $cmd.Parameters.Add($pp) | Out-Null } } } } if ($returnMode -eq 'Scalar') { return $cmd.ExecuteScalar() } $reader = $cmd.ExecuteReader() try { $dt = New-Object System.Data.DataTable $dt.Load($reader) if ($returnMode -eq 'DataTable') { return $dt } $out = foreach ($row in $dt.Rows) { $obj = New-Object PSObject foreach ($col in $dt.Columns) { $obj | Add-Member -NotePropertyName $col.ColumnName -NotePropertyValue $row[$col.ColumnName] } $obj } return ,$out } finally { $reader.Close() } } finally { try { $conn.Close() } catch {}; $conn.Dispose() } } } if ($Queries -and $Queries.Count -gt 0) { $results = @() foreach ($q in $Queries) { $results += Exec -sql $q } return $results } elseif ($Query) { return Exec -sql $Query } else { throw 'Invoke-AdoSQLiteQuery: provide -Query or -Queries.' } } # --- NonQuery wrapper (INSERT/UPDATE/DELETE) --- function Invoke-AdoSQLiteNonQuery { [CmdletBinding()] param( [Parameter(Mandatory=$true,Position=0)][string] $Database, [Parameter(Mandatory=$true,Position=1)][string] $Query, [hashtable] $Parameters, [int] $Timeout = 30 ) if (-not $Database) { throw 'Invoke-AdoSQLiteNonQuery: -Database required.' } $prov = Get-AdoSqliteProvider if ($prov.ProviderName -eq 'sqlite3') { $out = & sqlite3 $Database $Query 2>&1 if ($LASTEXITCODE -ne 0) { throw "sqlite3 error: $out" } return $LASTEXITCODE } else { $connType = $prov.Types['Connection']; $cmdType = $prov.Types['Command']; $paramType = $prov.Types['Parameter'] $cs = New-AdoSqliteConnectionString -DatabaseFilePath $Database $conn = [Activator]::CreateInstance($connType, $cs) try { $conn.Open() $cmd = [Activator]::CreateInstance($cmdType, $Query, $conn) $cmd.CommandTimeout = $Timeout if ($Parameters) { foreach ($k in $Parameters.Keys) { $pname = if ($k -like '@*') { $k } else { '@' + $k } $pval = $Parameters[$k] try { $p = [Activator]::CreateInstance($paramType, $pname, $pval); $cmd.Parameters.Add($p) | Out-Null } catch { try { $cmd.Parameters.AddWithValue($pname,$pval) | Out-Null } catch { $pp = New-Object -TypeName $paramType; $pp.ParameterName=$pname; $pp.Value=$pval; $cmd.Parameters.Add($pp)|Out-Null } } } } $affected = $cmd.ExecuteNonQuery() return $affected } finally { try { $conn.Close() } catch {}; $conn.Dispose() } } } # --- Bulk insert using prepared statement + transaction --- function Invoke-AdoSQLiteBulkCopy { <# .SYNOPSIS Bulk-insert rows into a SQLite table using prepared statements and transaction. .PARAMETER Database Path to sqlite DB. .PARAMETER Table Target table name. .PARAMETER DataTable Can be a System.Data.DataTable, an array of PSObjects, or array of hashtables. .PARAMETER Conflict Conflict behaviour: e.g., 'REPLACE' or 'IGNORE' (will use "INSERT OR <Conflict> ..."). .PARAMETER BatchSize Rows per transaction commit. #> [CmdletBinding()] param( [Parameter(Mandatory=$true,Position=0)][string] $Database, [Parameter(Mandatory=$true,Position=1)][string] $Table, [Parameter(Mandatory=$true,Position=2)][object] $DataTable, [string] $Conflict = '', [int] $BatchSize = 1000, [int] $Timeout = 120 ) if (-not $Database) { throw 'Invoke-AdoSQLiteBulkCopy: -Database required.' } if (-not $Table) { throw 'Invoke-AdoSQLiteBulkCopy: -Table required.' } if (-not $DataTable){ throw 'Invoke-AdoSQLiteBulkCopy: -DataTable required.' } # Normalize rows if ($DataTable -is [System.Data.DataTable]) { $columns = $DataTable.Columns | ForEach-Object { $_.ColumnName } $rowsEnum = $DataTable.Rows } else { $arr = @($DataTable) if ($arr.Count -eq 0) { return 0 } $first = $arr[0] if ($first -is [hashtable]) { $columns = $first.Keys } else { $columns = $first | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name } $rowsEnum = $arr } $prov = Get-AdoSqliteProvider if ($prov.ProviderName -eq 'sqlite3') { # CSV import fallback $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName() + '.csv') $table = New-Object System.Data.DataTable foreach ($c in $columns) { $table.Columns.Add($c) | Out-Null } foreach ($r in $rowsEnum) { $nr = $table.NewRow() foreach ($c in $columns) { if ($r -is [System.Data.DataRow]) { $nr[$c] = $r[$c] } elseif ($r -is [hashtable]) { $nr[$c] = if ($r.ContainsKey($c)) { $r[$c] } else { $null } } else { $nr[$c] = $r.$c } } $table.Rows.Add($nr) } $table | Export-Csv -Path $tmp -NoTypeInformation -Encoding UTF8 $cmds = ".mode csv`n.import `"$tmp`" $Table" $out = & sqlite3 $Database $cmds 2>&1 Remove-Item $tmp -ErrorAction SilentlyContinue if ($LASTEXITCODE -ne 0) { throw "sqlite3 import failed: $out" } return $true } else { $connType = $prov.Types['Connection']; $cmdType = $prov.Types['Command']; $paramType = $prov.Types['Parameter'] $cs = New-AdoSqliteConnectionString -DatabaseFilePath $Database $conn = [Activator]::CreateInstance($connType, $cs) try { $conn.Open() $trans = $conn.BeginTransaction() $colList = ($columns -join ', ') $paramList = ($columns | ForEach-Object { '@' + $_ }) -join ', ' $insertSql = if ($Conflict) { "INSERT OR $Conflict INTO $Table ($colList) VALUES ($paramList);" } else { "INSERT INTO $Table ($colList) VALUES ($paramList);" } $cmd = [Activator]::CreateInstance($cmdType, $insertSql, $conn) $cmd.Transaction = $trans $cmd.CommandTimeout = $Timeout # create and reuse parameters $paramMap = @{} foreach ($c in $columns) { $pname = '@' + $c try { $p = [Activator]::CreateInstance($paramType, $pname, $null) } catch { $p = New-Object -TypeName $paramType -ArgumentList $pname, $null } $cmd.Parameters.Add($p) | Out-Null $paramMap[$c] = $p } $count = 0; $rowsAffected = 0 foreach ($r in $rowsEnum) { foreach ($c in $columns) { if ($r -is [System.Data.DataRow]) { $val = $r[$c] } elseif ($r -is [hashtable]) { $val = if ($r.ContainsKey($c)) { $r[$c] } else { $null } } else { $val = $r.$c } if ($null -eq $val) { $paramMap[$c].Value = [System.DBNull]::Value } else { $paramMap[$c].Value = $val } } $rowsAffected += $cmd.ExecuteNonQuery() $count++ if ($count -ge $BatchSize) { $trans.Commit(); $trans = $conn.BeginTransaction(); $cmd.Transaction = $trans; $count = 0 } } $trans.Commit() return $rowsAffected } finally { try { $conn.Close() } catch {}; $conn.Dispose() } } } # Export the public functions function New-AdoSQLiteConnection { [CmdletBinding()] param( [Parameter(Mandatory=$true,Position=0)][string] $Database, [switch] $Open, [int] $Timeout = 30 ) # Returns an opened ADO.NET connection object (or closed if -Open not specified) $prov = Get-AdoSqliteProvider $cs = New-AdoSqliteConnectionString -DatabaseFilePath $Database if ($prov.ProviderName -eq 'sqlite3') { throw 'New-SQLiteConnection: sqlite3 CLI provider does not expose a live connection object.' } $connType = $prov.Types['Connection'] $conn = [Activator]::CreateInstance($connType, $cs) if ($Open) { $conn.Open(); $conn.DefaultCommandTimeout = $Timeout -as [int] } else { } return $conn } function Out-AdoDataTable { <# Convert incoming objects (PSObjects / hashtables / DataRow arrays) into a System.Data.DataTable. Usage: $dt = $collection | Out-DataTable #> [CmdletBinding()] param( [Parameter(ValueFromPipeline=$true)][object] $InputObject, [switch] $InferFromFirst ) begin { $table = New-Object System.Data.DataTable $firstSeen = $false } process { if ($null -eq $InputObject) { return } if ($InputObject -is [System.Data.DataTable]) { $table = $InputObject; $firstSeen = $true; continue } if (-not $firstSeen) { # infer columns from first object if ($InputObject -is [hashtable]) { foreach ($k in $InputObject.Keys) { [void]$table.Columns.Add($k) } } elseif ($InputObject -is [System.Data.DataRow]) { $table = $InputObject.Table.Clone(); $firstSeen = $true } else { $props = $InputObject | Get-Member -MemberType NoteProperty, Property | Select-Object -ExpandProperty Name -Unique foreach ($p in $props) { [void]$table.Columns.Add($p) } } $firstSeen = $true } # add row $row = $table.NewRow() if ($InputObject -is [hashtable]) { foreach ($k in $InputObject.Keys) { $row[$k] = $InputObject[$k] } } elseif ($InputObject -is [System.Data.DataRow]) { $row.ItemArray = $InputObject.ItemArray } else { foreach ($col in $table.Columns) { $row[$col.ColumnName] = $InputObject.$($col.ColumnName) } } $table.Rows.Add($row) } end { return $table } } function Update-AdoSqlite { <# Convenience helper to UPDATE a table using parameters. Parameters: -Database <path> -Table <name> -Set <hashtable> # column->value -Where <string> # WHERE clause (without 'WHERE') optionally containing parameter names like @id -Parameters <hashtable> # extra parameters for WHERE or others Returns: number of rows affected #> [CmdletBinding()] param( [Parameter(Mandatory=$true)][string] $Database, [Parameter(Mandatory=$true)][string] $Table, [Parameter(Mandatory=$true)][hashtable] $Set, [string] $Where, [hashtable] $Parameters, [int] $Timeout = 30 ) if (-not $Set -or $Set.Count -eq 0) { throw 'Update-Sqlite: -Set must contain at least one column/value.' } $setPairs = @() $paramHash = @{} foreach ($k in $Set.Keys) { $pname = '@' + $k $setPairs += "$k = $pname" $paramHash[$k] = $Set[$k] } if ($Parameters) { foreach ($k in $Parameters.Keys) { $paramHash[$k] = $Parameters[$k] } } $setClause = $setPairs -join ', ' $whereClause = if ($Where) { "WHERE $Where" } else { '' } $sql = "UPDATE $Table SET $setClause $whereClause;" # Use existing non-query wrapper return Invoke-AdoSQLiteNonQuery -Database $Database -Query $sql -Parameters $paramHash -Timeout $Timeout } # Export the public functions (include both original and PSSQLite-compatible names) Export-ModuleMember -Function Invoke-AdoSQLiteQuery, Invoke-AdoSQLiteNonQuery, Invoke-AdoSQLiteBulkCopy, New-AdoSQLiteConnection, Out-AdoDataTable, Update-AdoSqlite |