# Mariadb Connector C Usage

> Explains MariaDB Connector/C's `mysql_`-prefixed API, connection setup, result handling, prepared statements, and transaction behavior for writing or reviewing C code that talks to MariaDB.

- Skill: `mariadb-corporation/mariadb-connector-c-usage` (Agent Skill)
- Install (CLI): `npx skillmds add mariadb-corporation/mariadb-connector-c-usage`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mariadb-corporation/mariadb-connector-c-usage/raw
- Safety review: PASS (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs, Coding & Dev Tools, REST & GraphQL APIs
- Tags: Connector C, Libmariadb, Mariadb, Mysql Api, Prepared Statements, Transactions
- Author: mariadb-corporation (https://skillmd.com/u/mariadb-corporation)
- Updated: 2026-08-22
- Page: https://skillmd.com/skills/mariadb-corporation/mariadb-connector-c-usage

---


# MariaDB Connector/C

*Last updated: 2026-08-10*

MariaDB Connector/C is the LGPL-licensed C client library (`libmariadb`) implementing the MariaDB/MySQL client-server protocol. It is the foundation other MariaDB connectors build on — Connector/Python's C extension, Connector/ODBC, and Connector/C++ all link against it — and it is API-compatible with the classic MySQL C client library, so `mysql.h` and the `mysql_*` function names carry over directly. This skill covers the connector-specific behavior and the traps that bite generated application code. For installing the library, compiling against it, and configuring it through option files, see **`mariadb-connector-c-install`**.

> **Default context:** Assume the **3.4** stable release series (GA February 2025; `mariadb_config --cc_version` reports the exact patch, e.g. `3.4.10`) unless the user states otherwise. Connector/C is compatible with all MariaDB and MySQL server versions — its version is independent of the server version it connects to, and behavior below applies across the 3.x line unless annotated.

## What LLMs Often Miss

| If the agent writes / assumes… | …prefer the MariaDB form |
|---|---|
| Calls `mysql_real_connect()` on a raw/stack `MYSQL` struct, or skips init | Call **`mysql_init(NULL)`** first (it allocates and returns a `MYSQL *`, or initializes a struct you pass in) — every other function except `mysql_options()` fails until `mysql_real_connect()` succeeds on that handle |
| Reaches for `mariadb_connect()`, `mariadb_query()`, etc., assuming a `mariadb_`-prefixed API | The public API is still **`mysql_`-prefixed** — `mysql_init`, `mysql_real_connect`, `mysql_query`, `mysql_store_result`, `mysql_close`, and friends — for MySQL C API compatibility. Only a handful of *extension* functions use `mariadb_` (e.g. `mariadb_reconnect()`, `mariadb_get_info()`, `mariadb_stmt_execute_direct()`); `mariadb_connect()` itself is just a macro wrapping `mysql_real_connect()` |
| Builds SQL by concatenating/`sprintf`-ing string values into the query text passed to `mysql_query()` | **SQL-injection trap.** Escape every literal string with **`mysql_real_escape_string(mysql, to, from, length)`** first (the `to` buffer must be `length*2+1` bytes), or — preferably — use the prepared-statement API with `?` placeholders instead of building SQL text at all |
| Calls `mysql_query()`/`mysql_store_result()` and moves straight to the next query | A result-returning statement needs one of **`mysql_store_result()`** (buffered — pulls the whole set into client memory; enables `mysql_num_rows()`/`mysql_data_seek()`) or **`mysql_use_result()`** (unbuffered — row-by-row, blocks the connection until every row is fetched or the result is freed). One of the two **must** be called even for a query with no rows, and the result **must** be released with **`mysql_free_result()`** — otherwise the next query on that connection fails |
| Calls `mysql_num_rows()` / `mysql_data_seek()` on an unbuffered result | Those require **`mysql_store_result()`**; `mysql_use_result()` sets don't support random access or a row count until fully consumed |
| Interpolates `%d`/`%s`-style values into a query string instead of using placeholders | Use the **prepared-statement API** with `?` markers: `mysql_stmt_init()` → `mysql_stmt_prepare(stmt, "...WHERE id=?", length)` → build a `MYSQL_BIND` array → `mysql_stmt_bind_param(stmt, bind)` → `mysql_stmt_execute(stmt)`. For a result set, bind a second `MYSQL_BIND` array with `mysql_stmt_bind_result()` and loop `mysql_stmt_fetch(stmt)` until it returns `MYSQL_NO_DATA` |
| Assumes autocommit is off, or that DML needs an explicit `START TRANSACTION` | **Autocommit is ON by default** — each statement is its own transaction. For multi-statement transactions, disable it once with **`mysql_autocommit(conn, 0)`**, then use **`mysql_commit()`**/**`mysql_rollback()`** (or the `COMMIT`/`ROLLBACK` SQL statements); a new transaction starts automatically after each commit/rollback — no manual `START TRANSACTION` needed |
| Checks one error API for everything, or checks connection errors after a statement-handle call | Connection-level calls (`mysql_real_connect`, `mysql_query`, `mysql_store_result`, ...) report through **`mysql_errno()`**/**`mysql_error()`**; `MYSQL_STMT`-level calls (`mysql_stmt_prepare`, `mysql_stmt_execute`, ...) have their **own** error context: **`mysql_stmt_errno()`**/**`mysql_stmt_error()`**/`mysql_stmt_sqlstate()`. Checking the wrong one silently misses the real error |
| Sets TLS/charset/timeout options after connecting, or via `mysql_real_connect()` args alone | Set options with **`mysql_optionsv()`** (or the older `mysql_options()`) **after `mysql_init()` but before `mysql_real_connect()`** — e.g. `MYSQL_OPT_SSL_CA`, `MYSQL_SET_CHARSET_NAME`, `MYSQL_OPT_CONNECT_TIMEOUT`. Options set post-connect are ignored until the next connect/reconnect |
| Uses `mysql_query()` for statements that may carry binary data or embedded NUL bytes | `mysql_query()` takes a **NUL-terminated** string and is **not** binary-safe. Use **`mysql_real_query(mysql, query, length)`**, which takes an explicit length, for binary-safe execution |
| Reads only the first result after `CALL`ing a stored procedure or a multi-statement query | Stored-procedure calls and multi-statement queries (`MARIADB_OPT_MULTI_STATEMENTS`) can return **multiple result sets** — loop with **`mysql_next_result()`** (or `mysql_stmt_next_result()` for prepared statements), calling `mysql_store_result()`/`mysql_use_result()` + `mysql_free_result()` each time, until no more results remain |
| Passes `-1` as a length to `mysql_real_query()` or `mysql_real_escape_string()`, expecting auto-detection everywhere | Only **`mysql_stmt_prepare()`** treats `length == (unsigned long)-1` as "compute via `strlen()`". The binary-safe functions (`mysql_real_query()`, `mysql_real_escape_string()`) need the **actual** byte length — passing `-1` there is a bug, not a shortcut |
| Calls `mysql_close()` while `MYSQL_STMT`/`MYSQL_RES` handles from that connection are still open | Free every result with `mysql_free_result()` and close every prepared statement with `mysql_stmt_close()` **before** `mysql_close()` on the connection |
| Uses one `MYSQL` handle from several threads, or spawns threads without initializing them | A `MYSQL` handle is **not** thread-safe: one connection per thread. Each thread that uses the library must call **`mysql_thread_init()`** on entry and **`mysql_thread_end()`** on exit; the process should call `mysql_library_init()` before the first connection and `mysql_library_end()` at shutdown |
| Loops `mysql_stmt_execute()` once per row for a bulk insert | Bind arrays instead: set **`STMT_ATTR_ARRAY_SIZE`** with `mysql_stmt_attr_set()` and point each `MYSQL_BIND` at an array of values, then execute **once**. This is a MariaDB extension and collapses the round trips |
| Prepares a statement it will execute exactly once | **`mariadb_stmt_execute_direct()`** prepares and executes in a single round trip — the right call for one-shot parameterized statements |
| Treats a `NULL` from `mysql_store_result()` as "no rows" | It means either "no result set" or "an error occurred". Distinguish with **`mysql_field_count()`**: zero means the statement genuinely returned no result set, non-zero means the fetch failed and `mysql_error()` has the reason |

## Connect, Prepared Statement, and Transaction

```c
#include <mysql.h>
#include <stdio.h>
#include <string.h>

MYSQL *conn = mysql_init(NULL);
if (!mysql_real_connect(conn, "localhost", "app", "secret",
                         "appdb", 3306, NULL, 0))
{
    fprintf(stderr, "connect failed: %s\n", mysql_error(conn));
    mysql_close(conn);
    return 1;
}

/* Prepared statement with a `?` placeholder */
MYSQL_STMT *stmt = mysql_stmt_init(conn);
const char *sql = "SELECT name, qty FROM t WHERE qty > ?";
mysql_stmt_prepare(stmt, sql, strlen(sql));

int min_qty = 0;
MYSQL_BIND param[1];
memset(param, 0, sizeof(param));
param[0].buffer_type = MYSQL_TYPE_LONG;
param[0].buffer      = &min_qty;
mysql_stmt_bind_param(stmt, param);

mysql_stmt_execute(stmt);

char name[64];
int qty;
unsigned long name_len;
my_bool is_null[2];

MYSQL_BIND result[2];
memset(result, 0, sizeof(result));
result[0].buffer_type   = MYSQL_TYPE_STRING;
result[0].buffer        = name;
result[0].buffer_length = sizeof(name);
result[0].length        = &name_len;
result[0].is_null       = &is_null[0];
result[1].buffer_type   = MYSQL_TYPE_LONG;
result[1].buffer        = &qty;
result[1].is_null       = &is_null[1];
mysql_stmt_bind_result(stmt, result);

while (mysql_stmt_fetch(stmt) == 0)
    printf("%s: %d\n", name, qty);

mysql_stmt_close(stmt);

/* Transaction: autocommit is on by default, so turn it off explicitly */
mysql_autocommit(conn, 0);
if (mysql_query(conn, "UPDATE t SET qty = qty - 1 WHERE id = 1") != 0 ||
    mysql_query(conn, "UPDATE t SET qty = qty + 1 WHERE id = 2") != 0)
{
    fprintf(stderr, "update failed: %s\n", mysql_error(conn));
    mysql_rollback(conn);
}
else
{
    mysql_commit(conn);
}

mysql_close(conn);
```

## Bulk insert with array binding

`STMT_ATTR_ARRAY_SIZE` turns a single prepared statement into a multi-row insert — one execute, one round trip:

```c
enum { ROWS = 3 };
unsigned int  ids[ROWS]  = { 1, 2, 3 };
char         *names[ROWS] = { "widget", "gadget", "doohickey" };
unsigned long lens[ROWS];
size_t        i;

for (i = 0; i < ROWS; i++)
    lens[i] = strlen(names[i]);

MYSQL_STMT *stmt = mysql_stmt_init(conn);
const char *sql = "INSERT INTO t (id, name) VALUES (?, ?)";
mysql_stmt_prepare(stmt, sql, strlen(sql));

MYSQL_BIND bind[2];
memset(bind, 0, sizeof(bind));
bind[0].buffer_type = MYSQL_TYPE_LONG;
bind[0].buffer      = ids;
bind[1].buffer_type = MYSQL_TYPE_STRING;
bind[1].buffer      = names;
bind[1].length      = lens;

size_t array_size = ROWS;
mysql_stmt_attr_set(stmt, STMT_ATTR_ARRAY_SIZE, &array_size);
mysql_stmt_bind_param(stmt, bind);
mysql_stmt_execute(stmt);        /* all three rows, one execution */
mysql_stmt_close(stmt);
```

## Threading

```c
mysql_library_init(0, NULL, NULL);      /* once, before any connection */

/* …in each worker thread: */
mysql_thread_init();
MYSQL *conn = mysql_init(NULL);         /* one connection per thread */
/* … */
mysql_close(conn);
mysql_thread_end();

mysql_library_end();                    /* once, at shutdown */
```

Sharing a single `MYSQL` handle across threads corrupts the protocol state rather than merely serializing badly, so a pool of per-thread connections is the only safe arrangement.

## See Also

- **`mariadb-connector-c-install`** — installing the library, compiling against it, and configuring it through option files and TLS options
- **`mariadb-connector-python-usage`** — the `mariadb` module's C extension links against this library
- **`mariadb-connector-odbc-usage`** / **`mariadb-connector-cpp-usage`** — also built on top of Connector/C
- **`mariadb-transactions`** — the server-side semantics behind `mysql_commit()`/`mysql_rollback()`
- **`mariadb-prepare`** — server-side prepared statements, what the `MYSQL_STMT` API drives
- Canonical reference on `mariadb.com/docs`, consult for edge cases not covered here: <https://mariadb.com/docs/connectors/mariadb-connector-c>

<sub>_This page is: Copyright © 2026 MariaDB. All rights reserved._</sub>

