Status: Reviewed Stack: PowerShell → SQL / stored procs → Blazor Depends on: SQL-CLI-01 helpful (when to prefer sqlcmd vs Invoke-Sqlcmd); SQL-CLI-02 for bulk file loads Goal: Install and use the SqlServer module from the command line for queries, backups, and ops that feed PS → SQL → Blazor — and know when Invoke-Sqlcmd wins over sqlcmd/bcp.
Rick’s loop: AD/Windows/PowerShell → SQL stored procs → Blazor Server. Pick the client by object shape, volume, and whether you need an exit code.
| Need | SqlServer module (Invoke-Sqlcmd, backup/restore, Read/Write-SqlTableData) |
sqlcmd / go-sqlcmd (SQL-CLI-01) | bcp (SQL-CLI-02) | SMO / .NET SqlBulkCopy |
|---|---|---|---|---|
PS objects → ConvertTo-Json → Blazor / API |
✅ Best | Text only | Files only | Possible in app code |
Call procs / deploy .sql from PS scripts |
✅ Invoke-Sqlcmd |
✅ Exit-code friendly | ❌ | Overkill |
Agent / CI that must fail on $LASTEXITCODE |
Weak (exceptions) | ✅ -b |
✅ $LASTEXITCODE |
App-controlled |
:r includes, :!!, interactive editing |
❌ Partial sqlcmd dialect | ✅ Full (classic/go) | N/A | N/A |
| Full / diff / log backup & restore from CLI | ✅ Backup-SqlDatabase / Restore-SqlDatabase |
Via T-SQL only | ❌ | SMO Backup class |
| Multi‑GB CSV / native dump in/out | ❌ Don’t loop rows | Verify only | ✅ Best client-side | ✅ In-process |
| Small/medium DataTable into existing table | ✅ Write-SqlTableData |
❌ | Overkill | ✅ Prefer in Blazor |
| Azure SQL + Entra token | ✅ -AccessToken |
✅ go-sqlcmd Entra | Limited | Connection string / token |
| Inventory loop many servers, collect objects | ✅ Pipe ServerInstance |
✅ + parse text | ❌ | Custom |
Rule of thumb
Invoke-Sqlcmd (this doc).:setvar files → SQL-CLI-01.Write-SqlTableData only for modest PS-held tables.IDataReader/DataTable → SqlBulkCopy in the Blazor worker, not PowerShell.Use SqlServer from the PowerShell Gallery — not legacy SQLPS. SSMS (17+) does not ship the module; Agent still auto-loads SQLPS unless you opt out.
# Current user (no elevation) — fine for jump-box / laptop
Install-Module -Name SqlServer -Scope CurrentUser -AllowClobber
# All users (elevated) — needed for SQL Agent jobs under service account
Install-Module -Name SqlServer -Scope AllUsers -AllowClobber
Get-Module SqlServer -ListAvailable | Select-Object Name, Version, Path
Import-Module SqlServer # pin in scripts; don't rely on auto-load with SQLPS present
| Topic | Practice |
|---|---|
| Gallery version | As of research: 22.4.x on Gallery (22.3.0 widely cached). Always Find-Module SqlServer before pinning. |
| PS edition | Module requires 5.1+; ships Desktop + Core. PS 7+ uses the coreclr payload. |
| CurrentUser vs AllUsers | CurrentUser = your profile only; Agent / other users won’t see it. Prefer AllUsers on shared DBA hosts. |
| AllowClobber | Required when SQLPS (or older SqlServer) already exported the same cmdlet names. |
| Version pinning | Install-Module SqlServer -RequiredVersion 22.4.5.1 -AllowClobber then Import-Module SqlServer -RequiredVersion 22.4.5.1 |
| Offline | Save-Module SqlServer -Path \\share\psmods → copy folder to %ProgramFiles%\WindowsPowerShell\Modules\SqlServer (works for PS5/PS7). |
| Update | Update-Module SqlServer -AllowClobber leaves old versions side-by-side; prune with Uninstall-Module -RequiredVersion … |
| SQLPS warning | SQLPS ships with the engine, is not updated, and collides on names. Scripts: Import-Module SqlServer. Agent (2019+): first lines #NOSQLPS then Import-Module SqlServer. |
| Prerelease | Find-Module SqlServer -AllowPrerelease / Install-Module … -AllowPrerelease only when you intend to. |
# Sanity
Get-Command Invoke-Sqlcmd | Format-List Source, Version
# Source must be SqlServer, not SQLPS
| Mode | How | Notes |
|---|---|---|
| Windows Integrated | Omit user/password; run as domain account | Default for on-prem AD. Prefer this. |
| SQL auth | -Credential (Get-Credential) or New-Object PSCredential |
Prefer SecureString / PSCredential. Avoid -Password plaintext (visible in history / script). |
| Entra / Azure SQL | -AccessToken $token |
Token audience https://database.windows.net. Do not mix with Username/Password/Credential. |
| Full control | -ConnectionString '…' |
Less-common props (AE, ApplicationIntent, etc.). |
| TLS | -Encrypt Mandatory\|Optional\|Strict, -TrustServerCertificate, -HostNameInCertificate |
New in v22. Lab self-signed: -TrustServerCertificate. Prod: FQDN + valid cert; use -HostNameInCertificate when connecting by short name under Force Encryption. |
# Windows Integrated (lab often needs TrustServerCertificate under v22+ driver defaults)
Invoke-Sqlcmd -ServerInstance 'SQL01' -Database master `
-Query 'SELECT @@VERSION AS v' -TrustServerCertificate
# SQL auth via PSCredential (never -Password on cmdline in shared sessions)
$cred = Get-Credential -UserName 'deploy_login' -Message 'SQL auth'
Invoke-Sqlcmd -ServerInstance 'SQL01' -Database AppDb -Credential $cred `
-Query 'SELECT SUSER_SNAME() AS me' -TrustServerCertificate
# ConnectionString
Invoke-Sqlcmd -ConnectionString 'Data Source=SQL01;Initial Catalog=AppDb;Integrated Security=True;Encrypt=Mandatory;TrustServerCertificate=True' `
-Query 'SELECT DB_NAME() AS db'
# Azure SQL + Entra user token (Az.Accounts)
Import-Module Az.Accounts
Connect-AzAccount
$token = (Get-AzAccessToken -ResourceUrl 'https://database.windows.net').Token # string or SecureString — both OK on modern SqlServer
Invoke-Sqlcmd -ServerInstance 'myserver.database.windows.net' -Database mydb `
-AccessToken $token -Query 'SELECT SYSTEM_USER AS me'
Azure / Entra notes
Connect-AzAccount -Identity), then -AccessToken.Get-AzAccessToken may return SecureString for .Token; Invoke-Sqlcmd accepts string or SecureString.Skip the AS/cube/Always Encrypted marketing surface. These are the CLI workhorses (all still in Gallery 22.x):
| Cmdlet | Use |
|---|---|
Invoke-Sqlcmd |
Queries, procs, deploy scripts; returns DataRows/DataTables/DataSet |
Backup-SqlDatabase |
Full / diff / log / file backups (SMO-backed) |
Restore-SqlDatabase |
Restore from .bak / log chain |
Read-SqlTableData |
Read table/view slice (-TopN, -ColumnName, -OutputAs) |
Write-SqlTableData |
Append PS objects / DataTable to a table; -Force can create missing objects (schema inferred — often NVARCHAR(MAX)) |
Get-SqlInstance |
SMO instance object (version / product level) |
Get-SqlDatabase |
Database objects for piping / inspection |
Import-Module SqlServer
Get-SqlInstance -ServerInstance 'SQL01' -TrustServerCertificate |
Select-Object InstanceName, VersionString, Edition
Get-SqlDatabase -ServerInstance 'SQL01' -TrustServerCertificate |
Where-Object Status -eq 'Normal' |
Select-Object Name, Size, RecoveryModel
Backup-SqlDatabase -ServerInstance 'SQL01' -Database 'AppDb' `
-BackupFile '\\backup\SQL01\AppDb_$(Get-Date -Format yyyyMMdd_HHmm).bak' `
-TrustServerCertificate
# Read / write table data (still current cmdlets)
Read-SqlTableData -ServerInstance 'SQL01' -DatabaseName AppDb -SchemaName dbo `
-TableName Customers -TopN 100 -TrustServerCertificate
$rows = @(
[pscustomobject]@{ Sku = 'A1'; Qty = 3 }
[pscustomobject]@{ Sku = 'B2'; Qty = 1 }
)
Write-SqlTableData -ServerInstance 'SQL01' -DatabaseName AppDb -SchemaName dbo `
-TableName StagingSku -InputData $rows -TrustServerCertificate
Backup-SqlDatabase defaults to full backup; set -BackupAction Database|Log|Files as needed. Prefer UNC/share paths the SQL service account can write.
-Query vs -InputFile# Inline
Invoke-Sqlcmd -ServerInstance SQL01 -Database AppDb -Query 'EXEC dbo.usp_RefreshInventory' -TrustServerCertificate
# File (UTF-8). Supports GO batches + many sqlcmd scripting bits — NOT full sqlcmd.exe
Invoke-Sqlcmd -ServerInstance SQL01 -Database AppDb -InputFile 'C:\deploy\AppDb_procs.sql' `
-TrustServerCertificate -AbortOnError
Not supported (unlike sqlcmd.exe — see SQL-CLI-01): :!!, :connect, :error, :out, :ed, :list, :listvar, :reset, :perftrace, :serverlist. Do not expect :r include chains to behave like classic sqlcmd for complex deploy packs — prefer pre-merged scripts or sqlcmd/go-sqlcmd for those.
-Variable# Array form (v21+; values trimmed — don't rely on leading/trailing spaces)
$vars = "DbName='AppDb'", "Env='PROD'"
Invoke-Sqlcmd -Query "SELECT `$(DbName) AS db, `$(Env) AS env" -Variable $vars
# Hashtable form — v22+ only (preferred)
Invoke-Sqlcmd -Query "SELECT `$(DbName) AS db" -Variable @{ DbName = 'AppDb' }
Escape $ in double-quoted PS strings with ` so PowerShell doesn’t expand them before sqlcmd variables.
-OutputAs / -As| Value | Shape | Typical use |
|---|---|---|
| DataRows (default) | Collection of DataRow |
Quick pipeline / ConvertTo-Json |
| DataTables | DataTable[] (multi-result sets) |
Multiple SELECTs / procs |
| DataSet | DataSet |
Hand off to .NET / multi-table |
$rows = Invoke-Sqlcmd -ServerInstance SQL01 -Database AppDb `
-Query 'EXEC dbo.usp_ListOpenOrders @Top = 50' -OutputAs DataRows -TrustServerCertificate
$tables = Invoke-Sqlcmd -ServerInstance SQL01 -Database AppDb `
-Query 'SELECT 1 AS a; SELECT GETDATE() AS t' -As DataTables -TrustServerCertificate
| Parameter | Why |
|---|---|
-QueryTimeout |
Seconds; unset = no query timeout. Set for procs that must die (e.g. 120). |
-ConnectionTimeout |
Connect wait (0–65534). |
-MaxCharLength |
Default 4000; raise for big nvarchar(max) JSON columns feeding Blazor. |
-MaxBinaryLength |
Default 1024. |
-AbortOnError |
Stop on error; severity mapping to ERRORLEVEL-style behavior. |
-OutputSqlErrors $true |
Surface SQL errors (proc name / line) in output. |
-IncludeSqlUserErrors |
Closer to sqlcmd default for user script errors. |
-ErrorAction Stop |
Turn terminating failures into catchable exceptions for PS control flow. |
-StatisticsVariable stats |
Capture ExecutionTime, IduRows, etc. |
try {
$rows = Invoke-Sqlcmd -ServerInstance SQL01 -Database AppDb `
-Query 'EXEC dbo.usp_BuildBlazorSnapshot' `
-QueryTimeout 180 -MaxCharLength 1000000 `
-OutputAs DataRows -OutputSqlErrors $true -AbortOnError `
-TrustServerCertificate -ErrorAction Stop
} catch {
Write-Error "SQL failed: $($_.Exception.Message)"
throw
}
$rows = Invoke-Sqlcmd -ServerInstance SQL01 -Database AppDb `
-Query 'EXEC dbo.usp_GetDashboardFeed' -OutputAs DataRows `
-MaxCharLength 1000000 -TrustServerCertificate
# DataRow → note: ConvertTo-Json on raw DataRow can be noisy; project first
$payload = $rows | Select-Object * | ConvertTo-Json -Depth 5 -Compress
[System.IO.File]::WriteAllText('C:\share\blazor\dashboard.json', $payload)
For Blazor Server, prefer stored procs that return the exact columns the UI needs — keep PowerShell as orchestration, not business logic.
Import-Module SqlServer
$out = 'C:\data\blazor\open-orders.json'
$rows = Invoke-Sqlcmd -ServerInstance SQL01 -Database AppDb `
-Query 'EXEC dbo.usp_OpenOrders' -OutputAs DataRows `
-MaxCharLength 500000 -TrustServerCertificate
($rows | Select-Object *) | ConvertTo-Json -Depth 4 |
Set-Content -Path $out -Encoding utf8
.sql deploy packImport-Module SqlServer
Invoke-Sqlcmd -ServerInstance SQL01 -Database AppDb `
-InputFile 'C:\deploy\2026-09-12_procs.sql' `
-AbortOnError -OutputSqlErrors $true -TrustServerCertificate -ErrorAction Stop
# Complex :r trees → use sqlcmd/go-sqlcmd (SQL-CLI-01) instead
Import-Module SqlServer
$db = 'AppDb'
$file = "\\backup\SQL01\${db}_$(Get-Date -Format 'yyyyMMdd_HHmmss').bak"
Backup-SqlDatabase -ServerInstance SQL01 -Database $db -BackupFile $file `
-CompressionOption On -TrustServerCertificate
Write-Host "Backed up to $file"
Import-Module SqlServer
$servers = Get-Content 'C:\config\sql-servers.txt' # one host\instance per line
$report = foreach ($s in $servers) {
try {
Invoke-Sqlcmd -ServerInstance $s -Database master -TrustServerCertificate -ErrorAction Stop `
-Query @"
SELECT @@SERVERNAME AS ServerName,
(SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process=1) AS UserSessions,
GETDATE() AS CapturedUtc;
"@
} catch {
[pscustomobject]@{ ServerName = $s; UserSessions = $null; CapturedUtc = Get-Date; Error = $_.Exception.Message }
}
}
$report | Select-Object * | ConvertTo-Json -Depth 3 |
Set-Content C:\data\blazor\sql-health.json -Encoding utf8
Import-Module SqlServer, Az.Accounts
Connect-AzAccount # or -Identity / SP login in automation
$token = (Get-AzAccessToken -ResourceUrl 'https://database.windows.net').Token
Invoke-Sqlcmd -ServerInstance 'contoso.database.windows.net' -Database AppDb `
-AccessToken $token -Query 'EXEC dbo.usp_RefreshCache' -QueryTimeout 120
Write-SqlTableData vs bcp — when each wins# WIN: Write-SqlTableData — small/medium objects already in PS (config, AD sample, API pull)
Import-Module SqlServer
$users = Get-ADUser -Filter * -SearchBase 'OU=App,DC=contoso,DC=com' |
Select-Object @{n='Sam';e={$_.SamAccountName}}, @{n='Enabled';e={$_.Enabled}}
Write-SqlTableData -ServerInstance SQL01 -DatabaseName AppDb -SchemaName dbo `
-TableName StagingAdUsers -InputData $users -Force -TrustServerCertificate
Invoke-Sqlcmd -ServerInstance SQL01 -Database AppDb `
-Query 'EXEC dbo.usp_MergeAdUsers' -TrustServerCertificate
# WIN: bcp — multi-million-row CSV / native file (see SQL-CLI-02)
# bcp AppDb.dbo.StagingInventory in C:\drop\inv.csv -c -t, -S SQL01 -T -b 10000
# Then: Invoke-Sqlcmd ... 'EXEC dbo.usp_LoadInventory'
| Situation | Winner |
|---|---|
Rows already PSCustomObject / DataTable in memory, thousands not millions |
Write-SqlTableData |
| Flat file on disk, need speed / format file / native | bcp (SQL-CLI-02) |
| Blazor/worker already in .NET with a reader | SqlBulkCopy |
| Need verify counts after load | Invoke-Sqlcmd or sqlcmd |
Import-Module SqlServer
Restore-SqlDatabase -ServerInstance SQLDEV -Database AppDb_Copy `
-BackupFile '\\backup\SQL01\AppDb_20260912.bak' `
-ReplaceDatabase -TrustServerCertificate
Import-Module SqlServer
$top = 25
$rows = Invoke-Sqlcmd -ServerInstance SQL01 -Database AppDb -TrustServerCertificate `
-Variable @{ TopN = "$top" } `
-Query 'EXEC dbo.usp_TopCustomers @Take = $(TopN)' `
-OutputAs DataRows
$rows | Select-Object * | ConvertTo-Json -Depth 3
| Gotcha | Fix |
|---|---|
| SQLPS vs SqlServer clash | Auto-import may bind the wrong Invoke-Sqlcmd (no -TrustServerCertificate). Always Import-Module SqlServer; Agent: #NOSQLPS. |
| SMO assembly load / wrong CLR | Mixing SSMS-era SQLPS, side-by-side module versions, or PS5 vs PS7 sessions can load conflicting SMO. One session → one module version. Prefer PS 7+ Core build on modern hosts. |
Invoke-Sqlcmd ≠ sqlcmd.exe |
No :!!, :connect, :out, etc. GO works; interactive sqlcmd features do not. Heavy `:rg deploy trees → SQL-CLI-01. |
-Variable syntax by version |
Array "Name='Val'" everywhere; Hashtable only in v22+. Array values are trimmed. |
| Encrypt defaults (v22+) | -Encrypt / -TrustServerCertificate added in v22. Learn docs disagree slightly: Invoke-Sqlcmd page says Encrypt default Mandatory; several other SqlServer cmdlets document v22 default Optional → Mandatory in v23+, and TrustServerCertificate default $true in v22 → $false in v23+. Pin module version and set -Encrypt / -TrustServerCertificate explicitly in scripts. |
-TrustServerCertificate missing |
You’re on SQLPS or SqlServer < 22. Upgrade module. |
| Credential in PSReadLine / history | Never -Password 'secret' on the interactive line. Use Get-Credential, SecretManagement, or CI secret stores. Clear history if you slipped: wipe relevant ConsoleHost_history.txt entries. |
-MaxCharLength silent truncate |
JSON/nvarchar(max) columns cut at 4000 unless raised — Blazor feeds look “mysteriously short”. |
-QueryTimeout unset |
Queries never time out — hung proc = hung script. Set it in automation. |
Write-SqlTableData -Force schema |
Auto-created columns often NVARCHAR(MAX) — fine for staging, bad for prod tables. Create tables in SQL first. |
| Multi-result display quirk | Without -OutputAs, later result sets with different columns may not display like the first. Use -As DataTables. |
| CurrentUser module invisible to Agent | Install AllUsers (or under the Agent account profile) or Agent won’t find SqlServer after #NOSQLPS. |
:r / :setvar, when text CLI beats objects.EXEC dbo.usp_*.