> For the complete documentation index, see [llms.txt](https://v2.dataos.info/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://v2.dataos.info/references/v1/engine-guide/sql-server.md).

# SQL Server

The full technical reference for building and running a Vulcan Data Product on Microsoft SQL Server, covering connection, grants, materialization, local Docker setup, indexing, and troubleshooting.

Microsoft SQL Server is an on-premises or VM-hosted relational database engine that works with Vulcan. Use it for existing SQL Server estates, Windows-centric environments, or when you need a governed RDBMS with full T-SQL surface area. This page is the full manual: connection and authentication, grants, how each model kind materializes, the local Docker setup, indexing and MERGE tuning, and the failure modes you hit in real projects.

### Engine characteristics

| Property            | Value                                                                                                                     |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Engine adapter type | `mssql`                                                                                                                   |
| Model dialect       | `tsql`                                                                                                                    |
| Query execution     | SQL Server instance; no separate warehouse to start, stop, or resize                                                      |
| Tested Vulcan image | `tmdcio/vulcan-mssql:0.228.1.26` against SQL Server 2019 to 2022 on the Draco 1.38 series                                 |
| Azure SQL Database  | Uses the same adapter with a different connection-config subclass. See [Authentication methods](#authentication-methods). |

## When to use MSSQL

Choose SQL Server for Data Products that already live in a SQL Server estate, for Windows-domain-integrated environments, or when you need on-premises control over data placement and licensing. There is no warehouse cold-start penalty, but there is also no elastic scale-out: performance is shaped by instance size, disk IOPS, indexes, and data volume. SQL Server is a full RDBMS, so most native features (indexes, constraints, computed columns, triggers, partitioned views) are reachable through guarded raw DDL in `pre_statements` / `post_statements`.

## Supported model kinds

`FULL`, `SEED`, `VIEW`, `INCREMENTAL_BY_TIME_RANGE`, `INCREMENTAL_BY_UNIQUE_KEY`, `INCREMENTAL_BY_PARTITION`, `INCREMENTAL_UNMANAGED`, `SCD_TYPE_2`, `SCD_TYPE_2_BY_TIME`, `SCD_TYPE_2_BY_COLUMN`, and `EMBEDDED`.

{% hint style="info" %}
`MANAGED` models (Snowflake Dynamic Tables and equivalents) and table cloning are not supported on this adapter.
{% endhint %}

## SQL Server-side permissions and grants

Three roles are required, each with a distinct scope.

| Role                | Who holds it                 | Purpose                                                            |
| ------------------- | ---------------------------- | ------------------------------------------------------------------ |
| Admin role          | SQL Server sysadmin/DBA      | Creates databases, logins, and grants privileges.                  |
| Vulcan service role | The login Vulcan connects as | Runs models and manages schemas and tables in the target database. |
| Consumer role       | BI users, endpoint consumers | Reads Data Product tables only.                                    |

Run minimum grants as a sysadmin:

```sql
-- Vulcan service role
CREATE LOGIN vulcan_svc WITH PASSWORD = '<strong-password>';
USE <database>;
CREATE USER vulcan_svc FOR LOGIN vulcan_svc;
ALTER ROLE db_ddladmin ADD MEMBER vulcan_svc;
ALTER ROLE db_datareader ADD MEMBER vulcan_svc;
ALTER ROLE db_datawriter ADD MEMBER vulcan_svc;
GRANT CREATE SCHEMA TO vulcan_svc;
```

Consumer role (read-only):

```sql
CREATE LOGIN vulcan_consumer WITH PASSWORD = '<strong-password>';
USE <database>;
CREATE USER vulcan_consumer FOR LOGIN vulcan_consumer;
ALTER ROLE db_datareader ADD MEMBER vulcan_consumer;
```

Verify grants:

```sql
SELECT dp.name AS principal, r.name AS role
FROM sys.database_role_members m
JOIN sys.database_principals dp ON m.member_principal_id = dp.principal_id
JOIN sys.database_principals r ON m.role_principal_id = r.principal_id;
```

`connection.database` must exist before `vulcan plan`. SQL Server has no `CREATE SCHEMA IF NOT EXISTS` semantics either; schemas referenced by models are created explicitly by the adapter's guarded DDL when the role has privilege.

## DataOS permissions

These permissions must be provisioned before you create and deploy Data Products.

| Permission                                                 | What it Allows                                                                                                                                   | Granted By                                                           |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| Tenant Access                                              | Build and manage Data Products within your tenant.                                                                                               | Tenant Admin                                                         |
| `roles:id:data-dev` (or your tenant's equivalent role tag) | Create and apply Vulcan resources. Verify with `dataos-ctl user get` (check the TAGS column).                                                    | Tenant Admin                                                         |
| `depot:rw:<mssql-depot-name>`                              | Read and write access to the SQL Server depot.                                                                                                   | Tenant Admin or Data Admin                                           |
| `depot:r:<mssql-depot-name>`                               | Read-only access to the SQL Server depot (for consumers).                                                                                        | Tenant Admin or Data Admin                                           |
| Can Use (Compute, Depots, Secrets)                         | Allows you to actually use Compute, Depots, and Secrets. Resource access is deny-by-default: a role tag or depot permission alone is not enough. | Tenant Admin or Data Admin                                           |
| git-sync Secret                                            | Allows Vulcan to pull model code from Git using a Secret. Request a git-sync Secret, not raw Git credentials.                                    | Tenant Admin, Data Admin, or the Secret owner (with Can Use granted) |

Run `dataos-ctl get depot` to confirm read access before you start. If the SQL Server depot is missing, request `depot:r` from your operator.

## Connection options

Connect Vulcan to SQL Server either directly or through a Depot. Direct connections support SQL Server username/password authentication and Microsoft Entra ID authentication. Choose the authentication method below for a direct connection, or use a [DataOS Depot](#using-dataos-depot) for production.A Depot stores the connection details and credentials in DataOS.

### Authentication methods

* **Username and password (default)**: set `user` and `password` with `driver: pymssql` (the default).
* **Microsoft Entra ID / Azure Active Directory**: install the `mssql-odbc` extra, set `driver: pyodbc`, `driver_name`, and `odbc_properties` (for example `authentication: ActiveDirectoryServicePrincipal`).
* Always inject the password from an environment variable: `password: {{ env_var('MSSQL_PASSWORD') }}`.

{% hint style="info" %}
**Azure SQL Database uses the same adapter**

Azure SQL Database is not a separate engine adapter: it reuses `MSSQLEngineAdapter` under an `azuresql` connection-config type. The only functional difference is catalog handling: Azure SQL is `SINGLE_CATALOG_ONLY` (one database per connection), while on-premises/generic MSSQL is `REQUIRES_SET_CATALOG` (cross-database `USE` is supported). Everything else on this page, including grants, materialization, MERGE behavior, and troubleshooting, applies equally to Azure SQL.
{% endhint %}

### Properties in Config.yaml

In `config.yaml`, set these under `gateways.<name>.connection`.

| Option            | Description                                                                                      | Type          | Required |
| ----------------- | ------------------------------------------------------------------------------------------------ | ------------- | -------- |
| `type`            | Engine type name. Must be `mssql`.                                                               | string        | Yes      |
| `host`            | The hostname of the SQL Server instance.                                                         | string        | Yes      |
| `user`            | The username for authentication.                                                                 | string        | No       |
| `password`        | The password for authentication.                                                                 | string        | No       |
| `port`            | The port number. Default `1433`.                                                                 | int           | No       |
| `database`        | The target database.                                                                             | string        | No       |
| `charset`         | The character set used for the connection.                                                       | string        | No       |
| `timeout`         | Query timeout in seconds. Default: no timeout.                                                   | int           | No       |
| `login_timeout`   | Seconds to wait for connection and login (default 60).                                           | int           | No       |
| `appname`         | The application name for the connection.                                                         | string        | No       |
| `conn_properties` | List of raw connection properties.                                                               | list\[string] | No       |
| `autocommit`      | Enable autocommit mode. Default: false.                                                          | bool          | No       |
| `driver`          | Driver to use for the connection. Default: `pymssql`. Also accepts `pyodbc`.                     | string        | No       |
| `driver_name`     | ODBC driver name (for example `ODBC Driver 18 for SQL Server`), only used when `driver: pyodbc`. | string        | No       |
| `odbc_properties` | ODBC connection properties (for example `authentication: ActiveDirectoryServicePrincipal`).      | dict          | No       |

## Gateway Config examples

### Minimum local connection with `pymssql`:

```yaml
gateways:
  local:
    connection:
      type: mssql
      host: localhost
      user: sa
      password: "{{ env_var('MSSQL_PASSWORD') }}"
      port: 1433
      database: warehouse
```

### Entra ID / Azure AD via `pyodbc`:

```yaml
gateways:
  prod:
    connection:
      type: mssql
      host: myserver.database.windows.net
      driver: pyodbc
      driver_name: "ODBC Driver 18 for SQL Server"
      odbc_properties:
        authentication: ActiveDirectoryServicePrincipal
      user: "{{ env_var('AZURE_CLIENT_ID') }}"
      password: "{{ env_var('AZURE_CLIENT_SECRET') }}"
      database: warehouse
```

### Local development with Docker

SQL Server is fully runnable on your laptop through the official container image.

Save the following as `docker/docker-compose.warehouse.yml`:

```yaml
version: "3.8"
services:
  mssql:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      ACCEPT_EULA: "Y"
      MSSQL_SA_PASSWORD: "${MSSQL_SA_PASSWORD}"
    ports:
      - "1433:1433"
    volumes:
      - mssql-data:/var/opt/mssql
volumes:
  mssql-data:
```

Use the matching connection (DuckDB for local state):

```yaml
state_connection:
  type: duckdb
  database: .state.db

gateways:
  local:
    connection:
      type: mssql
      host: localhost
      user: sa
      password: "{{ env_var('MSSQL_SA_PASSWORD') }}"
      port: 1433
      database: warehouse
```

Validate the connection:

```bash
vulcan --log-to-stdout plan
```

`--log-to-stdout` skips creating a `.logs/` directory, which avoids `PermissionError` when the working directory is not writable.

### Using DataOS Depot

DataOS Depot is recommended for production. Configure the gateway with `type: depot` and `address: dataos://<depot-name>?purpose=rw`. The Depot stores the SQL Server connection details and binds the required credential and access purpose.

```yaml
gateways:
  default:
    connection:
      type: depot
      address: dataos://<mssql-depot-name>?purpose=rw
```

Apply the Secret, Depot, Git Secret, project configuration, and Vulcan resource in that order. See [Production deployment](#production-deployment) for the required resources and Depot purposes.

## Production deployment

Production usually uses a Depot instead of a direct password in the project config. Apply resources in this order:

{% stepper %}
{% step %}
**`secret-mssql.yaml`**

Stores the SQL login/password or Entra service-principal credentials.
{% endstep %}

{% step %}
**`depot-mssql.yaml`**

Registers host, database, driver, and binds purposes to the secret.
{% endstep %}

{% step %}
**`secret-git-sync.yaml`**

Repo credentials for git-sync.
{% endstep %}

{% step %}
**`config.yaml`**

Gateway points at the depot (`type: depot`, `address: dataos://<depot-name>`).
{% endstep %}

{% step %}
**`workspace-deploy.yaml`**

Workflow plus API; references `engine: mssql`, the depot, and the repo.
{% endstep %}
{% endstepper %}

Two wiring styles are supported. Use a depot gateway (`type: depot`, `address: dataos://<name>`), or bind an instance secret into the workflow with `envFrom` and let `config.yaml` read `MSSQL_*` through `env_var()`. Pick one style per project; do not mix a depot gateway with conflicting environment variables.

### Depot purposes

Each purpose grants scoped access through the same depot. Assign only the purpose required by each workload.

| Purpose | Used By                | Grants                                                            |
| ------- | ---------------------- | ----------------------------------------------------------------- |
| `rw`    | Vulcan workflow        | Read source tables and create, update, delete model output.       |
| `scan`  | Metadata scanner       | Read `INFORMATION_SCHEMA` and `sys.*` catalog views for metadata. |
| `query` | Consumer direct access | Read-only access to Data Product tables.                          |

## Materialization behavior per model kind

SQL models compile to T-SQL and execute directly on the server.

| Model kind                            | Compiles to                                                                  | Notes                                                                                                                 |
| ------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| SEED                                  | `bulk_copy` staged insert via a temp table, then `INSERT ... SELECT`         | Loads CSV-backed reference data.                                                                                      |
| FULL                                  | `SELECT * INTO` on first run; `TRUNCATE TABLE` + `INSERT` on subsequent runs | SQL Server has no atomic `CREATE OR REPLACE TABLE`, so the adapter switches strategy after the first materialization. |
| VIEW                                  | `CREATE OR ALTER VIEW`                                                       | Always a full rewrite; no partition tracking.                                                                         |
| INCREMENTAL\_BY\_TIME\_RANGE          | `DELETE` by `time_column` window, then `INSERT`                              | Re-runs are safe; the affected window is fully replaced.                                                              |
| INCREMENTAL\_BY\_UNIQUE\_KEY          | Native `MERGE INTO ... USING ... ON <key>`                                   | See [MERGE and the `mssql_merge_exists` optimization](#merge-and-the-mssql_merge_exists-optimization).                |
| INCREMENTAL\_BY\_PARTITION            | `DELETE` by partition key, then `INSERT`                                     | Generic delete-plus-insert fallback, not native partition switching.                                                  |
| INCREMENTAL\_UNMANAGED                | Append-only pattern                                                          | Vulcan does not manage updates or deletes.                                                                            |
| SCD\_TYPE\_2 / SCD\_TYPE\_2\_BY\_TIME | History table with validity windows tracked by time                          |                                                                                                                       |
| SCD\_TYPE\_2\_BY\_COLUMN              | History table with validity windows tracked by checked columns               |                                                                                                                       |
| EMBEDDED                              | Not materialized                                                             | Inlined into downstream models.                                                                                       |

### SQL Server-specific behaviors

A few SQL Server-specific behaviors to keep in mind:

* **Unsupported features:** Materialized views and tuple `IN` clauses.
* **Schema metadata:** Retrieved through `INFORMATION_SCHEMA`, not `DESCRIBE`.
* **Unconditional delete:** Optimized to `TRUNCATE TABLE`.
* **Upsert and insert-overwrite operations use** **`MERGE:`** Enable `mssql_merge_exists` to skip updates when the target row already exists and no changes are needed.

### MERGE and the `mssql_merge_exists` optimization

By default, the generated `MERGE` statement updates all non-key columns of an existing row whenever a new row with the same key arrives, even if every value is identical. Enable `mssql_merge_exists` in `physical_properties` to skip unnecessary updates by wrapping the `WHEN MATCHED` clause in an `EXISTS ... EXCEPT` comparison:

```sql
MODEL (
    name warehouse.gold.unique_key,
    kind INCREMENTAL_BY_UNIQUE_KEY (
        unique_key id
    ),
    cron '@daily',
    physical_properties (
        mssql_merge_exists = true
    )
);
```

{% hint style="warning" %}
**Not all column types are supported**

The `EXCEPT` operator that powers `mssql_merge_exists` does not support `GEOMETRY`, `XML`, `TEXT`, `NTEXT`, `IMAGE`, or most user-defined types. Do not enable the optimization on a table with those column types.
{% endhint %}

`mssql_merge_exists = true` also changes the default overwrite strategy for FULL-style overwrites: an unconditional overwrite without the flag uses `TRUNCATE` + `INSERT`, while setting the flag (or issuing a conditional overwrite) routes through `MERGE` instead.

### Bare `VARCHAR` truncation gotcha

SQL Server defaults an unqualified `VARCHAR` (no explicit length) to `VARCHAR(1)`, which silently truncates data on insert. Always declare an explicit length (`VARCHAR(255)`, `VARCHAR(MAX)`) on every text column in model DDL and seed schemas.

### Python models

Python models materialize an in-process Pandas DataFrame through the adapter. Large DataFrames are staged into a temp table and bulk-loaded with `pymssql`'s `bulk_copy`; datetime and date columns are stringified before load because `pymssql` does not natively convert `datetime64`/`date` types, and timezone-aware timestamps are converted to ISO strings and cast after load. Python models do not support VIEW, EMBEDDED, SEED, or MANAGED kinds.

## Identifier casing and quoting

SQL Server's default collation is case-insensitive but case-preserving, and identifiers containing spaces or reserved words are quoted with square brackets (`[My Column]`). Vulcan quotes bracketed identifiers automatically; avoid relying on case to distinguish two identifiers, since a case-sensitive collation on the instance will break lineage and semantic-layer references that assume the default behavior.

## Indexing

SQL Server supports native indexes (`CREATE INDEX`, unique constraints, clustered/non-clustered). Because SQL Server has no `CREATE INDEX IF NOT EXISTS`, guarded DDL wraps index creation in an existence check against `sys.indexes`:

```sql
IF NOT EXISTS (
    SELECT * FROM sys.indexes WHERE name = 'ix_orders_customer_id' AND object_id = OBJECT_ID('warehouse.gold.orders')
)
CREATE INDEX ix_orders_customer_id ON warehouse.gold.orders (customer_id);
```

Add indexes on the incremental `time_column` / `unique_key` and on hot filter/join columns for the semantic API; performance on a fixed-size instance is far more sensitive to missing indexes than on an elastic warehouse.

## Lifecycle DDL and raw T-SQL

Wrap DDL in `pre_statements` / `post_statements` when it should run only during real execution, or a dry-run plan executes it:

```sql
@IF(
    @runtime_stage = 'evaluating',
    CREATE OR ALTER TRIGGER trg_orders_audit ON warehouse.gold.orders AFTER INSERT AS BEGIN SET NOCOUNT ON; END
)
```

Because SQL Server lacks `CREATE TABLE IF NOT EXISTS`, guarded lifecycle DDL for table-shaped objects is typically wrapped as `IF NOT EXISTS (SELECT * FROM sys.tables WHERE ...) EXEC(...)`.

## External source declarations

Declare non-owned source tables in `external_models.yaml` so linting and lineage resolve them. The physical table must exist only if a model actually selects from it.

## Semantic models and metrics

Use `{semantic_model.column}` braces for measure references and declare joins for multi-entity queries; cross-join-shaped semantic queries are one of the easiest ways to saturate a single instance. Metrics use the current `kind: metric` shape with `ts` and `granularity`. `COUNT(DISTINCT ...)` over wide tables is a primary cost driver on a fixed-size instance; pre-aggregate or add supporting indexes. Native constraints (`NOT NULL`, `CHECK`, `PRIMARY KEY`, `FOREIGN KEY`, `UNIQUE`) are enforced at write time and complement Vulcan audits.

## Endpoints

Every deployed Data Product gets REST, GraphQL, and MySQL-wire endpoints, all pushing down to the same SQL Server gateway. Large result sets are buffered before streaming; size `api.resource.limit.memory` for expected payloads, with 3 GiB a practical starting point for high-row responses.

## Metadata scanning and catalog

DataOS scans `INFORMATION_SCHEMA` (databases, schemas, tables, columns, grants) and `sys.*` catalog views (`sys.indexes`, `sys.foreign_keys`, `sys.sql_modules` for view/procedure definitions). SQL Server has no warehouse-style query-history table by default (Query Store must be enabled separately and is not scanned), so lineage comes primarily from parsed definitions and Vulcan's model graph. Use a read-only scanner login separate from the Vulcan service login:

```sql
CREATE LOGIN vulcan_scanner WITH PASSWORD = '<strong-password>';
USE <database>;
CREATE USER vulcan_scanner FOR LOGIN vulcan_scanner;
ALTER ROLE db_datareader ADD MEMBER vulcan_scanner;
```

Scanner cadence is commonly every 6 to 12 hours. If lineage is missing after `vulcan apply`, verify `external_models.yaml`, then wait for the next scan.

## Engine-native feature support

| Feature                      | How                                                         | Notes                                                              |
| ---------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------ |
| Database                     | `connection.database` (must exist before apply)             |                                                                    |
| Schema                       | Referenced by model names                                   | Created automatically when the login has privilege.                |
| Regular / incremental table  | FULL and incremental models                                 |                                                                    |
| View                         | `kind VIEW`                                                 | `CREATE OR ALTER VIEW`.                                            |
| SCD history                  | `SCD_TYPE_2_BY_TIME` / `SCD_TYPE_2_BY_COLUMN`               |                                                                    |
| Indexes                      | `CREATE INDEX` in guarded `post_statements`                 | Native support; see [Indexing](#indexing).                         |
| Triggers / stored procedures | Guarded T-SQL DDL                                           | Objects created this way are structural but opaque to SQL lineage. |
| Computed columns             | Guarded `ALTER TABLE ... ADD ... AS (...)` DDL              |                                                                    |
| Row-level security           | `CREATE SECURITY POLICY` plus inline table-valued functions | DBA-level setup.                                                   |
| Materialized view            | Not supported by this adapter                               | `SUPPORTS_MATERIALIZED_VIEWS` is off.                              |
| Table cloning                | Not supported                                               | No `SUPPORTS_CLONING` path.                                        |
| MANAGED model kind           | Not supported                                               | Snowflake Dynamic Tables equivalent only.                          |

{% hint style="info" %}
Boundaries: MANAGED model kind, materialized views, table cloning, elastic scale-out/auto-suspend, and cross-database queries in a single incremental strategy on `SINGLE_CATALOG_ONLY` connections (Azure SQL). Treat the server lifecycle, instance creation, licensing, replication, and Always On configuration as DBA boundaries.
{% endhint %}

## Operational boundaries

### Compute sizing

A 2 to 4 vCPU, 4 to 8 GiB instance is usually enough for dev. For first backfills, temporarily raise `tempdb` size and disk headroom, since `SELECT * INTO` and bulk MERGE both spill through `tempdb`. Index the incremental `time_column` / `unique_key` and size IOPS to write volume. For the semantic API and concurrent BI, watch connection count and consider a connection pool in front of the instance.

### Concurrency

Start with `concurrent_tasks: 2` for daily incrementals; raise it only if plan/run duration is the bottleneck and the instance has connection and I/O headroom. The API is stateless, but each replica consumes SQL Server connections, so pair replica increases with a pooler or higher `max server memory` / connection capacity.

### Scheduling and latency floor

Schedule after upstream lands, set `timezone: UTC`, set `endOn` 1 to 2 years out, and use `concurrencyPolicy: Forbid` (prevents overlapping windows and connection pile-ups, and avoids concurrent MERGE writers on the same table). The latency floor: typically no cold start on a provisioned instance; connection establishment is sub-second when warm; queries on unindexed filters pay table-scan cost; the first-run `SELECT * INTO` on a large FULL model is slower than subsequent `TRUNCATE` + `INSERT` runs.

## Performance reference

Measure active connections, plan/run duration per model, semantic query wall time, rows touched per incremental window, and the table-scan versus index-seek ratio (`SET STATISTICS IO ON` / execution plan). When concurrency degrades, fix in order: remove cross-join-shaped queries, add missing indexes on hot filter/join columns, add a connection pooler, size up the instance, then investigate `tempdb` contention on bulk loads and MERGE statements. Reproduce measurements on your own instance before quoting them externally.

## Troubleshooting

### Failure modes

The errors you are most likely to hit, with cause and fix.

| Symptom                                   | Likely cause                                                                     | Fix                                                                                       |
| ----------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `database "warehouse" does not exist`     | Target database missing or wrong name                                            | Create the database; fix connection.database.                                             |
| Login failed for user                     | Wrong password, or login not mapped to a database user                           | Fix the password; confirm `CREATE USER ... FOR LOGIN ...` was run in the target database. |
| String or binary data would be truncated  | Bare `VARCHAR` defaulted to length 1                                             | Declare explicit column lengths in model/seed DDL.                                        |
| `relation raw.raw_orders does not exist`  | Missing table, grant, or external declaration                                    | Grant `SELECT`; add to `external_models.yaml`.                                            |
| Plan succeeds but SSMS shows empty schema | Different login/database used for debugging                                      | Use the same database and a login with `db_datareader`.                                   |
| Semantic column does not exist            | Bracket/quoting mismatch                                                         | Quote identifiers consistently across models and semantic files.                          |
| Depot not resolvable on DataOS            | Depot name mismatch                                                              | Match `dataos://<depot-name>?purpose=rw` to the depot manifest.                           |
| `PermissionError: '.logs'`                | CWD not writable                                                                 | Run `vulcan --log-to-stdout ...` or make the directory writable.                          |
| `vulcan plan` runs DDL                    | Lifecycle DDL not guarded                                                        | Wrap in `@IF(@runtime_stage = 'evaluating', ...)`.                                        |
| Incremental reprocesses history           | Missing/wrong `time_column` or `unique_key`                                      | Verify the model kind config matches actual columns.                                      |
| Endpoint OOM on large result              | API memory too low                                                               | Raise API memory and limit result size.                                                   |
| Too many connections                      | Workers plus replicas plus BI exceed capacity                                    | Lower concurrency/replicas; add a connection pooler.                                      |
| `mssql_merge_exists` throws on `EXCEPT`   | Unsupported column type (`GEOMETRY`, `XML`, `TEXT`, `NTEXT`, `IMAGE`, most UDTs) | Disable the optimization for that table.                                                  |
| `pymssql` fails to build from source      | Local build lacks required headers                                               | Use the wheel/binary supplied by the Vulcan distribution, or switch to `driver: pyodbc`.  |

### Health-check queries

Run these queries to verify the current database, login, and active sessions.

```sql
SELECT DB_NAME() AS current_database, SUSER_SNAME() AS current_login;

SELECT s.name AS schema_name, t.name AS table_name
FROM sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id
ORDER BY s.name, t.name;

SELECT session_id, status, login_name, host_name
FROM sys.dm_exec_sessions WHERE is_user_process = 1;
```

### Recovery procedures

Use these procedures to restore failed workflows and resolve operational interruptions.

| Situation                              | Procedure                                                                               |
| -------------------------------------- | --------------------------------------------------------------------------------------- |
| Incremental run failed mid-window      | Re-run; time-range and partition incrementals reprocess the affected window.            |
| Table dropped manually                 | `vulcan plan` detects and recreates; use a rebuild command if needed.                   |
| Wrong database applied                 | Fix the connection/database setting, verify with `SELECT DB_NAME()`, re-run plan/apply. |
| Credentials leaked                     | Rotate the password; update `.env` / DataOS instance secret.                            |
| Connection exhaustion                  | Lower concurrency/replicas, add a pooler, investigate blocked/sleeping sessions.        |
| MERGE deadlock between concurrent runs | Set `concurrencyPolicy: Forbid`; stagger heavy MERGE-based models.                      |

## Related

* [Engine guide overview](/references/v1/engine-guide/engine-guide.md) for the cross-engine rules.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://v2.dataos.info/references/v1/engine-guide/sql-server.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
