SQL Server driver
Native Microsoft SQL Server as a built-in driver, next to MySQL, PostgreSQL and SQLite — no extra plugin to install. Phase 1 is done on the `feat/sql-server` branch. Not on `main` yet, not in any release. Read-only for now: connect, browse schemas and tables, run SELECT queries. Phase 2 adds editing, TLS options and composite primary keys — that's where help's wanted.
Why built-in, not a plugin
The plugin system (JSON-RPC over stdin/stdout, one subprocess per driver) is how DuckDB, Google Sheets, and Redis reach the app today. For SQL Server that model loses on four concrete axes:
- Streaming latency. Plugin drivers serialise every row through a JSON-RPC frame to the host process. On queries returning 100k+ rows that's a measurable hit; for the in-process built-in, the row hand-off is a
Vec<Row>move. - Capability flags. The ER diagram's batch snapshot, the data-grid paging loop and the explain tree all branch on
DriverCapabilities. Built-ins set those natively from the trait; plugins translate them through a JSON manifest, which drifts. - Credential + pool reuse. SSH tunnels, the keychain-backed credential cache, and the connection-health pinger hold
Arc<T>state inside the host binary. Plugin drivers re-implement the moving parts they need; built-ins share one pool manager. - Install step. The plugin manager UI exists and works, but expecting users to visit it for SQL Server specifically is the wrong default. Most of the target audience would hit it on day zero.
Trade-offs: ~2.5 MB added to the release binary (tiberius + deadpool + tokio-util compat layer), and the driver now ships on the same release cadence as the rest of the app — no independent fix channel.
Phase 1 — Done on feat/sql-server
Read-only preview. Lives on feat/sql-server. Not on main, not in any release. The driver is registered in src-tauri/src/lib.rs peer to the other three built-ins, and the UI honours the readonly: true manifest flag by hiding INSERT / UPDATE / DELETE automatically.
- SQL auth connect, schema listing (
sys.schemasfiltered against role schemas), table / view / routine discovery execute_querywith streaming overtiberius::Client::query; pagination via dialect-awarebuild_paginated_query_dialectinsrc-tauri/src/drivers/common/query.rsOFFSET … FETCH NEXTsynthesised withORDER BY (SELECT NULL)when the caller query has no top-levelORDER BY(paren-depth-aware matcher, documented false positives for string literals)- Type extraction keyed on
tiberius::ColumnType: int family, float family,Decimal+Numericfallback for NUMERIC(38),Uuid, chrono temporal types,varbinary→ base64,xml,sql_variant - Runtime version detection from
SERVERPROPERTY('ProductMajorVersion'); default major = 14 (2017) when parsing fails; feature gates for 2012 (supports_offset_fetch) and 2017 (supports_string_agg) - ER-diagram batch endpoints:
get_all_columns_batch+get_all_foreign_keys_batch+get_schema_snapshot - 471 Rust tests, 0 regressions; every pure util (query builders, SQL constants, decimal normalizer, datetime formatters) has co-located
#[cfg(test)] mod tests
Phase 2 — Open
Six issues. Independent where the dependency column is empty. First is good first issue.
| # | Task | Area | Depends on |
|---|---|---|---|
| #144 | ConnectionParams: trust_server_certificate, encrypt, instance_name, domain, auth_mode |
good first issue | — |
| #145 | delete_record_composite / update_record_composite trait defaults + commands.rs wiring |
rust | — |
| #146 | FK aggregation via STRING_AGG (2017+) with FOR XML PATH fallback for 2012–2016 |
rust | — |
| #147 | IDENTITY_INSERT ON/OFF wrapper + transactional guard |
rust | — |
| #148 | DataGrid pkColumns?: string[] + Editor composite invoke + SchemaDiagram group-by-constraint |
ts / react | #145, #146 |
| #149 | Flip manifest readonly:false, manage_tables:true (closes Phase 2) |
close-out | #144, #145, #146, #147, #148 |
Phase 3 — Planned
- DDL generation:
sp_renamefor renames,IDENTITY(1,1)instead of SERIAL, theDROP CONSTRAINT … ADD CONSTRAINT …dance forALTER COLUMN DEFAULT SET SHOWPLAN_XML ON(estimated) andSET STATISTICS XML ON(actual) parsed withquick-xmlinto the existingExplainPlantree;PhysicalOp,EstimatedTotalSubtreeCost,ActualElapsedmsmapped one-to-onesys.triggers,sys.computed_columns,sys.extended_propertiesfor trigger lists, computed-column definitions andMS_Description- Azure AD via
AuthMethod::AADToken+azure_identity::DefaultAzureCredential, feature-gated behindazure-auth - Windows Integrated via
AuthMethod::Integratedunder#[cfg(windows)]
Not yet issue-tracked. Comment on #150 if you want to scope one.
Architecture
Module layout
Everything lives under src-tauri/src/drivers/sqlserver/:
| File | Contents |
|---|---|
mod.rs |
SqlServerDriver struct + impl DatabaseDriver (manifest, CRUD routes, batch snapshot) |
pool.rs |
Custom deadpool::managed::Manager wrapping tiberius::Client over tokio::net::TcpStream via tokio-util::compat; build_config from ConnectionParams |
helpers.rs |
bracket_quote / quote_identifier / qualify / escape_single_quoted — pure, unit-tested |
version.rs |
SERVERPROPERTY parsing + feature gates (supports_offset_fetch, supports_string_agg, supports_drop_if_exists) |
introspection.rs |
sys.* + INFORMATION_SCHEMA.* metadata queries; pure builders build_table_column / build_foreign_key |
extract/mod.rs |
ColumnType-keyed dispatcher turning tiberius rows into serde_json::Value |
extract/temporal.rs |
Pure chrono formatters for date / time / datetime / datetimeoffset |
Pagination lives outside the driver. The PaginationDialect enum and build_paginated_query_dialect are in src-tauri/src/drivers/common/query.rs and are shared with the other three built-in drivers. The legacy build_paginated_query(q, ps, p) signature still produces the same output it always has.
Version-gated features
SERVERPROPERTY('ProductMajorVersion') is parsed on first use and cached per pool. Default = 14 (2017) when parsing fails.
| Feature | Min major | Fallback below threshold |
|---|---|---|
OFFSET … ROWS FETCH NEXT |
11 (2012) | ROW_NUMBER() OVER (…) CTE |
STRING_AGG |
14 (2017) | FOR XML PATH('') + STUFF |
DROP TABLE IF EXISTS |
13 (2016) | IF OBJECT_ID(…) IS NOT NULL guard |
The fallback branches are not all implemented yet — they land alongside the Phase 2 / Phase 3 features that need them.
Type coverage
- Numeric:
BIT,INT1/INT2/INT4/INT8/INTN,FLOAT4/FLOAT8/FLOATN,MONEY/MONEY4,DECIMALN/NUMERICN(viarust_decimal::Decimalwithtiberius::numeric::Numericfallback for NUMERIC(38) beyond Decimal's range) - Temporal:
DATETIME,DATETIME2,DATETIME4,DATEN,TIMEN,DATETIMEOFFSETN— formatted to match the MySQL driver's output, trailing zeros in fractional seconds trimmed - Strings:
TEXT,NTEXT,BIGCHAR,BIGVARCHAR,NCHAR,NVARCHAR,XML - Binary:
IMAGE,BIGBINARY,BIGVARBIN→ base64-encoded - Other:
GUID→ UUID string;UDTandSSVARIANTfall back to best-effort string
Non-finite floats (NaN, ±Inf) are returned as strings instead of JSON numbers (which aren't valid JSON). Decimal-to-string conversion trims insignificant trailing zeros — 3.1400 becomes 3.14.
Invariants enforced at review
- No GPL-licensed code copied from other open source SQL clients. The driver is written against Microsoft TDS / T-SQL docs and observable server behaviour; Tabularis stays Apache-2.0.
- New struct fields are
Option<T>/Vec<T>with#[serde(default)]+skip_serializing_if. Saved connections from previous releases must deserialize untouched. MySQL / Postgres / SQLite drivers stay byte-identical — caught by the fullcargo test --libsuite. - Every pure helper ships with
#[cfg(test)] mod testsin the same PR. Happy path and at least one edge case. SQL-string constants count as pure helpers — assert the query contains the expectedsys.*/INFORMATION_SCHEMA.*tables and@P1/@P2placeholders.
Local setup
Start a throwaway SQL Server 2022 (~1.5 GB image, pulls once):
docker run -e 'ACCEPT_EULA=Y' \
-e 'MSSQL_SA_PASSWORD=Strong!Pass123' \
-p 1433:1433 \
mcr.microsoft.com/mssql/server:2022-latest
The driver isn't on main. Clone, switch to feat/sql-server, confirm the baseline is green before you touch anything:
git clone https://github.com/TabularisDB/tabularis.git
cd tabularis
git checkout feat/sql-server
cargo test --lib # expect 471 passing, 0 failed
npm install
npm run typecheck
Branch off feat/sql-server and target your PR at it — not at main. The whole branch squashes into main once Phase 2 closes.
Launch the app (cargo tauri dev) and add a connection — driver SQL Server, host localhost, port 1433, user sa, password Strong!Pass123, database master. You should see dbo, sys, INFORMATION_SCHEMA in the schema tree and be able to run SELECT TOP 10 * FROM sys.objects.
Pick a sub-issue from the Phase 2 table above. Comment on it before you start — avoids two people on the same task and lets reviewers flag edge cases early.
Claiming an issue
- Comment on the issue ("I'd like to take this"). No formal assignment needed — first-come, first-served. The courtesy comment just avoids duplicate work.
- Stuck for more than a couple of days? Drop an update so others know the slot is either moving or freeing up.
- Small PRs land faster. Splitting an issue into a backend PR and a frontend PR is encouraged when the issue spans both.
Closes #NNNtrailer in the PR description so the issue closes on merge.- Design questions, alternative approaches, blockers → epic #150. We read every comment.
