--- name: mariadb-connector-c-usage description: "MariaDB-specific behavior of MariaDB Connector/C (the `libmariadb` client library implementing the MariaDB client-server protocol in C) for application code — that the public API is still `mysql_`-prefixed (`mysql_init`, `mysql_real_connect`, `mysql_query`, ...) with only a few `mariadb_`-prefixed extensions; that `mysql_init()` must precede `mysql_real_connect()`, with `mysql_optionsv()` options set in between; the buffered-vs-unbuffered split between `mysql_store_result()` and `mysql_use_result()` plus the mandatory `mysql_free_result()`; that literals need `mysql_real_escape_string()` when not using prepared statements; the `?`-placeholder prepared-statement API (`MYSQL_BIND` arrays, `mysql_stmt_bind_param`/`execute`/`fetch`); and that autocommit is ON by default so transactions need `mysql_autocommit(conn,0)`. Use when writing or reviewing C code that talks to MariaDB via Connector/C (`libmariadb`)." --- # 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 #include #include 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: _This page is: Copyright © 2026 MariaDB. All rights reserved._