Skip to main content

MySQL connector

Extracts a MySQL table into canonical events with keyset pagination (never OFFSET): rows are ordered by (cursor_column, primary key) and fetched in pages of 50,000 rows through a streaming cursor. The session is read-only by construction (SET SESSION TRANSACTION READ ONLY) and pinned to UTC (SET time_zone = '+00:00', so TIMESTAMP values arrive as UTC instants). The connector never writes to your database — no temp tables, no DDL.

A primary key is required: MySQL has no ctid-style fallback, so a PK-less table fails PUT /sources/{id}/mapping with keyset_tiebreak_unavailable.

Prerequisites​

  • An API key with the admin scope.
  • MySQL 5.7+ over TLS (8.0.16+ recommended: row-value keyset comparisons use indexes natively; older servers get an equivalent expanded predicate).
  • A network path from Tessera to the database: a reachable host, or a bastion for the SSH tunnel.

Allowed destinations​

Tessera connects only to public internet addresses. It checks config.host, or config.tunnel.host with a tunnel, before any connection, using the same rules as the Postgres connector:

  • one DNS name or one IP address;
  • no private or cluster names;
  • every resolved address must be public;
  • the port is 3306 (22 for the bastion) or 1024–65535.

Tessera then connects to the address it checked. The TLS server name stays the configured host. A refused destination fails tcp_or_tunnel with failure: configuration. PUT /mapping returns 400 invalid_argument, and a sync fails with extraction_failed. Both carry a field_violation naming the refused field:

FieldRefused when
config/hostthe host is malformed, private or cluster-internal, or does not resolve to public addresses
config/portthe port is privileged and not 3306
config/tunnel/hostthe bastion is malformed, private or cluster-internal, or does not resolve to public addresses. Use a bastion with a public address; the database behind it can stay private.
config/tunnel/portthe bastion port is privileged and not 22

Create the credential (least privilege)​

CREATE USER 'tessera_reader'@'%' IDENTIFIED BY '...' REQUIRE SSL;
GRANT SELECT ON app.orders TO 'tessera_reader'@'%';

SELECT on the one table is all the connector uses: it also makes the table's columns and primary key visible in information_schema (schema and keyset discovery), and the session's SET SESSION TRANSACTION READ ONLY and SET time_zone need no privilege. Narrow the host part ('%') to Tessera's egress address when you know it.

Create the source​

{
"name": "orders",
"kind": "mysql",
"config": {
"host": "db.internal.example.com",
"port": 3306,
"database": "app",
"table": "orders",
"ssl_mode": "required"
},
"credentials": {
"username": "tessera_reader",
"password": "..."
}
}

ssl_mode is required (default: TLS enforced, certificate unverified) or verify_ca (TLS + CA chain verification) — plaintext connections are never allowed. The config object validates against the connector's registered schema:

{
"host": "db.internal.example.com",
"port": 3306,
"database": "app",
"table": "orders",
"ssl_mode": "required"
}

SSH tunnel​

When the database is only reachable through a bastion, set config.tunnel and vault the SSH key alongside the database credentials — identical to the Postgres connector:

{
"host": "10.0.3.7",
"database": "app",
"table": "orders",
"tunnel": {"host": "bastion.example.com", "port": 22, "username": "tessera"}
}

All database TCP traffic then flows through an SSH local forward established with credentials.tunnel_private_key.

Test the connection​

POST /sources/{id}/connection-diagnostics runs three checks in order, each tagged with the diagnostic stage it verifies:

CheckStageVerifies
tcp_or_tunnelnetworka TCP connection to the host, or the SSH tunnel to the bastion
authenticateauthenticationa TLS session with the stored username and password
table_readableauthorizationa zero-row SELECT on the table succeeds and its columns are visible

MySQL's error number decides a failed check's failure cause: 1045 (the password was refused) is authentication; 1044, 1130 and 1142 (no access to the database, host not allowed, SELECT denied) are authorization; 1049 and 1146 are not_found; 2003 (unreachable host or failed TLS handshake) is network. Details quote the number, never the password or the server's message; later checks report not_attempted. The response's checked_at is the server time of the test, which is also recorded in the audit log as source.test_connection. POST /sources/{id}/test-connection runs the same checks and returns only their names, statuses and details.

Rotate the credential​

The source holds the account's password, and the SSH tunnel private key when a tunnel is on. To rotate the password: set a new one on the account, or create a second account with the same grants, then PATCH /sources/{id} with credentials. The body replaces the stored credentials entirely, so repeat the username and, with a tunnel, the tunnel_private_key. Confirm POST /sources/{id}/connection-diagnostics passes before you retire the old password or account. A sync already running keeps the password it started with.

Type map​

Column types map onto canonical attribute types exactly as follows; mapping a column whose type is unsupported fails PUT /sources/{id}/mapping naming the column and its type. MySQL has no array type — no column maps to array.

MySQL typeCanonical typeNotes
tinyint(1)booleanThe MySQL boolean convention (BOOL/BOOLEAN): 0 → false, non-zero → true.
tinyintintegerExact integers (display width ≠ 1).
smallintintegerExact integers.
mediumintintegerExact integers.
intintegerExact integers (INTEGER).
bigintintegerExact 64-bit integers.
decimalnumberNUMERIC — converted to float64 (beyond-precision values convert lossily). A column mapped as an entity or event id lands as an exact decimal instead (up to 38 digits before and after the point), so ids above 2^53 - 1 stay exact; a wider value is refused for its row only, counted in the sync's details (cast such a column to text in a view to keep its ids).
floatnumberIEEE-754 floats.
doublenumberIEEE-754 float64 (DOUBLE PRECISION, REAL), as-is.
charstringFixed-length strings, as returned by MySQL.
varcharstringUTF-8 strings, as-is (≤ 4096 chars per 002 §2).
textstringTINYTEXT/TEXT/MEDIUMTEXT/LONGTEXT, as UTF-8 strings.
enumstringThe enum label as a string.
setstringMembers joined as a comma string ("a,b"), in table-definition order.
timestamptimestampUTC instants (the extraction session sets time_zone = '+00:00').
datetimetimestampNaive wall-clock time: the mapping's IANA timezone is required — without it, rows carrying a value are rejected.
datetimestampMidnight UTC of the date (YYYY-MM-DDT00:00:00Z).
jsonunsupportedCannot be mapped (flatten upstream); PUT /mapping fails naming the column.
blobunsupportedTINYBLOB/BLOB/MEDIUMBLOB/LONGBLOB cannot be mapped.
binaryunsupportedBINARY/VARBINARY cannot be mapped; PUT /mapping fails naming the column.
geometryunsupportedAll spatial types cannot be mapped; PUT /mapping fails naming the column.
bitunsupportedCannot be mapped (R15 marks BIT(>1) unsupported; BIT(1) is undefined there and denies by default — use TINYINT(1) for booleans).

DATE maps to midnight UTC by convention; if your dates carry local-day semantics, expose a naive DATETIME column instead and set the mapping timezone.

A DECIMAL id value the exact decimal cannot carry lands, with the other such rows of its fetched page, after that page's other rows and in a landed part of its own. Row numbers in the rejected-rows report follow that landed order.

Incremental syncs​

Set a mapping cursor on a TIMESTAMP/DATETIME/DATE or integer column. Each sync extracts only rows with cursor > watermark (strictly greater), and the watermark advances only when a sync commits — so a failed sync re-extracts the same rows. Rows whose cursor is NULL are never extracted by incremental syncs.