Status: Reviewed
Stack: PowerShell → SQL / stored procs → Blazor
Depends on: none (foundation)
Goal: Training expansion of the original lean deep-dive — choose and use sqlcmd vs go-sqlcmd vs Invoke-Sqlcmd correctly, then run the labs and recipes until they are muscle memory for Rick’s AD/Windows/PowerShell → stored procs → Blazor Server loop.
Cross-links (do not duplicate): SQL-CLI-03 Invoke-Sqlcmd / SqlServer module · SQL-CLI-06 auth cheat sheet · SQL-CLI-07 PS→SQL→Blazor patterns
This page owns the CLI client: install, which binary is on PATH, flags, Entra switches, -b / $LASTEXITCODE, :setvar / :r, and paste-ready recipes. Object-shaping lives in CLI-03. Kerberos/double-hop and the full auth matrix live in CLI-06. End-to-end Collect→Stage→Load→EXEC→Blazor orchestration lives in CLI-07.
After this page you can:
-C / -N / -F are required (lab self-signed vs prod certs; SQL Server 2025 encrypt default).-i / -Q / -o, -v / :setvar, GO batches, and :r includes.$LASTEXITCODE as a contract — -b always, check immediately, throw on non-zero.GO inside -Q, password on argv, sqlcmd vars vs PowerShell $().Microsoft ships two sqlcmd variants that share a name:
| Variant | Engine | Typical Windows source | Identity |
|---|---|---|---|
| sqlcmd (ODBC) — “classic” | ODBC Driver for SQL Server | Command Line Utilities, or Client SDK from the engine / CU | sqlcmd -? → Microsoft (R) SQL Server Command Line Tool + Version 16.x / 17.x NT |
| sqlcmd (Go) — go-sqlcmd | go-mssqldb |
winget / Chocolatey / GitHub .msi or .zip |
sqlcmd --version → Version: 1.x.x (current GitHub release as of research: 1.10.0) |
Invoke-Sqlcmd is not either of those. It is a cmdlet in the SqlServer Gallery module. It does not shell out to sqlcmd.exe. Details: SQL-CLI-03.
| Need | Classic sqlcmd (ODBC) | go-sqlcmd | Invoke-Sqlcmd (SqlServer module) |
|---|---|---|---|
| Agent / SSIS / old batch jobs pinned to ODBC tools | ✅ Default — pin the full path | Possible if PATH swapped; test first | Avoid |
| Cross-platform CI (Win / Linux / macOS) | Possible via mssql-tools |
✅ Preferred | ✅ (PS Core + module) |
| Entra ID / Azure SQL (MFA, managed identity, SP) | -G (limited; ODBC version-gated) |
✅ Best (--authentication-method) |
✅ (-AccessToken / connection string) — CLI-03 |
| Interactive / DAC / emergency console | ✅ -A (Windows) |
✅ | ❌ Not interactive |
| Structured rows for PS objects → Blazor / JSON | Text only | Text only | ✅ Returns DataRow / DataTable |
Exit-code driven pipelines ($LASTEXITCODE) |
✅ -b |
✅ -b |
Exceptions / -ErrorAction — not $LASTEXITCODE |
:r includes, :setvar, GO batches as files |
✅ Full | ✅ Mostly compatible | Partial (no :connect, :out, :!!, …) |
Modern “context” UX (sqlcmd create, config) |
❌ | ✅ Extra | ❌ |
| Zero extra module on jump box | Often already present | Install once | Needs Install-Module SqlServer |
On-prem ops / deploy / Agent / loop servers → sqlcmd or go-sqlcmd + -b + $LASTEXITCODE
Need DataRows → JSON / Blazor shaping → Invoke-Sqlcmd (CLI-03)
Azure / Entra-first CLI → go-sqlcmd -G or --authentication-method
Legacy Windows-only Agent PATH → classic ODBC sqlcmd; do not swap PATH blind
Kerberos / double-hop / Encrypt matrix → CLI-06 (this page only shows sqlcmd flags)
Collect → Stage → Load → EXEC → Blazor → CLI-07 (this page only shows the sqlcmd hop)
Rule of thumb: if the next consumer is a file, a log, or a SQL Agent jobstep, use the exe. If the next consumer is a PowerShell object that becomes JSON for Blazor, use Invoke-Sqlcmd.
Get-Command sqlcmd | Format-List Source, Path, Version
where.exe sqlcmd
sqlcmd "-?" # Learn-recommended; works in PS (quote the ?)
sqlcmd --version # go-sqlcmd only — ODBC will error
sqlcmd --help # go-sqlcmd modern subcommands
| Output | You have |
|---|---|
Version: 1.8.2 / 1.10.0 (and --version works) |
go-sqlcmd |
Microsoft (R) SQL Server Command Line Tool + Version 16.0.… NT (or 15/17) |
ODBC sqlcmd |
Both paths from where.exe |
PATH order decides. Go default install dir is C:\Program Files\sqlcmd. ODBC lives under Client SDK\ODBC\<nnn>\Tools\Binn. |
Learn note: installing go-sqlcmd via a package manager puts Go ahead of ODBC on PATH. ODBC is not uninstalled — call it by full path. Close and reopen the shell after install.
Do this on the jump box (or a throwaway VM) before you trust any script.
| Source | Notes |
|---|---|
| Microsoft Command Line Utilities for SQL Server | Official standalone. Learn (sql-server-ver17, researched 2026-09): release 17.0.4055.5 (2026-06-30). Requires the latest Microsoft ODBC Driver for SQL Server. |
| SQL Server engine / CU | Often lands under Client SDK ODBC tools. Build number may differ from the standalone download — expected. |
| Not bundled with modern SSMS | SSMS Query Editor “SQLCMD Mode” uses .NET SqlClient, not sqlcmd.exe. Different defaults. |
Typical locations (version folders vary):
C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\SQLCMD.EXE
C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\180\Tools\Binn\SQLCMD.EXE
Verify:
where.exe sqlcmd
sqlcmd "-?"
# Expect: Microsoft (R) SQL Server Command Line Tool / Version 15+ (Learn: have at least 15.0.4298.1)
# Entra (-G) and Always Encrypted (-g) need ≥ 13.1; interactive Entra needs ODBC sqlcmd ≥ 15.0.1000.34 + ODBC Driver ≥ 17.2
OS floor from Learn: Windows 10 / Windows Server 2016 or later.
Microsoft documents two variants. Go install paths on Windows:
| Method | Command / action |
|---|---|
| winget (preferred) | winget install sqlcmd → winget upgrade sqlcmd |
| Chocolatey | choco install sqlcmd |
| MSI (signed) | sqlcmd-amd64.msi or sqlcmd-arm64.msi from microsoft/go-sqlcmd releases |
| Zip | -windows-amd64.zip or -windows-arm64.zip — extract sqlcmd.exe and put it on PATH (or call full path) |
Default Go install directory: C:\Program Files\sqlcmd.
Not official: dotnet tool install for sqlcmd. That path is SqlPackage, not go-sqlcmd.
Caveat from Learn: package managers may lag; versions newer than 1.6 might not be in every feed. GitHub latest at research time is v1.10.0 (2026-03-03). If you need that build (or a specific older one), download the MSI/zip.
winget install sqlcmd
# new session, then:
sqlcmd --version # expect Version: 1.x.x (want ≥ 1.0.0; prefer current 1.10.x)
sqlcmd --help # modern commands: query, config, create, open, …
sqlcmd "-?" # ODBC-compatible flag list
Linux / macOS exist (apt/yum/brew install sqlcmd, tarballs) but this lab is Windows-first. Cloud Shell ships a sqlcmd already.
# After winget/choco, reopen the host. Then:
where.exe sqlcmd
# If you still need ODBC in Agent:
Get-ChildItem 'C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\*\Tools\Binn\SQLCMD.EXE'
| Situation | Practice |
|---|---|
| Laptop / jump box, Entra + Azure | Let go-sqlcmd win PATH. |
| SQL Agent CmdExec pinned to classic | Store the full ODBC path in the jobstep. Do not rely on the service account’s PATH. |
| Both must coexist | Keep both installed; pick per script. |
Install-Module -Name SqlServer -Scope CurrentUser
Import-Module SqlServer # not the legacy SQLPS module
Get-Command Invoke-Sqlcmd | Format-List Source, Version
SSMS 17+ no longer ships the module. CurrentUser vs AllUsers, #NOSQLPS in Agent, Encrypt flags: SQL-CLI-03.
Flags here. The decision matrix (when Integrated vs SQL vs Entra, plus Kerberos/double-hop, Encrypt across every tool) is SQL-CLI-06. Do not copy that sheet into scripts — link it.
sqlcmd -S sql01.contoso.local -d AppDb -E -Q "SELECT SUSER_SNAME(), HOST_NAME();"
-E = trusted connection. Default on Windows when -U / -P are omitted — you can leave -E off, but scripts should keep it so the intent is obvious.
Do not mix -E with -U / -P. Do not mix -E with -G. Either combination errors.
# Prefer env var over -P on the command line (visible in process lists / PSHistory / Agent logs)
$env:SQLCMDPASSWORD = '***' # scrub after; never commit
sqlcmd -S sql01 -d AppDb -U deploy_user -Q "SELECT 1"
Remove-Item Env:SQLCMDPASSWORD
If -U is set and neither -P nor SQLCMDPASSWORD is set, sqlcmd prompts. Fine interactively; fatal in Agent.
SQLCMDPASSWORD wins over the legacy OSQLPASSWORD. Same pattern for SQLCMDUSER / SQLCMDSERVER.
Azure AD still appears in flags and error text. Microsoft Entra ID is the current name. They are the same thing here.
-G is mostly ODBC-compatible:
| You pass | go-sqlcmd uses |
|---|---|
-G alone |
DefaultAzureCredential (az login / MI / env) |
-G -U user |
Interactive (browser / MFA) |
-G -U user -P pwd |
Entra password (no MFA) |
--authentication-method is explicit (Learn, sql-server-ver17):
| Method | When | How you pass identity |
|---|---|---|
ActiveDirectoryDefault |
Same script on laptop (az login) and in Azure (MI / env) |
Env: AZURE_TENANT_ID + AZURE_CLIENT_ID, then one of AZURE_CLIENT_SECRET / AZURE_CLIENT_CERTIFICATE_PATH / AZURE_USERNAME |
ActiveDirectoryInteractive |
MFA / browser | -U UPN; browser pops |
ActiveDirectoryPassword |
Entra user + password, no MFA | -U / -P or SQLCMD* env; set AZURE_TENANT_ID if not the user’s default tenant |
ActiveDirectoryManagedIdentity |
Azure VM / App Service / Agent with MI | SAMI: no -U. UAMI: -U <client-id> |
ActiveDirectoryServicePrincipal |
Unattended CI | -U <appId> and secret via -P or SQLCMDPASSWORD. Cert: AZURE_CLIENT_CERTIFICATE_PATH |
ActiveDirectoryIntegrated |
Documented | Not implemented in go-sqlcmd — falls back to ActiveDirectoryDefault |
:Connect on go-sqlcmd accepts the same method names as an optional -G parameter (SqlAuthentication, ActiveDirectoryDefault, …).
SQLCMDCLIENTID (go-sqlcmd): app registration id for ActiveDirectoryInteractive / ActiveDirectoryPassword.
-G only| Scenario | Command shape | Notes |
|---|---|---|
| Entra password (no MFA) | -G -U user -P pwd |
Backend AUTHENTICATION=ActiveDirectoryPassword |
| Interactive / MFA | -G -U user (-G before -U) |
Known ODBC bug: -U then -G can fail |
| Integrated against Azure SQL | -G alone |
Backend Authentication=ActiveDirectoryIntegrated. Needs ODBC Driver ≥ 17.6.1 + Kerberos |
| Token file (Linux/macOS, v17.8+) | -G -P <token-file> without -U |
UTF-16 LE, no BOM. Windows ops: prefer go-sqlcmd |
ODBC -G requires sqlcmd ≥ 13.1. Interactive needs sqlcmd ≥ 15.0.1000.34 and ODBC Driver ≥ 17.2. Interactive is not supported on Linux/macOS for ODBC. -G applies to Azure SQL Database and Azure Synapse. -A (DAC) is not supported with -G.
# go-sqlcmd — DefaultAzureCredential (az login / MI / env)
sqlcmd -S myserver.database.windows.net -d AppDb -G -l 30 -Q "SELECT SUSER_SNAME()"
# Explicit method — service principal (secrets from env, not argv, on shared hosts)
sqlcmd -S myserver.database.windows.net -d AppDb -l 30 `
--authentication-method ActiveDirectoryServicePrincipal `
-U $env:AZURE_CLIENT_ID -P $env:AZURE_CLIENT_SECRET -Q "SELECT 1"
# ODBC interactive — -G BEFORE -U
sqlcmd -S myserver.database.windows.net -d AppDb -G -U 'rick@contoso.com' -l 30
Azure login timeout: Learn recommends -l 30 or higher with -G.
The Entra principal must exist in the database (CREATE USER [name] FROM EXTERNAL PROVIDER). That is a DBA step, not a flag.
| Flag | Meaning |
|---|---|
-C |
Trust server certificate (TrustServerCertificate=true). Lab / broken PKI. |
-N |
Encrypt negotiation. ODBC: s (strict) / m (mandatory) / o (optional). Go: string — s[trict], t[rue] / m[andatory] / yes / 1, o[ptional] / no / 0 / f[alse], or disable. |
-F |
Hostname expected in the cert (aliases / CN mismatch). |
Breaking change (SQL Server 2025 / current tools): if you omit -N, the default is -Nm (mandatory). SQL Server 2022 and earlier defaulted to optional (-No). Lab boxes with self-signed certs usually need -C (and often an explicit -N). Prefer real certs in prod; -C is a deliberate ops choice.
go-sqlcmd encrypt negotiation (Learn):
-N and no -C → negotiate without validating the server cert.-N without -C → require cert validation.SQL Server 2025 also introduces TDS 8.0 support in sqlcmd (-N s / strict).
-J (server certificate file) is ODBC Linux/macOS in Learn’s option table; go-sqlcmd has been adding cert-pinning — verify with sqlcmd "-?" on your build before you script it.
Full Encrypt / TSC / Strict matrix across sqlcmd, bcp, Invoke-Sqlcmd, sqlpackage: CLI-06.
Windows Integrated from workstation → jump host → SQL fails the second hop unless constrained delegation / CredSSP / gMSA is in place.
| Pattern | Works? |
|---|---|
Interactive RDP on jump, then sqlcmd -E |
✅ Single hop from jump — Rick’s normal DBA path |
PS Remoting to jump, then -E to SQL |
❌ Classic double-hop |
| Scheduled Task / Agent as a domain account on the SQL host | ✅ |
| SQL auth or Entra SP/MI from automation | ✅ Sidesteps Kerberos |
For Blazor Server app pools: run the site as a domain identity that has direct SQL rights, or use SQL auth / Entra. Do not hop the end-user’s Windows token IIS → SQL without constrained delegation.
Full table, klist checks, gMSA vs CredSSP, app-pool identity: SQL-CLI-06 §2 / §8. Stop here.
Only flags you will actually type. “Why in scripts” is the point. Full syntax: sqlcmd utility (Learn, ver17).
| Flag | Purpose | Why it matters in scripts |
|---|---|---|
-S |
Server (tcp:host,1433 / host\instance) |
Force tcp: for remote. Local default may hit lpc/np and surprise firewalls. |
-d |
Initial database (USE) |
Sets SQLCMDDBNAME. Missing DB → sqlcmd exits. |
-E |
Trusted connection | Default on Windows; still write it. Incompatible with -U/-P/-G. |
-U / -P |
SQL / Entra user / password | Prefer SQLCMDPASSWORD. Never bake -P into Agent jobsteps. |
-G |
Entra auth | Go: richer with --authentication-method. ODBC: put -G before -U. |
-l |
Login timeout (seconds) | Default 8. Azure / Entra: ≥ 30. 0 = infinite. |
-C |
Trust server certificate | Labs. Pair with a comment so it does not become the prod default. |
-N |
Encrypt | 2025 default is mandatory. Pin m/s in prod scripts. |
-F |
Hostname in certificate | DNS alias / listener name ≠ cert CN. |
-H |
Workstation name | Shows in sp_who / sys.sysprocesses — tag Agent vs laptop. |
-K |
ReadOnly application intent |
AG readable secondary. Omit = no secondary connectivity. |
-M |
Multi-subnet failover | ODBC: pass when hitting an AG listener / FCI. Go: always on; flag ignored. |
-A |
Dedicated admin connection | Emergency / DAC. Windows. Not with -G. |
| Flag | Purpose | Why it matters in scripts |
|---|---|---|
-Q |
Query and exit | Automation / CI. No GO inside the string. |
-q |
Query and stay | Interactive leftover. Rarely for Agent. |
-i |
Input .sql file(s) |
Mutually exclusive with -Q/-q. Multiple files: comma-separated no spaces, or repeat -i. Go quirk: space required after the -i value if more args follow. |
-o |
Output file | Overwrites. Not concurrent-safe — one writer per file. |
-e |
Echo input to stdout | Useful in deploy logs; noisy in CSV dumps. |
-u |
Unicode output file | Go writes UTF-16 LE + BOM. Blazor/PS prefer UTF-8 — convert or use Invoke-Sqlcmd. |
-f |
Input/output code page | ODBC. chcp shows the console page. Go does not document -f the same way. |
| Flag | Purpose | Why it matters in scripts |
|---|---|---|
-b |
Abort on error → nonzero exit | Always in PS pipelines. Without it, many SQL errors still exit 0. |
-V |
Min severity that sets ERRORLEVEL | Pair with -b. -V 11 is a solid default; Learn also suggests -V 16 for “general errors you can fix”. |
-m |
Min severity printed to stdout | -m-1 = include informational (no space). |
-t |
Query timeout (seconds) | Default = wait forever. Set it for Agent / CI. |
-r 0 / -r 1 |
Msgs to stderr | 0 (or omitted on ODBC) = severity ≥ 11; 1 = all including PRINT. Go requires explicit 0 or 1. No effect with -o. |
| Flag | Purpose | Why it matters in scripts |
|---|---|---|
-s"," |
Column separator | Crude CSV. Prefer Invoke-Sqlcmd → Export-Csv when you need real CSV. |
-W |
Trim trailing spaces | Use with -s for export. Incompatible with -y/-Y. |
-h-1 |
No header rows | -h = reprint headers every N rows. |
-w |
Screen width | Default 80 — widen before you parse columns. |
-y / -Y |
Max display width (var / fixed types) | -y 0 can crush the network — do not use casually. |
-k / -k1 / -k2 |
Strip / replace control characters | Keeps column layout when data has tabs/newlines. |
| Flag | Purpose | Why it matters in scripts |
|---|---|---|
-v name=value |
Scripting variables | Windows (ODBC -v is Windows-only in Learn). On Linux use a :setvar file. Values with spaces need quotes. |
-x |
Disable $(var) substitution |
Needed when the script inserts literal $(name) strings. |
-X / -X1 |
Disable !!, startup script (SQLCMDINI), env passthrough |
Hardening for untrusted scripts. -X1 exits on a disabled command. |
-I |
Quoted identifiers ON | Go: always ON; flag ignored. ODBC default is OFF — this is a real SSMS-vs-CLI footgun. |
-c |
Batch terminator | Default GO. Do not pick a T-SQL keyword. |
| Flag / command | Purpose |
|---|---|
--authentication-method |
Explicit Entra / SQL method (see §3.3) |
--vertical |
One column per line (SQLCMDFORMAT also controls this) |
--driver-logging-level |
go-mssqldb traces; 64 = all |
sqlcmd --help |
Modern commands: query, config, create, open, start, stop, delete |
sqlcmd query "…", sqlcmd config view |
Context-based workflow (optional; not required for Rick’s loop) |
| Topic | Behavior |
|---|---|
| Help | -? = ODBC-compatible flags; --help = modern commands |
-I |
Ignored — quoted identifiers always on |
-M |
Ignored — multi-subnet failover always on |
-R |
Ignored — Go runtime has no user-locale hook |
-r |
Requires explicit 0 or 1 |
-N |
String values (strict / true / false / disable …) |
-i + extra args |
Space after the -i value (known Go quirk) |
-u |
UTF-16 LE BOM |
| Protocols | Tries lpc → np → tcp (skips lpc when remote) |
Interactive EXIT(query) |
Must fit on one line — no multi-line prompt like ODBC |
The full Collect→Stage→Load→EXEC→Blazor pipeline is SQL-CLI-07. This section is the sqlcmd hop only: how PowerShell should call the exe so Blazor is not fed a silent failure.
-Q vs -i vs -o| Pattern | Use |
|---|---|
-Q "…" |
One-shot T-SQL. Semicolon-separated statements. No GO. Then exit. |
-i .\Deploy\Procs.sql |
File with GO batches, :r, :setvar. |
-i a.sql,b.sql |
Multiple files, in order, no spaces between names. Or -i a.sql -i b.sql. |
-o .\logs\run.log |
Capture everything (overwrites). Pair with -b and a $LASTEXITCODE check. |
sqlcmd -S $Server -d $Db -E -b -V 11 -i .\Deploy\Procs.sql -o .\logs\deploy.log
if ($LASTEXITCODE -ne 0) {
throw "sqlcmd failed with exit $LASTEXITCODE — see deploy.log"
}
Without -b, many SQL errors still exit 0. That silently poisons Blazor data loads.
-v / :setvar)Precedence, low → high (Learn): system env → user env → shell SET / $env: → sqlcmd -v → :setvar.
sqlcmd -S $Server -d $Db -E -b `
-v Environment="Prod" AppSchema="dbo" `
-i .\Scripts\Seed.sql
-- Seed.sql
PRINT 'Env=$(Environment)';
INSERT INTO $(AppSchema).Config(Env) VALUES (N'$(Environment)');
GO
Rules that bite:
$(var).:setvar VarName with no value clears the variable.-v OutDir="C:\temp\run1".-v is Windows-only on ODBC. On Linux, prepend a :setvar file (or use go-sqlcmd and verify -v on that build).$(Environment) inside double-quoted -Q strings. Use a single-quoted here-string when the $( ) is for sqlcmd, not for PS.GO batches and :r includesGO is a client batch separator, not T-SQL. The engine never sees it.
-- MasterDeploy.sql
:r .\Procs\usp_GetOrders.sql
:r .\Procs\usp_UpsertCustomer.sql
GO
Set-Location $PSScriptRoot # :r paths are relative to process CWD, not the master file
sqlcmd -S $Server -d $Db -E -b -i .\MasterDeploy.sql
GO inside IF / a stored-proc body / a -Q string.GO — re-declare.:r is expanded by the client, then the combined text is batched on GO.# Crude CSV (good enough for a drop file that bcp or a proc will reload)
sqlcmd -S $Server -d $Db -E -b -W -s"," -h-1 `
-Q "SET NOCOUNT ON; SELECT OrderId, CustomerId, Total FROM dbo.Orders WHERE ModifiedUtc >= '2026-01-01';" `
-o .\out\orders.csv
SET NOCOUNT ON first — (N rows affected) breaks parsers.
For typed objects / real CSV / JSON that a Blazor service will deserialize, stop using sqlcmd and switch to Invoke-Sqlcmd → Export-Csv -Encoding utf8 / ConvertTo-Json.
Capturing stdout in PS without -o:
$raw = & sqlcmd -S $Server -d $Db -E -b -Q "SET NOCOUNT ON; SELECT @@SERVERNAME;"
if ($LASTEXITCODE -ne 0) { throw "sqlcmd exit $LASTEXITCODE" }
# $raw is text. Do not pretend it is a DataRow.
Check $LASTEXITCODE immediately. Any later native command overwrites it.
| Problem | Fix |
|---|---|
PS expands $() inside double quotes |
Single-quoted -Q 'SELECT $(Var)' when talking to sqlcmd vars, or escape `$ |
Outer PS "..." eats inner SQL quotes |
Splatting / single quotes / here-strings |
-v Path=C:\temp backslashes |
Quote values: -v OutDir="C:\temp\run1" |
Empty $LASTEXITCODE after a later cmdlet |
Read it on the next line after sqlcmd |
GO in a -Q string |
Move the script to -i |
$query = @'
SET NOCOUNT ON;
EXEC dbo.usp_GetOrders @Since = '2026-01-01';
'@
sqlcmd -S $Server -d $Db -E -b -Q $query
if ($LASTEXITCODE -ne 0) { throw "sqlcmd exit $LASTEXITCODE" }
$rows = Invoke-Sqlcmd -ServerInstance $Server -Database $Db -TrustServerCertificate `
-Query "EXEC dbo.usp_GetOrders @Since = '2026-01-01'" -QueryTimeout 60
$payload = $rows | Select-Object OrderId, CustomerId, Total
$payload | ConvertTo-Json -Compress | Set-Content .\out\orders.json -Encoding utf8
Use it when you need objects, not log text. Parameter styles differ by module version — CLI-03. Do not read $LASTEXITCODE after Invoke-Sqlcmd; it is not the SQL result.
This is the discipline that keeps Blazor from rendering yesterday’s data as if the load succeeded. CLI-07 repeats it for the whole pipeline; here it is sqlcmd-specific.
-b actually does (Learn)ERRORLEVEL / $LASTEXITCODE when an error occurs.1 when the SQL error severity is greater than 10; otherwise 0.-V raises the bar: sqlcmd does not treat a message as an error if its severity is below -V.1.ERRORLEVEL is not @@ERROR. Do not compare it to a SQL error number.Recommended pair in this stack: -b -V 11. Use -V 16 when you only want “correctable general errors” to fail the job (Learn best practice).
sqlcmd -S $Server -d $Db -E -b -V 11 -Q "SET NOCOUNT ON; EXEC dbo.usp_Inventory_LoadFromStaging;"
if ($LASTEXITCODE -ne 0) { throw "sqlcmd failed exit=$LASTEXITCODE" }
| Check | Use? |
|---|---|
$LASTEXITCODE -ne 0 immediately after sqlcmd |
✅ The contract |
$? alone |
⚠️ Can lie after native exes; prefer $LASTEXITCODE |
No -b |
❌ Batch can fail mid-file and still exit 0 |
Read $LASTEXITCODE after Invoke-Sqlcmd or Get-Content |
❌ Wrong process |
Wrapper you can drop into a profile / common.ps1 — see Recipe 7.
Agent CmdExec: the jobstep already surfaces ERRORLEVEL. Still pass -b. Still log -o.
| Gotcha | Detail |
|---|---|
| GO batches | Client-side. Not inside IF / proc body / -Q. T-SQL variables reset across GO. |
| SET NOCOUNT ON | Stops (N rows affected) noise that breaks CSV / parsers. First line in scripts and in procs that feed pipelines. |
| Encoding | Classic: console code page / -f / -u. Go -u → UTF-16 LE + BOM. Blazor/PS prefer UTF-8 — convert, or use Invoke-Sqlcmd + Export-Csv -Encoding utf8. |
| Named Pipes vs TCP | Local default may hit np/lpc. Remote firewalls expect tcp:host,1433. Be explicit in scripts. |
| Azure SQL | Encryption on; -l ≥ 30; firewall / private endpoint; Entra principals mapped in-DB. |
| Passwords on cmdline | Visible in process lists / PSHistory / Agent jobstep logs. Use SQLCMDPASSWORD, SecretStore, Key Vault, or Entra MI/SP. |
| sqlcmd variables vs PS variables | $(Name) is sqlcmd. $Name / $(Get-Date) is PowerShell. Double-quoted -Q lets PS win. |
| SSMS SQLCMD Mode ≠ CLI | .NET SqlClient vs ODBC/Go → different quoted-identifier and encrypt defaults. Validate deploys with the same tool CI uses. |
| Which sqlcmd on PATH | After winget install sqlcmd, Go shadows ODBC. Pin full path in critical jobs until the swap is intentional. |
| Invoke-Sqlcmd ≠ exe | Does not call sqlcmd.exe. No :!!, :out, :connect. Different error model. |
-v on Linux ODBC |
Not supported (Learn). Use a :setvar file concatenated ahead of the script. |
| Quoted identifiers | ODBC default OFF unless -I. Go always ON. A script that deploys in SSMS and fails in Agent is often this. |
| Encrypt default (2025) | Omit -N and you now get mandatory encryption. Old 2022-era scripts that assumed optional will start failing against self-signed labs. |
ActiveDirectoryIntegrated on Go |
Documented but not implemented — falls back to Default. Do not write runbooks that depend on it. |
Placeholders: sql01 / AppDb / myserver.database.windows.net. Swap in yours. Every exe recipe uses -b.
sqlcmd -S sql01 -d AppDb -E -b -Q "SET NOCOUNT ON; SELECT @@SERVERNAME, DB_NAME(), SUSER_SNAME();"
if ($LASTEXITCODE -ne 0) { throw "sqlcmd exit $LASTEXITCODE" }
.sql file with abort-on-errorsqlcmd -S sql01 -d AppDb -E -b -V 11 -i .\Deploy\usp_GetOrders.sql -o .\logs\usp_GetOrders.log
if ($LASTEXITCODE) { throw "Deploy failed: $LASTEXITCODE" }
sqlcmd -S sql01 -d AppDb -E -b -W -s"," -h-1 -o .\out\customers.csv `
-Q "SET NOCOUNT ON; SELECT CustomerId, Name, Email FROM dbo.Customer;"
if ($LASTEXITCODE) { throw "Export failed: $LASTEXITCODE" }
Need real CSV / UTF-8 / typed columns? CLI-03 Invoke-Sqlcmd | Export-Csv.
Get-Content .\servers.txt | ForEach-Object {
$s = $_.Trim()
if (-not $s -or $s.StartsWith('#')) { return }
Write-Host "=== $s ===" -ForegroundColor Cyan
sqlcmd -S $s -d master -E -b -Q "SET NOCOUNT ON; SELECT @@SERVERNAME AS Srv, @@VERSION AS Ver;"
if ($LASTEXITCODE) { Write-Warning "$s failed ($LASTEXITCODE)" }
}
# Interactive / MFA
sqlcmd -S myserver.database.windows.net -d AppDb -G -U 'rick@contoso.com' -l 30 `
-Q "SET NOCOUNT ON; SELECT SUSER_SNAME();"
# Service principal (secrets from env)
sqlcmd -S myserver.database.windows.net -d AppDb -l 30 `
--authentication-method ActiveDirectoryServicePrincipal `
-U $env:AZURE_CLIENT_ID -P $env:AZURE_CLIENT_SECRET `
-Q "SET NOCOUNT ON; SELECT SUSER_SNAME();"
# System-assigned MI
sqlcmd -S myserver.database.windows.net -d AppDb -l 30 `
--authentication-method ActiveDirectoryManagedIdentity `
-Q "SET NOCOUNT ON; SELECT SUSER_SNAME();"
# User-assigned MI — -U is the client id
sqlcmd -S myserver.database.windows.net -d AppDb -l 30 `
--authentication-method ActiveDirectoryManagedIdentity `
-U $env:AZURE_CLIENT_ID `
-Q "SET NOCOUNT ON; SELECT SUSER_SNAME();"
function Invoke-SqlcmdExe {
param(
[Parameter(Mandatory)][string]$Server,
[Parameter(Mandatory)][string]$Database,
[string]$InputFile,
[string]$Query,
[string]$OutputFile,
[hashtable]$Variables
)
$cmdArgs = @('-S', $Server, '-d', $Database, '-E', '-b', '-V', '11')
if ($InputFile) { $cmdArgs += @('-i', $InputFile) }
if ($Query) { $cmdArgs += @('-Q', $Query) }
if ($OutputFile) { $cmdArgs += @('-o', $OutputFile) }
foreach ($k in $Variables.Keys) { $cmdArgs += @('-v', "$k=$($Variables[$k])") }
& sqlcmd @cmdArgs
if ($LASTEXITCODE -ne 0) {
throw "sqlcmd exit $LASTEXITCODE (Server=$Server Db=$Database)"
}
}
# Usage
Invoke-SqlcmdExe -Server sql01 -Database AppDb -InputFile .\Deploy\Procs.sql `
-Variables @{ Environment = 'Prod' }
sqlcmd -S "tcp:sql01.contoso.local,1433" -d AppDb -E -C -b `
-Q "SET NOCOUNT ON; SELECT 1 AS Ok;"
:setvar deploy (same script, many environments)sqlcmd -S sql01 -d AppDb -E -b -V 11 `
-v Environment="Prod" AppSchema="dbo" TargetTable="Config" `
-i .\Scripts\Seed.sql -o .\logs\seed-prod.log
if ($LASTEXITCODE) { throw "Seed failed: $LASTEXITCODE" }
-- Scripts\Seed.sql
PRINT 'Deploy $(Environment) → $(AppSchema).$(TargetTable)';
INSERT INTO $(AppSchema).$(TargetTable)(Env) VALUES (N'$(Environment)');
GO
:r master)Set-Location $PSScriptRoot\Deploy
sqlcmd -S sql01 -d AppDb -E -b -V 11 -i .\MasterDeploy.sql -o .\..\logs\master.log
if ($LASTEXITCODE) { throw "MasterDeploy failed: $LASTEXITCODE" }
-- Deploy\MasterDeploy.sql
:r .\Procs\usp_GetOrders.sql
:r .\Procs\usp_UpsertCustomer.sql
:r .\Procs\usp_Inventory_LoadFromStaging.sql
GO
Import-Module SqlServer
$rows = Invoke-Sqlcmd -ServerInstance 'sql01' -Database 'AppDb' -TrustServerCertificate `
-Query 'EXEC dbo.usp_GetOrders @Days = 7' -QueryTimeout 60
$rows | ConvertTo-Json -Depth 4 | Set-Content .\out\orders.json -Encoding utf8
Full cmdlet surface: SQL-CLI-03.
# Dedicated admin connection — engine must allow DAC. Not valid with -G.
sqlcmd -S sql01 -A -E
# then T-SQL interactively; quit with :EXIT
| Go here | When |
|---|---|
| SQL-CLI-03 — SqlServer module / Invoke-Sqlcmd | You need DataRows, Backup-SqlDatabase, or Write-SqlTableData instead of text. |
| SQL-CLI-06 — Connection & auth cheat sheet | Kerberos/double-hop, Encrypt/TSC across every tool, Blazor app-pool identity, “never put secrets on argv”. |
| SQL-CLI-07 — PS → SQL → Blazor patterns | The full Collect→Stage→Load→EXEC proc→Blazor contract, watermarks, multi-server glue. |
Need objects in PowerShell / Blazor JSON? → Invoke-Sqlcmd (CLI-03)
Need Azure Entra (MI / SP / MFA) from CLI? → go-sqlcmd
Need Agent / legacy PATH fidelity? → classic ODBC sqlcmd
Need deploy scripts + exit codes + :r? → sqlcmd / go-sqlcmd -b -i
Need the hop/Encrypt/identity matrix? → CLI-06
Need the pipeline, not the client? → CLI-07
Sources (verify periodically):
Download/install sqlcmd · Check installed version · sqlcmd utility / Go differences · Entra auth in sqlcmd · Scripting variables · Invoke-Sqlcmd · go-sqlcmd GitHub (latest v1.10.0 at research)