Coding Standards

SQL / T-SQL Coding Standards

Foundational

The full reference for writing SQL against SQL Server with Dapper: safety, naming, formatting, correct queries, NULL handling, joins, transactions, concurrency, indexing, sargability, and migrations. Because we hand-write SQL (no ORM), these standards decide whether our data is correct, isolated, and fast, or quietly wrong. See also Gotchas: SQL, Database Design, and SQL Performance Tuning.

We use Dapper over Microsoft SQL Server, so the SQL we write is exactly what runs. Correctness, safety, and performance are all ours. Two rules are absolute and appear throughout the handbook because they matter so much: every value is a parameter (never concatenated), and every tenant-scoped statement carries its tenant predicate. The rest keeps our SQL consistent, correct, and efficient.

Treat SQL as first-class code: keep it in a data-access layer, review it, keep it consistent, and test it on realistic data.

Safety — non-negotiable

Concatenated, tenant-blind, SELECT * var sql = "SELECT * FROM Accounts WHERE Status='" + status + "'";
conn.Query<Account>(sql);

SQL injection through status, every tenant's rows returned (no tenant predicate), and SELECT * pulling back columns you do not need. Three of the most serious SQL mistakes in two lines.

Parameterised, scoped, explicit const string sql = @"SELECT Id, Status, Balance FROM Accounts
WHERE TenantId=@t AND Status=@status";
conn.Query<Account>(sql, new { t = ctx.TenantId, status });

Input cannot change the query, rows are limited to the caller's tenant, and only the needed columns are returned.

Naming & formatting

Projection & SELECT

NULL, types & arithmetic

Integer division truncates SELECT 5 / 2 AS Half; -- returns 2, not 2.5

Both operands are integers, so SQL Server does integer division and drops the fraction. On a fee or rate calculation this silently rounds money down.

Cast before dividing SELECT CAST(5 AS DECIMAL(18,4)) / 2 AS Half; -- returns 2.5000

Casting one operand to DECIMAL makes the division decimal, so the fraction is kept.

Joins, sets & correctness

Transactions & concurrency

Atomic, concurrency-safe balance update var rows = conn.Execute(@"UPDATE Accounts SET Balance = Balance - @amt, Version = Version + 1
WHERE Id=@id AND TenantId=@t AND Balance >= @amt AND Version=@expected", p, tx);
if (rows == 0) throw new ConcurrencyConflict();

A single atomic statement, guarded by tenant, sufficient funds, and the expected version. A concurrent change is detected, not silently lost, and the balance cannot go negative.

Indexing, sargability & performance

Function on an indexed column SELECT Id FROM Users
WHERE UPPER(Email) = @email; -- index on Email is skipped

Wrapping the column in UPPER() makes the filter non-sargable, so SQL Server scans every row instead of seeking the index.

Leave the column bare SELECT Id FROM Users
WHERE Email = @email; -- a case-insensitive collation handles the matching

With the column left untouched the index can be used. Let the column's collation handle case, or store a normalised copy to filter on.

Migrations, procs & security

Self-review checklist

Why it matters: SQL mistakes usually succeed quietly: wrong rows, another tenant's data, miscounted money. That makes them especially dangerous, and with hand-written Dapper SQL the care is entirely ours. Consistent, safe, correct, fast SQL is the foundation a financial platform stands on.