Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ The integration tests require a running SQL Server instance configured in `test/

Unit tests (`npm run test-unit`) do **not** require a database, so you can develop and validate most changes without a SQL Server instance.

For the cases the dev container cannot cover — exercising `msnodesqlv8` against the `SQL Server Native Client 11.0` driver that `node-mssql` defaults to on Windows, or testing behaviour that depends on the SQL Server edition — `contrib/azure-sql-vm` provisions a Windows VM running SQL Server Developer edition. It creates billable Azure resources and is not exercised by CI, so read its [README](contrib/azure-sql-vm/README.md) first.

## Coding Standards

- **Style:** The project uses [StandardJS](https://standardjs.com/) — no semicolons, 2-space indentation, single quotes. There is no config file or override; run `npm run lint` to check your work, and `npx standard --fix` to auto-fix many issues.
Expand Down
191 changes: 191 additions & 0 deletions contrib/azure-sql-vm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Azure SQL Server test VM

A [Bicep](https://learn.microsoft.com/azure/azure-resource-manager/bicep/) template that
provisions a Windows VM with SQL Server Developer edition pre-installed, for integration
testing `node-mssql` against a full SQL Server engine.

> [!WARNING]
> This creates **billable Azure resources** in your own subscription, and it is **not
> exercised by CI** — nobody is alerted when an API version or marketplace SKU goes stale.
> Treat it as a contributor convenience, not supported infrastructure. See
> [Cost and teardown](#cost-and-teardown).

## Why this exists

The [dev container](../../.devcontainer) runs SQL Server for Linux in Docker and covers the
`tedious` driver for most work. Two things it does not cover:

- **`npm run test-msnodesqlv8` on Windows.** On Windows, `node-mssql` defaults to the
`SQL Server Native Client 11.0` ODBC driver rather than `ODBC Driver 17 for SQL Server`
(see [`lib/msnodesqlv8/connection-pool.js`](../../lib/msnodesqlv8/connection-pool.js)).
That is the code path Windows users hit, and CI only exercises it in the `test-windows`
job — the Linux job's msnodesqlv8 step is commented out.
- **Engine-edition-dependent behaviour.** Features such as CLR and user-defined types depend
on the SQL Server edition. If your Docker host cannot run Microsoft's SQL Server image and
you have substituted Azure SQL Edge, that engine reports `EngineEdition` 9 and has no CLR,
so those tests fail for reasons unrelated to your change.

If neither applies to you, use the dev container — it is faster and free.

## Deploying

Requires the [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) and an
authenticated subscription (`az login`).

The template takes two separate passwords: one for the Windows administrator and one for the
SQL login. Read them without echoing, so they do not land in your shell history:

```bash
read -rs -p 'Windows admin password: ' ADMIN_PW; echo
read -rs -p 'SQL login password: ' SQL_PW; echo

az group create -n rg-mssql-test -l uksouth

az deployment group create -g rg-mssql-test -f sqlvm.bicep \
-p adminPassword="$ADMIN_PW" \
sqlAdminPassword="$SQL_PW" \
allowedSourceIp="$(curl -s https://api.ipify.org)"
```

Command-line arguments are still visible to other users of the same machine via the process
list. If that matters, put the parameters in a `chmod 600` JSON file and use
`--parameters @params.json` instead, deleting it afterwards.

By default this opens **RDP only**. The usual workflow is to RDP in and run the suites on the
VM itself against `localhost`, which needs no exposed SQL port. Pass `exposeSqlPort=true` only
if you want to drive the VM from your own machine.

### Parameters

| Parameter | Default | Notes |
| --- | --- | --- |
| `adminPassword` | *(required)* | Windows administrator password, used for RDP. |
| `sqlAdminPassword` | *(required)* | Password for the SQL login. Deliberately separate from `adminPassword`: this one gets written to `test/.mssql.json` in cleartext and crosses the wire, so it should not also unlock RDP as administrator. |
| `allowedSourceIp` | `''` | Single IP allowed to reach the VM. **Empty creates no inbound rules at all**, which is the safe default — pass your own public IP to reach it. |
| `exposeSqlPort` | `false` | Also open SQL (1433) to `allowedSourceIp`, and set the SQL VM connectivity type to `PUBLIC`. Leave off unless you are connecting from outside the VM. |
| `vmSize` | `Standard_B2ms` | Cheapest size that comfortably runs SQL Server. |
| `imageOffer` | `sql2022-ws2022` | Also `sql2019-ws2022`, `sql2017-ws2019`, `sql2016sp3-ws2019`. |
| `imageSku` | `sqldev-gen2` | Use `sqldev` for the older offers. |
| `vmName` | `sqlvm` | Name prefix, and the Windows computer name — ARM caps that at 15 characters, so the template enforces it. Set it when deploying several VMs into one resource group. |
| `adminUsername` | `mssqladmin` | Windows administrator username. |
| `sqlAdminUsername` | `sqladmin` | SQL sysadmin login created via the SQL VM resource provider. |
| `location` | resource group's location | |

Never widen `allowedSourceIp` to `0.0.0.0/0` — with `exposeSqlPort` on, that publishes SQL
Server to the whole internet.

## Two non-obvious steps

**The `Microsoft.SqlVirtualMachine` resource is not optional.** Deploying the marketplace
image as a plain VM leaves you with no usable SQL login: `sa` is disabled,
`NT AUTHORITY\SYSTEM` is not a sysadmin, and the only sysadmin is that disabled `sa`. You
cannot then create a login without single-user-mode recovery. The template registers the VM
with the SQL IaaS agent, which creates a sysadmin login through a supported API. `sa` stays
disabled — use `sqlAdminUsername`.

**Install SQL Server Native Client before running the msnodesqlv8 suite.** The marketplace
image ships only ODBC Driver 17, but `node-mssql` asks for `SQL Server Native Client 11.0` on
Windows, so connections fail until it is present. On the VM:

```
msiexec /i sqlncli.msi IACCEPTSQLNCLILICENSETERMS=YES /qn /norestart
```

The installer is published by Microsoft as
[`sqlncli.msi`](https://download.microsoft.com/download/B/E/D/BED73AAC-3C8A-43F5-AF4F-EB4FEA6C8F3A/ENU/x64/sqlncli.msi).

Setting `driver` in `test/.mssql.json` will **not** let you skip this step. Both harnesses
overwrite that field before it reaches the library — `test/msnodesqlv8/msnodesqlv8.js` sets
`cfg.driver = 'msnodesqlv8'`, which `connection-pool.js` treats as "unspecified" and replaces
with the platform default. The only override that survives is a full `connectionString`, which
bypasses the builder entirely:

```json
{
"connectionString": "Driver={ODBC Driver 17 for SQL Server};Server=localhost,1433;Database=master;Uid=sqladmin;Pwd=<sqlAdminPassword>;TrustServerCertificate=yes"
}
```

That works, but it stops exercising the default Windows code path — usually the reason for
using this VM at all.

## Running the tests

Point `test/.mssql.json` at the VM, using the shape in
[`.devcontainer/.mssql.json`](../../.devcontainer/.mssql.json). **The two drivers need
different TLS settings**, because the VM presents a self-signed certificate.

For `npm run test-tedious`:

```json
{
"user": "sqladmin",
"password": "<sqlAdminPassword>",
"server": "localhost",
"port": 1433,
"database": "master",
"requestTimeout": 30000,
"options": {
"abortTransactionOnError": true,
"encrypt": true,
"trustServerCertificate": true
}
}
```

`tedious` defaults `encrypt` to `true` and `trustServerCertificate` to `false` (see
[`lib/tedious/connection-pool.js`](../../lib/tedious/connection-pool.js)), so without the
explicit trust the handshake fails against a self-signed certificate. This matches what the
Linux CI job does.

For `npm run test-msnodesqlv8`:

```json
{
"user": "sqladmin",
"password": "<sqlAdminPassword>",
"server": "localhost",
"port": 1433,
"database": "master",
"requestTimeout": 30000,
"options": {
"abortTransactionOnError": true,
"encrypt": false
}
}
```

`trustServerCertificate` has no effect here. The msnodesqlv8 connection string is built from
only `Driver`, `Server`, `Database`, `Uid`, `Pwd`, `Trusted_Connection` and `Encrypt` (see
`lib/msnodesqlv8/connection-pool.js`), so the key is silently dropped and `Encrypt=yes` would
fail certificate validation with no way to override it. Turning encryption off is what the
Windows CI job does for exactly this reason. To test an *encrypted* msnodesqlv8 connection,
use the `connectionString` form above with `TrustServerCertificate=yes`.

If you set `exposeSqlPort=true` and are connecting from your own machine, replace `localhost`
with the deployment's `publicIp` output. Note that a self-signed certificate issued for the
VM's hostname will not match an IP literal, so the `connectionString` form is the only way to
get an encrypted connection in that case.

Then run the suites as described in [`CONTRIBUTING.md`](../../CONTRIBUTING.md).

## Cost and teardown

SQL Server Developer edition carries no SQL licence cost, but the compute rate for a Windows
VM includes a Windows Server licence, so it is higher than the equivalent Linux VM. You pay
for **compute, the OS disk and the static public IP**. Check the
[pricing calculator](https://azure.microsoft.com/pricing/calculator/) for your region rather
than relying on a figure quoted here.

Deallocating stops the **compute** charge only — the disk and the public IP keep billing:

```bash
az vm deallocate -g rg-mssql-test -n sqlvm
```

Deleting the resource group is the only thing that stops all charges. It is the single unit of
teardown, so this removes the VM, disk, IP, network and the SQL VM registration together:

```bash
az group delete -n rg-mssql-test --yes --no-wait
```
182 changes: 182 additions & 0 deletions contrib/azure-sql-vm/sqlvm.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// A Windows VM running SQL Server Developer edition, for integration testing against a full
// SQL Server engine and for exercising msnodesqlv8 on Windows, where node-mssql defaults to
// the `SQL Server Native Client 11.0` ODBC driver rather than `ODBC Driver 17 for SQL Server`
// (see lib/msnodesqlv8/connection-pool.js). The dev container covers the tedious driver
// against SQL Server on Linux; this covers the cases it cannot.
//
// This template is NOT exercised by CI, and it creates billable Azure resources.
// Read ./README.md before deploying it — it covers cost, teardown, and the two
// non-obvious steps needed before the msnodesqlv8 suite will connect.

@description('Location for all resources.')
param location string = resourceGroup().location

@description('VM size. B2ms is the cheapest that comfortably runs SQL Server.')
param vmSize string = 'Standard_B2ms'

@description('Local administrator username.')
param adminUsername string = 'mssqladmin'

@description('Local administrator password, used for RDP. Not used for SQL — see sqlAdminPassword.')
@secure()
param adminPassword string

@description('SQL Server sysadmin login created via the SQL VM resource provider. `sa` stays disabled.')
param sqlAdminUsername string = 'sqladmin'

@description('Password for sqlAdminUsername. Kept separate from adminPassword because this one is written to test/.mssql.json in cleartext and crosses the wire, and so should not also unlock RDP as administrator.')
@secure()
param sqlAdminPassword string

@description('Single IP allowed to reach the VM. Use your own public IP; empty means no inbound rules at all.')
param allowedSourceIp string = ''

@description('Also open SQL (1433) to allowedSourceIp. Off by default: the usual workflow is to RDP in and run the suites on the VM against localhost, which needs no exposed SQL port. Only turn this on to drive the VM from your own machine.')
param exposeSqlPort bool = false

@description('Resource name prefix. Also the Windows computer name, which ARM limits to 15 characters.')
@minLength(1)
@maxLength(15)
param vmName string = 'sqlvm'

@description('Marketplace offer, e.g. sql2022-ws2022, sql2019-ws2022, sql2017-ws2019, sql2016sp3-ws2019.')
param imageOffer string = 'sql2022-ws2022'

@description('Marketplace SKU, e.g. sqldev-gen2 (2022) or sqldev (older offers).')
param imageSku string = 'sqldev-gen2'

var name = vmName
var hasIp = !empty(allowedSourceIp)

var rdpRule = [
{
name: 'AllowRDP'
properties: {
priority: 110
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourceAddressPrefix: allowedSourceIp
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '3389'
}
}
]

var sqlRule = [
{
name: 'AllowSQL'
properties: {
priority: 100
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourceAddressPrefix: allowedSourceIp
sourcePortRange: '*'
destinationAddressPrefix: '*'
destinationPortRange: '1433'
}
}
]

resource nsg 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
name: '${name}-nsg'
location: location
properties: {
securityRules: hasIp ? (exposeSqlPort ? concat(rdpRule, sqlRule) : rdpRule) : []
}
}

resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
name: '${name}-vnet'
location: location
properties: {
addressSpace: { addressPrefixes: ['10.0.0.0/16'] }
subnets: [
{
name: 'default'
properties: {
addressPrefix: '10.0.0.0/24'
networkSecurityGroup: { id: nsg.id }
}
}
]
}
}

resource pip 'Microsoft.Network/publicIPAddresses@2023-11-01' = {
name: '${name}-pip'
location: location
sku: { name: 'Standard' }
properties: { publicIPAllocationMethod: 'Static' }
}

resource nic 'Microsoft.Network/networkInterfaces@2023-11-01' = {
name: '${name}-nic'
location: location
properties: {
ipConfigurations: [
{
name: 'ipconfig1'
properties: {
subnet: { id: vnet.properties.subnets[0].id }
publicIPAddress: { id: pip.id }
privateIPAllocationMethod: 'Dynamic'
}
}
]
}
}

resource vm 'Microsoft.Compute/virtualMachines@2024-07-01' = {
name: name
location: location
properties: {
hardwareProfile: { vmSize: vmSize }
storageProfile: {
// SQL Server Developer, pre-installed by Microsoft — no self-install, no SQL licence cost
imageReference: {
publisher: 'MicrosoftSQLServer'
offer: imageOffer
sku: imageSku
version: 'latest'
}
osDisk: {
createOption: 'FromImage'
managedDisk: { storageAccountType: 'StandardSSD_LRS' }
}
}
osProfile: {
computerName: name
adminUsername: adminUsername
adminPassword: adminPassword
windowsConfiguration: { provisionVMAgent: true, enableAutomaticUpdates: false }
}
networkProfile: { networkInterfaces: [{ id: nic.id }] }
}
}

// Registers the VM with the SQL IaaS agent and creates a sysadmin login. Without this the
// image has no usable SQL login at all — see the README's "Two non-obvious steps".
resource sqlvm 'Microsoft.SqlVirtualMachine/sqlVirtualMachines@2023-10-01' = {
name: name
location: location
properties: {
virtualMachineResourceId: vm.id
sqlManagement: 'Full'
sqlServerLicenseType: 'PAYG'
serverConfigurationsManagementSettings: {
sqlConnectivityUpdateSettings: {
connectivityType: exposeSqlPort ? 'PUBLIC' : 'PRIVATE'
port: 1433
sqlAuthUpdateUserName: sqlAdminUsername
sqlAuthUpdatePassword: sqlAdminPassword
}
}
}
}

output publicIp string = pip.properties.ipAddress
output rdp string = 'mstsc /v:${pip.properties.ipAddress}'
output sqlLogin string = sqlAdminUsername