>_STRYKE-MSSQL
Microsoft SQL Server / Azure SQL client for stryke. Parametrized query and execute, transaction batches, scalar/exists helpers, and schema introspection — over tiberius, the pure-Rust TDS driver. No FreeTDS, no ODBC. The cdylib owns one tokio runtime and presents a blocking facade, caching a client per (host, db, auth, encrypt). An opt-in package, kept out of the stryke core binary so the daily-driver install stays slim.
Install
Add the package, then use Mssql. On first use, stryke dlopens the cdylib in-process and registers every mssql__* export via rust_ffi::load_cdylib — only the symbols declared in stryke.toml's [ffi].exports are resolved.
s add github.com/MenkeTechnologies/stryke-mssql
The minimum target is SQL Server 2012 or any Azure SQL instance. The transport is tiberius (pure-Rust TDS) — there is no FreeTDS, ODBC, or system client library to install.
Quick start
use Mssql
var %conn = ( host => "localhost", database => "app",
username => "sa", password => $ENV{MSSQL_PASS}, trust_cert => 1 )
# parametrized query — @P1, @P2, … bound from params (never interpolated)
val @users = Mssql::query("SELECT id, name FROM users WHERE active = @P1", params => [1], %conn)
p len(@users)
# scalar (first column of first row) + exists (at least one row?)
p Mssql::scalar("SELECT COUNT(*) FROM users", %conn)
p Mssql::exists("SELECT 1 FROM users WHERE id = @P1", params => [42], %conn)
# write — returns rows affected
Mssql::execute("UPDATE users SET name = @P1 WHERE id = @P2", params => ["Ada", 42], %conn)
# transaction batch — commit on success, roll back if any statement fails
Mssql::batch(
["INSERT INTO audit (msg) VALUES ('x')", "UPDATE users SET seen = 1 WHERE id = 42"],
%conn,
)
Connecting
Every Mssql::* call that touches the server takes connection params as a trailing %opts hash. When neither url nor host is given and $MSSQL_URL is set, that ADO connection string is used as a fallback. A tiberius Client is cached per (host, port, database, auth, encrypt); a connection that errors is evicted and reopened on the next call.
| url | ADO string, e.g. Server=db;Database=app;User Id=sa;Password=…. Overrides the discrete keys below. |
| host | Server host. Default 127.0.0.1. |
| port | TDS port. Default 1433. |
| database | Initial database. Default is the server's default for the login. |
| username | SQL auth user. |
| password | SQL auth password. |
| encrypt | required (default), off, or not_supported — maps to the TDS EncryptionLevel. |
| trust_cert | When truthy, accepts a self-signed server certificate (dev only). Default off. |
API surface
The Mssql package exposes 36 functions across seven groups. Parametrized statements use @P1, @P2, … placeholders bound from params (an arrayref of null / bool / integer / float / string). Identifiers (table/column names) can't be parameterized — use the SQL helpers below for those.
| Liveness | version, ping, server_version |
| Query | query, query_one, scalar, exists, simple_query |
| Write | execute, insert (multi-row), batch (transaction) |
| DDL | create_table, drop_table, truncate |
| Introspection | databases, tables, columns, views, schemas, primary_keys, foreign_keys, indexes, table_exists, current_database, count, row_count |
| SQL helpers | quote_ident, quote_literal, valid_identifier, escape_like, format_value, format_in_list, split_batch |
| URL helpers | parse_url, redact_url, build_url |
Liveness
| version() | The package version string. No connection. |
| ping(%conn) | Connectivity probe (SELECT 1). Returns 1 on success. |
| server_version(%conn) | SELECT @@VERSION — the server version banner string. |
Query
| query($sql, %conn) | Run a SELECT. Returns an array of row hashrefs. params binds @P1… placeholders. |
| query_one($sql, %conn) | The first row hashref, or undef. |
| scalar($sql, %conn) | The first column of the first row (a scalar), or undef. |
| exists($sql, %conn) | True when the query returns at least one row. |
| simple_query($sql, %conn) | Non-parametrized statement (may contain several ;-separated statements). Returns the first result set as rows. |
val @rows = Mssql::query("SELECT id, name FROM users WHERE active = @P1", params => [1], %conn)
val $u = Mssql::query_one("SELECT * FROM users WHERE id = @P1", params => [42], %conn)
val $total = Mssql::scalar("SELECT COUNT(*) FROM users", %conn)
val $has = Mssql::exists("SELECT 1 FROM users WHERE id = @P1", params => [42], %conn)
Write
| execute($sql, %conn) | Run an INSERT / UPDATE / DELETE / DDL. Returns rows affected. params binds placeholders. |
| insert($table, $rows, %conn) | Multi-row insert from an arrayref of row hashrefs. Columns are the sorted keys of the first row; a missing key in a later row becomes NULL. Values are emitted as T-SQL literals. Returns rows affected. |
| batch($statements, %conn) | Run an arrayref of statements inside one transaction — BEGIN TRANSACTION … COMMIT on success, roll back if any statement fails. Returns total rows affected. |
Mssql::execute("UPDATE users SET name = @P1 WHERE id = @P2", params => ["Ada", 42], %conn)
Mssql::insert("dbo.users",
[ { id => 1, name => "Ada" }, { id => 2, name => "Linus" } ],
%conn)
Mssql::batch(
["INSERT INTO audit (msg) VALUES ('x')", "UPDATE users SET seen = 1 WHERE id = 42"],
%conn)
DDL
| create_table($table, $columns, %conn) | CREATE TABLE from an ordered arrayref of { name, type } hashrefs. Names are bracket-quoted; the type string (e.g. INT NOT NULL, NVARCHAR(100)) is passed through verbatim. |
| drop_table($table, %conn) | DROP TABLE IF EXISTS (schema-qualified name bracket-quoted). |
| truncate($table, %conn) | TRUNCATE TABLE (schema-qualified name bracket-quoted). |
Mssql::create_table("dbo.users",
[ { name => "id", type => "INT PRIMARY KEY" },
{ name => "name", type => "NVARCHAR(100) NOT NULL" } ],
%conn)
Introspection
| databases(%conn) | List databases (sys.databases). Array of { name } rows. |
| tables(%conn) | List tables (INFORMATION_SCHEMA.TABLES). Rows of { TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE }. |
| columns($table, %conn) | Column metadata for a table (INFORMATION_SCHEMA.COLUMNS). |
| views(%conn) | User views (INFORMATION_SCHEMA.VIEWS). Rows of { TABLE_SCHEMA, TABLE_NAME }. |
| schemas(%conn) | User schemas (sys.schemas, built-in role/system schemas excluded). Rows of { name }. |
| primary_keys($table, %conn) | Primary-key columns in key order: { COLUMN_NAME, ORDINAL_POSITION } rows. |
| foreign_keys($table, %conn) | Foreign-key relationships on the table (sys.foreign_keys): constraint name, source column, referenced table/column. |
| indexes($table, %conn) | Indexes on the table (sys.indexes): index name, uniqueness, whether it backs the PK, type description. |
| table_exists($table, %conn) | True when the table exists. A schema-qualified name (dbo.users) matches on both schema and name; unqualified matches in any schema. |
| current_database(%conn) | The database the connection is using (SELECT DB_NAME()). |
| count($table, $where = undef, %conn) | Exact row count (SELECT COUNT(*)). Optional $where is caller-owned T-SQL; use @P1… with params, or pass undef for the whole table. |
| row_count($table, %conn) | Fast row-count estimate from sys.dm_db_partition_stats — the engine-maintained count. Cheaper than count on large tables, but an estimate. |
SQL helpers
Pure functions for the cases that can't be parameterized (identifiers, LIKE patterns, IN lists, dynamic SQL). They take no connection and are unit-tested in-crate.
| quote_ident($name) | Quote a T-SQL identifier as [name], doubling any ]. |
| quote_literal($value) | Wrap a value as a single-quoted T-SQL literal, doubling any '. |
| valid_identifier($value) | True when the value is a safe unquoted identifier ([A-Za-z_][A-Za-z0-9_]*). |
| escape_like($value) | Escape %, _, [ as [%] / [_] / [[] character classes so the value matches literally inside a LIKE pattern — no ESCAPE clause needed. |
| format_value($value) | Format a scalar as a T-SQL literal: string→'…', number→as-is, bool→1/0 (T-SQL bit), null→NULL. |
| format_in_list($values) | Render an arrayref of scalars into a T-SQL IN (…) list. Empty → (NULL) (matches nothing). |
| split_batch($script) | Split a T-SQL script into batches on lines containing only GO (the SSMS batch separator, case-insensitive). Empty batches are dropped. Returns an array of strings. |
# a column name that can't be a parameter
val $col = Mssql::quote_ident("user name") # → [user name]
# match a literal substring inside LIKE
val $pat = Mssql::escape_like("100%_off") # → 100[%][_]off
Mssql::query("SELECT * FROM promo WHERE code LIKE @P1", params => ["%" . $pat . "%"], %conn)
URL helpers
| parse_url($url) | Parse an ADO key=value;… connection string into a hashref (keys lowercased; password/pwd redacted to ***). |
| redact_url($url) | Return the same connection string with the password value replaced by ***, structure preserved. |
| build_url($parts) | Build an ADO connection string from a parts hashref. Known keys (server, database, user_id, password, encrypt, trust_server_certificate) are emitted in a stable order; unknown keys pass through, sorted, after the known set. |
Typed rows
SQL Server result sets are dynamically typed at the wire level. Rather than match every TDS column type, each cell is converted to JSON by trying the candidate Rust types in order — bool, integer, float, string, decimal, uuid, datetime, date, time, binary — and taking the first the driver accepts. Decimals serialize as strings to keep full precision, binary is base64, and datetimes are ISO-8601. Arbitrary queries round-trip without the caller declaring a schema.
Build & test
The Makefile wraps the cargo + stryke commands:
| make debug | cargo build (debug cdylib). |
| make test | cargo test, then s test t/ (live path needs $MSSQL_HOST). |
| make install | s pkg install -g . |
cargo test runs the in-crate unit tests (ADO parse/redact, connection-key defaults, the pure SQL helpers) with no database. Point $MSSQL_HOST (plus $MSSQL_USER / $MSSQL_PASS) at a throwaway SQL Server container to exercise the query path.
Troubleshooting
| TLS / certificate errors | Encryption defaults to required. Against a dev server with a self-signed certificate, set trust_cert => 1, or encrypt => "off" where the server permits it. |
| Stale connection after an error | A call that errors evicts its cached Client; the next call reconnects. No manual reset is needed. |
| Quoting identifiers | Table/column names can't be bound as @P1 parameters — use quote_ident / valid_identifier to build dynamic SQL safely. |
| Errors surface as exceptions | An { error } JSON payload from the cdylib is raised as Mssql::<fn>: <reason> by the stryke wrapper. |
Why a package, not a builtin
The TDS + TLS stack (tiberius, tokio, rustls) ships once, on demand, as an opt-in package — stryke core is never linked against it. On first use Mssql, stryke dlopens the cdylib in-process and registers every mssql__* export. The cdylib owns one tokio runtime and presents a blocking facade, so from stryke's perspective every Mssql::* function is an ordinary blocking call.
Sibling packages
Part of the stryke package family. Browse the others via the MenkeTechnologiesMeta umbrella repo:
- stryke-arrow — Apache Arrow / Parquet / Feather
- stryke-aws — S3, DynamoDB, SQS, Lambda, STS
- stryke-azure — Blob, Queues, Cosmos DB, Key Vault, Entra
- stryke-clickhouse — ClickHouse
- stryke-demo — live demos for every connector
- stryke-docker — Docker daemon API
- stryke-duckdb — embedded DuckDB SQL engine
- stryke-email — email + campaign client
- stryke-fleet — parallel expect/PTY automation
- stryke-gcp — Cloud Storage, Pub/Sub, Secret Manager, BigQuery, Firestore
- stryke-grpc — reflection-based gRPC client
- stryke-gui — mouse / keyboard / screen / clipboard automation
- stryke-k8s — Kubernetes
- stryke-kafka — Apache Kafka
- stryke-mcpd — MCP servers without a runtime
- stryke-mongo — MongoDB
- stryke-mysql — MySQL / MariaDB
- stryke-neo4j — Neo4j graph (Bolt)
- stryke-office — Office docs / PDF / images
- stryke-parquet — Parquet file inspector
- stryke-polars — Polars + ndarray + linalg + FFT
- stryke-postgres — PostgreSQL
- stryke-redis — Redis / Valkey
- stryke-scrape — web scraping / crawling
- stryke-scylla — ScyllaDB / Cassandra
- stryke-search — Elasticsearch / OpenSearch
- stryke-selenium — browser automation (WebDriver)
- stryke-spark — Spark Connect (no JVM)
- stryke-utils — pure-stryke utility belt
- stryke-zmq — ZeroMQ messaging