The SQL Server wire protocol
This page records the earlier SQL Server-compatible TDS listener. TDS is the network protocol used by SQL Server clients; implementing part of it does not make OriginChainDB a SQL Server installation.
TDS is not offered for new connections. Existing listener state and historical examples remain separate from that product decision. For new work, use HTTPS or a supported read-only PostgreSQL database login.
- Identify the legacy clientRecord the queries and connection settings it uses.
- Confirm listener availabilityAsk the operator for the actual endpoint and supported contract.
- Verify the secure loginCheck TLS and the required database identity.
- Run a supported SELECTValidate read results before relying on the integration.
Check what is bound, not what is configured
The capability flag reports configuration readiness, not proof that a port is listening. For an existing listener, confirm its status and perform an authorized login using the supplied host and port.
# 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
The historical TDS listener requires TLS. A missing or unusable certificate can prevent it from starting. Check listener status, certificates and network access when a connection fails.
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.
TLS behavior differs between the historical adapters. Follow the requirements for the specific listener rather than copying another protocol’s settings.
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 listener 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.
A tenant with database grants or row policies requires per-user authentication. Those sessions remain read-only in this adapter.
The write examples below belong to the historical shared-credential model. For new application writes, use the supported HTTPS API. The currently offered PostgreSQL database login is read-only.
If a tenant requires per-user authorization but has no database user with a password, neither historical model can accept a login. Resolve the identity requirements with your administrator.
The T-SQL boundary, exactly
The adapter translates a supported T-SQL subset into OriginChainDB SQL. T-SQL is SQL Server’s query language. Review the translated and refused constructs below before porting a statement.
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 nothingThree 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
Prepared statements use protocol calls such as sp_prepare and sp_execute. Support for those calls does not mean the server can run stored procedures written by your application.
User-written T-SQL stored procedures are not served by this path. Plan to rewrite them or move their logic to your application.
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;
$$;Inventory and test application procedures individually. A protocol compatibility layer does not translate an entire procedural program.
How far this has been proven
The table records a historical interoperability test using the named Microsoft drivers. It is evidence for those versions and request shapes, not a claim that every TDS client or current deployment has been tested.
| 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 |
The recorded test date is 2026-09-07. The checks covered TLS login, table creation, inserts, decoded SELECT results, prepared statements, batches and rejection of a wrong credential.
These were manual checks, not a continuous test service. Re-run the operations your application needs against its target deployment; the results below do not establish current production availability.
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.
For new connections, use the HTTPS SQL endpoint or a supported read-only PostgreSQL database login. Their own limits still apply.
Third-party names identify the protocols and driver versions discussed here. Microsoft, SQL Server and T-SQL are Microsoft trademarks; PostgreSQL and MySQL belong to their respective owners. OriginChainDB is independent and is not affiliated with or endorsed by these organizations.