From 1c182256d985577cf1dab00b86d34709d1528cb5 Mon Sep 17 00:00:00 2001 From: Juan Castellanos Date: Sat, 27 Sep 2025 00:20:21 -0300 Subject: [PATCH] add c bindings for transactions and update example.c --- bindings/c/example.c | 146 +++++++++++++++++++++++++++++++----- bindings/c/include/libsql.h | 16 +++- bindings/c/src/lib.rs | 108 +++++++++++++++++++++++++- bindings/c/src/types.rs | 41 ++++++++++ 4 files changed, 291 insertions(+), 20 deletions(-) diff --git a/bindings/c/example.c b/bindings/c/example.c index 7243ea60f0..0f15121aa8 100644 --- a/bindings/c/example.c +++ b/bindings/c/example.c @@ -5,10 +5,10 @@ int main(int argc, char *argv[]) { - libsql_connection_t conn; - libsql_rows_t rows; - libsql_row_t row; - libsql_database_t db; + libsql_connection_t conn = NULL; + libsql_rows_t rows = NULL; + libsql_row_t row = NULL; + libsql_database_t db = NULL; libsql_config config; const char *err = NULL; int retval = 0; @@ -20,9 +20,10 @@ int main(int argc, char *argv[]) char auth_token[1024]; auth_token[0] = '\0'; if (argc > 2) { - strncpy(auth_token, argv[2], strlen(argv[2])); + snprintf(auth_token, sizeof(auth_token), "%s", argv[2]); } - strncpy(db_path, "test.db", strlen("test.db")); + snprintf(db_path, sizeof(db_path), "%s", "test.db"); + memset(&config, 0, sizeof(config)); config.db_path = db_path; config.primary_url = url; config.auth_token = auth_token; @@ -56,16 +57,123 @@ int main(int argc, char *argv[]) goto quit; } - retval = libsql_execute(conn, "INSERT INTO guest_book_entries VALUES('hi there')", &err); - if (retval != 0) { - fprintf(stderr, "%s\n", err); - goto quit; + // --- ROLLBACK should discard changes + { + libsql_tx_t tx = NULL; + retval = libsql_tx_begin(conn, 0 /* Deferred */, &tx, &err); + if (retval != 0) { + fprintf(stderr, "tx_begin (rollback test): %s\n", err); + goto quit; + } + + retval = libsql_execute(conn, "DELETE FROM guest_book_entries", &err); + if (retval != 0) { + fprintf(stderr, "delete before rollback test: %s\n", err); + libsql_tx_free(tx); + goto quit; + } + + retval = libsql_execute(conn, "INSERT INTO guest_book_entries VALUES('tx will be rolled back')", &err); + if (retval != 0) { + fprintf(stderr, "insert (rollback test): %s\n", err); + libsql_tx_free(tx); + goto quit; + } + + retval = libsql_tx_rollback(tx, &err); + if (retval != 0) { + fprintf(stderr, "tx_rollback: %s\n", err); + libsql_tx_free(tx); + goto quit; + } + tx = NULL; + + rows = NULL; row = NULL; err = NULL; + retval = libsql_query(conn, "SELECT COUNT(*) FROM guest_book_entries", &rows, &err); + if (retval != 0) { + fprintf(stderr, "query count after rollback: %s\n", err); + goto quit; + } + retval = libsql_next_row(rows, &row, &err); + if (retval != 0 || !row) { + fprintf(stderr, "next_row (count after rollback): %s\n", err ? err : "no row"); + goto quit; + } + long long count = -1; + retval = libsql_get_int(row, 0, &count, &err); + if (retval != 0) { + fprintf(stderr, "get_int (count after rollback): %s\n", err); + goto quit; + } + libsql_free_row(row); row = NULL; + libsql_free_rows(rows); rows = NULL; + + if (count != 0) { + fprintf(stderr, "rollback test failed: expected 0 rows, got %lld\n", count); + retval = 1; + goto quit; + } else { + printf("[tx-rollback] OK: count=%lld\n", count); + } } - retval = libsql_execute(conn, "INSERT INTO guest_book_entries VALUES('some more hi there')", &err); - if (retval != 0) { - fprintf(stderr, "%s\n", err); - goto quit; + // --- COMMIT should persist changes + { + libsql_tx_t tx = NULL; + retval = libsql_tx_begin(conn, 0 /* Deferred */, &tx, &err); + if (retval != 0) { + fprintf(stderr, "tx_begin (commit test): %s\n", err); + goto quit; + } + + retval = libsql_execute(conn, "INSERT INTO guest_book_entries VALUES('hello from tx-commit 1')", &err); + if (retval != 0) { + fprintf(stderr, "insert 1 (commit test): %s\n", err); + libsql_tx_free(tx); + goto quit; + } + retval = libsql_execute(conn, "INSERT INTO guest_book_entries VALUES('hello from tx-commit 2')", &err); + if (retval != 0) { + fprintf(stderr, "insert 2 (commit test): %s\n", err); + libsql_tx_free(tx); + goto quit; + } + + retval = libsql_tx_commit(tx, &err); + if (retval != 0) { + fprintf(stderr, "tx_commit: %s\n", err); + libsql_tx_free(tx); + goto quit; + } + + tx = NULL; + rows = NULL; row = NULL; err = NULL; + retval = libsql_query(conn, "SELECT COUNT(*) FROM guest_book_entries", &rows, &err); + if (retval != 0) { + fprintf(stderr, "query count after commit: %s\n", err); + goto quit; + } + retval = libsql_next_row(rows, &row, &err); + if (retval != 0 || !row) { + fprintf(stderr, "next_row (count after commit): %s\n", err ? err : "no row"); + goto quit; + } + long long count = -1; + retval = libsql_get_int(row, 0, &count, &err); + if (retval != 0) { + fprintf(stderr, "get_int (count after commit): %s\n", err); + goto quit; + } + libsql_free_row(row); row = NULL; + libsql_free_rows(rows); rows = NULL; + + if (count != 2) { + fprintf(stderr, "commit test failed: expected 2 rows, got %lld\n", count); + retval = 1; + goto quit; + } else { + printf("[tx-commit] OK: count=%lld\n", count); + } } retval = libsql_query(conn, "SELECT text FROM guest_book_entries", &rows, &err); @@ -87,6 +195,9 @@ int main(int argc, char *argv[]) libsql_free_string(value); value = NULL; } + + libsql_free_row(row); + row = NULL; err = NULL; } @@ -106,9 +217,10 @@ int main(int argc, char *argv[]) } quit: - libsql_free_rows(rows); - libsql_disconnect(conn); - libsql_close(db); + if (row) libsql_free_row(row); + if (rows) libsql_free_rows(rows); + if (conn) libsql_disconnect(conn); + if (db) libsql_close(db); return retval; } diff --git a/bindings/c/include/libsql.h b/bindings/c/include/libsql.h index eec2d5b4d9..a61251c29e 100644 --- a/bindings/c/include/libsql.h +++ b/bindings/c/include/libsql.h @@ -25,6 +25,8 @@ typedef struct libsql_rows_future libsql_rows_future; typedef struct libsql_stmt libsql_stmt; +typedef struct libsql_tx libsql_tx; + typedef const libsql_database *libsql_database_t; typedef struct { @@ -32,6 +34,10 @@ typedef struct { int frames_synced; } replicated; +typedef const libsql_connection *libsql_connection_t; + +typedef const libsql_tx *libsql_tx_t; + typedef struct { const char *db_path; const char *primary_url; @@ -44,8 +50,6 @@ typedef struct { const char *remote_encryption_key; } libsql_config; -typedef const libsql_connection *libsql_connection_t; - typedef const libsql_stmt *libsql_stmt_t; typedef const libsql_rows *libsql_rows_t; @@ -85,6 +89,14 @@ int libsql_open_sync_with_webpki(const char *db_path, libsql_database_t *out_db, const char **out_err_msg); +int libsql_tx_begin(libsql_connection_t conn, int behavior, libsql_tx_t *out_tx, const char **out_err_msg); + +int libsql_tx_commit(libsql_tx_t tx, const char **out_err_msg); + +int libsql_tx_rollback(libsql_tx_t tx, const char **out_err_msg); + +void libsql_tx_free(libsql_tx_t tx); + int libsql_open_sync_with_config(libsql_config config, libsql_database_t *out_db, const char **out_err_msg); int libsql_open_ext(const char *url, libsql_database_t *out_db, const char **out_err_msg); diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index cb369b1d22..2b9a7bdc88 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -8,11 +8,12 @@ mod types; use crate::types::libsql_config; use http::Uri; use libsql::{errors, Builder, LoadExtensionGuard}; +use std::ffi::c_int; use tokio::runtime::Runtime; use types::{ blob, libsql_connection, libsql_connection_t, libsql_database, libsql_database_t, libsql_row, libsql_row_t, libsql_rows, libsql_rows_future_t, libsql_rows_t, libsql_stmt, libsql_stmt_t, - replicated, stmt, + libsql_tx, libsql_tx_t, replicated, stmt, }; lazy_static! { @@ -154,6 +155,111 @@ fn maybe_remove_offline_query_param(url: &str) -> anyhow::Result> Ok(Some(url[..query_idx].to_owned() + "?" + &query)) } +fn to_tx_behavior(b: c_int) -> libsql::TransactionBehavior { + match b { + 1 => libsql::TransactionBehavior::Immediate, + 2 => libsql::TransactionBehavior::Exclusive, + 3 => libsql::TransactionBehavior::ReadOnly, + _ => libsql::TransactionBehavior::Deferred, + } +} + +#[no_mangle] +pub unsafe extern "C" fn libsql_tx_begin( + conn: libsql_connection_t, + behavior: c_int, + out_tx: *mut libsql_tx_t, + out_err_msg: *mut *const std::ffi::c_char, +) -> c_int { + if out_tx.is_null() { + set_err_msg("Null out_tx".to_string(), out_err_msg); + return 1; + } + if conn.is_null() { + set_err_msg("Null connection".to_string(), out_err_msg); + return 2; + } + + let c = conn.get_ref(); + let beh = to_tx_behavior(behavior); + + let tx_res = match beh { + libsql::TransactionBehavior::Deferred => RT.block_on(c.transaction()), + _ => RT.block_on(c.transaction_with_behavior(beh)), + }; + + match tx_res { + Ok(tx) => { + let handle = Box::new(libsql_tx { tx: Some(tx) }); + *out_tx = libsql_tx_t::from(Box::leak(handle)); + 0 + } + Err(e) => { + set_err_msg(format!("Error beginning transaction: {}", e), out_err_msg); + 3 + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn libsql_tx_commit( + tx: libsql_tx_t, + out_err_msg: *mut *const std::ffi::c_char, +) -> c_int { + if tx.is_null() { + set_err_msg("Null transaction".to_string(), out_err_msg); + return 1; + } + + let mut handle: Box = Box::from_raw(tx.as_const_ptr() as *mut libsql_tx); + let Some(inner) = handle.tx.take() else { + return 0; + }; + + match RT.block_on(inner.commit()) { + Ok(_) => 0, + Err(e) => { + set_err_msg(format!("Error committing transaction: {}", e), out_err_msg); + 2 + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn libsql_tx_rollback( + tx: libsql_tx_t, + out_err_msg: *mut *const std::ffi::c_char, +) -> c_int { + if tx.is_null() { + set_err_msg("Null transaction".to_string(), out_err_msg); + return 1; + } + + let mut handle: Box = Box::from_raw(tx.as_const_ptr() as *mut libsql_tx); + let Some(inner) = handle.tx.take() else { + return 0; + }; + + match RT.block_on(inner.rollback()) { + Ok(_) => 0, + Err(e) => { + set_err_msg( + format!("Error rolling back transaction: {}", e), + out_err_msg, + ); + 2 + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn libsql_tx_free(tx: libsql_tx_t) { + if tx.is_null() { + return; + } + let _ = Box::from_raw(tx.as_const_ptr() as *mut libsql_tx); +} + #[cfg(test)] mod test { use super::*; diff --git a/bindings/c/src/types.rs b/bindings/c/src/types.rs index de679a917a..89094f2643 100644 --- a/bindings/c/src/types.rs +++ b/bindings/c/src/types.rs @@ -308,3 +308,44 @@ impl From<&mut libsql_row> for libsql_row_t { Self { ptr: value } } } + +pub struct libsql_tx { + pub(crate) tx: Option, +} + +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct libsql_tx_t { + ptr: *const libsql_tx, +} + +impl libsql_tx_t { + pub fn null() -> libsql_tx_t { + libsql_tx_t { + ptr: std::ptr::null(), + } + } + + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + #[inline] + pub(crate) fn as_const_ptr(&self) -> *const libsql_tx { + self.ptr + } +} + +#[allow(clippy::from_over_into)] +impl From<&libsql_tx> for libsql_tx_t { + fn from(value: &libsql_tx) -> Self { + Self { ptr: value } + } +} + +#[allow(clippy::from_over_into)] +impl From<&mut libsql_tx> for libsql_tx_t { + fn from(value: &mut libsql_tx) -> Self { + Self { ptr: value } + } +}