Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
TypeScript enums are not fully type-safe and can [cause surprises][enum-surprises]. Your code should
use [constant objects][constant-object-pattern] instead of introducing a new enum.

## Our Recommended Approach ([ADR-0025](../../architecture/adr/0025-ts-deprecate-enums.md))
## Our Recommended Approach ([ADR-0025](../../../architecture/adr/0025-ts-deprecate-enums.md))

- Use the same name for your type- and value-declaration.
- Use `type` to derive type information from the const object.
Expand Down
228 changes: 31 additions & 197 deletions docs/contributing/code-style/sql.md
Original file line number Diff line number Diff line change
@@ -1,222 +1,56 @@
---
toc_max_heading_level: 4
---

# T-SQL
# SQL

## Repositories

We use the [Repository pattern][repository] with the MSSQL repositories being written using
[Dapper][dapper]. Each repository method in turn calls a _Stored Procedure_, which primarily fetches
data from _Views_.

## Deployment scripts

There are specific ways deployment scripts should be structured. The goal for these standards is to
ensure that the scripts should be re-runnable. We never intend to run scripts multiple times on an
environment, but the scripts should support it.

### Tables

#### Creating a table

When creating a table, you must first check if the table exists:

```sql
IF OBJECT_ID('[dbo].[{table_name}]') IS NULL
BEGIN
CREATE TABLE [dbo].[{table_name}] (
[Id] UNIQUEIDENTIFIER NOT NULL,
...
CONSTRAINT [PK_{table_name}] PRIMARY KEY CLUSTERED ([Id] ASC)
);
END
GO
```

#### Deleting a table

When deleting a table, use `IF EXISTS` to avoid an error if the table doesn't exist.

```sql
DROP IF EXISTS [dbo].[{table_name}]
GO
```

#### Adding a column to a table

You must first check to see if the column exists before adding it to the table.

```sql
IF COL_LENGTH('[dbo].[{table_name}]', '{column_name}') IS NULL
BEGIN
ALTER TABLE [dbo].[{table_name}]
ADD [{column_name}] {DATATYPE} {NULL|NOT NULL};
END
GO
```

When adding a new `NOT NULL` column to an existing table, please re-evaluate the need for it to
truly be required. Do not be afraid of using Nullable\<T\> primitives in C# and in the application
layer, which is almost always going to be better than taking up unnecessary space in the DB per row
with a default value, especially for new functionality or features where it will take a very long
time to be useful for most row-level data, if at all.

If you do decide to add a `NOT NULL` column, **use a DEFAULT constraint** instead of creating the
column, updating rows and changing the column. This is especially important for the largest tables
like `dbo.User` and `dbo.Cipher`. Our version of SQL Server in Azure uses metadata for default
constraints. This means we can update the default column value **without** updating every row in the
table (which will use a lot of DB I/O).

This is slow:

```sql
IF COL_LENGTH('[dbo].[Table]', 'Column') IS NULL
BEGIN
ALTER TABLE
[dbo].[Table]
ADD
[Column] INT NULL
END
GO

UPDATE
[dbo].[Table]
SET
[Column] = 0
WHERE
[Column] IS NULL
GO

ALTER TABLE
[dbo].[Column]
ALTER COLUMN
[Column] INT NOT NULL
GO
```

This is better:

```sql
IF COL_LENGTH('[dbo].[Table]', 'Column' IS NULL
BEGIN
ALTER TABLE
[dbo].[Column]
ADD
[Column] INT NOT NULL CONSTRAINT D_Table_Column DEFAULT 0
END
GO
```

#### Changing a column data type

You must wrap the `ALTER TABLE` statement in a conditional block, so that subsequent runs of the
script will not modify the data type again.

```sql
IF EXISTS (
SELECT *
FROM INFORMATION_SCHEMA.COLUMNS
WHERE COLUMN_NAME = '{column_name}' AND
DATA_TYPE = '{datatype}' AND
TABLE_NAME = '{table_name}')
BEGIN
ALTER TABLE [dbo].[{table_name}]
ALTER COLUMN [{column_name}] {NEW_TYPE} {NULL|NOT NULL}
END
GO
```

#### Adjusting metadata

