Skip to content

Commit 07fe128

Browse files
authored
T-Sql Code Style and Data Migration Updates (#786)
* Sql updates * Addressing PR comments * Updated stored proc naming example.
1 parent 668463c commit 07fe128

2 files changed

Lines changed: 259 additions & 19 deletions

File tree

docs/contributing/code-style/sql.md

Lines changed: 244 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ These standards should be applied across any T-SQL scripts that you write.
8484
- **Blank lines**: Separate sections of code with at least one blank line
8585
- **Commas**: Commas should be placed at the right end of the line
8686
- **Parentheses**: Parentheses should be vertically aligned with spanning multiple lines
87+
- **Data type modifiers**: Omit the space between type name and opening parenthesis (e.g.,
88+
`NVARCHAR(50)` not `NVARCHAR (50)`, `DATETIME2(7)` not `DATETIME2 (7)`)
89+
- **ID generation**: Use `CoreHelpers.GenerateComb()` in application code, not `NEWID()` in the
90+
database -- see [GUID generation](./csharp#guid-generation)
8791

8892
### `SELECT` statements
8993

@@ -114,6 +118,146 @@ WHERE
114118
U.[Enabled] = 1
115119
```
116120

121+
#### `WHERE` clause conditions
122+
123+
- `AND` / `OR` keywords go at the **start** of the next line, indented to align with the condition
124+
above it
125+
- Wrap grouped `OR` conditions in parentheses, with the opening `(` on the **same line** as `AND`
126+
and the closing `)` on its own line aligned with `AND`
127+
- Use inline comments to explain non-obvious conditions, such as status code meanings
128+
129+
```sql
130+
WHERE
131+
O.[Enabled] = 1
132+
AND O.[UsePolicies] = 1
133+
AND (
134+
-- Active users linked by UserId
135+
(OU.[Status] != 0 AND OU.[UserId] = @UserId)
136+
-- Invited users matched by email (Status = 0)
137+
OR EXISTS (
138+
SELECT
139+
1
140+
FROM
141+
[dbo].[UserView] U
142+
WHERE
143+
U.[Id] = @UserId
144+
AND OU.[Email] = U.[Email]
145+
AND OU.[Status] = 0
146+
)
147+
)
148+
```
149+
150+
#### `IN` clauses
151+
152+
- For bulk operations, prefer table-valued parameters (TVPs) using the `IN (SELECT [Id] FROM @Ids)`
153+
pattern
154+
- For direct value lists, use no spaces after commas: `IN (0,1,2)`
155+
156+
```sql
157+
WHERE
158+
[Id] IN (SELECT [Id] FROM @Ids)
159+
```
160+
161+
#### Subqueries
162+
163+
- Use `EXISTS` (not `IN`) for correlated subqueries -- `EXISTS` short-circuits on the first match,
164+
whereas `IN` evaluates all matching values first
165+
- Use `IN` for non-correlated subqueries (where the inner query does not reference the outer query)
166+
-- the optimizer typically produces equivalent plans, and `IN` reads more naturally in this
167+
context
168+
- Use `SELECT 1` inside `EXISTS` checks, not `SELECT *`
169+
- Indent the subquery body 4 spaces within the parentheses; align the closing `)` with the opening
170+
context
171+
172+
Correlated subquery (references outer query -- use `EXISTS`):
173+
174+
```sql
175+
CASE WHEN EXISTS (
176+
SELECT
177+
1
178+
FROM
179+
[dbo].[ProviderUserView] PU
180+
INNER JOIN
181+
[dbo].[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId]
182+
WHERE
183+
PU.[UserId] = OU.[UserId]
184+
AND PO.[OrganizationId] = P.[OrganizationId]
185+
) THEN 1 ELSE 0 END AS [IsProvider]
186+
```
187+
188+
Non-correlated subquery (self-contained -- use `IN`):
189+
190+
```sql
191+
SELECT
192+
[Id],
193+
[Name]
194+
FROM
195+
[dbo].[OrganizationView]
196+
WHERE
197+
[Id] IN (
198+
SELECT
199+
[OrganizationId]
200+
FROM
201+
[dbo].[CollectionView]
202+
WHERE
203+
[ExternalId] IS NOT NULL
204+
)
205+
```
206+
207+
#### Common table expressions (CTEs)
208+
209+
- Prefix the `WITH` keyword with a semicolon: `;WITH`
210+
- Place the CTE name and `AS` followed by the opening `(` on the same line
211+
- Put the closing `)` on its own line; follow it with a comma and the next CTE name when chaining
212+
- Each CTE `SELECT` follows the same formatting rules as a regular `SELECT` statement
213+
- Put `UNION ALL` on its own line, with a blank line above and below it
214+
215+
```sql
216+
;WITH OrgUsers AS
217+
(
218+
-- Active users: direct UserId match
219+
SELECT
220+
OU.[Id],
221+
OU.[OrganizationId],
222+
OU.[Status]
223+
FROM
224+
[dbo].[OrganizationUserView] OU
225+
WHERE
226+
OU.[Status] <> 0
227+
AND OU.[UserId] = @UserId
228+
229+
UNION ALL
230+
231+
-- Invited users: matched by email
232+
SELECT
233+
OU.[Id],
234+
OU.[OrganizationId],
235+
OU.[Status]
236+
FROM
237+
[dbo].[OrganizationUserView] OU
238+
WHERE
239+
OU.[Status] = 0
240+
AND OU.[Email] = @UserEmail
241+
),
242+
Providers AS
243+
(
244+
SELECT
245+
[OrganizationId]
246+
FROM
247+
[dbo].[UserProviderAccessView]
248+
WHERE
249+
[UserId] = @UserId
250+
)
251+
SELECT
252+
OU.[Id],
253+
OU.[OrganizationId],
254+
CASE WHEN PR.[OrganizationId] IS NULL THEN 0 ELSE 1 END AS [IsProvider]
255+
FROM
256+
OrgUsers OU
257+
LEFT JOIN
258+
Providers PR ON PR.[OrganizationId] = OU.[OrganizationId]
259+
```
260+
117261
### Stored procedures
118262

119263
- **Stored Procedure Name**: `{EntityName}_{Action}` format (e.g., `[dbo].[User_ReadById]`)
@@ -132,8 +276,19 @@ WHERE
132276

133277
:::
134278

279+
:::warning Do not use `Get` in procedure names
280+
281+
Some procedures in the codebase use `Get` instead of `Read` in the name (e.g.,
282+
`CipherOrganizationPermissions_GetManyByOrganizationId`, `OrganizationReport_GetSummaryDataById`).
283+
These are incorrect and should not be used as a reference. Always use `Read` or `ReadMany` for
284+
`SELECT` operations.
285+
286+
:::
287+
135288
#### Basic structure
136289

290+
- Wrap the entire procedure body in `BEGIN`/`END` statements
291+
137292
```sql
138293
CREATE PROCEDURE [dbo].[EntityName_Action]
139294
@Parameter1 DATATYPE,
@@ -148,6 +303,65 @@ BEGIN
148303
END
149304
```
150305

306+
#### Common examples
307+
308+
**Read by ID** -- select a single record from a view:
309+
310+
```sql
311+
CREATE PROCEDURE [dbo].[EntityName_ReadById]
312+
@Id UNIQUEIDENTIFIER
313+
AS
314+
BEGIN
315+
SET NOCOUNT ON
316+
317+
SELECT
318+
*
319+
FROM
320+
[dbo].[EntityNameView]
321+
WHERE
322+
[Id] = @Id
323+
END
324+
```
325+
326+
**Read many by IDs** -- bulk read using a table-valued parameter:
327+
328+
```sql
329+
CREATE PROCEDURE [dbo].[EntityName_ReadManyByIds]
330+
@Ids AS [dbo].[GuidIdArray] READONLY
331+
AS
332+
BEGIN
333+
SET NOCOUNT ON
334+
335+
SELECT
336+
*
337+
FROM
338+
[dbo].[EntityNameView]
339+
WHERE
340+
[Id] IN (SELECT [Id] FROM @Ids)
341+
END
342+
```
343+
344+
**Read many with filter** -- multiple `AND` conditions with an inline status code comment:
345+
346+
```sql
347+
CREATE PROCEDURE [dbo].[EntityName_ReadManyByOrganizationIdRole]
348+
@OrganizationId UNIQUEIDENTIFIER,
349+
@Role TINYINT
350+
AS
351+
BEGIN
352+
SET NOCOUNT ON
353+
354+
SELECT
355+
*
356+
FROM
357+
[dbo].[EntityNameDetailsView]
358+
WHERE
359+
[OrganizationId] = @OrganizationId
360+
AND [Status] = 2 -- 2 = Confirmed
361+
AND [Type] = @Role
362+
END
363+
```
364+
151365
#### Parameter declaration
152366

153367
- One parameter per line
@@ -204,7 +418,8 @@ WHERE
204418

205419
### Tables
206420

207-
- **Table Name**: PascalCase (e.g., [dbo].[User], [dbo].[AuthRequest])
421+
- **Table Name**: Singular form of the object name, PascalCase (e.g., `[dbo].[User]` not
422+
`[dbo].[Users]`, `[dbo].[AuthRequest]` not `[dbo].[AuthRequests]`)
208423
- **Column Names**: PascalCase (e.g., [Id], [CreationDate], [MasterPasswordHash])
209424
- **Primary Key**: `PK_{TableName}` (e.g., [PK_User], [PK_Organization])
210425
- **Foreign Keys**: `FK_{TableName}_{ReferencedTable}` (e.g., FK_Device_User)
@@ -220,6 +435,8 @@ WHERE
220435
- `VARCHAR(n)` for ASCII text
221436
- `BIT` for boolean values
222437
- `TINYINT`, `SMALLINT`, `INT`, `BIGINT` for integers
438+
- **Data type modifiers**: No space between the type name and its size or precision modifier (e.g.,
439+
`NVARCHAR(50)` not `NVARCHAR (50)`, `DATETIME2(7)` not `DATETIME2 (7)`)
223440
- **Nullability**: Explicitly specify `NOT NULL` or `NULL`
224441
- **Standard Columns**: Most tables include:
225442
- `[Id] UNIQUEIDENTIFIER NOT NULL` - Primary key
@@ -229,12 +446,13 @@ WHERE
229446
```sql
230447
CREATE TABLE [dbo].[TableName]
231448
(
232-
[Column1] INT IDENTITY(1,1) NOT NULL,
233-
[Column2] NVARCHAR(100) NOT NULL,
234-
[Column3] NVARCHAR(255) NULL,
235-
[Column4] BIT NOT NULL CONSTRAINT [DF_TableName_Column4] DEFAULT (1),
236-
237-
CONSTRAINT [PK_TableName] PRIMARY KEY CLUSTERED ([Column1] ASC)
449+
[Id] UNIQUEIDENTIFIER NOT NULL,
450+
[Column2] NVARCHAR(100) NOT NULL,
451+
[Column3] NVARCHAR(255) NULL,
452+
[CreationDate] DATETIME2(7) NOT NULL,
453+
[RevisionDate] DATETIME2(7) NOT NULL,
454+
[Column6] BIT NOT NULL CONSTRAINT [DF_TableName_Column6] DEFAULT (1),
455+
CONSTRAINT [PK_TableName] PRIMARY KEY CLUSTERED ([Id] ASC)
238456
);
239457
```
240458

@@ -295,8 +513,8 @@ CREATE FUNCTION [dbo].[FunctionName](@Parameter DATATYPE)
295513
RETURNS TABLE
296514
AS RETURN
297515
SELECT
298-
Column1,
299-
Column2,
516+
[Column1],
517+
[Column2],
300518
CASE
301519
WHEN Condition
302520
THEN Value1
@@ -503,11 +721,15 @@ script will not modify the data type again.
503721

504722
```sql
505723
IF EXISTS (
506-
SELECT *
507-
FROM INFORMATION_SCHEMA.COLUMNS
508-
WHERE COLUMN_NAME = '{column_name}' AND
509-
DATA_TYPE = '{datatype}' AND
510-
TABLE_NAME = '{table_name}')
724+
SELECT
725+
*
726+
FROM
727+
INFORMATION_SCHEMA.COLUMNS
728+
WHERE
729+
COLUMN_NAME = '{column_name}'
730+
AND DATA_TYPE = '{datatype}'
731+
AND TABLE_NAME = '{table_name}'
732+
)
511733
BEGIN
512734
ALTER TABLE [dbo].[{table_name}]
513735
ALTER COLUMN [{column_name}] {NEW_TYPE} {NULL|NOT NULL}
@@ -554,8 +776,7 @@ GO
554776
#### Adjusting metadata
555777

