OriginChainDB docs
reference · mysql wire

Compatible with the MySQL wire protocol

An OriginChainDB instance can expose a listener that is compatible with the MySQL wire protocol, so a stock MySQL client or driver connects, authenticates and runs SQL without a shim. It reads the same store your HTTP API calls and your PostgreSQL-wire sessions see - there is one copy of your data, not a replica. Whether it also writes depends on which credential model your listener runs: a shared-credential listener reads and writes; a per-user listener is read-only, and refuses every write.

off by default · enabled per instance

The adapter is built into the engine but bound to nothing. No instance gets a MySQL-wire listener by default and enabling one is not self-serve today: it needs an address, a tenant, a credential and TLS material, so talk to us and we will tell you whether your instance can be served at all - the credential models section explains the one case where it cannot. Everything on this page also works today over the SQL endpoint of the HTTP API, which needs no setup.

What this is, and what it is not

This is a compatibility statement about a protocol, not a database product. OriginChainDB implements the MySQL wire protocol independently so that clients written for it can talk to an OriginChainDB instance. We do not run, embed, fork or redistribute MySQL server software, and an instance is not a drop-in replacement for one: the SQL surface is OriginChainDB's, and the differences are listed under limits.

Concretely, the listener speaks protocol version 10: the standard handshake, the text protocol for ordinary queries, and binary prepared statements (PREPARE / EXECUTE / CLOSE / RESET, with ? placeholders bound server-side). Statements arrive in MySQL's dialect and are translated into the engine's, construct by construct - never guessed at. Anything the translator does not recognise comes back as a clean error naming the construct, so a query either runs or fails; it is never quietly turned into a different query.

Row-level security and column masking are applied inside the engine, below the wire, so a MySQL session sees exactly the rows and columns your policies allow - the same ones an HTTP call under that identity would see.

Connect

First find out whether your instance has a listener at all. Ask the capabilities endpoint:

curl -s -H "Authorization: Bearer $OC_TOKEN" \
  "https://$OC_HOST/v1/capabilities" | jq '.wire.mysql'

# Until a listener is configured for your instance:
#   { "available": false }
#
# "available" reports that CONFIGURATION IS COMPLETE. It is a precondition
# for starting a listener, not an observation of a bound socket - so when it
# turns true, still check the port and a real login before you rely on it.

Once it is enabled, connect the way you would connect to anything speaking this protocol:

# The port follows the convention MySQL clients expect. The host and port
# your instance is actually published on are shown in the console once the
# listener is enabled - use those, not this default.

mysql --host "$OC_MYSQL_HOST" --port 3306 \
      --user "$OC_DB_USER" --password \
      --ssl-mode=REQUIRED \
      --database "$OC_DB"

The authentication plugin depends on which credential model your listener runs: mysql_native_password for a shared credential, caching_sha2_password for per-user login. Both are what a stock client already negotiates, so you do not configure the plugin - you configure the model, and the plugin follows.

TLS, and why a certificate is not optional

The listener implements the standard TLS upgrade: it advertises TLS support in the handshake, a capable client answers by asking to upgrade before it authenticates, and the rest of the session runs inside TLS. That is exactly what --ssl-mode=REQUIRED does, so stock clients interoperate without special handling.

a shared-credential listener fails open

Configured with a shared credential and no certificate, the listener still binds and still serves - with TLS simply off. It does not refuse to start, and a client that does not ask to upgrade is answered in cleartext: passwords, queries, and every result row, including rows your policies filtered and columns they masked, cross the network in the clear. Nothing warns you at connect time, because from the client's point of view it worked. Never enable a shared-credential listener without a certificate, and set the option that refuses plaintext authentication rather than relying on every client to opt in.

Per-user login is the opposite: it cannot be configured without TLS. That model's authentication delivers the password to the server in cleartext inside the session, so the engine forces the refuse-plaintext option on and will not start a per-user listener that has no certificate. If the tenant you are enabling must use per-user login - see below - TLS is settled for you.

