Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,12 @@ services:
MYSQL_DATABASE: db0
MYSQL_USER: test
MYSQL_PASSWORD: test
mariadb:
# https://hub.docker.com/_/mariadb
image: mariadb
ports: ["3307:3306"]
environment:
MARIADB_ROOT_PASSWORD: test
MARIADB_DATABASE: db0
MARIADB_USER: test
MARIADB_PASSWORD: test
30 changes: 30 additions & 0 deletions docs/2.connectors/mariadb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
icon: simple-icons:mariadb
---

# MariaDB

> Connect DB0 to MariaDB Database using mariadb

## Usage

For this connector, you need to install [`mariadb`](https://www.npmjs.com/package/mariadb) dependency:

:pm-install{name="mariadb"}

Use `mariadb` connector:

```js
import { createDatabase } from "db0";
import mariadb from "db0/connectors/mariadb";

const db = createDatabase(
mariadb({
/* options */
}),
);
```

## Options

:read-more{to="https://github.com/mariadb-corporation/mariadb-connector-nodejs/blob/main/types/share.d.ts#L603"}
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"eslint": "^10.0.3",
"eslint-config-unjs": "^0.6.2",
"jiti": "^2.6.1",
"mariadb": "^3.4.5",
"mlly": "^1.8.1",
"mysql2": "^3.20.0",
"obuild": "^0.4.32",
Expand All @@ -76,6 +77,7 @@
"@libsql/client": "*",
"better-sqlite3": "*",
"drizzle-orm": "*",
"mariadb": "*",
"mysql2": "*",
"sqlite3": "*"
},
Expand All @@ -92,6 +94,9 @@
"mysql2": {
"optional": true
},
"mariadb": {
"optional": true
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"@electric-sql/pglite": {
"optional": true
},
Expand Down
2,046 changes: 1,218 additions & 828 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion src/_connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import type { ConnectorOptions as LibSQLCoreOptions } from "db0/connectors/libsq
import type { ConnectorOptions as LibSQLHttpOptions } from "db0/connectors/libsql/http";
import type { ConnectorOptions as LibSQLNodeOptions } from "db0/connectors/libsql/node";
import type { ConnectorOptions as LibSQLWebOptions } from "db0/connectors/libsql/web";
import type { ConnectorOptions as MariaDBOptions } from "db0/connectors/mariadb";
import type { ConnectorOptions as MySQL2Options } from "db0/connectors/mysql2";
import type { ConnectorOptions as NodeSQLiteOptions } from "db0/connectors/node-sqlite";
import type { ConnectorOptions as PgliteOptions } from "db0/connectors/pglite";
import type { ConnectorOptions as PlanetscaleOptions } from "db0/connectors/planetscale";
import type { ConnectorOptions as PostgreSQLOptions } from "db0/connectors/postgresql";
import type { ConnectorOptions as SQLite3Options } from "db0/connectors/sqlite3";

export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "cloudflare-hyperdrive-mysql" | "cloudflare-hyperdrive-postgresql" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mysql2" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3";
export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "cloudflare-hyperdrive-mysql" | "cloudflare-hyperdrive-postgresql" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mariadb" | "mysql2" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3";

export type ConnectorOptions = {
"better-sqlite3": BetterSQLite3Options;
Expand All @@ -32,6 +33,7 @@ export type ConnectorOptions = {
/** alias of libsql-node */
"libsql": LibSQLNodeOptions;
"libsql-web": LibSQLWebOptions;
"mariadb": MariaDBOptions;
"mysql2": MySQL2Options;
"node-sqlite": NodeSQLiteOptions;
/** alias of node-sqlite */
Expand All @@ -56,6 +58,7 @@ export const connectors: Record<ConnectorName, string> = Object.freeze({
/** alias of libsql-node */
"libsql": "db0/connectors/libsql/node",
"libsql-web": "db0/connectors/libsql/web",
"mariadb": "db0/connectors/mariadb",
"mysql2": "db0/connectors/mysql2",
"node-sqlite": "db0/connectors/node-sqlite",
/** alias of node-sqlite */
Expand Down
71 changes: 71 additions & 0 deletions src/connectors/mariadb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import mariadb from "mariadb";
import type { Connector, Primitive } from "db0";
import { BoundableStatement } from "./_internal/statement.ts";

export type ConnectorOptions = mariadb.ConnectionConfig;

type InternalQuery = (
sql: string,
params?: unknown[],
) => Promise<mariadb.UpsertResult | Record<string, unknown>[]>;

export default function mariadbConnector(
opts: ConnectorOptions,
): Connector<mariadb.Connection> {
let _connection: mariadb.Connection | undefined;
const getConnection = async () => {
if (_connection) {
return _connection;
}

_connection = await mariadb.createConnection({
...opts,
});

return _connection;
};

const query: InternalQuery = (sql, params) =>
getConnection().then((c) => c.query(sql, params));

return {
name: "mariadb",
dialect: "mariadb",
getInstance: () => getConnection(),
exec: (sql) => query(sql),
prepare: (sql) => new StatementWrapper(sql, query),
dispose: async () => {
await _connection?.end?.();
_connection = undefined;
},
Comment on lines +15 to +40

@coderabbitai coderabbitai Bot Mar 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Memoize the first createConnection() call.

Line 21 assigns _connection only after the await, so two concurrent first callers can each open a separate MariaDB connection. One then falls out of the cache, getInstance() can observe different instances, and dispose() never closes the loser.

🔒 Proposed fix for connection initialization
 export default function mariadbConnector(
   opts: ConnectorOptions,
 ): Connector<mariadb.Connection> {
   let _connection: mariadb.Connection | undefined;
+  let _connectionPromise: Promise<mariadb.Connection> | undefined;
+
   const getConnection = async () => {
     if (_connection) {
       return _connection;
     }
+    if (_connectionPromise) {
+      return _connectionPromise;
+    }
 
-    _connection = await mariadb.createConnection({
-      ...opts,
-    });
+    _connectionPromise = mariadb.createConnection({ ...opts }).then((connection) => {
+      _connection = connection;
+      return connection;
+    }).catch((error) => {
+      _connectionPromise = undefined;
+      throw error;
+    });
 
-    return _connection;
+    return _connectionPromise;
   };
 
   const query: InternalQuery = (sql, params) =>
     getConnection().then((c) => c.query(sql, params));
@@
     exec: (sql) => query(sql),
     prepare: (sql) => new StatementWrapper(sql, query),
     dispose: async () => {
-      await _connection?.end?.();
+      const connection = _connection ?? (await _connectionPromise);
+      await connection?.end?.();
       _connection = undefined;
+      _connectionPromise = undefined;
     },
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/connectors/mariadb.ts` around lines 15 - 40, The bug is that
getConnection awaits mariadb.createConnection before assigning _connection,
allowing concurrent callers to create multiple connections; fix by memoizing the
in-flight creation: introduce an internal promise (e.g. _connectionPromise) and
in getConnection assign _connectionPromise = mariadb.createConnection(...)
immediately, then await it, set _connection from the resolved connection, and
clear _connectionPromise on dispose; update getInstance to await/return the
resolved connection (via getConnection/_connectionPromise) and change dispose to
await the promise (if any) then call .end() on the resolved _connection and
clear both _connection and _connectionPromise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same lazy-init pattern used across all connectors here (mysql2, postgresql, etc). Fixing the race would be a separate change touching every connector, not scoped to this PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check lazy-init patterns in other connectors
rg '_connection' src/connectors/ -n -A2 -B2 --type=ts

Length of output: 3883


@oritwoen, confirmed — mysql2.ts and cloudflare-hyperdrive-mysql.ts use the exact same unguarded _connection pattern, so the new connector is consistent with the existing codebase. Keeping it scoped to a separate cross-cutting fix makes sense.

Would you like me to open a follow-up GitHub issue to track fixing the promise-memoization race across all affected connectors (mysql2, mariadb, cloudflare-hyperdrive-mysql, etc.)?

🐇


✏️ Learnings added
Learnt from: oritwoen
Repo: unjs/db0 PR: 187
File: src/connectors/mariadb.ts:15-40
Timestamp: 2026-03-16T20:03:01.114Z
Learning: In the db0 repository (unjs/db0), all connection-based connectors (mysql2, mariadb, cloudflare-hyperdrive-mysql) intentionally share the same lazy-init `_connection` pattern without promise memoization. The concurrent-call race condition is a known cross-cutting concern to be addressed in a separate PR touching all connectors, not within individual connector PRs.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pi0 FYI, to be verified for race condition.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

};
}

class StatementWrapper extends BoundableStatement<void> {
#query: InternalQuery;
#sql: string;

constructor(sql: string, query: InternalQuery) {
super();
this.#sql = sql;
this.#query = query;
}

async all(...params: Primitive[]) {
const res = await this.#query(this.#sql, params);
return res as Record<string, unknown>[];
}

async run(...params: Primitive[]) {
const res = (await this.#query(this.#sql, params)) as mariadb.UpsertResult;
return {
success: true,
...res,
};
}

async get(...params: Primitive[]) {
const res = await this.#query(this.#sql, params);
return (res as Record<string, unknown>[])[0];
}
}
7 changes: 6 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
*/
export type Primitive = string | number | boolean | undefined | null;

export type SQLDialect = "mysql" | "postgresql" | "sqlite" | "libsql";
export type SQLDialect =
| "mysql"
| "postgresql"
| "sqlite"
| "libsql"
| "mariadb";

export type Statement = {
/**
Expand Down
6 changes: 4 additions & 2 deletions test/connectors/_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export function testConnector<TConnector extends Connector = Connector>(opts: {
it("drop and create table", async () => {
await db.sql`DROP TABLE IF EXISTS users`;
switch (opts.dialect) {
case "mysql": {
case "mysql":
case "mariadb": {
await db.sql`CREATE TABLE users (\`id\` VARCHAR(4) PRIMARY KEY, \`firstName\` TEXT, \`lastName\` TEXT, \`email\` TEXT)`;
break;
}
Expand All @@ -53,7 +54,8 @@ export function testConnector<TConnector extends Connector = Connector>(opts: {

it("insert", async () => {
switch (opts.dialect) {
case "mysql": {
case "mysql":
case "mariadb": {
await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`;
break;
}
Expand Down
16 changes: 16 additions & 0 deletions test/connectors/mariadb.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe } from "vitest";
import connector from "../../src/connectors/mariadb";
import { testConnector } from "./_tests";

describe.runIf(process.env.MARIADB_URL)("connectors: mariadb.test", () => {
testConnector({
dialect: "mariadb",
connector: connector({
host: "localhost",
port: 3307,
user: "test",
password: "test",
database: "db0",
}),
Comment on lines +5 to +14

@coderabbitai coderabbitai Bot Mar 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use MARIADB_URL for the actual connection config.

Line 5 gates the suite on MARIADB_URL, but Lines 8-13 ignore its value and always connect to localhost:3307 with fixed credentials. Any non-default CI or local DSN will still enable the suite and then hit the wrong server.

🔧 Proposed test setup
 import { describe } from "vitest";
 import connector from "../../src/connectors/mariadb";
 import { testConnector } from "./_tests";
 
-describe.runIf(process.env.MARIADB_URL)("connectors: mariadb.test", () => {
+const mariadbUrl = process.env.MARIADB_URL;
+
+describe.runIf(Boolean(mariadbUrl))("connectors: mariadb.test", () => {
+  const url = new URL(mariadbUrl!);
+
   testConnector({
     dialect: "mariadb",
     connector: connector({
-      host: "localhost",
-      port: 3307,
-      user: "test",
-      password: "test",
-      database: "db0",
+      host: url.hostname,
+      port: Number(url.port || 3306),
+      user: decodeURIComponent(url.username),
+      password: decodeURIComponent(url.password),
+      database: url.pathname.replace(/^\//, ""),
     }),
   });
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe.runIf(process.env.MARIADB_URL)("connectors: mariadb.test", () => {
testConnector({
dialect: "mariadb",
connector: connector({
host: "localhost",
port: 3307,
user: "test",
password: "test",
database: "db0",
}),
import { describe } from "vitest";
import connector from "../../src/connectors/mariadb";
import { testConnector } from "./_tests";
const mariadbUrl = process.env.MARIADB_URL;
describe.runIf(Boolean(mariadbUrl))("connectors: mariadb.test", () => {
const url = new URL(mariadbUrl!);
testConnector({
dialect: "mariadb",
connector: connector({
host: url.hostname,
port: Number(url.port || 3306),
user: decodeURIComponent(url.username),
password: decodeURIComponent(url.password),
database: url.pathname.replace(/^\//, ""),
}),
});
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/connectors/mariadb.test.ts` around lines 5 - 14, The test currently
gates with describe.runIf(process.env.MARIADB_URL) but then uses hardcoded
host/port/credentials in testConnector and connector, so CI/local DSNs are
ignored; update the test to derive the connection config from
process.env.MARIADB_URL instead of the literals: inside the test setup (the call
to testConnector and connector) parse process.env.MARIADB_URL (or pass it into
connector if it accepts a DSN) to populate host/port/user/password/database,
falling back to the existing literals only if the env var is absent; reference
the describe.runIf, testConnector and connector call sites when making the
change.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matches the existing pattern - mysql2.test.ts does the same thing. The env var is just a gate, not the actual DSN.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check mysql2.test.ts to verify the existing pattern
cat test/connectors/mysql2.test.ts 2>/dev/null || fd mysql2.test -e ts --exec cat {}

Length of output: 380


@oritwoen, you're right — mysql2.test.ts uses exactly the same pattern. My suggestion was incorrect given the established convention in this codebase. The env var is intentionally just a gate to skip the suite when the DB isn't available, not a DSN to configure the connection. I'll note this for future reviews.


🧠 Learnings used
Learnt from: oritwoen
Repo: unjs/db0 PR: 187
File: src/connectors/mariadb.ts:15-40
Timestamp: 2026-03-16T20:03:01.114Z
Learning: In the db0 repository (unjs/db0), all connection-based connectors (mysql2, mariadb, cloudflare-hyperdrive-mysql) intentionally share the same lazy-init `_connection` pattern without promise memoization. The concurrent-call race condition is a known cross-cutting concern to be addressed in a separate PR touching all connectors, not within individual connector PRs.

});
});