Skip to content

MySQL / MariaDB

import { Aside } from ‘@astrojs/starlight/components’;

One mysql adapter targets both MySQL 8 and MariaDB 11 — the same project runs against either. SQLAnvil generates portable MySQL-dialect SQL; engine-specific features ride through operations.

Non-secret settings go in workflow_settings.yaml (committed); the connection — including the password — goes in .df-credentials.json (gitignored):

workflow_settings.yaml
warehouse: mysql
defaultDataset: analytics # the MySQL DATABASE your models build into
defaultAssertionDataset: sqlanvil_assertions
sqlanvilCoreVersion: 1.6.0
// .df-credentials.json (gitignored) — MysqlConnection
{
"host": "localhost",
"port": 3306,
"database": "analytics",
"user": "root",
"password": "your-password",
"sslMode": "disable"
}

Try it locally:

Terminal window
docker run --rm -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=analytics -p 3306:3306 -d mysql:8
# or MariaDB: ... -p 3307:3306 -d mariadb:11
-- definitions/orders.sqlx
config { type: "table" }
SELECT order_id, customer_id, total FROM ${ref("raw_orders")}

Generates a drop + CREATE TABLE … AS SELECT. Identifiers compile to two-part backticks — `analytics`.`orders`.

config { type: "incremental", uniqueKey: ["order_id"] }
SELECT order_id, customer_id, total FROM ${ref("raw_orders")}
${ when(incremental(), `WHERE updated_at > (SELECT MAX(updated_at) FROM ${self()})`) }

uniqueKey compiles to INSERT … ON DUPLICATE KEY UPDATE, and the adapter auto-creates the matching unique index on the first/--full-refresh build — you don’t add your own PK.

Declare secondary indexes and table options:

config {
type: "table",
mysql: {
engine: "InnoDB",
charset: "utf8mb4",
collation: "utf8mb4_unicode_ci",
indexes: [
{ name: "ix_customer", columns: ["customer_id"], unique: false }
]
}
}
SELECT ...
FieldTypeDescription
enginestringStorage engine → ENGINE= (e.g. InnoDB)
charsetstringDefault character set → DEFAULT CHARSET=
collationstringDefault collation → COLLATE=
rowFormatstringRow format → ROW_FORMAT= (e.g. DYNAMIC, COMPRESSED) (≥ 1.19.0)
indexes[].namestringIndex name (derived if omitted)
indexes[].columnsstring[]Indexed columns; a column may carry a MySQL prefix length — "body(50)"`body`(50) (required to index TEXT/BLOB) (≥ 1.19.0)
indexes[].uniquebooleanCreate a UNIQUE index
indexes[].typestring"fulltext" or "spatial" (≥ 1.19.0); mutually exclusive with unique

No where/include/opclass (those are Postgres-only). A SPATIAL index requires a NOT NULL SRID-attributed geometry column — CTAS-created columns are nullable, so spatial normally needs an ALTER TABLE … MODIFY in post_operations first.

Native MySQL/MariaDB partitioning via mysql: { partition: {...} } (≥ 1.11.0):

// RANGE (explicit child partitions)
config { type: "table", mysql: { partition: {
kind: "RANGE", expression: "id",
partitions: [
{ name: "p0", values: "values less than (1000)" },
{ name: "pmax", values: "values less than maxvalue" }
]
}}}
// HASH / KEY (partition count)
config { type: "table", mysql: { partition: { kind: "HASH", expression: "id", count: 4 } } }

kind is RANGE/LIST/HASH/KEY. RANGE/LIST take explicit partitions[] (the values body is emitted verbatim — values less than (...) / values in (...), use MAXVALUE for a catch-all); HASH/KEY take count. MySQL requires every column in the partition expression to be in every UNIQUE/PRIMARY key, so a partitioned incremental’s uniqueKey must include the partition column(s). RANGE COLUMNS / LIST COLUMNS aren’t supported yet — use raw DDL in operations.

description: and columns: apply as real table/column comments, read back from information_schema:

config {
type: "table",
description: "Cleaned orders.",
columns: { order_id: "primary key", total: "order total in cents" }
}
SELECT ...

MySQL/MariaDB have no native materialized views, so materialized: true is emulated as a refreshed table snapshot — rebuilt (drop + CREATE TABLE … AS SELECT) on every run, honoring the mysql: {} block:

config { type: "view", materialized: true, mysql: { indexes: [{ columns: ["id"], unique: true }] } }
SELECT id, count(*) AS n FROM ${ref("events")} GROUP BY id

It reads back as a regular table; refresh = re-run. (The mysql: {} block on a type: "view" config compiles through since 1.19.0 — earlier versions silently ignored it there.)

Use type: "operations". Statements are separated by ---, never ;, so a CREATE PROCEDURE … BEGIN … END body is one statement (its internal ; survive) — don’t use DELIMITER (it’s a client-only directive and will fail).

workflow_settings.yaml (committed):

FieldRequiredDescription
warehousemysql
defaultDatasetDefault database for actions
defaultAssertionDatasetDatabase for generated assertions
sqlanvilCoreVersionPin @sqlanvil/core (≥ 1.6.0 for the mysql: block)

.df-credentials.json (gitignored — the MysqlConnection):

FieldRequiredDescription
hostDatabase host
portDatabase port (default: 3306)
databaseDatabase name
userDatabase user
passwordPassword
sslModedisable or require
SymptomLikely causeFix
Could not connect to MySQL … Access deniedWrong user/passwordCheck .df-credentials.json; for the local container the user is root, password password.
Could not connect … ECONNREFUSEDWrong host/port, or DB not up yetConfirm the container is running and the port (3306 MySQL, 3307 MariaDB); wait for it to accept connections.
Unexpected property "defaultSchema"Postgres-shaped creds on a mysql projectMySQL .df-credentials.json has no defaultSchema — use host/port/database/user/password/sslMode.
Unsupported warehouse "mysql"sqlanvilCoreVersion pinned too lowMySQL warehouse support landed in core 1.5.0; remove the pin or set it ≥ 1.5.0.
TLS / HANDSHAKE_NO_SSL_SUPPORTsslMode: require against a server without TLSUse "sslMode": "disable" for local/non-TLS servers.