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 CALLing 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
#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:
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
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
This page is: Copyright © 2026 MariaDB. All rights reserved.
1---2name: mariadb-connector-c-usage3description: 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.4---56# MariaDB Connector/C78*Last updated: 2026-08-10*910MariaDB 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`**.1112> **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.1314## What LLMs Often Miss1516| If the agent writes / assumes… | …prefer the MariaDB form |17|---|---|18| 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 |19| 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()` |20| 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 |21| 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 |22| 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 |23| 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` |24| 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 |25| 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 |26| 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 |27| 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 |28| 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 |29| 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 |30| 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 |31| 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 |32| 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 |33| 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 |34| 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 |3536## Connect, Prepared Statement, and Transaction3738```c39#include <mysql.h>40#include <stdio.h>41#include <string.h>4243MYSQL *conn = mysql_init(NULL);44if (!mysql_real_connect(conn, "localhost", "app", "secret",45 "appdb", 3306, NULL, 0))46{47 fprintf(stderr, "connect failed: %s\n", mysql_error(conn));48 mysql_close(conn);49 return 1;50}5152/* Prepared statement with a `?` placeholder */53MYSQL_STMT *stmt = mysql_stmt_init(conn);54const char *sql = "SELECT name, qty FROM t WHERE qty > ?";55mysql_stmt_prepare(stmt, sql, strlen(sql));5657int min_qty = 0;58MYSQL_BIND param[1];59memset(param, 0, sizeof(param));60param[0].buffer_type = MYSQL_TYPE_LONG;61param[0].buffer = &min_qty;62mysql_stmt_bind_param(stmt, param);6364mysql_stmt_execute(stmt);6566char name[64];67int qty;68unsigned long name_len;69my_bool is_null[2];7071MYSQL_BIND result[2];72memset(result, 0, sizeof(result));73result[0].buffer_type = MYSQL_TYPE_STRING;74result[0].buffer = name;75result[0].buffer_length = sizeof(name);76result[0].length = &name_len;77result[0].is_null = &is_null[0];78result[1].buffer_type = MYSQL_TYPE_LONG;79result[1].buffer = &qty;80result[1].is_null = &is_null[1];81mysql_stmt_bind_result(stmt, result);8283while (mysql_stmt_fetch(stmt) == 0)84 printf("%s: %d\n", name, qty);8586mysql_stmt_close(stmt);8788/* Transaction: autocommit is on by default, so turn it off explicitly */89mysql_autocommit(conn, 0);90if (mysql_query(conn, "UPDATE t SET qty = qty - 1 WHERE id = 1") != 0 ||91 mysql_query(conn, "UPDATE t SET qty = qty + 1 WHERE id = 2") != 0)92{93 fprintf(stderr, "update failed: %s\n", mysql_error(conn));94 mysql_rollback(conn);95}96else97{98 mysql_commit(conn);99}100101mysql_close(conn);102```103104## Bulk insert with array binding105106`STMT_ATTR_ARRAY_SIZE` turns a single prepared statement into a multi-row insert — one execute, one round trip:107108```c109enum { ROWS = 3 };110unsigned int ids[ROWS] = { 1, 2, 3 };111char *names[ROWS] = { "widget", "gadget", "doohickey" };112unsigned long lens[ROWS];113size_t i;114115for (i = 0; i < ROWS; i++)116 lens[i] = strlen(names[i]);117118MYSQL_STMT *stmt = mysql_stmt_init(conn);119const char *sql = "INSERT INTO t (id, name) VALUES (?, ?)";120mysql_stmt_prepare(stmt, sql, strlen(sql));121122MYSQL_BIND bind[2];123memset(bind, 0, sizeof(bind));124bind[0].buffer_type = MYSQL_TYPE_LONG;125bind[0].buffer = ids;126bind[1].buffer_type = MYSQL_TYPE_STRING;127bind[1].buffer = names;128bind[1].length = lens;129130size_t array_size = ROWS;131mysql_stmt_attr_set(stmt, STMT_ATTR_ARRAY_SIZE, &array_size);132mysql_stmt_bind_param(stmt, bind);133mysql_stmt_execute(stmt); /* all three rows, one execution */134mysql_stmt_close(stmt);135```136137## Threading138139```c140mysql_library_init(0, NULL, NULL); /* once, before any connection */141142/* …in each worker thread: */143mysql_thread_init();144MYSQL *conn = mysql_init(NULL); /* one connection per thread */145/* … */146mysql_close(conn);147mysql_thread_end();148149mysql_library_end(); /* once, at shutdown */150```151152Sharing 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.153154## See Also155156- **`mariadb-connector-c-install`** — installing the library, compiling against it, and configuring it through option files and TLS options157- **`mariadb-connector-python-usage`** — the `mariadb` module's C extension links against this library158- **`mariadb-connector-odbc-usage`** / **`mariadb-connector-cpp-usage`** — also built on top of Connector/C159- **`mariadb-transactions`** — the server-side semantics behind `mysql_commit()`/`mysql_rollback()`160- **`mariadb-prepare`** — server-side prepared statements, what the `MYSQL_STMT` API drives161- Canonical reference on `mariadb.com/docs`, consult for edge cases not covered here: <https://mariadb.com/docs/connectors/mariadb-connector-c>162163<sub>_This page is: Copyright © 2026 MariaDB. All rights reserved._</sub>