Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
build-asan/
*.idx

# Build directory.
build/

Expand Down
67 changes: 67 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`mgclient` is a C library implementing the Bolt protocol client for [Memgraph](https://www.memgraph.com) (also compatible with Neo4j Bolt). The core library is C11. A header-only C++17 wrapper (`mgclient_cpp`) sits on top, and it can also be compiled to WebAssembly via Emscripten.

## Build & test

Standard build (produces `libmgclient.a` + `libmgclient.so`/`.dylib`):

```
mkdir build && cd build
cmake ..
make
```

With tests enabled (this also forces `BUILD_CPP_BINDINGS=ON`):

```
cmake -DBUILD_TESTING=ON -DBUILD_TESTING_INTEGRATION=ON ..
make
ctest
```

- **Run a single test:** `ctest -R encoder` (test names: `value`, `encoder`, `decoder`, `client`, `transport`, `allocator`, `unit_mgclient_value`, plus `integration_basic_c`, `integration_basic_cpp`, `example_*`).
- **Unit tests only (no running Memgraph):** `ctest -E "example|integration"`. The `integration_*` and `example_*` tests require a live Memgraph on `127.0.0.1:7687`.
- **OpenSSL not found:** pass `-DOPENSSL_ROOT_DIR=...` (see README for macOS/Windows specifics).
- **WASM build (Linux only):** `cmake .. -DWASM=ON && make` → emits `mgclient.js` + `mgclient.wasm`. WASM uses WebSocket transport and has no OpenSSL dependency.

## Formatting

`./tool/format.sh` runs `clang-format` (Google style, 80-col, right-aligned pointers) over all `*.c/*.h/*.cpp/*.hpp` files **in place** and fails if anything changed. CI runs this on every push/PR as the `clang_check` job — formatting failures break CI, so run it before committing.

Coverage report: `./tool/coverage.sh` (requires a build with `-DENABLE_COVERAGE=ON`; uses `llvm-profdata`/`llvm-cov`).

## Architecture

The library is layered. Public symbols are exported via the `MGCLIENT_EXPORT` macro (generated `mgclient-export.h`); everything in `src/*.h` is internal.

- **`include/mgclient.h`** — the entire public C API and its Doxygen documentation. The big comment block at the top is the authoritative spec for the **ownership model** (read it before touching value/container code): non-const pointer returns transfer ownership to the caller; const pointer returns are read-only views valid only while the owner lives; insert functions steal ownership of inserted values. Getting this wrong causes double-frees.

- **Session layer** (`mgsession.c`, `mgsession.h`) — `mg_session` is the connection object and is *single-command-at-a-time*: you `mg_session_run` a query, then `mg_session_pull` rows until it returns 0 before running anything else. `mg_connect` performs the Bolt handshake and HELLO. The session struct holds the in/out buffers, the negotiated Bolt `version`, transaction state (`explicit_transaction`), and two allocators (one general, one scoped to decoding).

- **Encoder / decoder** (`mgsession-encoder.c`, `mgsession-decoder.c`) — serialize/deserialize Bolt messages and values over the chunked Bolt framing. All Bolt markers, struct signatures, and message signatures live in `src/mgconstants.h` — this is the reference when adding a new value type or Bolt message. Note that Bolt has multiple protocol versions and some value types (temporal types, ZonedDateTime) are version-gated; check how `session->version` is consulted.

- **Transport layer** (`mgtransport.c`, `mgtransport.h`) — polymorphic `mg_transport` struct of function pointers (`send`/`recv`/`destroy`/suspend hooks). Three implementations: `mg_raw_transport` (plain socket), `mg_secure_transport` (OpenSSL/SSL, supports peer pubkey fingerprint verification via trust callback), and the WASM WebSocket path. The session talks only to the `mg_transport` interface and is agnostic to which one is in use.

- **Socket layer** — OS-specific, selected at CMake configure time: `src/{linux,apple,windows}/mgsocket.c` (matching `mgcommon.h` per platform). The build picks exactly one based on `MGCLIENT_ON_{LINUX,APPLE,WINDOWS}`.

- **Values** (`mgvalue.c`, `mgvalue.h`) — implementation of all Bolt data types (`mg_value`, `mg_string`, `mg_list`, `mg_map`, `mg_node`, `mg_relationship`, `mg_path`, temporal types, points). This is the largest file and where the ownership rules from `mgclient.h` are enforced.

- **Allocator** (`mgallocator.c`, `mgallocator.h`) — pluggable `mg_allocator` interface; the library allocates through it rather than calling `malloc` directly.

- **C++ wrapper** (`mgclient_cpp/include/`, header-only) — `mg::Client` (RAII connection with `Client::Connect(params)`), `mg::Value`, and an exception hierarchy (`MgException` → `ClientException`/`TransientException`/`DatabaseException`). Pure wrapper over the C API; no separate compiled library.

## Tests

- `tests/*.cpp` — unit tests (GTest, fetched via `FetchContent` at `release-1.8.1`). They link against `mgclient-static` and the C++ bindings.
- `tests/integration/` — require a running Memgraph instance; gated behind `BUILD_TESTING_INTEGRATION`.
- `client.cpp` mocks `mg_secure_transport_init` using the linker `--wrap` mechanism (`-Wl,--wrap=` on Linux, `-Wl,-alias,` on Apple) — see `tests/CMakeLists.txt`. If you rename that function, update the wrap flags too.
- `examples/` (`basic.c`, `basic.cpp`, `advanced.cpp`) are also compiled and registered as ctest tests; they double as API usage references.

## Versioning gotcha

`CMakeLists.txt` carries two independent version numbers: `project(... VERSION x.y.z)` and `mgclient_SOVERSION`. A minor version bump can mean ABI incompatibility — the SOVERSION must be bumped manually when the ABI changes (it is not derived from the project version).
83 changes: 66 additions & 17 deletions include/mgclient.h
Original file line number Diff line number Diff line change
Expand Up @@ -1374,35 +1374,84 @@ MGCLIENT_EXPORT int mg_session_run(mg_session *session, const char *query,
const mg_map *extra_run_information,
const mg_list **columns, int64_t *qid);

/// Sends a Bolt ROUTE message to a coordinator and returns the routing table.
///
/// ROUTE is available only on Bolt protocol versions >= 4.3, which must have
/// been negotiated during \ref mg_connect. It is used for client-side routing
/// against a Memgraph high-availability cluster: the client asks a coordinator
/// for the current cluster topology and then connects directly to the
/// appropriate server based on the desired access mode.
///
/// The session must be in the ready state (not executing/fetching a query and
/// not inside an explicit transaction).
///
/// \param session A \ref mg_session connected to a coordinator. The Bolt
/// version negotiated for this session must be >= 4.3.
/// \param routing A \ref mg_map with routing context (e.g. the address
/// used to contact the coordinator). Must not be NULL; use
/// an empty map if there is no routing context.
/// \param bookmarks A \ref mg_list of bookmark strings, or NULL for none
/// (treated as an empty list).
/// \param extra A \ref mg_map with extra information. On Bolt 4.4 it is
/// sent verbatim as the third ROUTE field; on Bolt 4.3
/// only its "db" string entry (if present) is used to
/// populate the separate database-name field. NULL is
/// allowed.
/// \param routing_table On success, a freshly allocated \ref mg_map holding the
/// routing table is stored here (ownership transferred to
/// the caller, who must call \ref mg_map_destroy on it).
/// NULL may be supplied to discard the result. The map has
/// the shape:
/// {
/// "ttl": <integer, time-to-live in seconds>,
/// "servers": [
/// {
/// "addresses": ["host:port", ...],
/// "role": "READ" | "WRITE" | "ROUTE"
/// },
/// ...
/// ]
/// }
/// \return Returns 0 if the routing table was obtained successfully.
/// Returns \ref MG_ERROR_BAD_PARAMETER if \p routing is NULL,
/// \ref MG_ERROR_BAD_CALL if the session is not ready,
/// \ref MG_ERROR_CLIENT_ERROR if the negotiated Bolt version is < 4.3,
/// or another non-zero error code otherwise.
MGCLIENT_EXPORT int mg_session_route(mg_session *session, const mg_map *routing,
const mg_list *bookmarks,
const mg_map *extra,
mg_map **routing_table);

/// Starts an Explicit transaction on the server.
///
/// Every run will be part of that transaction until its explicitly ended.
///
/// \param session A \ref mg_session on which the transaction
/// should be started. \param extra_run_information A \ref mg_map containing
/// extra information that will be used for every statement that is ran as part
/// of the transaction.
/// extra information that will be used for every statement that is ran as
/// part of the transaction.
/// It can contain the following information:
/// - bookmarks - list of strings containing some
/// kind of bookmark identification
/// - bookmarks - list of strings containing
/// some kind of bookmark identification
/// - tx_timeout - integer that specifies a
/// transaction timeout in ms.
/// - tx_metadata - dictionary taht can contain
/// some metadata information, mainly used for
/// logging.
/// - mode - specifies what kind of server is the
/// run targeting. For write access use "w" and
/// for read access use "r". Defaults to write
/// access.
/// - tx_metadata - dictionary taht can
/// contain some metadata information, mainly
/// used for logging.
/// - mode - specifies what kind of server is
/// the run targeting. For write access use
/// "w" and for read access use "r". Defaults
/// to write access.
/// - db - specifies the database name for
/// multi-database to select where the transaction
/// takes place. If no `db` is sent or empty
/// string it implies that it is the default
/// database.
/// multi-database to select where the
/// transaction takes place. If no `db` is
/// sent or empty string it implies that it is
/// the default database.
/// \return Returns 0 if the transaction was started successfuly.
/// Otherwise, a non-zero error code is returned.
MGCLIENT_EXPORT int mg_session_begin_transaction(
mg_session *session, const mg_map *extra_run_information);
MGCLIENT_EXPORT
int mg_session_begin_transaction(mg_session *session,
const mg_map *extra_run_information);

/// Commits current Explicit transaction.
///
Expand Down
170 changes: 161 additions & 9 deletions src/mgclient.c
Original file line number Diff line number Diff line change
Expand Up @@ -230,16 +230,21 @@ int validate_session_params(const mg_session_params *params,
}

static int mg_bolt_handshake(mg_session *session) {
const uint32_t VERSION_NONE = htobe32(0);
const uint32_t VERSION_1 = htobe32(1);
// Advertise supported Bolt versions, highest first. The version word is
// big-endian with the layout 0x0000<minor><major>, so e.g. 4.4 is 0x0404 and
// 1.0 is 0x0001. ROUTE (Bolt >= 4.3) requires negotiating 4.3 or 4.4; 4.1 and
// 1.0 are kept for backward compatibility.
const uint32_t VERSION_4_4 = htobe32(0x0404);
const uint32_t VERSION_4_3 = htobe32(0x0304);
const uint32_t VERSION_4_1 = htobe32(0x0104);
const uint32_t VERSION_1 = htobe32(0x0001);
mg_transport_suspend_until_ready_to_write(session->transport);
if (mg_transport_send(session->transport, MG_HANDSHAKE_MAGIC,
strlen(MG_HANDSHAKE_MAGIC)) != 0 ||
mg_transport_send(session->transport, (char *)&VERSION_4_4, 4) != 0 ||
mg_transport_send(session->transport, (char *)&VERSION_4_3, 4) != 0 ||
mg_transport_send(session->transport, (char *)&VERSION_4_1, 4) != 0 ||
mg_transport_send(session->transport, (char *)&VERSION_1, 4) != 0 ||
mg_transport_send(session->transport, (char *)&VERSION_NONE, 4) != 0 ||
mg_transport_send(session->transport, (char *)&VERSION_NONE, 4) != 0) {
mg_transport_send(session->transport, (char *)&VERSION_1, 4) != 0) {
mg_session_set_error(session, "failed to send handshake data");
return MG_ERROR_SEND_FAILED;
}
Expand All @@ -250,13 +255,18 @@ static int mg_bolt_handshake(mg_session *session) {
mg_session_set_error(session, "failed to receive handshake response");
return MG_ERROR_RECV_FAILED;
}
if (server_version == VERSION_1) {
uint32_t v = be32toh(server_version);
uint8_t major = (uint8_t)(v & 0xFF);
uint8_t minor = (uint8_t)((v >> 8) & 0xFF);
// Accept exactly the versions we advertised: 1.0, 4.1, 4.3, 4.4.
if (major == 1 && minor == 0) {
session->version = 1;
} else if (server_version == VERSION_4_1) {
session->version_minor = 0;
} else if (major == 4 && (minor == 1 || minor == 3 || minor == 4)) {
session->version = 4;
session->version_minor = minor;
} else {
mg_session_set_error(session, "unsupported protocol version: %" PRIu32,
be32toh(server_version));
mg_session_set_error(session, "unsupported protocol version: %" PRIu32, v);
return MG_ERROR_PROTOCOL_VIOLATION;
}
return 0;
Expand Down Expand Up @@ -803,6 +813,148 @@ int mg_session_run(mg_session *session, const char *query, const mg_map *params,
return status;
}

int mg_session_route(mg_session *session, const mg_map *routing,
const mg_list *bookmarks, const mg_map *extra,
mg_map **routing_table) {
if (!routing) {
mg_session_set_error(session, "routing map must not be NULL");
return MG_ERROR_BAD_PARAMETER;
}
if (session->status == MG_SESSION_BAD) {
mg_session_set_error(session, "bad session");
return MG_ERROR_BAD_CALL;
}
if (session->status == MG_SESSION_EXECUTING) {
mg_session_set_error(session, "already executing a query");
return MG_ERROR_BAD_CALL;
}
if (session->status == MG_SESSION_FETCHING) {
mg_session_set_error(session, "fetching results of a query");
return MG_ERROR_BAD_CALL;
}
if (session->explicit_transaction) {
mg_session_set_error(session,
"cannot route while in an explicit transaction");
return MG_ERROR_BAD_CALL;
}

assert(session->status == MG_SESSION_READY && !session->explicit_transaction);

if (session->version < 4 ||
(session->version == 4 && session->version_minor < 3)) {
mg_session_set_error(session, "ROUTE requires Bolt >= 4.3");
return MG_ERROR_BAD_CALL;
}

mg_message_destroy_ca(session->result.message, session->decoder_allocator);
session->result.columns = NULL;
session->result.message = NULL;

// The encoders dereference the bookmarks list, so a non-NULL empty list must
// be supplied when the caller passes NULL.
mg_list *empty_bookmarks = NULL;
if (!bookmarks) {
empty_bookmarks = mg_list_make_empty(0);
if (!empty_bookmarks) {
mg_session_set_error(session, "out of memory");
return MG_ERROR_OOM;
}
bookmarks = empty_bookmarks;
}

int status = 0;
if (session->version_minor >= 4) {
if (!extra) {
extra = &mg_empty_map;
}
status =
mg_session_send_route_message_v4_4(session, routing, bookmarks, extra);
} else {
// Bolt 4.3 carries the database name as a separate string field. Extract it
// from extra["db"] if present, otherwise send an empty string (default db).
// The mg_string data is not null-terminated, so carry its size explicitly.
const char *db = "";
uint32_t db_size = 0;
if (extra) {
const mg_value *db_tmp = mg_map_at(extra, "db");
if (db_tmp && mg_value_get_type(db_tmp) == MG_VALUE_TYPE_STRING) {
const mg_string *db_str = mg_value_string(db_tmp);
db = db_str->data;
db_size = db_str->size;
}
}
status = mg_session_send_route_message_v4_3(session, routing, bookmarks, db,
db_size);
}

if (empty_bookmarks) {
mg_list_destroy(empty_bookmarks);
}

if (status != 0) {
goto fatal_failure;
}

mg_transport_suspend_until_ready_to_read(session->transport);
status = mg_session_receive_message(session);
if (status != 0) {
goto fatal_failure;
}

mg_message *response;
status = mg_session_read_bolt_message(session, &response);
if (status != 0) {
goto fatal_failure;
}

if (response->type == MG_MESSAGE_TYPE_SUCCESS) {
const mg_value *rt = mg_map_at(response->success_v->metadata, "rt");
if (!rt || mg_value_get_type(rt) != MG_VALUE_TYPE_MAP) {
status = MG_ERROR_PROTOCOL_VIOLATION;
mg_message_destroy_ca(response, session->decoder_allocator);
mg_session_set_error(session, "invalid response metadata: missing 'rt'");
goto fatal_failure;
}
// Copy with the system allocator (not session->allocator): ownership is
// transferred to the caller, who releases it with the public
// mg_map_destroy, which itself uses the system allocator.
mg_map *copy = mg_map_copy_ca(mg_value_map(rt), &mg_system_allocator);
mg_message_destroy_ca(response, session->decoder_allocator);
if (!copy) {
mg_session_set_error(session, "out of memory");
status = MG_ERROR_OOM;
goto fatal_failure;
}
if (routing_table) {
*routing_table = copy;
} else {
mg_map_destroy(copy);
}
return 0;
}

if (response->type == MG_MESSAGE_TYPE_FAILURE) {
int failure_status = handle_failure_message(session, response->failure_v);

status = handle_failure(session);
if (status != 0) {
goto fatal_failure;
}

mg_message_destroy_ca(response, session->decoder_allocator);
return failure_status;
Comment thread
as51340 marked this conversation as resolved.
}

status = MG_ERROR_PROTOCOL_VIOLATION;
mg_message_destroy_ca(response, session->decoder_allocator);
mg_session_set_error(session, "unexpected message type");

fatal_failure:
mg_session_invalidate(session);
assert(status != 0);
return status;
}

int mg_session_pull(mg_session *session, const mg_map *pull_information) {
if (session->status == MG_SESSION_BAD) {
mg_session_set_error(session, "called pull while bad session");
Expand Down
3 changes: 3 additions & 0 deletions src/mgconstants.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ static const uint8_t MG_MARKERS_MAP[] = {MG_MARKER_TINY_MAP, MG_MARKER_MAP_8,
#define MG_SIGNATURE_MESSAGE_BEGIN 0x11
#define MG_SIGNATURE_MESSAGE_COMMIT 0x12
#define MG_SIGNATURE_MESSAGE_ROLLBACK 0x13
// 0x66 also equals MG_SIGNATURE_DATE_TIME_ZONE_ID, but the two are decoded in
// different contexts (struct value vs. message), so the collision is safe.
#define MG_SIGNATURE_MESSAGE_ROUTE 0x66

#ifdef __cplusplus
}
Expand Down
Loading
Loading