The SQL Server wire protocol
OriginChainDB implements TDS, the wire protocol used by Microsoft SQL Server. Point a SQL Server driver at an instance and it connects, authenticates, prepares statements and reads rows against the same store your HTTP API calls see. Whether a session may also write depends on its credential model: a shared-credential listener reads and writes; a per-user listener is read-only, and refuses every write. This page is the dialect reference: what the protocol carries, what the SQL translator accepts, and — the part people get wrong — what it refuses.
The wire is compatible. T-SQL is not the language. A batch that arrives over TDS is parsed with a SQL Server grammar and rewritten, statement by statement, into OriginChainDB's own SQL. That translation covers the query language. It does not cover the procedural one.
There are no T-SQL variables, no @@ functions, no
GO, no #temp tables,
and no T-SQL stored procedures.
Procedures on OriginChainDB are a PL/pgSQL-shaped subset, which is a different language with different
syntax and different error semantics.
So a stored procedure written for SQL Server has to be rewritten, not ported. If your application's logic lives in procedures, triggers or scripted batches, budget for that first — it is the largest single cost of moving, and it is not reduced by anything else on this page. The exact boundary is below.
The adapter is built into the shipping engine and is
inert until it is configured.
No instance gets a TDS listener by default, enabling one is not self-serve, and on production
instances today /v1/capabilities reports
available: false for it. Enabling requires an address, a
tenant, a credential and TLS material, and it is a
preview surface, not a
generally available one. Everything described here also runs today over the
SQL endpoint and the
PostgreSQL wire,
neither of which needs any of this. Talk to us
if you want a listener on your instance.
Check what is bound, not what is configured
The available flag under
mssql_tds_dialect means
configuration is complete. It is a precondition for starting a listener, not an observation
that one is listening — and, as the next section explains, this adapter deliberately refuses to bind
in states where it could not be safe. So check the flag, then the socket, then a real login.
# 1. What does this instance say about the adapter? curl -s "https://$OC_HOST/v1/capabilities" | jq '.families.mssql_tds_dialect' # 2. "available" means CONFIGURED, not "a socket is listening". # Check the socket too. nc -vz $OC_SQL_HOST 1433 # 3. Then prove it end to end with a real login, not a port scan. # (sqlcmd, or the JDBC / Go snippets further down this page.)
1433 is the port SQL Server clients expect and the one used
throughout this page, but the host and port your instance is published on are shown in the console
once a listener exists. Use those. The limitations your own
instance reports beside available are derived from its
configuration, and are authoritative for it.
TLS is mandatory, and it fails closed
There is no plaintext path to this listener — not a discouraged one, not a flag, none. That is worth
stating plainly because it changes what a misconfiguration looks like:
a broken certificate does not produce a
degraded connection, it produces no port at all. If nothing answers on
1433, check the certificate before you check the firewall.
It is enforced at four places, each of which fails closed:
- At startup. If the certificate and key are missing, empty or unloadable the listener does not bind, and the reason is logged. The check runs before the bind, so no port is ever presented as up.
- At PRELOGIN. A client that offers no encryption, or asks for it to be off, is answered with the protocol byte meaning encryption is required by the server and the connection is closed cleanly, before the login packet is read. It is not downgraded and not accepted-then-ignored.
- Against login-only encryption. Encrypting the login and then reverting to plaintext — which a real SQL Server will do — is rejected by design, not merely unimplemented. It protects the credential while streaming every query and every result row in clear, which for a database leaks exactly what the credential would have unlocked.
- In the type system. The command loop takes an encrypted stream that only a completed handshake can produce, so "no session runs unencrypted" is a compile-time property of the adapter rather than a rule someone has to remember.
Set encrypt=true and leave
trustServerCertificate at
false so your client actually validates the chain. The
handshake itself is the TDS-framed variant that commercial SQL Server drivers require during
negotiation; the stream switches to ordinary TLS records from the login packet onward.
This is the opposite of OriginChainDB's MySQL-wire adapter, which fails open — it binds without a certificate and then requires nothing. The two adapters behave differently on purpose, and a habit learned on one is the wrong habit on the other.
Connecting with a real client
Only drivers that have actually been driven against the engine appear here, at the versions they were driven at.
Java — mssql-jdbc 13.4.0 and 12.10.1
// Measured against mssql-jdbc 13.4.0 and 12.10.1 - 34 of 34 checks passed.
String url = "jdbc:sqlserver://" + System.getenv("OC_SQL_HOST") + ":1433"
+ ";databaseName=" + System.getenv("OC_DATABASE")
+ ";encrypt=true" // mandatory - there is no other way in
+ ";trustServerCertificate=false"
+ ";hostNameInCertificate=" + System.getenv("OC_SQL_HOST")
+ ";loginTimeout=30";
try (Connection conn = DriverManager.getConnection(
url, System.getenv("OC_DB_USER"), System.getenv("OC_DB_PASSWORD"))) {
try (PreparedStatement ps = conn.prepareStatement(
"SELECT TOP 50 id, customer_ref, amount_cents"
+ " FROM invoices WHERE status = ? ORDER BY amount_cents DESC")) {
ps.setString(1, "open");
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getInt("id") + " " + rs.getString("customer_ref"));
}
}
}
} Go — microsoft/go-mssqldb 1.11.0
// Measured against microsoft/go-mssqldb 1.11.0 - 21 of 21 checks passed.
package main
import (
"database/sql"
"fmt"
"net/url"
"os"
_ "github.com/microsoft/go-mssqldb"
)
func main() {
q := url.Values{}
q.Set("database", os.Getenv("OC_DATABASE"))
q.Set("encrypt", "true") // mandatory
q.Set("TrustServerCertificate", "false") // validate the chain
dsn := url.URL{
Scheme: "sqlserver",
User: url.UserPassword(os.Getenv("OC_DB_USER"), os.Getenv("OC_DB_PASSWORD")),
Host: os.Getenv("OC_SQL_HOST") + ":1433",
RawQuery: q.Encode(),
}
db, err := sql.Open("sqlserver", dsn.String())
if err != nil {
panic(err)
}
defer db.Close()
rows, err := db.Query(
"SELECT TOP 50 id, customer_ref FROM invoices WHERE status = @p1", "open")
if err != nil {
panic(err)
}
defer rows.Close()
for rows.Next() {
var id int
var ref string
if err := rows.Scan(&id, &ref); err != nil {
panic(err)
}
fmt.Println(id, ref)
}
} Worked, page-sized versions of both, plus the prepared-statement path, are in the TDS examples.
Credential models
A listener runs in one of two models, and the choice is not yours to make freely — it follows from whether the tenant has any database authorization to enforce: a database RBAC grant or a row-level security policy. Either one is enough; a row-security policy is not the lighter case.
| Model | What the login carries | Applies when |
|---|---|---|
| shared | One listener-wide credential. Every session arrives as the same identity. | The tenant has no database RBAC grants and no row-level security policies at all. A shared credential cannot enforce a per-user grant or evaluate a per-user row policy, so the engine will not let the two be combined. |
| per-user | Each database user logs in with its own password, under the mandatory TLS, and its own grants apply. | The tenant has any database RBAC grant, or any row-level security policy. This is the only model that can serve it, and the session is read-only. |
A per-user TDS session refuses every
write. Not only the writes a grant disallows — the listener marks the session read-only the
moment it admits an authenticated identity, before any grant is consulted, because there is no
write-parity bridge on this adapter. INSERT,
UPDATE, DELETE and DDL
all come back refused, and no configuration turns this on.
Put that beside the table above and the consequence is the one to plan around: a tenant with a single RBAC grant or a single row-level security policy cannot write over TDS at all. That tenant must be on per-user login, and per-user login does not write. No combination of settings yields an authorizing, writing TDS session today.
So every write shown on this page and under TDS examples — including statements in the translated set, which translate correctly and are still refused on a per-user session — applies to a shared-credential listener only. If your tenant has any authorization to enforce, write through the HTTP API or the PostgreSQL wire, and keep the TDS listener for reads.
Put those two rows together and there is a real state that neither model can serve: a tenant that carries database RBAC or a row-level security policy but has no database user with a password verifier. Shared login is inadmissible because that authorization is present; per-user login has no account to admit, so the listener refuses to bind — by design, rather than binding something it could not enforce. This is not an exotic corner: it is the ordinary state of a tenant whose access has only ever been through API tokens. Creating one enabled database user with a password is the whole fix.
The T-SQL boundary, exactly
A batch is parsed with a SQL Server grammar, the syntax tree is rewritten into OriginChainDB's SQL, and each statement is re-rendered and executed through the same engine every other surface uses. The rule the translator follows is soundness over coverage: anything without an exact equivalent is refused with a clear error and nothing executes. It never guesses, and it never returns a plausible wrong answer.
That makes the boundary easy to test and unpleasant to discover late. Here it is.
Translated
| T-SQL you write | What it becomes |
|---|---|
| [bracket] identifiers |
A plain name unwraps; a name with spaces or punctuation, or a reserved word such as
[order], becomes a double-quoted identifier. Every
position a statement can put one in.
|
| SELECT TOP n · TOP (n) | LIMIT n |
| OFFSET n ROWS FETCH NEXT m ROWS ONLY | LIMIT m OFFSET n. Always lowered — a
FETCH left alone would be ignored and silently return
every row.
|
| ISNULL · LEN · CHARINDEX · IIF | COALESCE, LENGTH,
POSITION, CASE |
| GETDATE · SYSDATETIME · SYSUTCDATETIME | One statement-time UTC value, folded so every call in a statement agrees. |
| CONVERT(type, expr) | CAST(expr AS type). The UTF-16 character types
(NVARCHAR, NCHAR,
NTEXT) collapse onto the engine's string types and
UNIQUEIDENTIFIER onto its UUID type, in casts and in
DDL column definitions alike. Only mappings that change no value representation are made:
BIT, MONEY and
DATETIME2 are deliberately left alone, so the engine
refuses them with a clear error rather than accept mis-typed data.
|
| a + b (string concatenation) | CONCAT(a, b), but
only when both operands are
provably string. Two numbers stay arithmetic; one string and one unknown is ambiguous
in T-SQL itself and is refused.
|
| SUBSTRING · REPLACE · LTRIM · RTRIM · UPPER · LOWER | Pass through unchanged — they are engine natives. |
-- Every line below is rewritten and executed. Send it as an ordinary
-- SQL_BATCH, or as the statement text of a parameterized RPC.
SELECT TOP 20 [order].[id], [customer name]
FROM [order]
WHERE ISNULL([status], 'open') = 'open'
ORDER BY [amount cents] DESC;
SELECT id,
LEN(customer_ref) AS ref_len,
CHARINDEX('-', customer_ref) AS dash_at,
IIF(amount_cents > 100000, 'large', 'small') AS bucket,
CONVERT(NVARCHAR(32), amount_cents) AS amount_text
FROM invoices
ORDER BY id
OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY; Refused, cleanly
Each of these comes back as an error token naming the construct and, where one exists, the portable rewrite. Nothing runs, nothing is half-applied, and the connection survives.
| Construct | Why, and what to write instead |
|---|---|
| @var · @@FUNCTION | Batch-level variables and built-in globals are not modelled. Bind a parameter, or compute the value in your application. |
| #temp objects | Per-session temporary objects are not modelled. Create an ordinary table. |
| GO |
A client-tool batch separator, not a server statement. Send one statement per batch. It is
caught before parsing, because the grammar would otherwise read a trailing
GO as a column alias.
|
| MERGE |
Use INSERT ... ON CONFLICT.
|
| SELECT ... INTO newtable |
The engine parses the INTO clause and drops it, so
passing it through would report a successful table copy that created nothing. Use
CREATE TABLE then
INSERT ... SELECT.
|
| DATEADD · DATEDIFF | The substrate has no calendar timestamp type — timestamps are ISO-8601 strings — so there is no sound mapping. Do date arithmetic in your application. |
| CONVERT with a style code |
Style codes and USING charsets are formatting semantics
that are not reproduced. Format in your application.
|
| TOP ... PERCENT · WITH TIES |
No LIMIT equivalent. The same applies to
FETCH ... PERCENT / WITH TIES.
|
| TOP in UPDATE / DELETE | Constrain the statement with a WHERE predicate. |
| Anything the SQL Server grammar cannot parse | Reported with the parse failure rather than attempted. A final safety net also refuses any translated output still carrying an un-rewritten bracket identifier, rather than letting it reach the engine as a wrong name. |
-- Each of these is REFUSED with an explicit error. Nothing executes, -- nothing is half-applied, and the connection stays open. DECLARE @cutoff INT = 100000; -- @variables are not modelled SELECT @@VERSION; -- @@functions are not modelled SELECT * INTO #recent FROM invoices; -- # temp objects are per-session GO -- a client-tool directive, not a statement MERGE invoices AS t USING staged AS s -- use INSERT ... ON CONFLICT instead ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.status = s.status; SELECT DATEADD(day, -7, GETDATE()); -- no calendar timestamp type SELECT CONVERT(VARCHAR(10), created_at, 112); -- style codes are formatting SELECT TOP 10 PERCENT * FROM invoices; -- no LIMIT equivalent DELETE TOP (100) FROM invoices; -- constrain with WHERE instead SELECT * INTO archive FROM invoices; -- SELECT ... INTO would create nothing
Three approximations to know about
These translate, and for ordinary inputs they agree with SQL Server. They are listed because for unusual inputs they do not:
-
LENdoes not reproduce T-SQL's trailing-space trim, soLEN('a ')counts the spaces. -
GETDATE()yields an ISO-8601 UTC string, because there is no datetime type to yield instead. -
CONCATtreats NULL as empty where T-SQL's+propagates it. Only reachable where both operands are provably string, which is the only case that translates at all.
Stored procedures: the distinction that costs money
TDS carries a remote-procedure-call path, and OriginChainDB serves the part of it every driver depends
on: sp_executesql,
sp_prepare, sp_execute,
sp_prepexec and
sp_unprepare. Seeing those names work is what makes people
assume stored procedures work.
They are not your procedures.
That family is the protocol's own machinery — it is how a driver ships a parameterized query and
reuses its plan handle. Serving it is what makes a
PreparedStatement work on its second execution and what makes
ODBC work on its first. It says nothing about running procedures you wrote. Calling a
user stored procedure over the wire is refused, and so is every other procedure, by numeric
id or by name.
Also refused on this path, cleanly rather than mis-served: the server-side cursor family, OUTPUT value parameters (there is no variable-assignment surface that could produce one), bulk load, and multiple active result sets. Temporal parameter values decode best-effort in this preview — integer, float, bit, string, numeric and unique-identifier parameters are exact.
OriginChainDB does have procedures. They are written in a PL/pgSQL-shaped subset, created and called over the SQL endpoint or the PostgreSQL wire, and they are a genuinely different language. Here is what a small one looks like on both sides — note that nothing about the T-SQL version survives except its intent:
-- SQL Server (T-SQL). This does NOT run on OriginChainDB.
CREATE PROCEDURE dbo.settle_invoice @id INT, @settled INT OUTPUT
AS
BEGIN
DECLARE @amount INT;
SELECT @amount = amount_cents FROM invoices WHERE id = @id;
IF @amount IS NULL
THROW 50001, 'no such invoice', 1;
UPDATE invoices SET status = 'settled' WHERE id = @id;
SET @settled = @@ROWCOUNT;
END;
-- OriginChainDB. A different procedural language, so this is a rewrite -
-- not a port. Issue it over the SQL endpoint or the PostgreSQL wire.
CREATE PROCEDURE settle_invoice(IN p_id INT, INOUT p_settled INT)
LANGUAGE plpgsql AS $$
DECLARE
v_amount INT;
BEGIN
SELECT amount_cents INTO v_amount FROM invoices WHERE id = p_id;
IF v_amount IS NULL THEN
RAISE EXCEPTION 'no such invoice';
END IF;
UPDATE invoices SET status = 'settled' WHERE id = p_id;
GET DIAGNOSTICS p_settled = ROW_COUNT;
END;
$$; For anything larger, estimate the rewrite the way you would estimate a rewrite: by counting the procedures and reading them, not by assuming a compatibility layer will absorb them.
How far this has been proven
Interop is measured by driving Microsoft's own shipped drivers against the real accept loop — not a client written against our own encoder, which is how earlier claims about this adapter turned out to be wrong while every in-house test was green.
| Driver | Version | Result |
|---|---|---|
| mssql-jdbc | 13.4.0 | 34 / 34 |
| mssql-jdbc | 12.10.1 | 34 / 34 |
| microsoft/go-mssqldb | 1.11.0 | 21 / 21 |
Thirteen consecutive rounds, all green, measured on 2026-09-07. What those rounds cover: connecting and logging in over the framed TLS handshake; DDL; insert; select with rows decoded and compared; the whole prepare / execute / unprepare family asserted through the driver's own prepared-statement handle rather than through our encoder; batch execution; and a wrong credential being refused. Both drivers are required — each one exercises framing, flush and row-count behaviour the other tolerates, and a single-driver run has already been shown to miss half of a defect set.
That battery runs in no automated job. It was executed deliberately and it passed, so the accurate word is verified — as of that date, against those driver versions, against that engine build. A regression introduced by a later build would not be caught by a scheduled run, because there is not one. Treat the figures above as a measurement with a date on it, and re-run the battery against the build you intend to deploy rather than assuming they still hold.
Limits, in one place
- The T-SQL procedural language is not
implemented. No variables, no built-in globals, no control-flow batches, no
GO, no temp tables, no T-SQL stored procedures, triggers or functions. Procedures are a PL/pgSQL-shaped subset and are a rewrite. - This is a preview surface. It is not generally available, it is off on every instance unless someone turned it on, and it is not self-serve.
- TLS is required absolutely. No certificate means no listener, and a client that will not encrypt is refused before it can log in.
- Server-side cursors, bulk load and multiple active result sets are refused — cleanly, but refused. So is calling a user stored procedure, and so are OUTPUT value parameters.
- Date and time arithmetic does not translate. There is no calendar timestamp type behind this surface.
- Interop is verified, not continuously verified, against the three driver versions named above and no others. Other TDS clients may work; they have not been measured, so we do not claim them.
If any of that is disqualifying, the same data is reachable today over the SQL endpoint and the PostgreSQL wire, both of which are ordinary supported surfaces with none of the caveats on this page.
Every third-party name on this page is used to describe protocol or dialect compatibility, and for no other purpose. Microsoft, SQL Server and T-SQL are trademarks of the Microsoft group of companies. PostgreSQL is a registered trademark of the PostgreSQL Community Association of Canada. MySQL is a registered trademark of Oracle Corporation and/or its affiliates. mssql-jdbc and go-mssqldb are Microsoft's own drivers and are named only to identify the versions this compatibility was measured against. OriginChainDB is not affiliated with, endorsed by, or sponsored by any of them, and does not distribute their software. OriginChainDB is not SQL Server: it is an independent database that implements the TDS wire protocol so that existing SQL Server clients can connect to it, and that accepts part of the T-SQL query language.