// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// What this driver sends: a SQL batch for a statement with no parameters, and an RPC to
// `sp_executesql` for one that has them. The second is how TDS binds values out of band —
// the statement text carries `@p1`, `@p2`, and the values travel beside it, never inside
// it.
///|
fn put_u16(out : Buffer, value : Int32) -> Unit {
out.write_byte((value & 0xFF).to_byte())
out.write_byte(((value >> 8) & 0xFF).to_byte())
}
///|
fn put_u32(out : Buffer, value : Int32) -> Unit {
for i = 0; i < 4; i = i + 1 {
out.write_byte(((value >> (i * 8)) & 0xFF).to_byte())
}
}
///|
fn put_u64(out : Buffer, value : Int64) -> Unit {
for i = 0; i < 8; i = i + 1 {
out.write_byte(((value >> (i * 8)) & 0xFF).to_byte())
}
}
///|
/// The `ALL_HEADERS` block every post-login message opens with: one transaction-manager
/// header that says "enlist in no transaction, and this is one request".
fn all_headers(txn : Bytes) -> Bytes {
let out = Buffer()
// 22 bytes in all: the total, an 18-byte header, its two-byte type, the eight-byte
// descriptor and the outstanding request count.
put_u32(out, 22)
put_u32(out, 18)
put_u16(out, 2)
// The descriptor the server handed out on BEGIN, or eight zeroes outside a transaction.
// A request inside one that omits it is refused with error 3989.
for i = 0; i < 8; i = i + 1 {
out.write_byte(if i < txn.length() { txn[i] } else { b'\x00' })
}
put_u32(out, 1)
out.to_bytes()
}
///|
/// A SQL batch: the statement as UTF-16 followed by nothing else.
fn build_sql_batch(sql : String, txn : Bytes) -> Bytes {
let out = Buffer()
out.write_bytes(all_headers(txn))
out.write_bytes(@utf16.encode(sql))
packet(0x01, out.to_bytes())
}
///|
/// Rewrite each `?` into `@pN`, skipping string literals, quoted identifiers, comments and
/// bracket-quoted names. A `?` inside any of those is data, not a placeholder.
fn rewrite(sql : String) -> String {
let out = Buffer()
let n = sql.length()
let mut i = 0
let mut start = 0
let mut slot = 0
while i < n {
let c = sql[i]
if c == '\'' || c == '"' {
// A string literal or a quoted identifier: `?` inside is data.
i += 1
while i < n {
if sql[i] == c {
if i + 1 < n && sql[i + 1] == c {
i += 2
continue
}
i += 1
break
}
i += 1
}
continue
}
if c == '[' {
i += 1
while i < n {
let done = sql[i] == ']'
i += 1
if done {
break
}
}
continue
}
if c == '-' && i + 1 < n && sql[i + 1] == '-' {
while i < n && sql[i] != '\n' {
i += 1
}
continue
}
if c == '/' && i + 1 < n && sql[i + 1] == '*' {
i += 2
while i < n {
if sql[i] == '*' && i + 1 < n && sql[i + 1] == '/' {
i += 2
break
}
i += 1
}
continue
}
if c == '?' {
out.write_bytes(@utf8.encode(sql[start:i]))
slot += 1
out.write_bytes(@utf8.encode("@p" + slot.to_string()))
i += 1
start = i
continue
}
i += 1
}
out.write_bytes(@utf8.encode(sql[start:]))
decode_lossy(out.to_bytes())
}
///|
/// The T-SQL type a value binds as.
fn declaration_of(value : @moondb.Value) -> String {
match value {
@moondb.Null => "int"
@moondb.Bool(_) => "bit"
@moondb.Int(_) => "int"
@moondb.Int64(_) => "bigint"
@moondb.Double(_) => "float"
@moondb.Text(_) | @moondb.Time(_) | @moondb.Numeric(_) | @moondb.Json(_) =>
"nvarchar(max)"
@moondb.Blob(_) => "varbinary(max)"
@moondb.Uuid(_) => "uniqueidentifier"
@moondb.Array(_) => "nvarchar(max)"
}
}
///|
/// A `PLP` value: one chunk, and the terminator.
///
/// The total length is `UNKNOWN_PLP_LEN` (`0xFFFFFFFFFFFFFFFE`) rather than the real one.
/// Both are legal on the wire, but the server reads a declared length as "exactly this many
/// bytes follow, chunk headers included" and then stops one chunk short: a parameter larger
/// than the packet size arrives truncated at the packet boundary, with no error, and the
/// value that comes back is a prefix of the one that went out.
fn plp(out : Buffer, data : Bytes) -> Unit {
put_u64(out, -2L)
if data.length() > 0 {
put_u32(out, data.length())
out.write_bytes(data)
}
put_u32(out, 0)
}
///|
/// A `NULL` where the column's values are sent as `PLP`: the total length is all ones.
fn plp_null(out : Buffer) -> Unit {
put_u64(out, -1L)
}
///|
/// The name of an RPC parameter: a one-byte character count, then the name in UTF-16. The
/// leading `@` is part of it — `sp_executesql` addresses its own formal parameters by
/// name, and an unnamed first parameter is refused with "the parameter name is invalid".
fn put_param_name(out : Buffer, name : String) -> Unit {
let encoded = @utf16.encode(name)
out.write_byte((name.length() & 0xFF).to_byte())
out.write_bytes(encoded)
}
///|
/// One value as an RPC parameter body: its type, then its value. The types are the
/// nullable spellings — INTN, BITN, FLTN — so a `NULL` is expressible without a second
/// declaration for every column that might be missing.
fn put_typed_value(out : Buffer, value : @moondb.Value) -> Unit {
match value {
@moondb.Null => {
out.write_byte(b'\x26') // INTN
out.write_byte(b'\x08')
out.write_byte(b'\x00') // length zero is NULL
}
@moondb.Bool(v) => {
out.write_byte(b'\x68') // BITN
out.write_byte(b'\x01')
out.write_byte(b'\x01')
out.write_byte((if v { 1 } else { 0 }).to_byte())
}
@moondb.Int(v) => {
out.write_byte(b'\x26') // INTN
out.write_byte(b'\x04')
out.write_byte(b'\x04')
put_u32(out, v)
}
@moondb.Int64(v) => {
out.write_byte(b'\x26')
out.write_byte(b'\x08')
out.write_byte(b'\x08')
put_u64(out, v)
}
@moondb.Double(v) => {
out.write_byte(b'\x6d') // FLTN
out.write_byte(b'\x08')
out.write_byte(b'\x08')
put_u64(out, v.reinterpret_as_int64())
}
@moondb.Blob(v) => {
out.write_byte(b'\xa5') // BIGVARBINARY
put_u16(out, 0xFFFF)
plp(out, v)
}
@moondb.Uuid(v) => {
out.write_byte(b'\x24') // GUID
out.write_byte(b'\x10')
out.write_byte(b'\x10')
out.write_bytes(v)
}
@moondb.Text(v) | @moondb.Time(v) | @moondb.Numeric(v) | @moondb.Json(v) =>
nvarchar_value(out, v)
@moondb.Array(v) => {
let text = Buffer()
text.write_byte(b'[')
for i = 0; i < v.length(); i = i + 1 {
if i > 0 {
text.write_byte(b',')
}
match v[i] {
@moondb.Int(n) => text.write_bytes(@utf8.encode(n.to_string()))
@moondb.Int64(n) => text.write_bytes(@utf8.encode(n.to_string()))
@moondb.Double(n) => text.write_bytes(@utf8.encode(n.to_string()))
_ => ()
}
}
text.write_byte(b']')
nvarchar_value(out, decode_lossy(text.to_bytes()))
}
}
}
///|
/// An `nvarchar(max)` value: its type, its collation, and the value as a `PLP` chunk run.
fn nvarchar_value(out : Buffer, text : String) -> Unit {
out.write_byte(b'\xe7') // NVARCHAR
put_u16(out, 0xFFFF)
// The collation of a `(max)` column: Latin1_General_CI_AS, the usual default.
for b in [0x09, 0x04, 0xD0, 0x00, 0x34] {
out.write_byte(b.to_byte())
}
plp(out, @utf16.encode(text))
}
///|
/// One RPC parameter: its name, a status byte, then its type and value. `by_ref` marks an
/// `OUTPUT` parameter — the handle `sp_prepare` hands back arrives that way.
priv struct RpcParam {
name : String
value : @moondb.Value
by_ref : Bool
}
///|
/// One RPC parameter on the wire.
fn put_rpc_param(out : Buffer, param : RpcParam) -> Unit {
put_param_name(out, param.name)
out.write_byte(if param.by_ref { b'\x01' } else { b'\x00' })
put_typed_value(out, param.value)
}
///|
/// An RPC message: the transaction-manager header, the procedure, then its parameters.
/// A system procedure is addressed by its well-known id — `0xFFFF` announces that the next
/// two bytes are that id — which is how `sp_executesql`, `sp_prepare`, `sp_execute` and
/// `sp_unprepare` are called without a name lookup.
fn build_rpc_message(
proc_id : Int32,
params : Array[RpcParam],
txn : Bytes,
) -> Bytes {
let out = Buffer()
out.write_bytes(all_headers(txn))
put_u16(out, 0xFFFF)
put_u16(out, proc_id)
put_u16(out, 0) // option flags
for param in params {
put_rpc_param(out, param)
}
packet(0x03, out.to_bytes())
}
///|
/// The declaration list `sp_executesql` and `sp_prepare` take beside the statement:
/// `@p1 int, @p2 nvarchar(max)`, one entry per value.
fn declaration_list(params : Array[@moondb.Value]) -> String {
let decls = Buffer()
for i = 0; i < params.length(); i = i + 1 {
if i > 0 {
decls.write_bytes(@utf8.encode(", "))
}
decls.write_bytes(@utf8.encode("@p" + (i + 1).to_string() + " "))
decls.write_bytes(@utf8.encode(declaration_of(params[i])))
}
decode_lossy(decls.to_bytes())
}
///|
/// The RPC message: an `sp_executesql` call whose first two parameters are the statement
/// and its parameter declaration, followed by the values themselves.
fn build_rpc(sql : String, params : Array[@moondb.Value], txn : Bytes) -> Bytes {
let call : Array[RpcParam] = [
{ name: "@statement", value: @moondb.Text(sql), by_ref: false, },
{
name: "@params",
value: @moondb.Text(declaration_list(params)),
by_ref: false,
},
]
for i = 0; i < params.length(); i = i + 1 {
call.push({
name: "@p" + (i + 1).to_string(),
value: params[i],
by_ref: false,
})
}
build_rpc_message(10, call, txn)
}