Skip to content
UQL

MySQL & MariaDB

MySQL and MariaDB share a dialect base but are two entry points with two drivers. Pick the one that matches the server you run: the generated SQL differs, and the MySQL dialect emits JSON operators MariaDB does not have.

Server Entry point Driver
MySQL 8.0.19+ uql-orm/mysql mysql2
MariaDB 10.5+ uql-orm/maria mariadb
Either, under Bun uql-orm/bunSql built in
Terminal window
npm install uql-orm mysql2 # or: mariadb
import { MySql2QuerierPool } from 'uql-orm/mysql';
// MariaDB: import { MariadbQuerierPool } from 'uql-orm/maria';
export const pool = new MySql2QuerierPool({
host: 'localhost',
user: 'app',
password: process.env.DB_PASSWORD,
database: 'app',
connectionLimit: 10,
});

Both take their driver’s own pool config verbatim. Sizing, lifetime and shutdown are the same on every driver: see Pool.

The one behavioral difference worth knowing before choosing. MariaDB 10.5+ has INSERT ... RETURNING, so ids come back exact. MySQL has none: the driver reports only the first id and UQL infers the rest by incrementing it.

const ids = await pool.insertMany(User, [
{ email: 'a@example.com' },
{ email: 'b@example.com' },
]);

That inference holds under innodb_autoinc_lock_mode 0 or 1. MySQL 8 defaults to mode 2 (interleaved), where a concurrent insert into the same table can interleave with your statement’s allocation and leave the block non-contiguous. On a hot table, set lock mode 1 or insert row by row.

  • Tables are ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; the surrogate key is BIGINT AUTO_INCREMENT, spelled from the type the @Id declares so a foreign key column can match it.
  • Transactions use START TRANSACTION with the isolation level set on the session just before. All four levels work; the default is repeatable read.
  • Upserts compile to INSERT ... ON DUPLICATE KEY UPDATE, or INSERT IGNORE when every non-conflict column is a conflict key. Neither takes a conflict target: the server picks the unique index.
  • Arrays are not native: a string[] field is stored as JSON and queried with the JSON operators.
  • JSON paths differ: MySQL uses -> / ->>, MariaDB uses JSON_VALUE() / JSON_EXTRACT(), which every version it supports has. Emitting the right one is the main reason the entry points are separate.
  • A JSON number compares as a DOUBLE on both sides, fractions included. On MySQL that is also what lets a jsonPath index serve it; MariaDB has no expression indexes.
  • $elemMatch explodes the array with JSON_TABLE, one JSON column per element whose fields are read as paths. One value, or one of several, compiles to JSON_CONTAINS() or, on MySQL, JSON_OVERLAPS() instead, which a jsonArray index serves. On MySQL the exploded form carries a NO_SEMIJOIN() hint: without it the planner can answer every row with one row’s elements.
  • Sorting by a JSON path orders a number by its value: MySQL sorts the JSON value, MariaDB the number and then the text.
  • Bound parameters cap at 65,535, so large insertMany payloads are chunked for you.
  • A populated to-many is aggregated in the parent’s statement: an ordered GROUP_CONCAT on MySQL (8.0.14+), JSON_ARRAYAGG on MariaDB. The statement lifts group_concat_max_len for itself.

$text compiles to MATCH(...) AGAINST(...), which needs a FULLTEXT index over exactly the columns searched, in order:

import { Index } from 'uql-orm';
@Index((post) => [post.title, post.body], { type: 'fulltext' })

Migrations create it, and follow one added to a table with rows by OPTIMIZE TABLE, without which InnoDB scores the index 0 or fails the search. Without it the server answers “Can’t find FULLTEXT index matching the column list”.

A weighted index ({ column: post.title, weight: 3 }) also gets a FULLTEXT index over each column heavier than the lightest, since MATCH scores a column through an index of its own. See column weights.

MariaDB 11.7+ has a native VECTOR(n) type and a VECTOR INDEX of its own, so semantic search works with no extension. A vector reads back from its packed bytes, every float32 digit kept. MySQL has the VECTOR column but no distance function outside HeatWave, so it has nothing to search on, and UQL stores a vector field there as JSON.

Both drivers stream natively: mysql2 through its result-set .stream(), mariadb through queryStream.