// STRYKE-MSSQL — MICROSOFT SQL SERVER CLIENT FOR STRYKE // QUERY + EXECUTE + PARAMS + TRANSACTIONS + INTROSPECTION

stryke package v0.7.0 · namespace Mssql · cdylib libstryke_mssql · 36 exported functions · opt-in (kept out of stryke core)

Report GitHub Issues

>_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.

urlADO string, e.g. Server=db;Database=app;User Id=sa;Password=…. Overrides the discrete keys below.
hostServer host. Default 127.0.0.1.
portTDS port. Default 1433.
databaseInitial database. Default is the server's default for the login.
usernameSQL auth user.
passwordSQL auth password.
encryptrequired (default), off, or not_supported — maps to the TDS EncryptionLevel.
trust_certWhen 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.

Livenessversion, ping, server_version
Queryquery, query_one, scalar, exists, simple_query
Writeexecute, insert (multi-row), batch (transaction)
DDLcreate_table, drop_table, truncate
Introspectiondatabases, tables, columns, views, schemas, primary_keys, foreign_keys, indexes, table_exists, current_database, count, row_count
SQL helpersquote_ident, quote_literal, valid_identifier, escape_like, format_value, format_in_list, split_batch
URL helpersparse_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 TRANSACTIONCOMMIT 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 debugcargo build (debug cdylib).
make testcargo test, then s test t/ (live path needs $MSSQL_HOST).
make installs 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 errorsEncryption 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 errorA call that errors evicts its cached Client; the next call reconnects. No manual reset is needed.
Quoting identifiersTable/column names can't be bound as @P1 parameters — use quote_ident / valid_identifier to build dynamic SQL safely.
Errors surface as exceptionsAn { 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: