sqlcmd & go-sqlcmd

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.


Learning outcomes

After this page you can:


1. Decision map (three tools)

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 --versionVersion: 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

Rick’s stack default

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.

Identify which binary PATH will run

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.


2. Install lab (Windows)

Do this on the jump box (or a throwaway VM) before you trust any script.

2.1 Classic sqlcmd (ODBC)

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.

2.2 go-sqlcmd (official as of 2026)

Microsoft documents two variants. Go install paths on Windows:

Method Command / action
winget (preferred) winget install sqlcmdwinget 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.

2.3 PATH discipline (do this once)

# 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.

2.4 Invoke-Sqlcmd (pointer only)

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.


3. Auth lab

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.

3.1 Windows Integrated

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.

3.2 SQL auth

# 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.

3.3 Entra ID / Azure AD

Azure AD still appears in flags and error text. Microsoft Entra ID is the current name. They are the same thing here.

go-sqlcmd — two switches

-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.

ODBC sqlcmd — -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.

3.4 TrustServerCertificate / encrypt

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):

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.

3.5 Kerberos / double-hop (pointer)

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.


4. Flag reference (practical, grouped)

Only flags you will actually type. “Why in scripts” is the point. Full syntax: sqlcmd utility (Learn, ver17).

Connection

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.

Query, input, output

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.

Errors and timeouts

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.

Format / crude export

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.

Hardening and variables

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.

go-sqlcmd extras (not on ODBC)

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)

go-sqlcmd compatibility (do not be surprised)

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 lpcnptcp (skips lpc when remote)
Interactive EXIT(query) Must fit on one line — no multi-line prompt like ODBC

5. Scripting workshop (PS orchestration)

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.

5.1 -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.

5.2 Variable substitution (-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:

5.3 GO batches and :r includes

GO 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

5.4 Capturing output for Blazor / files

# 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-SqlcmdExport-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.

5.5 Quoting pitfalls (PowerShell)

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" }

5.6 When Invoke-Sqlcmd wins

$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.


6. Exit codes and error handling

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.

What -b actually does (Learn)

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).

PowerShell contract

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.


7. Gotchas

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.

8. Recipes (paste and run)

Placeholders: sql01 / AppDb / myserver.database.windows.net. Swap in yours. Every exe recipe uses -b.

1) Quick query (integrated)

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" }

2) Run a .sql file with abort-on-error

sqlcmd -S sql01 -d AppDb -E -b -V 11 -i .\Deploy\usp_GetOrders.sql -o .\logs\usp_GetOrders.log
if ($LASTEXITCODE) { throw "Deploy failed: $LASTEXITCODE" }

3) Export crude CSV (sqlcmd)

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.

4) Loop servers from a list

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)" }
}

5) Entra auth (go-sqlcmd)

# 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();"

6) Managed identity (go-sqlcmd, Azure host)

# 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();"

7) Exit-code check wrapper

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' }

8) Trust cert + TCP pin (lab / broken PKI)

sqlcmd -S "tcp:sql01.contoso.local,1433" -d AppDb -E -C -b `
  -Q "SET NOCOUNT ON; SELECT 1 AS Ok;"

9) :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

10) Multi-file run (: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

11) Invoke-Sqlcmd when it wins (objects → Blazor JSON)

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.

12) DAC / emergency console (on-prem, Windows)

# Dedicated admin connection — engine must allow DAC. Not valid with -G.
sqlcmd -S sql01 -A -E
# then T-SQL interactively; quit with :EXIT

What’s next

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)