Credential models, and which one you will be on

There are two, and the choice is not a preference - it is decided by whether your 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 client sends You are on it when
shared One listener-wide password. Every session arrives as the same identity. Your tenant has no database RBAC grants and no row-level security policies at all. A shared credential cannot express a per-user grant or evaluate a per-user row policy, so the engine refuses to combine the two rather than serve them loosely.
per-user Each database user authenticates with its own password, and its own grants apply for the session. Your tenant has any database RBAC grant, or any row-level security policy. This is the only model that can serve it. TLS is mandatory here, and the session is read-only.
a per-user listener is read-only

A per-user MySQL session refuses every write. Not some writes, and not writes your grants happen to disallow - the listener marks the session read-only when 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.

Read that together with 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 the MySQL wire at all. That tenant must be on per-user login, and per-user login does not write. There is no combination of settings that yields an authorizing, writing MySQL session today.

So the write examples on this page and under MySQL examples - CRUD, transactions and ON DUPLICATE KEY UPDATE - apply 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 MySQL listener for reads.

Put the two rules together and there is a real state that neither model can serve: a tenant that has database RBAC or a row-security policy but has no database user carrying a password. Shared login is refused because that authorization is present; per-user login is refused because there is no account to admit. That is the ordinary state of a tenant whose access has only ever been through API tokens, so it is worth checking before you plan the work. Creating one enabled database user with a password is the whole fix.

A worked example: Sequelize over mysql2

This is the client we drove against the shipping engine - Sequelize 6.37 on top of mysql2 3.24 - so what follows is a transcript of what worked, not a sketch of what should. The shape to take away: declare your tables in code and the ORM drives a complete single-table workload; let it introspect the database instead and it will not get far.

Connect and define

import mysql2 from "mysql2";
import { Sequelize, DataTypes } from "sequelize";

// Verified against the shipping engine with Sequelize 6.37 over mysql2 3.24.
const sequelize = new Sequelize(process.env.OC_DB, process.env.OC_DB_USER, process.env.OC_DB_PASSWORD, {
  host: process.env.OC_MYSQL_HOST,
  port: 3306,
  dialect: "mysql",
  dialectModule: mysql2,
  // Ask for TLS explicitly. A listener with no certificate will happily
  // serve you in cleartext instead of refusing - see "TLS" above.
  dialectOptions: { ssl: { minVersion: "TLSv1.2", rejectUnauthorized: true } },
  logging: false,
});

// Describe the table in code. Do NOT call sequelize.sync() and do not rely on
// queryInterface.describeTable() - both introspect with SHOW, which is refused.
const Order = sequelize.define(
  "Order",
  {
    id: { type: DataTypes.STRING, primaryKey: true },
    customer: DataTypes.STRING,
    total_cents: DataTypes.INTEGER,
    status: DataTypes.STRING,
  },
  { tableName: "orders", timestamps: false, freezeTableName: true },
);

await sequelize.authenticate();

Insert, select, update, delete

// INSERT
await Order.create({ id: "o_1", customer: "c_7", total_cents: 4200, status: "open" });

// SELECT with a filter and an ordering
const open = await Order.findAll({
  where: { status: "open" },
  order: [["total_cents", "DESC"]],
  limit: 10,
});

// UPDATE
await Order.update({ status: "shipped" }, { where: { id: "o_1" } });

// DELETE, bounded by a WHERE
await Order.destroy({ where: { id: "o_1" } });

Transactions

Transactions are real, not acknowledged-and-ignored. A block that commits persists, and a block that rolls back leaves nothing behind - both were exercised.

