MySQL and SQL Server wire protocols
For new connections, use the HTTPS API or a PostgreSQL database login. PostgreSQL currently offers read-only access and uses a separate username and password.
The protocol details below are historical compatibility references. MySQL and SQL Server are no longer offered as new connection choices, but existing listeners have not been universally disabled. Do not use older enablement instructions as current setup guidance. Start with Connect a PostgreSQL client.
- Decide what the client needsSeparate application writes from SQL-client reads.
- Choose the supported interfaceUse HTTPS, or the PostgreSQL login for read-only tools.
- Copy that interface’s settingsDo not reuse another protocol’s endpoint or password.
- Test the intended operationVerify the actual queries your client sends.
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. Does the tenant report the listener as CONFIGURED?
# The families sit at the TOP level of the payload, not under a wrapper.
curl -s "https://$OC_HOST/v1/capabilities" \
| jq '.mysql_wire_dialect.available, .mssql_tds_dialect.available'
# 2. Is a socket actually LISTENING?
# (1) can say "configured" while (2) refused to bind, so run both -
# and give the port scan a control, or a silent "no" proves nothing.
nc -vz $OC_SQL_HOST 443 # control: this MUST answer
nc -vz $OC_SQL_HOST 3306 # MySQL wire
nc -vz $OC_SQL_HOST 1433 # TDS
# If the control line fails too, your path is blocked and you have
# measured your own network, not our listener.
# 3. Prove it end to end with a real login, not a port scan.
mysql --host=$OC_SQL_HOST --port=3306 --user=$OC_DB_USER \
--ssl-mode=REQUIRED -e "SELECT 1"Use the host and port supplied for the existing listener. The numbers in the examples are conventions, not deployment settings.
Credential models, and which one your tenant is on
The historical listeners had shared and per-user credential models. Database permissions determine which model can be admitted; consult the table before evaluating an existing integration.
| Model | What the client sends | Use it when |
|---|---|---|
| shared | One listener-wide password. Every session arrives with the same identity. | Your tenant has no database RBAC grants at all. A shared credential cannot enforce per-user grants, so the engine will not let you combine the two. |
| per-user | Each database user authenticates with its own password verifier, and its own grants apply. | Your tenant has any database RBAC grant. This is the only model that can serve it. |
Two refusals you should expect, and what they mean
A listener can refuse to start when it cannot enforce the configured access model. Read its error before changing credentials or permissions.
1. A shared credential against a tenant that has RBAC:
DENIED: this tenant has database RBAC configured on N object(s) (...).
ANY grant countsRemedy: switch the listener to per-user login. Any grant counts - one grant on one object is enough to make a shared credential inadmissible, and so does any row-level security policy; a row policy is not the lighter case. Note what that buys and what it costs: the session becomes enforced, and read-only.
2. Per-user login against a tenant with no usable database user:
DENIED: per-user login is configured but this tenant has NO enabled
database user with a password verifier, so no session could ever be
admitted. Refusing to bind.Remedy: create at least one enabled database user that has a password verifier.
-- Give the tenant at least one ENABLED database user WITH a password
-- verifier, and the per-user listener has something it can admit.
CREATE USER app_service WITH PASSWORD 'generate-a-real-secret-here';
GRANT SELECT, INSERT, UPDATE ON invoices TO app_service;per-user login is read-only - on both listeners
Per-user MySQL and TDS sessions are read-only in this historical contract. They refuse INSERT, UPDATE, DELETE and schema changes even when a grant would otherwise permit the operation.
A tenant with database grants or row policies must use the authorization-capable login model. In this historical adapter, that model remains read-only.
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 also 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; do not weaken permissions to make the connection succeed.
MySQL wire protocol
The following MySQL example records an earlier client setup. Use only confirmed settings for an existing listener; it is not a new-connection walkthrough.
TLS is not optional here
In the historical shared-credential mode, a missing certificate could leave the MySQL listener serving plaintext. Do not use that configuration. Require TLS and certificate verification at the client. Per-user mode requires a certificate before the listener can start.
Worked example - Sequelize 6.37 over mysql2 3.24
// Verified against the engine with Sequelize 6.37 on mysql2 3.24.
import { Sequelize, DataTypes } from "sequelize";
const sequelize = new Sequelize(
process.env.OC_DATABASE,
process.env.OC_DB_USER,
process.env.OC_DB_PASSWORD,
{
host: process.env.OC_SQL_HOST,
port: 3306,
dialect: "mysql",
dialectModule: require("mysql2"),
dialectOptions: {
// Required in practice. See "TLS is not optional here" below.
ssl: { minVersion: "TLSv1.2", rejectUnauthorized: true },
},
// The engine cannot reflect a schema yet, so DEFINE every model
// explicitly and never call sequelize.sync() to discover one.
define: { timestamps: false, freezeTableName: true },
},
);
const Invoice = sequelize.define("invoices", {
id: { type: DataTypes.INTEGER, primaryKey: true },
customer_ref: DataTypes.STRING,
amount_cents: DataTypes.INTEGER,
status: DataTypes.STRING,
});
await sequelize.authenticate();
// Single-table reads and writes work, transactions included.
const tx = await sequelize.transaction();
try {
await Invoice.create(
{ id: 4101, customer_ref: "ACME-77", amount_cents: 129900, status: "open" },
{ transaction: tx },
);
await Invoice.update({ status: "settled" }, { where: { id: 4101 }, transaction: tx });
await tx.commit();
} catch (err) {
await tx.rollback(); // Rollback is honoured; the row does not appear.
throw err;
}
const open = await Invoice.findAll({
where: { status: "open" },
order: [["amount_cents", "DESC"]],
limit: 50,
});What this does not do
Measured against the real engine, on a listener using the shared credential model: a full ORM connects, authenticates, and runs a complete single-table workload - reads, writes, and transactions that both commit and roll back. On a per-user listener the writes in that sentence do not happen at all - see below. Two further things it cannot do on either model:
- Schema reflection. The ORM cannot read table shapes back from the listener, so anything that discovers a schema -
describeTable,sync, model autoloading, and the migration tooling built on them - does not work. Declare your models by hand. - Associations. Related models do not load. Eager loading with
includeand the lazy accessors that go with it do not resolve.
do not call sync()
An ORM schema-sync failure may leave earlier DDL applied. Schema changes are not necessarily rolled back with data writes. Review the resulting objects before retrying, and use a tested migration procedure instead of assuming a failed sync made no changes.
The historical MySQL adapter does not support all schema-reflection or association queries. Test those operations separately; a working single-table example is not full ORM compatibility.
// NOT served today. Both of these fail against the MySQL listener.
// 1. Schema reflection - there is nothing to read a table shape back from.
await sequelize.getQueryInterface().describeTable("invoices"); // fails
await sequelize.sync(); // fails
// 2. Associations - a modelled domain does not load.
Invoice.hasMany(LineItem, { foreignKey: "invoice_id" });
await Invoice.findAll({ include: [LineItem] }); // fails
// Do this instead: declare every model by hand (as above) and issue the
// second query yourself, joining in your own application code.
const invoices = await Invoice.findAll({ where: { status: "open" } });
const items = await LineItem.findAll({
where: { invoice_id: invoices.map((i) => i.id) },
});TDS - the SQL Server wire protocol
Point a TDS client at $OC_SQL_HOST:1433. The same two credential models apply.
TLS is mandatory, and it fails closed
A certificate and key are required. Without loadable PEMs the port is not bound at all - there is no cleartext fallback and no degraded mode to ship by accident. If the port does not answer, an unloadable certificate is the first thing to check. Set encrypt=true and leave trustServerCertificate at false so your client actually validates the chain.
Worked example - mssql-jdbc 13.4.0 and 12.10.1
// Verified with 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 - the listener is TLS-only
+ ";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"))) {
// Prepared statements, including server-side handles and reuse.
try (PreparedStatement ps = conn.prepareStatement(
"SELECT 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"));
}
}
}
// Batch execution.
conn.setAutoCommit(false);
try (PreparedStatement ins = conn.prepareStatement(
"INSERT INTO invoices (id, customer_ref, amount_cents, status)"
+ " VALUES (?, ?, ?, ?)")) {
for (Object[] row : rows) {
ins.setInt(1, (Integer) row[0]);
ins.setString(2, (String) row[1]);
ins.setInt(3, (Integer) row[2]);
ins.setString(4, (String) row[3]);
ins.addBatch();
}
ins.executeBatch();
}
conn.commit();
}Worked example - microsoft/go-mssqldb 1.11.0
// Verified with microsoft/go-mssqldb 1.11.0 - 21 of 21 checks passed.
package main
import (
"context"
"database/sql"
"fmt"
"net/url"
"os"
_ "github.com/microsoft/go-mssqldb"
)
func main() {
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: url.Values{
"database": {os.Getenv("OC_DATABASE")},
// Mandatory - the listener is TLS-only and validates.
"encrypt": {"true"},
"TrustServerCertificate": {"false"},
}.Encode(),
}).String()
db, err := sql.Open("sqlserver", dsn)
if err != nil {
panic(err)
}
defer db.Close()
rows, err := db.QueryContext(context.Background(),
"SELECT id, customer_ref, amount_cents FROM invoices"+
" WHERE status = @p1 ORDER BY amount_cents DESC",
sql.Named("p1", "open"))
if err != nil {
panic(err)
}
defer rows.Close()
for rows.Next() {
var id, amount int
var ref string
if err := rows.Scan(&id, &ref, &amount); err != nil {
panic(err)
}
fmt.Println(id, ref, amount)
}
}How far this has been proven
The driver-level defects that previously blocked both commercial drivers are fixed, and interop is proven end to end against the real engine - the framed TLS handshake, prepared-statement handles and batch execution included. The measured results were 34 of 34 for mssql-jdbc (13.4.0 and 12.10.1) and 21 of 21 for microsoft/go-mssqldb 1.11.0.
Historical driver results apply only to their tested versions and engine build. They are not ongoing verification; repeat the required checks on any existing integration you intend to keep.
Those historical driver runs used the shared-credential model. They do not verify the per-user read-only login or current production sign-in. Test the actual identity and deployment you will use.
Applications written for Oracle Database
dialect, not transport
This distinction decides whether you can adopt us, so it is worth being blunt about. We support a slice of the Oracle SQL dialect, and we serve it over the PostgreSQL wire. We do not implement Oracle Database's TNS network protocol, there is no listener on 1521, and we do not intend to build one. That is a decision, not a backlog item - the compatibility matrix answers no, not later.
The consequence: an Oracle client library cannot connect to us at all. An Oracle-shaped application connects through a PostgreSQL driver, and what has to port is the SQL, not the transport. If your application can be repointed at a PostgreSQL driver, a large slice of its SQL already works unchanged. If it is welded to an Oracle client library, it cannot connect, and no amount of dialect coverage changes that.
# An application written for Oracle Database connects with a PostgreSQL
# driver, on the PostgreSQL wire port. There is no TNS listener to point
# an Oracle client library at.
psql "host=$OC_SQL_HOST port=5432 dbname=$OC_DATABASE user=$OC_DB_USER sslmode=require"Served, and equivalent within scope
| Construct | Scope |
|---|---|
| DUAL | The one-row dual table. |
| NVL | Two-argument form; the operands must share one type. An empty string is not NULL here. |
| NVL2 | Three-argument form; the branches must share one type. An empty string is not NULL here. |
| Sequences | CREATE SEQUENCE and the sequence object itself. |
| NEXTVAL pseudocolumn | seq.NEXTVAL as a projection - the Oracle spelling. |
| MINUS | A pure spelling alias for EXCEPT: the same operator, whole-word rewritten. |
Served, but narrower than Oracle Database
Each of these ships and is useful, and each is narrower than the construct you know. Where a shape falls outside the served scope it is refused by name rather than executed with the clause quietly dropped - a dropped clause would return a confident, wrong answer.
| Construct | Where it narrows |
|---|---|
| CONNECT BY | START WITH ... CONNECT BY [PRIOR] col = col over a single relation, rewritten to the equivalent WITH RECURSIVE. The hierarchy walk is served, and so are the pseudocolumns LEVEL, CONNECT_BY_ROOT col and SYS_CONNECT_BY_PATH(col, sep) - each carried down the recursion, so WHERE LEVEL <= 3 post-filters the walked hierarchy exactly as Oracle Database does. Refused by name: CONNECT_BY_ISLEAF, ORDER SIBLINGS BY, a PRIOR outside the CONNECT BY condition, more than one relationship, anything but a single PRIOR col = col equality, joins or multiple tables, a subquery in the FROM, and GROUP BY / HAVING / DISTINCT. A pseudocolumn is also refused as a sort key in every spelling - sort on a plain column instead. Nothing is executed with the clause dropped: that would return the unwalked base table with a 200. |
| ROWNUM | The row-cap idioms port, and Oracle Database's counter semantics are preserved rather than approximated. WHERE ROWNUM <= n becomes a row cap; a projected ROWNUM rn numbers the returned rows from 1; the nested pagination idiom (... WHERE ROWNUM <= 20) WHERE rn > 10 returns the window Oracle returns; and WHERE ROWNUM > 1 matches nothing, as in Oracle, rather than being lowered to an offset. Refused by name, because ROWNUM is assigned before the sort and before grouping: a cap in the same block as ORDER BY, or alongside DISTINCT / GROUP BY / HAVING / an aggregate / a window function; a cap on a level that already carries LIMIT / OFFSET / FETCH; and ROWNUM under an OR, compared to a column, or used in GROUP BY / HAVING / ORDER BY / a join condition. For top-n write ORDER BY x FETCH FIRST n ROWS ONLY. One inconsistency to know about: the capability payload still folds ROWNUM into its rowid entry and reports it as roadmap, because that entry covers both pseudocolumns and has not been split yet. The behaviour above is what the engine does. |
| DECODE | NULL equals NULL, and there is no implicit conversion of the search terms. |
| INSTR | Two-argument form only; the position form is refused. |
| SUBSTR | PostgreSQL semantics, positive offsets only. |
| MERGE | Ships, and is narrower than Oracle Database's full MERGE grammar. |
| (+) outer join | One provably equivalent shape: a single SELECT over exactly two comma-joined tables, all marks on one side, each marked predicate a plain col = col. Every other shape is refused rather than answered as an inner join, which would silently lose the unmatched rows the query was written to keep. |
| Recursive queries | WITH RECURSIVE, within a documented narrower scope. |
| SYSDATE | Ships, narrower than Oracle Database. |
| TO_DATE | Ships, narrower than Oracle Database. |
Refused today, with the portable rewrite
| Construct | What to write instead |
|---|---|
| ROWID | Refused. Row identity here is the declared PRIMARY KEY, which every registered table has. A ROWID in Oracle Database is a physical address and a primary key is not one, so code that persisted ROWIDs was relying on something a primary key does not provide - that logic needs revisiting rather than translating. The row-cap uses of ROWNUM are a different construct and do port; see the preview table above. |
| PIVOT / UNPIVOT | Refused. Build the cross-tab with conditional aggregates - SUM(CASE WHEN ... THEN ... END) with a GROUP BY - which is served over a single table and over a join. |
| Flashback query | Refused: AS OF TIMESTAMP, AS OF SCN, VERSIONS BETWEEN. A historical read here is an operator-driven point-in-time restore at the backup surface, not a clause inside a SELECT. |
| PL/SQL | Refused. Stored program units are not accepted. |
| TNS network protocol | Not implemented, and not planned - the compatibility matrix answers no, not later. See the callout above. |
-- Served, and equivalent within the documented scope:
SELECT NVL(discount_pct, 0), NVL2(shipped_at, 'shipped', 'pending') FROM orders;
SELECT seq_invoice.NEXTVAL FROM DUAL;
SELECT sku FROM catalogue MINUS SELECT sku FROM discontinued;
-- Served, but narrower than Oracle Database (see the scope column):
SELECT LEVEL, id, manager_id FROM staff
START WITH manager_id IS NULL
CONNECT BY PRIOR id = manager_id;
SELECT DECODE(status, 'O', 'open', 'C', 'closed', 'other') FROM invoices;
MERGE INTO invoices t USING staging s ON (t.id = s.id)
WHEN MATCHED THEN UPDATE SET t.status = s.status;
-- ROWNUM row caps port, with Oracle's counter semantics kept:
SELECT id FROM invoices WHERE ROWNUM <= 10; -- becomes a row cap
SELECT id FROM invoices WHERE ROWNUM > 1; -- returns NOTHING, as in Oracle
SELECT * FROM ( -- the pagination idiom, verbatim
SELECT a.*, ROWNUM rn FROM ( SELECT id, ref FROM invoices ORDER BY id ) a
WHERE ROWNUM <= 20
) WHERE rn > 10;
-- REFUSED by name, because ROWNUM is assigned BEFORE the sort: this is
-- ten arbitrary rows THEN sorted in Oracle, not the top ten by date.
-- SELECT * FROM invoices WHERE ROWNUM <= 10 ORDER BY created_at;
SELECT * FROM invoices ORDER BY created_at FETCH FIRST 10 ROWS ONLY;
-- NOT served. ROWID has no equivalent; row identity is the PRIMARY KEY.
-- SELECT ROWID, id FROM invoices; -- refusedA note on the names used on this page
Every third-party name here is used to describe protocol or dialect compatibility, and for no other purpose. MySQL and Oracle are registered trademarks of Oracle Corporation and/or its affiliates. Microsoft and SQL Server are trademarks of the Microsoft group of companies. PostgreSQL is a registered trademark of the PostgreSQL Community Association of Canada. Sequelize, mysql2, mssql-jdbc and go-mssqldb are the property of their respective owners. OriginChainDB is not affiliated with, endorsed by, or sponsored by any of them. OriginChainDB is not any of these products - it is an independent database that speaks their wire protocols and accepts parts of their SQL.