556778
When altering views, you may also need to refresh modules (stored procedures or functions) that
557-
reference that view or function so that SQL Server to update its statistics and compiled references
558-
to it.
779+
reference that view so that SQL Server can update its cached metadata and compiled references to it.
559780

560781
```sql
561782
IF OBJECT_ID('[dbo].[{procedure_or_function}]') IS NOT NULL
@@ -615,9 +836,13 @@ old index available for queries during the rebuild.
615836

616837
```sql
617838
IF EXISTS (
618-
SELECT * FROM sys.indexes
619-
WHERE name = 'IX_Organization_Enabled'
620-
AND object_id = OBJECT_ID('[dbo].[Organization]')
839+
SELECT
840+
*
841+
FROM
842+
sys.indexes
843+
WHERE
844+
name = 'IX_Organization_Enabled'
845+
AND object_id = OBJECT_ID('[dbo].[Organization]')
621846
)
622847
BEGIN
623848
CREATE NONCLUSTERED INDEX [IX_Organization_Enabled]

docs/contributing/database-migrations/mssql.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,18 @@ will call `dbo_finalization`.
9696

9797
Upon execution any finalization scripts will be [automatically moved](./edd.mdx#online-environments)
9898
for proper history.
99+
100+
### Data migrations
101+
102+
Data migrations are sometimes needed during deployment — for example, seeding a new column or
103+
inserting rows required by a new feature.
104+
105+
However, large or active tables can make this infeasible, as migrations may take hours or days.
106+
Migrations also run before code releases, so data can become stale by the time the app deploys.
107+
108+
Therefore, new features must handle both old and new data states. This decouples migrations from
109+
code releases and allows natural migration over time (e.g. write the expected value on first access,
110+
then use the new value going forward).
111+
112+
Once the bulk of data has been naturally migrated, a cleanup script can bring remaining data to the
113+
desired state, at which point the old code path can be removed.

0 commit comments

Comments
 (0)