Skip to content

feat(mssql): a SQL Server engine family (P4) - #2

Merged
jtomaszewski merged 2 commits into
fshfrom
fsh-mssql-engine
Oct 12, 2026
Merged

jtomaszewski merged 2 commits into
fshfrom
fsh-mssql-engine

Conversation

@jtomaszewski

@jtomaszewski jtomaszewski commented Oct 12, 2026 •

Copy link
Copy Markdown

Problem

treeman gives every Conductor worktree its own Postgres database plus a clone per test worker. A project whose source of truth is SQL Server got nothing — one shared container on a fixed name and port, hardcoded row ids, and a test tier that two worktrees (or two jest workers) cannot run at the same time.

This adds mssql as a full engine family: per-worktree databases, cached snapshot templates, per-test-worker clones, pre-warmed spares, prefix reclaim, branch_scoped, doctor/MCP coverage.

connections:
  mssql:
    host: 127.0.0.1
    port: 14333
    user: sa
    password: $MSSQL_SA_PASSWORD
    collation: Polish_CI_AS
databases:
  - engine: mssql              # or `sqlserver`
    name_template: tm_app_{slug}
    migrate: { run: ./migrate.sh, env: { DB_DATABASE: "{target_db}" } }
    test_clones: { clones: 8, name_template: tm_app_{slug}_w{n} }

How a clone works, and why it isn't a TEMPLATE

SQL Server has no CREATE DATABASE … TEMPLATE, and the two things that look like one are not: DBCC CLONEDATABASE copies schema and statistics without rows, and a database snapshot is read-only. The only database copy a client can drive is BACKUP + RESTORE.