When adjusting a table, you should also check to see if that table is referenced in any views. If
the underlying table in a view has been modified, you should run `sp_refreshview` to re-generate the
view metadata.

```sql
EXECUTE sp_refreshview N'[dbo].[{view_name}]'
GO
```

### Views

#### Creating or modifying a view

We recommend using the `CREATE OR ALTER` syntax for adding or modifying a view.
## Structuring SQL code

```sql
CREATE OR ALTER VIEW [dbo].[{view_name}]
AS
SELECT
*
FROM
[dbo].[{table_name}]
GO
```
For writing our SQL code and segregating responsibility across the SQL entities, we follow a
Separation of Concerns with a Layered Data Access Pattern [link?]. This informs us to use the
following design guidelines:

#### Deleting a view

When deleting a view, use `IF EXISTS` to avoid an error if the table doesn't exist.

```sql
DROP IF EXISTS [dbo].[{view_name}]
GO
```

#### Adjusting metadata
### Views

When altering views, you may also need to refresh modules (stored procedures or functions) that
reference that view or function so that SQL Server to update its statistics and compiled references
to it.
### Stored Procedures

```sql
IF OBJECT_ID('[dbo].[{procedure_or_function}]') IS NOT NULL
BEGIN
EXECUTE sp_refreshsqlmodule N'[dbo].[{procedure_or_function}]';
END
GO
```
#### Single Responsibility Principle

### Functions and stored procedures
- Views: Responsible ONLY for complex data logic (joins, CTEs, filtering)
- Stored Procedures: Responsible ONLY for parameterized data access (simple SELECT with WHERE)

#### Creating or modifying a function or stored procedure
This follows the “Tell, Don’t Ask” principle - procedures tell views what parameters they need,
views handle the complex “how” internally.

We recommend using the `CREATE OR ALTER` syntax for adding or modifying a function or stored
procedure.
#### Separation of Concerns

```sql
CREATE OR ALTER {PROCEDURE|FUNCTION} [dbo].[{sproc_or_func_name}]
...
GO
```
- Business Logic Layer (Views): Contains the “what” - complex query logic, joins, windowing
functions
- Data Access Layer (Procedures): Contains the “how” - parameter handling, security context

#### Deleting a function or stored procedure
#### Dependency Inversion

When deleting a function or stored procedure, use `IF EXISTS` to avoid an error if it doesn't exist.
- High-level modules (stored procedures) don’t depend on low-level modules (tables)
- Both depend on abstractions (views)
- Tables → Views → Procedures (dependency flows upward)

```sql
DROP IF EXISTS [dbo].[{sproc_or_func_name}]
GO
```
#### Interface Segregation

### Creating or modifying an index
- Views act as stable interfaces that can change internal implementation without breaking procedures
- Procedures provide consistent API regardless of underlying view complexity

When creating indexes, especially on heavily used tables, our production database can easily become
offline, unusable, hit 100% CPU and many other bad behaviors. It is often best to do this using
online index builds so as not to lock the underlying table. This may cause the index operation to
take longer, but you will not create an underlying schema table lock which prevents all reads and
connections to the table and instead only locks the table of updates during the operation.
Specific Pattern: Repository + Strategy Tables (Data Storage) ↓ Views (Data Logic Strategy) ↓
Procedures (Data Access Repository) ↓ Application Code

A good example is when creating an index on `dbo.Cipher` or `dbo.OrganizationUser`, those are
heavy-read tables and the locks can cause exceptionally high CPU, wait times and worker exhaustion
in Azure SQL.
### Benefits Achieved

```sql
CREATE NONCLUSTERED INDEX [IX_OrganizationUser_UserIdOrganizationIdStatus]
ON [dbo].[OrganizationUser]([UserId] ASC, [OrganizationId] ASC, [Status] ASC)
INCLUDE ([AccessAll])
WITH (ONLINE = ON); -- ** THIS ENSURES ONLINE **
```
1. Maintainability: Change complex logic in views without touching procedures
2. Testability: Views can be tested independently of procedures
3. Reusability: Multiple procedures can use the same view
4. Performance: Database engine optimizes view logic once
5. Security: Consistent data access patterns through procedures

[repository]:
https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design
Expand Down
Loading