// Both outcomes were exercised against the engine: a transaction that
// commits, and one that rolls back leaving no trace.
const tx = await sequelize.transaction();
try {
  await Order.create({ id: "o_2", customer: "c_7", total_cents: 900, status: "open" }, { transaction: tx });
  await Order.update({ status: "paid" }, { where: { id: "o_2" }, transaction: tx });
  await tx.commit();
} catch (err) {
  await tx.rollback();
  throw err;
}

More recipes, one per page, are under MySQL examples.

The catalog surface

This is the section that decides whether a tool works, so read it before you adopt one. Metadata is split down the middle: the information_schema views are answered, and every SHOW form is refused.

Answered: the information_schema views

Five views are served, by the same shared catalog the other wire protocols read: tables, columns, table_constraints, key_column_usage and referential_constraints. They are ordinary queries, so they work from any client:

-- Answered by the shared catalog, the same one the other wire
-- protocols and the HTTP API read.
SELECT table_name FROM information_schema.tables WHERE table_schema = 'shop';
SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'orders';

Refused: every SHOW form

mysql> SHOW TABLES;
ERROR 1235 (42000): MySQL `SHOW …` metadata commands are not supported —
the engine keeps no MySQL information schema.

-- Refusing is the deliberate choice. The shared front door carries a
-- PostgreSQL "SHOW <guc>" shim that would otherwise answer ANY SHOW with a
-- single fabricated row reading  x = 'on'  - so SHOW TABLES would return a
-- confident, wrong answer instead of an error.

SHOW TABLES, SHOW DATABASES, SHOW COLUMNS, SHOW CREATE TABLE, SHOW INDEX, SHOW WARNINGS and SHOW VARIABLES LIKE … all return an error. We would rather hand you an error you can act on than a fabricated row you would believe.

Session variables are handled separately and honestly: the ones a connector reads at startup are answered with their true values, and one that does not exist is refused with the unknown-variable error rather than a made-up value. SET NAMES is accepted as a no-op, because every official connector opens with one.

Limits

Every one of these returns a clear error naming the construct. None of them silently returns a different answer.

the one that decides adoption

Schema reflection is split, and associations do not load. An ORM pointed at this listener can connect, authenticate and run a full single-table workload including transactions - we measured exactly that - but it cannot reflect a schema it was not told about, and it cannot follow relations between tables. So it suits an application whose tables you declare in code, and it does not suit one that models a domain with associations. If your application depends on relations, use the SQL endpoint or the PostgreSQL wire instead. Improving this is work in progress, and work in progress is not a feature - plan against what is written here.

Not supported Why, and what to write instead
SHOW … No MySQL information schema is kept. Query the information_schema views listed above instead.
REPLACE INTO Its delete-then-insert semantics reset unmentioned columns to defaults, which is not what the engine's upsert does. Use INSERT … ON DUPLICATE KEY UPDATE, which is translated.
INSERT IGNORE Silently swallowing errors is not a behaviour we will emulate. Handle the conflict explicitly.
Multi-table DML DELETE t1, t2 FROM …, DELETE … USING …, DELETE … ORDER BY / LIMIT and joined UPDATEs. The engine models single-table DML; a DELETE bounded by WHERE works.
GROUP_CONCAT, SUBSTRING_INDEX No engine equivalent to map them onto. Most other common scalar functions are translated or pass through unchanged.
User variables (@x) The engine has nowhere to keep session-scoped user variables. Keep the value in your application.
Multi-statement queries Send one statement per query. Do not enable your driver's multi-statement option.
Double-quoted strings "x" is parsed as an identifier, not a string. Spell strings with single quotes - 'x' - which is what clients do anyway.
Chunked parameter sends A prepared statement whose parameter was streamed in chunks is refused rather than executed with an incomplete value. Bind the value in one go.

Trademark

MySQL is a registered trademark of Oracle Corporation and/or its affiliates. OriginChainDB is not affiliated with, endorsed by, or sponsored by Oracle Corporation. OriginChainDB implements a compatible wire protocol so that existing MySQL clients can connect to it; it does not distribute MySQL software.