Skip to content
Khanetor
Go back

A MariaDB driver for Effect SQL

Edit page

If you run MariaDB in production — and plenty of us do, whether by distro default, hosting choice, or preference for what MariaDB does with its forks of MySQL features — the Effect ecosystem has quietly had a gap: @effect/sql ships adapters for MySQL, PostgreSQL, SQLite, MSSQL, even ClickHouse, but not MariaDB. The usual workaround is to use the MySQL adapter, since every MySQL client library will happily connect to a MariaDB server. It works, mostly. It’s just that “mostly” carries a small asterisk, which I’ll get to.

Close-up of a serene sea lion basking in the sun at Copenhagen Zoo
Photo by Lars H Knudsen

I won’t re-litigate why @effect/sql is worth using — the short version is that it gives you a single, typed, Effect-native interface over your database regardless of which engine sits underneath.

effect-sql-mariadb fills the gap: a SqlClient layer backed by the official mariadb Node.js driver, published from Codeberg. This post is about what’s in the box, and why I deliberately picked the mariadb driver over the more common mysql2.

What you get

If you’ve used any @effect/sql adapter, the shape is familiar. You get SqlClient.SqlClient from a scoped layer, tagged templates for statements, and the rest of the @effect/sql toolkit — schema-validated queries, streaming, transactions — on top:

import { SqlClient } from "@effect/sql"
import { Effect, Schema } from "effect"
import { MariadbClient } from "effect-sql-mariadb"

const program = Effect.gen(function* () {
  const sql = yield* SqlClient.SqlClient

  const Person = Schema.Struct({
    name: Schema.String,
    age: Schema.Number,
  })

  const findAdults = SqlSchema.findAll({
    Request: Schema.Struct({ minAge: Schema.Number }),
    Result: Person,
    execute: ({ minAge }) =>
      sql`SELECT name, age FROM people WHERE age >= ${minAge} ORDER BY age DESC`,
  })

  return yield* findAdults({ minAge: 18 })
})

const MariaLive = MariadbClient.layer({
  host: "127.0.0.1",
  port: 3306,
  user: "app",
  password: "secret",
  database: "app",
  connectionLimit: 5,
})

Effect.runPromise(Effect.provide(program, MariaLive))

MariadbClient.layer accepts the mariadb driver’s PoolConfig directly — there is no parallel config format to learn. The pool and the reserved connections used for transactions are scoped: when the layer’s scope closes, they’re released automatically, so teardown behaves the way Effect resources should. Fail-fast is built in — the layer pings the server on acquisition instead of discovering a bad host on your first query.

Under the hood it’s a small, readable adapter: prepared and unprepared statements, raw results, rowsAsArray, streaming via the driver’s queryStream, nested transactions, and a statement compiler wired for MySQL dialect syntax with one MariaDB-flavored extra — multi-row upserts through ${sql.insert(rows)} ${sql.updateValues(rows, "t")}, which compiles to INSERT ... ON DUPLICATE KEY UPDATE col = VALUES(col). Because it speaks the SqlClient interface, everything else in @effect/sql — SqlResolver, SqlSchema, migrations tooling — drops in without adaptation.

One MariaDB quirk gets first-class treatment: the boolean schema. MariaDB (like MySQL) has no native BOOLEAN type — it’s TINYINT(1), so queries return 0/1 and your Schema.Boolean validation fails on every row. The package ships MariadbSchema.MariaBool, a transform that decodes 0/1 into boolean and encodes true/false back to 1/0, rejecting anything in between — so a column holding 2 or a stray string fails loudly instead of silently coercing:

import { MariadbSchema } from "effect-sql-mariadb"

const Person = Schema.Struct({
  name: Schema.String,
  active: MariadbSchema.MariaBool,
})

The unit tests don’t touch the network: the driver’s pool is replaced with an in-process fake, so bun test runs anywhere, including CI without a database.

Why the mariadb driver and not mysql2

This is the decision worth explaining, because on the surface it looks like the wrong one. mysql2 is the default choice in the Node ecosystem — heavily used, well tested, constantly maintained. The mariadb package is the official connector from MariaDB itself, and its Node bindings get a fraction of the community attention. Why pick it?

Because MariaDB’s wire protocol has genuinely diverged from MySQL’s, and a client built for MySQL implements MySQL’s side of that conversation, not MariaDB’s. The divergence is not ancient history, and it is not cosmetic:

To be fair to mysql2: that PR also put MariaDB 11.8 into its CI matrix, and recent versions negotiate extended metadata for MariaDB-specific types. It works better than it used to. But it remains a MySQL client adding MariaDB support, with a docs page cataloguing the workarounds — whereas the mariadb driver is built by MariaDB for MariaDB, implements every auth plugin a MariaDB server can request, and treats the MariaDB protocol extensions as first-class features (pipelining, bulk insert, streaming) rather than compatibility notes.

The honest counterpoint: if your app must one day run against both MySQL and MariaDB — some customers on one, some on the other — mysql2 remains the pragmatic pick, since the @effect/sql MySQL adapter targets it directly. This library exists for the opposite situation: MariaDB is the target, not a server it happens to tolerate. The driver was made for this database; using anything else is a compatibility shim.

Try it

The package is published on Codeberg’s npm registry, not npmjs — install the tarball directly:

bun add https://codeberg.org/api/packages/khanetor/npm/effect-sql-mariadb/-/effect-sql-mariadb-0.1.2.tgz

(Or with npm/pnpm — same URL, same command shape. A scoped package name with an .npmrc registry override is planned, but that’s a breaking rename, so the tarball install is the stable path for now.)

The repo ships runnable examples — a schema-validated query and a CRUD tour — you can point at any MariaDB instance:

bun run example

Details, API, and source are in the repo README. If you hit anything odd — or you’re the one running both MySQL and MariaDB and want that adapter story — issues and PRs are open.


Edit page
Share this post:

Previous Post
Branching Keycloak login on identity-provider claims