Skip to content
Open
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
146 changes: 129 additions & 17 deletions bindings/c/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -87,6 +195,9 @@ int main(int argc, char *argv[])
libsql_free_string(value);
value = NULL;
}

libsql_free_row(row);
row = NULL;
err = NULL;
}

Expand All @@ -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;
}
16 changes: 14 additions & 2 deletions bindings/c/include/libsql.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@ 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 {
int frame_no;
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
108 changes: 107 additions & 1 deletion bindings/c/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down Expand Up @@ -154,6 +155,111 @@ fn maybe_remove_offline_query_param(url: &str) -> anyhow::Result<Option<String>>
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<libsql_tx> = 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<libsql_tx> = 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::*;
Expand Down
41 changes: 41 additions & 0 deletions bindings/c/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,44 @@ impl From<&mut libsql_row> for libsql_row_t {
Self { ptr: value }
}
}

pub struct libsql_tx {
pub(crate) tx: Option<libsql::Transaction>,
}

#[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 }
}
}
Loading