MySQL
MySQL and MariaDB. Same Model surface as
Postgres and
SQLite
— hash .where, associations, aggregates,
migrations, jobs — as _key + JSON
doc tables or a
column-aware
mapping onto a real schema.
SoliDB remains the full stack (graph, vector, timeseries, raw SDBQL).
Use MySQL when the data already lives there, or as a secondary connection beside SoliDB via
config/database.toml.
Connect
Whole app on MySQL:
SOLI_DB_ADAPTER=mysql
DATABASE_URL=mysql://user:pass@localhost:3306/myapp
# optional
SOLI_DB_POOL_SIZE=10
Aliases: mysql, mariadb.
A named connection:
default = "primary"
[connections.warehouse]
adapter = "mysql"
url = "${WAREHOUSE_DATABASE_URL}"
pool = 5
class FactSale < Model
connection "warehouse"
end
The mysql Cargo feature is on by default.
A binary built without it refuses to open a MySQL connection at boot and tells you to rebuild.
URL
mysql://USER:PASSWORD@HOST:3306/DATABASE
Placeholders in raw SQL are ?.
Identifiers are backtick-quoted.
TLS
The client speaks TLS — rustls with the ring provider,
compiled in, so there is no system OpenSSL to install. The modes are MySQL's; libpq's spellings
(prefer, require, verify-ca, verify-full) parse to
the same rungs, so a URL copied from either ecosystem works.
ssl-mode |
Encrypts | Verifies the chain | Verifies the hostname |
|---|---|---|---|
DISABLED | no | — | — |
PREFERRED (default) | when the server supports it | no | no |
REQUIRED | yes | no | no |
VERIFY_CA | yes | yes | no |
VERIFY_IDENTITY | yes | yes | yes |
# managed MySQL — the mode to use
DATABASE_URL=mysql://user:pass@db.example.com:3306/myapp?ssl-mode=VERIFY_IDENTITY
# verify against the server's own CA (MySQL writes `ca.pem` into its data dir)
DATABASE_URL=mysql://user:pass@db.internal:3306/myapp?ssl-mode=VERIFY_CA&ssl-ca=/var/lib/mysql/ca.pem
PREFERREDis the default. The driver has no opportunistic mode, so Soli probes once with TLS when the pool opens and falls back to cleartext if the server declines. Anything stronger never downgrades.REQUIREDand up connect over TCP, even for alocalhostURL. The driver skips TLS on a Unix socket outright, so preferring the socket would satisfyREQUIREDwith a cleartext connection — the promise has to be real.- MySQL's auto-generated server certificate is self-signed:
REQUIREDaccepts it (it encrypts without identifying), whileVERIFY_CAneedsssl-capointing at that server'sca.pem. ssl-careplaces the built-in Mozilla roots rather than adding to them, and a CA file supplied with a mode that would not consult it is refused, not silently ignored.- A mandatory mode fails at boot naming the reason, e.g.
connection "primary" asked for ssl-mode=verify-full: invalid peer certificate: UnknownIssuer.
Create and drop
A SQL server does not create the database on first use. On a fresh target:
soli db:create # CREATE DATABASE IF NOT EXISTS (db-less connection)
soli db:migrate up
soli db:schema:dump
soli db:drop # DROP DATABASE IF EXISTS
db:create connects to the server
without selecting a database (the target may not exist yet),
then CREATE DATABASE IF NOT EXISTS. Both accept
--connection NAME.
Document tables
Each collection is:
table <collection>
_key VARCHAR(255) PRIMARY KEY
doc JSON NOT NULL
The table is created on first write. JSON operators are
JSON_EXTRACT / JSON_UNQUOTE /
JSON_SET / JSON_MERGE_PATCH.
String equality in .where({ "status": "open" })
compares on the text extract.
MySQL has no RETURNING:
an atomic increment writes with JSON_SET then reads the new
value on the same connection.
Indexes
MySQL cannot index a JSON extract directly.
index "status" (or soli db:indexes) therefore:
- Adds a generated
STOREDcolumn for the extract (skipped if it already exists — MySQL has noIF NOT EXISTSonALTER TABLE … ADD COLUMN). - Creates an index on that column (skipped if the index name already exists).
Multi-field and unique: work the same way.
Numbers and booleans keep JSON comparison and are not covered by that generated-column index.
Migrations can index a document field with a doc. prefix:
db.add_index("posts", ["doc.status"], { "unique": true }).
fulltext / vector_index /
geo_index are reported as skipped on SQL.
Background jobs
The queue lives in _jobs on the same connection.
MySQL has no SKIP LOCKED in the form Soli uses on Postgres,
so a claim writes a unique token into locked_by and then
selects the rows that hold that token. Several workers can poll; a row belongs to whoever
wrote the token first. On first enqueue Soli indexes state,
run_at, and priority.
See Jobs.
Schema dump
soli db:schema:dump # writes db/schema.sql
soli db:schema:load # recreate a fresh database from that file
The dump is SHOW TABLES +
SHOW CREATE TABLE for each, plus a
-- versions: header so load records applied migrations
without replaying every file. SHOW CREATE is the server's own SQL, so defaults,
keys, and engine options come through.
Column-aware models
class Order < Model
connection "warehouse"
table "orders"
end
Introspection uses information_schema.
AUTO_INCREMENT keys are left to the database on insert.
| MySQL | Soli |
|---|---|
smallint / mediumint / int / bigint | Int |
float / double | Float |
decimal | Float (written as text; values beyond f64 lose precision on read) |
tinyint(1), boolean, bit(1) | Bool |
char / varchar / text / enum | String |
date / datetime / timestamp | DateTime, interpreted as UTC |
json | Hash / Array |
blob, geometry, unsigned bigint | unsupported |
There is no native ILIKE.
Hash .where({ "email": { "ilike": "%@x.com" } })
compiles to LOWER(email) LIKE LOWER(?).
Migrations emit foreign keys at table level:
MySQL parses an inline REFERENCES and then ignores it.
CREATE INDEX has no IF NOT EXISTS;
DROP INDEX needs the table name.
Honest limits
- No timezone-aware timestamp.
datetime/timestampvalues are UTC. - A Unix socket is never encrypted.
ssl-mode=REQUIREDand up therefore force TCP; a socket-only server is reachable in cleartext modes only. - No graph, vector, fulltext, geo, columnar, timeseries, or raw SDBQL. Those stay on SoliDB. Use
Model.find_by_sqlfor SQL the portable surface cannot express. grouped {}read-coalescing is SoliDB-only.- Composite primary keys are refused at boot on column-aware models.
encryptsand STI work in column mode: encrypted fields must be text columns; STI subclasses need a stringtypecolumn.- A reserved name (
_migrations,_jobs,_cron_jobs) cannot be created, dropped, or renamed by a migration.
The shared portable surface is documented under SQL document backends.