Microsoft SQL Server access governance — every statement reviewed before it runs
Last updated
AccessFlow governs Microsoft SQL Server as a full query proxy: your team connects to AccessFlow, not to the database, and every statement is parsed, checked against a schema allow-list, risk-scored, routed to an approver, and only then executed — under column masking, row-level security and a row cap. The pipeline itself — parse, risk-score, route, approve, execute, audit — is the same for every relational engine and is described in full on the database access governance page. What follows is what is specific to Microsoft SQL Server.
- Family
- Relational
- Query language
- SQL
- Runs as
- In-process pooled JDBC
- Default port
1433- Default SSL mode
REQUIRE- Install
- One-click from the connector catalog
The driver and the connection
The driver is com.microsoft.sqlserver:mssql-jdbc, resolved from the connector catalog on first use and verified before it loads.
The datasource takes host, port (1433 by default), database, username and password — AES-256-GCM encrypted at rest, decrypted once at pool initialisation, or held as a secret-manager reference. SSL defaults to REQUIRE.
AccessFlow builds the connection string from the datasource fields using the template the connector manifest declares, so what actually reaches the driver is predictable and reviewable:
jdbc:sqlserver://{host}:{port};databaseName={database_name}
com.microsoft.sqlserver.jdbc.SQLServerDriver
Drivers are resolved on demand — the first time a datasource of this type is created or tested — and each one gets its own classloader. Two datasources pointing at different driver versions, even two Microsoft SQL Server datasources, load disjoint copies and cannot interfere with each other through static state.
What AccessFlow parses
Statements are parsed to a syntax tree and the schema allow-list is checked against that tree rather than the raw text. T-SQL bracket-quoted identifiers are read as identifiers, and the resulting classification drives the capability check and the approval chain.
What it refuses
Multi-statement input is refused, except for a BEGIN; … COMMIT; envelope around a homogeneous write batch. Unparseable input is rejected with HTTP 422 before a connection is used.
Row-level security and masking
A policy becomes a bound predicate in the WHERE clause, and shapes the rewriter cannot provably narrow are refused rather than run unfiltered. Masking is applied after fetch. The interaction between those bound values and the SHOWPLAN path is the one place SQL Server behaves differently from the other engines — see below.
What the rewrite actually does
A policy restricting claims to the caller's region narrows the statement before SQL Server compiles it:
-- submitted SELECT TOP 100 [id], [amount] FROM [claims] WHERE [state] = 'OPEN'; -- executed (policy: claims.region = the caller's region) SELECT TOP 100 [id], [amount] FROM [claims] WHERE ([state] = 'OPEN') AND [claims].[region] = ?;
This is also the exact statement whose dry-run degrades. That bound parameter is the thing a plain Statement cannot carry, which is why the plan preview reports itself unsupported rather than planning something other than what would run. Execution is unaffected.
Introspection, dry-run and cost estimates
A dry-run plans the statement with SET SHOWPLAN_ALL ON. This is the only place in the entire proxy that uses a plain JDBC Statement rather than a PreparedStatement, and it is a documented, deliberate exception: the Microsoft driver returns no plan rows at all over the prepared path, so a language batch is the only shape that yields one. It carries no user values and it never executes the statement.
Where the dry-run degrades, and why that is the safe answer
A plain Statement cannot carry bound parameters, and row-level security emits its policy values as binds. Inlining them into the SQL text is not an option — that is precisely the string concatenation the proxy exists to prevent. So a dry-run of a statement whose rewrite produced binds reports itself unsupported, with a specific reason, rather than surfacing a driver error or quietly planning a statement other than the one that would run.
Real execution is unaffected: it binds the same predicate through a PreparedStatement exactly as every other engine does. The degradation is confined to the non-executing plan preview, which is the right place to lose a feature rather than a guarantee.