So a template here is a pair — the template database (which is what treeman's DatabaseExists cache-hit probe asks about, identically for every engine) plus one .bak file beside it that every clone restores from. Captured once, hydrated N times.

Everything goes over TDS, the file paths included: TO DISK, FROM DISK and MOVE … TO all accept variables, so paths are bound as parameters and only database names are interpolated — through the same strict [A-Za-z0-9_] validator the other engines use (ident.QuoteMSSQL, bracket-quoted so QUOTED_IDENTIFIER can't matter).

Three engine differences worth a reviewer's attention

  • Restored files carry a random token, not the database's name. The spare claim is ALTER DATABASE … MODIFY NAME, which moves the database but not its files — so a claimed spare keeps files named for the spare, and naming files after the database would collide on the next restore of that freed name. TestClaimSpareRename covers exactly that sequence.
  • Drops take the database single-user first. There is no pg_terminate_backend, and DROP DATABASE fails outright while anybody is connected — which a jest worker's idle pool routinely is. TestDropMatchingEvictsConnections holds a live connection open across the drop.
  • dump: is rejected, with the reason in the message. RESTORE reads a path on the server, which for a containerised server is a different filesystem; a host path would silently restore whatever happens to sit there, or nothing. Seed from seed: instead — it is a command, so it can copy the file to the server. db_dump refuses for the same reason (sqlcmd is not a pg_dump equivalent).

WriteWatermark declines (returns "") rather than guessing. The caller skips a capture when the token is unchanged, and SQL Server's per-database write counters are per index — writes to a heap move nothing, and the rows vanish on restart — so a token that can miss a write is worse than none.

Beyond the registry

docs/internals.md promised "≤4 touchpoints". The registry does cover prepare, teardown, GC and recovery, but four places switch on the concrete adapter type instead, and each fails silently: prepareFamily, connectBranchEngine (branch_scoped quietly no-ops), branchreap's untrackedMarker + listDurableCandidates (durables leak forever), and resolve's credential copy-back (a password: $NAME ref resolves to empty). That section now names all four. snapshot.SpareCounts was also hardcoded to Postgres — stale since MySQL became prewarm-capable — so it now walks every prewarm-capable family.

Verification

  • go test ./... — 1252 pass. golangci-lint run ./... — clean (two pre-existing findings in internal/daemon/socket_unix_other.go remain, confirmed present before this branch). golangci-lint fmt --diff clean. Schemas and generated docs regenerated.
  • go test -tags e2e ./e2e/mssql/ — 12 pass, against SQL Server 2022 (the suite's image) and 2025 (by hand). The prepare cycle end to end — cold build → cache hit → input edit → rebuild → fan-out → teardown keeping the cache — plus the driver's own behaviour: eight concurrent restores of one template, the configured collation reaching the databases treeman creates (and SIMPLE recovery reaching the restored ones), LIKE-escaping against the sibling-wipe, the rename race having exactly one winner and leaving the loser usable, a drop with a live connection open, and a restore after the backup file was deleted behind the template's back.
  • Registered in BATCHES[engines], so just test-e2e engines runs it. Note SQL Server ships no arm64 image: on an Apple Silicon host the suite runs under emulation (it does pass there — that is where these numbers come from), and the compose file caps the server at 2 GB because its default is "all of it".

Fork bookkeeping

This is the first patch that edits upstream's own files rather than only adding ours — an engine family has to touch the engine registry, the config schema, prepare, resolve and the docs. FORK.md carries the P4 row (why, upstream intent, drop-when) and fsh-vet.yml's allowlist carries the blast radius; the allowlist was checked against the actual diff and covers it exactly.

One dependency added, github.com/microsoft/go-mssqldb v1.11.2: no OSV advisory of its own, and it drags golang.org/x/crypto from 0.33.0 to 0.55.0, clearing 34 of the 37 advisories that single stale indirect contributes to the baseline. The three left on 0.55.0 are all already baselined, so the deps check stays green with no re-baseline. Its Azure, Key Vault and Kerberos modules are in its go.mod but not in ours — they belong to packages we do not import, and module-graph pruning keeps them out of go.sum.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

jtomaszewski and others added 2 commits October 12, 2026 12:01
treeman gives every Conductor worktree its own Postgres database and a
clone per test worker. A project whose source of truth is SQL Server got
nothing: one shared container, hardcoded ids, and a test tier that two
worktrees — or two jest workers — cannot run at once.

SQL Server has no `CREATE DATABASE … TEMPLATE`, and the two things that
look like one are not: `DBCC CLONEDATABASE` copies schema and statistics
without rows, and a database snapshot is read-only. The only copy a
client can drive is BACKUP + RESTORE, so a template here is a pair — the
template database, which is what treeman's cache-hit probe asks about
for every engine, plus one `.bak` beside it that each clone restores
from. Captured once, hydrated N times.

Everything runs over TDS, file paths included: `TO DISK`, `FROM DISK` and
`MOVE … TO` all take variables, so only database names are interpolated,
through the same strict `[A-Za-z0-9_]` validator the other engines use.

Three engine differences worth knowing:

* Restored files carry a random token rather than the database's name.
  The spare claim is `ALTER DATABASE … MODIFY NAME`, which moves the
  database but not its files, so a claimed spare keeps files named for
  the spare — and naming by database would collide on the next restore
  of that freed name.
* Drops take the database single-user first. There is no
  pg_terminate_backend, and DROP fails outright while anyone is
  connected, which a jest worker's idle pool routinely is.
* `dump:` is rejected, with the reason in the message: RESTORE reads a
  path on the server, which for a containerised server is a different
  filesystem. Seed from `seed:` instead, which is a command and can get
  a file there. `db_dump` refuses for the same reason.

`WriteWatermark` declines rather than guessing. The caller SKIPS a
capture when the token is unchanged, and SQL Server's per-database write
counters are per index — writes to a heap move nothing — so a token that
can miss a write is worse than none.

Verified against SQL Server 2022 and 2025 (Developer): 12 e2e tests
covering the prepare cycle end to end and the driver's own behaviour —
eight concurrent restores of one template, the collation reaching the
databases treeman creates, LIKE-escaping against the sibling-wipe, the
rename race having exactly one winner, a drop with a live connection
open, and a restore after the backup file was removed behind the
template's back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream's `ci.yml` triggers on PRs to main/master only, so a PR into
`fsh` — which is every patch this fork carries — reaches merge with no
build, vet or test gate at all. The three vet jobs answer questions
about upstream and about the shape of our diff; none of them compiles
anything.

Not a copy of upstream's job: no lint (the flake-pinned golangci-lint
is a nix download this job's egress policy would have to admit, and a
finding in upstream's tree is not ours to gate on), no macOS matrix,
no e2e tier — that one needs docker engines and is run by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgolang/​github.com/​microsoft/​go-mssqldb@​v1.11.274100100100100
Updatedgolang/​golang.org/​x/​sys@​v0.46.0 ⏵ v0.47.084100100100100
Updatedgolang/​golang.org/​x/​sync@​v0.21.0 ⏵ v0.22.099 +1100100100100

View full report

@jtomaszewski
jtomaszewski merged commit 39274ef into fsh Oct 12, 2026
7 checks passed
@jtomaszewski
jtomaszewski deleted the fsh-mssql-engine branch October 12, 2026 10:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant