From 9c3bd0b8df044a53ee1b21db9d720177ba455049 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 15 Oct 2025 11:03:27 +0200 Subject: [PATCH 01/14] Get simple example running --- CMakeLists.txt | 22 +++++++ examples/cpp_queries.cpp | 33 ++++++++++ libpowersync/build.rs | 21 +++++++ libpowersync/include/powersync.h | 87 ++++++++++++++++++++++++++ libpowersync/src/database.rs | 102 +++++++++++++++++++++++++++++++ libpowersync/src/error.rs | 28 +++++++++ libpowersync/src/executor.rs | 17 ++++++ libpowersync/src/powersync.cpp | 63 +++++++++++++++++++ 8 files changed, 373 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 examples/cpp_queries.cpp create mode 100644 libpowersync/build.rs create mode 100644 libpowersync/include/powersync.h create mode 100644 libpowersync/src/database.rs create mode 100644 libpowersync/src/error.rs create mode 100644 libpowersync/src/executor.rs create mode 100644 libpowersync/src/powersync.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..24e638c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 4.0) + +project(powersync) +find_package(CURL) + +add_custom_target( + powersync_c_rust + COMMAND cargo build -p libpowersync +) + +add_library(powersync_c SHARED IMPORTED) +add_dependencies(powersync_c powersync_c_rust) +target_link_libraries(powersync_c INTERFACE CURL::libcurl) +set_target_properties(powersync_c PROPERTIES + IMPORTED_LOCATION "${CMAKE_SOURCE_DIR}/target/debug/libpowersync_c.a" + INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_SOURCE_DIR}/libpowersync/include" +) + +add_executable(cpp_queries) +target_sources(cpp_queries PRIVATE "examples/cpp_queries.cpp") +target_link_libraries(cpp_queries powersync_c) +target_compile_features(cpp_queries PRIVATE cxx_std_17) diff --git a/examples/cpp_queries.cpp b/examples/cpp_queries.cpp new file mode 100644 index 0000000..dfd4c09 --- /dev/null +++ b/examples/cpp_queries.cpp @@ -0,0 +1,33 @@ +#include + +#include "powersync.h" + +void check_rc(int rc) { + if (rc != SQLITE_OK) { + throw std::runtime_error("SQLite error: " + std::string(sqlite3_errstr(rc))); + } +} + +int main() { + using namespace powersync; + + constexpr Schema schema{}; + auto db = Database::in_memory(schema); + + { + auto writer = db.writer(); + check_rc(sqlite3_exec(writer, "CREATE TABLE foo (bar TEXT) STRICT;", nullptr, nullptr, nullptr)); + check_rc(sqlite3_exec(writer, "INSERT INTO foo (bar) VALUES ('testing');", nullptr, nullptr, nullptr)); + } + + { + auto reader = db.reader(); + sqlite3_stmt *stmt = nullptr; + check_rc(sqlite3_prepare_v2(reader, "SELECT * FROM foo", -1, &stmt, nullptr)); + + while (sqlite3_step(stmt) == SQLITE_ROW) { + std::cout << sqlite3_column_text(stmt, 0) << std::endl; + } + sqlite3_finalize(stmt); + } +} diff --git a/libpowersync/build.rs b/libpowersync/build.rs new file mode 100644 index 0000000..a22b65a --- /dev/null +++ b/libpowersync/build.rs @@ -0,0 +1,21 @@ +use std::env; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + cbindgen::Builder::new() + .with_crate(crate_dir) + .with_namespace("powersync::internal") + .generate() + .expect("Unable to generate bindings") + .write_to_file("src/bindings.h"); + + cc::Build::new() + .cpp(true) + .std("c++17") + .include("include/") + .include("src") + .file("src/powersync.cpp") + .link_lib_modifier("+whole-archive") + .compile("powersync_bridge"); +} diff --git a/libpowersync/include/powersync.h b/libpowersync/include/powersync.h new file mode 100644 index 0000000..acbac81 --- /dev/null +++ b/libpowersync/include/powersync.h @@ -0,0 +1,87 @@ +#pragma once + +#include "sqlite3.h" +#include +#include +#include +#include + +namespace powersync { + enum ColumnType { + TEXT, + INTEGER, + REAL +}; + + struct Column { + std::string name; + ColumnType type; + + static Column text(std::string name) { + return {.name = std::move(name), .type = TEXT}; + } + static Column real(std::string name) { + return {.name = std::move(name), .type = REAL}; + } + static Column integer(std::string name) { + return {.name = std::move(name), .type = INTEGER}; + } + }; + + struct Table { + std::string name; + std::vector columns; + //std::vector indices = {}; + bool local_only = true; + bool insert_only = false; + std::optional view_name_override = std::nullopt; + bool track_metadata = false; + bool ignore_empty_updates = false; + //std::optional track_previous_values = std::nullopt; + + Table(std::string name, std::vector columns); + }; + +struct Schema { + +}; + +struct LeasedConnection { +private: + sqlite3* db; + void* raw_lease; + LeasedConnection(sqlite3* db, void* raw_lease) + :db(db), raw_lease(raw_lease) {}; + +public: + friend class Database; + + ~LeasedConnection(); + operator sqlite3*() const; +}; + +class Database { +private: + void* rust_db; + + explicit Database(void* rust_db) : rust_db(rust_db) {} +public: + void disconnect(); + + [[nodiscard]] LeasedConnection reader() const; + [[nodiscard]] LeasedConnection writer() const; + + static Database in_memory(Schema schema); +}; + +class Exception final : public std::exception { +private: + const int rc; + char* msg; +public: + explicit Exception(int rc, char* msg) : rc(rc), msg(msg) {} + + [[nodiscard]] const char *what() const noexcept override; + ~Exception() noexcept override; +}; +} diff --git a/libpowersync/src/database.rs b/libpowersync/src/database.rs new file mode 100644 index 0000000..bea7584 --- /dev/null +++ b/libpowersync/src/database.rs @@ -0,0 +1,102 @@ +use std::ops::Deref; +use std::ffi::{c_char, c_void, CStr, CString}; +use std::ptr::null; +use std::sync::Arc; +use futures_lite::future; +use http_client::isahc::IsahcClient; +use rusqlite::Connection; +use rusqlite::ffi::sqlite3; +use powersync::{ConnectionPool, InnerPowerSyncState, LeasedConnection, PowerSyncDatabase}; +use powersync::env::PowerSyncEnvironment; +use powersync::error::PowerSyncError; +use powersync::schema::Schema; +use crate::error::{PowerSyncResultCode, LAST_ERROR}; + +fn create_db(pool: ConnectionPool) -> PowerSyncDatabase { + let env = PowerSyncEnvironment::custom( + Arc::new(IsahcClient::new()), + pool, + Box::new(PowerSyncEnvironment::async_io_timer()) + ); + + let schema = Schema::default(); + PowerSyncDatabase::new(env, schema) +} + +#[repr(C)] +struct RawPowerSyncDatabase { + db: *mut InnerPowerSyncState, // *const InnerPowerSyncState +} + +impl RawPowerSyncDatabase { + unsafe fn as_db(&self) -> PowerSyncDatabase { + unsafe { PowerSyncDatabase::interpret_raw(self.db) } + } +} + +struct RawConnectionLease<'a> { + lease: Box +} + +#[repr(C)] +struct ConnectionLeaseResult<'a> { + sqlite3: *mut sqlite3, + lease: *mut RawConnectionLease<'a>, +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_db_in_memory(out_db: &mut RawPowerSyncDatabase) -> PowerSyncResultCode { + ps_try!(PowerSyncEnvironment::powersync_auto_extension()); + let conn = ps_try!(Connection::open_in_memory().map_err(PowerSyncError::from)); + let raw = create_db(ConnectionPool::single_connection(conn)).into_raw(); + + *out_db = RawPowerSyncDatabase { + db: raw as *mut _ + }; + PowerSyncResultCode::OK +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_db_reader<'a>(db: &'a InnerPowerSyncState, out_lease: &mut ConnectionLeaseResult<'a>) -> PowerSyncResultCode { + let reader = ps_try!(future::block_on(db.reader())); + + out_lease.sqlite3 = unsafe { reader.deref().handle() }; + out_lease.lease = Box::into_raw(Box::new(RawConnectionLease { lease: Box::new(reader) })); + PowerSyncResultCode::OK +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_db_writer<'a>(db: &'a InnerPowerSyncState, out_lease: &mut ConnectionLeaseResult<'a>) -> PowerSyncResultCode { + let writer = ps_try!(future::block_on(db.writer())); + + out_lease.sqlite3 = unsafe { writer.deref().handle() }; + out_lease.lease = Box::into_raw(Box::new(RawConnectionLease { lease: Box::new(writer) })); + PowerSyncResultCode::OK +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_db_return_lease(lease: *mut RawConnectionLease) { + let lease = unsafe{ Box::from_raw(lease) }; + drop(lease); +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_last_error_desc() -> *mut c_char { + if let Some(err) = LAST_ERROR.take() { + CString::into_raw(CString::new(format!("{}", err)).unwrap()) + } else { + null() + }.cast_mut() +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_free_str(str: *mut c_char) { + drop(unsafe { CString::from_raw(str) }); +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_db_free(db: RawPowerSyncDatabase) { + unsafe { + PowerSyncDatabase::drop_raw(db.db.cast()); + } +} diff --git a/libpowersync/src/error.rs b/libpowersync/src/error.rs new file mode 100644 index 0000000..01f5307 --- /dev/null +++ b/libpowersync/src/error.rs @@ -0,0 +1,28 @@ +use std::cell::Cell; +use log::warn; +use powersync::error::PowerSyncError; + +thread_local! { + pub static LAST_ERROR: Cell> = Cell::new(None); +} + +#[repr(C)] +pub enum PowerSyncResultCode { + OK = 0, + ERROR = 1, +} + +impl Default for PowerSyncResultCode { + fn default() -> Self { + Self::OK + } +} + +impl Into for PowerSyncError { + fn into(self) -> PowerSyncResultCode { + warn!("Returning error: {}", self); + LAST_ERROR.replace(Some(self.into())); + + PowerSyncResultCode::ERROR + } +} diff --git a/libpowersync/src/executor.rs b/libpowersync/src/executor.rs new file mode 100644 index 0000000..f9e0907 --- /dev/null +++ b/libpowersync/src/executor.rs @@ -0,0 +1,17 @@ +use async_executor::Executor; +use futures_lite::future; +use futures_lite::future::block_on; +use powersync::PowerSyncDatabase; + +/// Runs asynchronous PowerSync tasks on the current thread. +/// +/// This blocks the thread until the database is closed. +pub fn run_powersync_tasks(db: &PowerSyncDatabase) { + let executor = Executor::new(); + let downloader = executor.spawn(db.download_actor()); + let uploader = executor.spawn(db.upload_actor()); + + // The actors will run until the source database is closed, so we wait for that to happen. + let future = future::or(downloader, uploader); + block_on(executor.run(future)); +} diff --git a/libpowersync/src/powersync.cpp b/libpowersync/src/powersync.cpp new file mode 100644 index 0000000..ed4c902 --- /dev/null +++ b/libpowersync/src/powersync.cpp @@ -0,0 +1,63 @@ +#include "powersync.h" + +namespace powersync::internal { + // Not generated by cbindgen + struct InnerPowerSyncState; +} + +#include "bindings.h" + +namespace powersync { + static void handle_result(internal::PowerSyncResultCode rc) { + if (rc != internal::PowerSyncResultCode::OK) { + auto msg = internal::powersync_last_error_desc(); + throw Exception(1, msg); + } + } + + const char * Exception::what() const noexcept { + if (this->msg) { + return this->msg; + } + + return "Unknown error"; + } + + Exception::~Exception() noexcept { + if (this->msg) { + internal::powersync_free_str(this->msg); + } + } + + Database Database::in_memory(Schema schema) { + internal::RawPowerSyncDatabase db{}; + handle_result(internal::powersync_db_in_memory(&db)); + + return Database(db.db); + } + + LeasedConnection Database::reader() const { + internal::ConnectionLeaseResult result{}; + + auto raw = static_cast(this->rust_db); + handle_result(internal::powersync_db_reader(raw, &result)); + return {result.sqlite3, result.lease}; + } + + LeasedConnection Database::writer() const { + internal::ConnectionLeaseResult result{}; + + auto raw = static_cast(this->rust_db); + handle_result(internal::powersync_db_writer(raw, &result)); + return {result.sqlite3, result.lease}; + } + + LeasedConnection::~LeasedConnection() { + internal::powersync_db_return_lease(static_cast(this->raw_lease)); + } + + LeasedConnection::operator sqlite3 *() const { + return this->db; + } +} + From 5c3a936f7b8c7ac3f4a57d83648960ee5d07b96b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 16 Oct 2025 14:30:08 +0200 Subject: [PATCH 02/14] Get local queries working in C++ # Conflicts: # powersync/src/db/internal.rs # powersync/src/db/schema.rs --- examples/cpp_queries.cpp | 12 ++-- libpowersync/include/powersync.h | 11 ++-- libpowersync/src/database.rs | 76 +++++++++++++++---------- libpowersync/src/executor.rs | 6 +- libpowersync/src/helpers.h | 17 ++++++ libpowersync/src/powersync.cpp | 76 ++++++++++++++++++++++++- libpowersync/src/schema.rs | 95 ++++++++++++++++++++++++++++++++ 7 files changed, 250 insertions(+), 43 deletions(-) create mode 100644 libpowersync/src/helpers.h create mode 100644 libpowersync/src/schema.rs diff --git a/examples/cpp_queries.cpp b/examples/cpp_queries.cpp index dfd4c09..8c4724e 100644 --- a/examples/cpp_queries.cpp +++ b/examples/cpp_queries.cpp @@ -11,22 +11,24 @@ void check_rc(int rc) { int main() { using namespace powersync; - constexpr Schema schema{}; + Schema schema{}; + schema.tables.emplace_back(Table{"users", { + Column::text("name") + }}); auto db = Database::in_memory(schema); { auto writer = db.writer(); - check_rc(sqlite3_exec(writer, "CREATE TABLE foo (bar TEXT) STRICT;", nullptr, nullptr, nullptr)); - check_rc(sqlite3_exec(writer, "INSERT INTO foo (bar) VALUES ('testing');", nullptr, nullptr, nullptr)); + check_rc(sqlite3_exec(writer, "INSERT INTO users (id, name) VALUES (uuid(), 'Simon');", nullptr, nullptr, nullptr)); } { auto reader = db.reader(); sqlite3_stmt *stmt = nullptr; - check_rc(sqlite3_prepare_v2(reader, "SELECT * FROM foo", -1, &stmt, nullptr)); + check_rc(sqlite3_prepare_v2(reader, "SELECT id, name FROM users", -1, &stmt, nullptr)); while (sqlite3_step(stmt) == SQLITE_ROW) { - std::cout << sqlite3_column_text(stmt, 0) << std::endl; + std::cout << sqlite3_column_text(stmt, 0) << ": " << sqlite3_column_text(stmt, 1) << std::endl; } sqlite3_finalize(stmt); } diff --git a/libpowersync/include/powersync.h b/libpowersync/include/powersync.h index acbac81..00b8160 100644 --- a/libpowersync/include/powersync.h +++ b/libpowersync/include/powersync.h @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace powersync { @@ -32,18 +33,18 @@ namespace powersync { std::string name; std::vector columns; //std::vector indices = {}; - bool local_only = true; + bool local_only = false; bool insert_only = false; std::optional view_name_override = std::nullopt; bool track_metadata = false; bool ignore_empty_updates = false; //std::optional track_previous_values = std::nullopt; - Table(std::string name, std::vector columns); + Table(std::string name, std::vector columns): name(std::move(name)), columns(std::move(columns)) {} }; struct Schema { - + std::vector tables; }; struct LeasedConnection { @@ -63,15 +64,17 @@ struct LeasedConnection { class Database { private: void* rust_db; + std::optional worker; explicit Database(void* rust_db) : rust_db(rust_db) {} public: void disconnect(); + void spawn_sync_thread(); [[nodiscard]] LeasedConnection reader() const; [[nodiscard]] LeasedConnection writer() const; - static Database in_memory(Schema schema); + static Database in_memory(const Schema& schema); }; class Exception final : public std::exception { diff --git a/libpowersync/src/database.rs b/libpowersync/src/database.rs index bea7584..0f9d6d8 100644 --- a/libpowersync/src/database.rs +++ b/libpowersync/src/database.rs @@ -1,25 +1,25 @@ -use std::ops::Deref; -use std::ffi::{c_char, c_void, CStr, CString}; -use std::ptr::null; -use std::sync::Arc; +use crate::error::{LAST_ERROR, PowerSyncResultCode}; +use crate::schema::RawSchema; use futures_lite::future; use http_client::isahc::IsahcClient; -use rusqlite::Connection; -use rusqlite::ffi::sqlite3; -use powersync::{ConnectionPool, InnerPowerSyncState, LeasedConnection, PowerSyncDatabase}; use powersync::env::PowerSyncEnvironment; use powersync::error::PowerSyncError; use powersync::schema::Schema; -use crate::error::{PowerSyncResultCode, LAST_ERROR}; +use powersync::{ConnectionPool, InnerPowerSyncState, LeasedConnection, PowerSyncDatabase}; +use rusqlite::Connection; +use rusqlite::ffi::sqlite3; +use std::ffi::{CString, c_char}; +use std::ops::Deref; +use std::ptr::null; +use std::sync::Arc; -fn create_db(pool: ConnectionPool) -> PowerSyncDatabase { +fn create_db(schema: Schema, pool: ConnectionPool) -> PowerSyncDatabase { let env = PowerSyncEnvironment::custom( Arc::new(IsahcClient::new()), pool, - Box::new(PowerSyncEnvironment::async_io_timer()) + Box::new(PowerSyncEnvironment::async_io_timer()), ); - let schema = Schema::default(); PowerSyncDatabase::new(env, schema) } @@ -35,7 +35,7 @@ impl RawPowerSyncDatabase { } struct RawConnectionLease<'a> { - lease: Box + lease: Box, } #[repr(C)] @@ -45,58 +45,74 @@ struct ConnectionLeaseResult<'a> { } #[unsafe(no_mangle)] -extern "C" fn powersync_db_in_memory(out_db: &mut RawPowerSyncDatabase) -> PowerSyncResultCode { +extern "C" fn powersync_db_in_memory( + schema: RawSchema, + out_db: &mut RawPowerSyncDatabase, +) -> PowerSyncResultCode { ps_try!(PowerSyncEnvironment::powersync_auto_extension()); let conn = ps_try!(Connection::open_in_memory().map_err(PowerSyncError::from)); - let raw = create_db(ConnectionPool::single_connection(conn)).into_raw(); + let raw = create_db( + schema.copy_to_rust(), + ConnectionPool::single_connection(conn), + ) + .into_raw(); - *out_db = RawPowerSyncDatabase { - db: raw as *mut _ - }; + *out_db = RawPowerSyncDatabase { db: raw as *mut _ }; PowerSyncResultCode::OK } #[unsafe(no_mangle)] -extern "C" fn powersync_db_reader<'a>(db: &'a InnerPowerSyncState, out_lease: &mut ConnectionLeaseResult<'a>) -> PowerSyncResultCode { +extern "C" fn powersync_db_reader<'a>( + db: &'a InnerPowerSyncState, + out_lease: &mut ConnectionLeaseResult<'a>, +) -> PowerSyncResultCode { let reader = ps_try!(future::block_on(db.reader())); out_lease.sqlite3 = unsafe { reader.deref().handle() }; - out_lease.lease = Box::into_raw(Box::new(RawConnectionLease { lease: Box::new(reader) })); + out_lease.lease = Box::into_raw(Box::new(RawConnectionLease { + lease: Box::new(reader), + })); PowerSyncResultCode::OK } #[unsafe(no_mangle)] -extern "C" fn powersync_db_writer<'a>(db: &'a InnerPowerSyncState, out_lease: &mut ConnectionLeaseResult<'a>) -> PowerSyncResultCode { +extern "C" fn powersync_db_writer<'a>( + db: &'a InnerPowerSyncState, + out_lease: &mut ConnectionLeaseResult<'a>, +) -> PowerSyncResultCode { let writer = ps_try!(future::block_on(db.writer())); out_lease.sqlite3 = unsafe { writer.deref().handle() }; - out_lease.lease = Box::into_raw(Box::new(RawConnectionLease { lease: Box::new(writer) })); + out_lease.lease = Box::into_raw(Box::new(RawConnectionLease { + lease: Box::new(writer), + })); PowerSyncResultCode::OK } #[unsafe(no_mangle)] extern "C" fn powersync_db_return_lease(lease: *mut RawConnectionLease) { - let lease = unsafe{ Box::from_raw(lease) }; + let lease = unsafe { Box::from_raw(lease) }; drop(lease); } +#[unsafe(no_mangle)] +extern "C" fn powersync_db_free(db: RawPowerSyncDatabase) { + unsafe { + PowerSyncDatabase::drop_raw(db.db.cast()); + } +} + #[unsafe(no_mangle)] extern "C" fn powersync_last_error_desc() -> *mut c_char { if let Some(err) = LAST_ERROR.take() { CString::into_raw(CString::new(format!("{}", err)).unwrap()) } else { null() - }.cast_mut() + } + .cast_mut() } #[unsafe(no_mangle)] extern "C" fn powersync_free_str(str: *mut c_char) { drop(unsafe { CString::from_raw(str) }); } - -#[unsafe(no_mangle)] -extern "C" fn powersync_db_free(db: RawPowerSyncDatabase) { - unsafe { - PowerSyncDatabase::drop_raw(db.db.cast()); - } -} diff --git a/libpowersync/src/executor.rs b/libpowersync/src/executor.rs index f9e0907..cc81c63 100644 --- a/libpowersync/src/executor.rs +++ b/libpowersync/src/executor.rs @@ -1,12 +1,14 @@ use async_executor::Executor; use futures_lite::future; use futures_lite::future::block_on; -use powersync::PowerSyncDatabase; +use powersync::{InnerPowerSyncState, PowerSyncDatabase}; /// Runs asynchronous PowerSync tasks on the current thread. /// /// This blocks the thread until the database is closed. -pub fn run_powersync_tasks(db: &PowerSyncDatabase) { +#[unsafe(no_mangle)] +pub extern "C" fn powersync_run_tasks(db: *const InnerPowerSyncState) { + let db = unsafe { PowerSyncDatabase::interpret_raw(db) }; let executor = Executor::new(); let downloader = executor.spawn(db.download_actor()); let uploader = executor.spawn(db.upload_actor()); diff --git a/libpowersync/src/helpers.h b/libpowersync/src/helpers.h new file mode 100644 index 0000000..60c18af --- /dev/null +++ b/libpowersync/src/helpers.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace powersync { + +struct RustTableHelper { + std::vector> columns = {}; + std::vector tables = {}; + + static void map_column(std::vector& target, const Column& column); + void map_table(const Table& table); + + internal::RawSchema to_rust(); +}; + +} diff --git a/libpowersync/src/powersync.cpp b/libpowersync/src/powersync.cpp index ed4c902..5ced8b8 100644 --- a/libpowersync/src/powersync.cpp +++ b/libpowersync/src/powersync.cpp @@ -6,6 +6,7 @@ namespace powersync::internal { } #include "bindings.h" +#include "helpers.h" namespace powersync { static void handle_result(internal::PowerSyncResultCode rc) { @@ -23,19 +24,90 @@ namespace powersync { return "Unknown error"; } + void RustTableHelper::map_column(std::vector& target, const Column &column) { + internal::ColumnType type = {}; + switch (column.type) { + case TEXT: + type = internal::ColumnType::Text; + break; + case INTEGER: + type = internal::ColumnType::Integer; + break; + case REAL: + type = internal::ColumnType::Real; + break; + } + + target.emplace_back() = { + .name = column.name.c_str(), + .column_type = type, + }; + } + + void RustTableHelper::map_table(const Table &table) { + std::vector columns = {}; + for (const auto& column : table.columns) { + map_column(columns, column); + } + + auto& managedColumns= this->columns.emplace_back(std::move(columns)); + const char* view_name_override = nullptr; + if (table.view_name_override.has_value()) { + view_name_override = table.view_name_override.value().c_str(); + } + + this->tables.emplace_back() = { + .name = table.name.c_str(), + .view_name_override = view_name_override, + .columns = &managedColumns.front(), + .column_len = managedColumns.size(), + .local_only = table.local_only, + .insert_only = table.insert_only, + .track_metadata = table.track_metadata, + .ignore_empty_updates = table.ignore_empty_updates, + }; + } + + internal::RawSchema RustTableHelper::to_rust() { + return { + .tables = &this->tables.front(), + .tables_len = this->tables.size(), + }; + } + + static RustTableHelper schema_helper(const Schema& schema) { + RustTableHelper result; + for (const auto& table : schema.tables) { + result.map_table(table); + } + + return result; + } + Exception::~Exception() noexcept { if (this->msg) { internal::powersync_free_str(this->msg); } } - Database Database::in_memory(Schema schema) { + Database Database::in_memory(const Schema& schema) { + auto helper = schema_helper(schema); + internal::RawPowerSyncDatabase db{}; - handle_result(internal::powersync_db_in_memory(&db)); + handle_result(internal::powersync_db_in_memory(helper.to_rust(), &db)); return Database(db.db); } + void Database::spawn_sync_thread() { + auto raw = this->rust_db; + std::thread thread([raw]() { + internal::powersync_run_tasks(static_cast(raw)); + }); + + this->worker = std::move(thread); + } + LeasedConnection Database::reader() const { internal::ConnectionLeaseResult result{}; diff --git a/libpowersync/src/schema.rs b/libpowersync/src/schema.rs new file mode 100644 index 0000000..6d3b914 --- /dev/null +++ b/libpowersync/src/schema.rs @@ -0,0 +1,95 @@ +use powersync::schema as ps; +use std::borrow::Cow; + +use std::ffi::{CStr, c_char}; + +#[repr(C)] +enum ColumnType { + Text = 0, + Integer = 1, + Real = 2, +} + +#[repr(C)] +pub struct Column { + name: *const c_char, + column_type: ColumnType, +} + +fn copy_string(ptr: *const c_char) -> String { + unsafe { CStr::from_ptr(ptr) }.to_str().unwrap().to_string() +} + +fn copy_nullable_string(ptr: *const c_char) -> Option { + if ptr.is_null() { + None + } else { + Some(copy_string(ptr)) + } +} + +impl Column { + pub fn copy_to_rust(&self) -> ps::Column { + ps::Column { + name: Cow::Owned(copy_string(self.name)), + column_type: match self.column_type { + ColumnType::Text => ps::ColumnType::Text, + ColumnType::Integer => ps::ColumnType::Integer, + ColumnType::Real => ps::ColumnType::Real, + }, + } + } +} + +#[repr(C)] +pub struct Table { + name: *const c_char, + view_name_override: *const c_char, + columns: *const Column, + column_len: usize, + local_only: bool, + insert_only: bool, + track_metadata: bool, + ignore_empty_updates: bool, +} + +impl Table { + fn columns(&self) -> &[Column] { + unsafe { std::slice::from_raw_parts(self.columns, self.column_len) } + } + + pub fn copy_to_rust(&self) -> ps::Table { + ps::Table { + name: Cow::Owned(copy_string(self.name)), + view_name_override: copy_nullable_string(self.view_name_override).map(Cow::from), + columns: self.columns().iter().map(|c| c.copy_to_rust()).collect(), + indexes: vec![], + local_only: self.local_only, + insert_only: self.insert_only, + track_metadata: self.track_metadata, + track_previous_values: None, + ignore_empty_updates: self.ignore_empty_updates, + } + } +} + +#[repr(C)] +pub struct RawSchema { + tables: *const Table, + tables_len: usize, +} + +impl RawSchema { + fn tables(&self) -> &[Table] { + unsafe { std::slice::from_raw_parts(self.tables, self.tables_len) } + } + + pub fn copy_to_rust(&self) -> ps::Schema { + let mut schema = ps::Schema::default(); + for table in self.tables() { + schema.tables.push(table.copy_to_rust()); + } + + schema + } +} From 811f18bc79698477d3ddfe3b1f9d7e3132ef5078 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 16 Oct 2025 09:16:31 +0200 Subject: [PATCH 03/14] Missing headers for libpowersync --- libpowersync/Cargo.toml | 20 ++++++++++ libpowersync/bindings.h | 5 +++ libpowersync/src/bindings.h | 77 +++++++++++++++++++++++++++++++++++++ libpowersync/src/lib.rs | 13 +++++++ 4 files changed, 115 insertions(+) create mode 100644 libpowersync/Cargo.toml create mode 100644 libpowersync/bindings.h create mode 100644 libpowersync/src/bindings.h create mode 100644 libpowersync/src/lib.rs diff --git a/libpowersync/Cargo.toml b/libpowersync/Cargo.toml new file mode 100644 index 0000000..df65df4 --- /dev/null +++ b/libpowersync/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "libpowersync" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib", "staticlib"] +name = "powersync_c" + +[dependencies] +powersync = { path = "../powersync", features = ["smol", "ffi"] } +http-client = { version = "6.5.3", features = ["curl_client"], default-features = false } +rusqlite = { version = "0.37.0", features = ["load_extension", "bundled"] } +async-executor = "1.13.3" +futures-lite = "2.6.1" +log = "0.4.28" + +[build-dependencies] +cbindgen = "0.29.0" +cc = "1.2.41" diff --git a/libpowersync/bindings.h b/libpowersync/bindings.h new file mode 100644 index 0000000..da5bc5a --- /dev/null +++ b/libpowersync/bindings.h @@ -0,0 +1,5 @@ +#include +#include +#include +#include +#include diff --git a/libpowersync/src/bindings.h b/libpowersync/src/bindings.h new file mode 100644 index 0000000..41527cc --- /dev/null +++ b/libpowersync/src/bindings.h @@ -0,0 +1,77 @@ +#include +#include +#include +#include +#include + +namespace powersync::internal { + +enum class ColumnType { + Text = 0, + Integer = 1, + Real = 2, +}; + +enum class PowerSyncResultCode { + OK = 0, + ERROR = 1, +}; + +struct RawConnectionLease; + +struct Column { + const char *name; + ColumnType column_type; +}; + +struct Table { + const char *name; + const char *view_name_override; + const Column *columns; + uintptr_t column_len; + bool local_only; + bool insert_only; + bool track_metadata; + bool ignore_empty_updates; +}; + +struct RawSchema { + const Table *tables; + uintptr_t tables_len; +}; + +struct RawPowerSyncDatabase { + InnerPowerSyncState *db; +}; + +struct ConnectionLeaseResult { + sqlite3 *sqlite3; + RawConnectionLease *lease; +}; + +extern "C" { + +PowerSyncResultCode powersync_db_in_memory(RawSchema schema, RawPowerSyncDatabase *out_db); + +PowerSyncResultCode powersync_db_reader(const InnerPowerSyncState *db, + ConnectionLeaseResult *out_lease); + +PowerSyncResultCode powersync_db_writer(const InnerPowerSyncState *db, + ConnectionLeaseResult *out_lease); + +void powersync_db_return_lease(RawConnectionLease *lease); + +void powersync_db_free(RawPowerSyncDatabase db); + +char *powersync_last_error_desc(); + +void powersync_free_str(char *str); + +/// Runs asynchronous PowerSync tasks on the current thread. +/// +/// This blocks the thread until the database is closed. +void powersync_run_tasks(const InnerPowerSyncState *db); + +} // extern "C" + +} // namespace powersync::internal diff --git a/libpowersync/src/lib.rs b/libpowersync/src/lib.rs new file mode 100644 index 0000000..dd2554b --- /dev/null +++ b/libpowersync/src/lib.rs @@ -0,0 +1,13 @@ +macro_rules! ps_try { + ($result:expr) => { + match $result { + Ok(value) => value, + Err(e) => return e.into(), + } + }; +} + +mod database; +mod error; +mod executor; +mod schema; From 21137d10f232722ccee26ce40bdea7664d9e9328 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 20 Oct 2025 17:53:07 +0200 Subject: [PATCH 04/14] New FFI interface --- Cargo.lock | 114 ++++++++++++++++++++++++++++++- Cargo.toml | 7 +- examples/cpp_queries.cpp | 1 + libpowersync/include/powersync.h | 16 ++++- libpowersync/src/bindings.h | 10 +-- libpowersync/src/database.rs | 31 +++------ libpowersync/src/executor.rs | 17 ++--- libpowersync/src/powersync.cpp | 21 ++++-- powersync/src/db/mod.rs | 39 +---------- powersync/src/ffi.rs | 72 +++++++++++++++++++ powersync/src/lib.rs | 7 +- 11 files changed, 246 insertions(+), 89 deletions(-) create mode 100644 powersync/src/ffi.rs diff --git a/Cargo.lock b/Cargo.lock index e7438e6..db4efc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -838,6 +838,25 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cbindgen" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975982cdb7ad6a142be15bdf84aea7ec6a9e5d4d797c004d43185b24cfe4e684" +dependencies = [ + "clap", + "heck", + "indexmap 2.11.4", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.106", + "tempfile", + "toml", +] + [[package]] name = "cc" version = "1.2.41" @@ -898,6 +917,33 @@ dependencies = [ "generic-array", ] +[[package]] +name = "clap" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + [[package]] name = "clipboard-win" version = "5.4.1" @@ -2812,6 +2858,20 @@ dependencies = [ "libc", ] +[[package]] +name = "libpowersync" +version = "0.1.0" +dependencies = [ + "async-executor", + "cbindgen", + "cc", + "futures-lite 2.6.1", + "http-client", + "log", + "powersync", + "rusqlite", +] + [[package]] name = "libredox" version = "0.1.10" @@ -3766,7 +3826,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.7", ] [[package]] @@ -4394,6 +4454,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5028,6 +5097,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + [[package]] name = "toml_datetime" version = "0.7.3" @@ -5037,6 +5127,20 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.11.4", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + [[package]] name = "toml_edit" version = "0.23.7" @@ -5044,7 +5148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap 2.11.4", - "toml_datetime", + "toml_datetime 0.7.3", "toml_parser", "winnow", ] @@ -5058,6 +5162,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 382030f..2358b94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,8 @@ [workspace] resolver = "3" -members = [ "examples/egui_todolist","powersync", "powersync_test_utils"] +members = [ + "examples/egui_todolist", + "powersync", + "libpowersync", + "powersync_test_utils" +] diff --git a/examples/cpp_queries.cpp b/examples/cpp_queries.cpp index 8c4724e..791ee24 100644 --- a/examples/cpp_queries.cpp +++ b/examples/cpp_queries.cpp @@ -16,6 +16,7 @@ int main() { Column::text("name") }}); auto db = Database::in_memory(schema); + db.spawn_sync_thread(); { auto writer = db.writer(); diff --git a/libpowersync/include/powersync.h b/libpowersync/include/powersync.h index 00b8160..6d75d34 100644 --- a/libpowersync/include/powersync.h +++ b/libpowersync/include/powersync.h @@ -8,6 +8,13 @@ #include namespace powersync { + namespace internal { + struct RawPowerSyncDatabase { + void* sync; + void* inner; + }; + } + enum ColumnType { TEXT, INTEGER, @@ -63,14 +70,19 @@ struct LeasedConnection { class Database { private: - void* rust_db; + internal::RawPowerSyncDatabase raw; std::optional worker; - explicit Database(void* rust_db) : rust_db(rust_db) {} + explicit Database(internal::RawPowerSyncDatabase raw) : raw(raw) {} + + // Databases can't be copied, the internal::RawPowerSyncDatabase is an exclusive reference in Rust. + Database(const Database&) = delete; public: void disconnect(); void spawn_sync_thread(); + ~Database(); + [[nodiscard]] LeasedConnection reader() const; [[nodiscard]] LeasedConnection writer() const; diff --git a/libpowersync/src/bindings.h b/libpowersync/src/bindings.h index 41527cc..dca126d 100644 --- a/libpowersync/src/bindings.h +++ b/libpowersync/src/bindings.h @@ -40,10 +40,6 @@ struct RawSchema { uintptr_t tables_len; }; -struct RawPowerSyncDatabase { - InnerPowerSyncState *db; -}; - struct ConnectionLeaseResult { sqlite3 *sqlite3; RawConnectionLease *lease; @@ -53,10 +49,10 @@ extern "C" { PowerSyncResultCode powersync_db_in_memory(RawSchema schema, RawPowerSyncDatabase *out_db); -PowerSyncResultCode powersync_db_reader(const InnerPowerSyncState *db, +PowerSyncResultCode powersync_db_reader(const RawPowerSyncDatabase *db, ConnectionLeaseResult *out_lease); -PowerSyncResultCode powersync_db_writer(const InnerPowerSyncState *db, +PowerSyncResultCode powersync_db_writer(const RawPowerSyncDatabase *db, ConnectionLeaseResult *out_lease); void powersync_db_return_lease(RawConnectionLease *lease); @@ -70,7 +66,7 @@ void powersync_free_str(char *str); /// Runs asynchronous PowerSync tasks on the current thread. /// /// This blocks the thread until the database is closed. -void powersync_run_tasks(const InnerPowerSyncState *db); +void powersync_run_tasks(const RawPowerSyncDatabase *db); } // extern "C" diff --git a/libpowersync/src/database.rs b/libpowersync/src/database.rs index 0f9d6d8..a0f1c38 100644 --- a/libpowersync/src/database.rs +++ b/libpowersync/src/database.rs @@ -4,8 +4,9 @@ use futures_lite::future; use http_client::isahc::IsahcClient; use powersync::env::PowerSyncEnvironment; use powersync::error::PowerSyncError; +use powersync::ffi::RawPowerSyncDatabase; use powersync::schema::Schema; -use powersync::{ConnectionPool, InnerPowerSyncState, LeasedConnection, PowerSyncDatabase}; +use powersync::{ConnectionPool, LeasedConnection, PowerSyncDatabase}; use rusqlite::Connection; use rusqlite::ffi::sqlite3; use std::ffi::{CString, c_char}; @@ -23,17 +24,6 @@ fn create_db(schema: Schema, pool: ConnectionPool) -> PowerSyncDatabase { PowerSyncDatabase::new(env, schema) } -#[repr(C)] -struct RawPowerSyncDatabase { - db: *mut InnerPowerSyncState, // *const InnerPowerSyncState -} - -impl RawPowerSyncDatabase { - unsafe fn as_db(&self) -> PowerSyncDatabase { - unsafe { PowerSyncDatabase::interpret_raw(self.db) } - } -} - struct RawConnectionLease<'a> { lease: Box, } @@ -51,22 +41,21 @@ extern "C" fn powersync_db_in_memory( ) -> PowerSyncResultCode { ps_try!(PowerSyncEnvironment::powersync_auto_extension()); let conn = ps_try!(Connection::open_in_memory().map_err(PowerSyncError::from)); - let raw = create_db( + *out_db = create_db( schema.copy_to_rust(), ConnectionPool::single_connection(conn), ) - .into_raw(); + .into(); - *out_db = RawPowerSyncDatabase { db: raw as *mut _ }; PowerSyncResultCode::OK } #[unsafe(no_mangle)] extern "C" fn powersync_db_reader<'a>( - db: &'a InnerPowerSyncState, + db: &'a RawPowerSyncDatabase, out_lease: &mut ConnectionLeaseResult<'a>, ) -> PowerSyncResultCode { - let reader = ps_try!(future::block_on(db.reader())); + let reader = ps_try!(future::block_on(db.lease_reader())); out_lease.sqlite3 = unsafe { reader.deref().handle() }; out_lease.lease = Box::into_raw(Box::new(RawConnectionLease { @@ -77,10 +66,10 @@ extern "C" fn powersync_db_reader<'a>( #[unsafe(no_mangle)] extern "C" fn powersync_db_writer<'a>( - db: &'a InnerPowerSyncState, + db: &'a RawPowerSyncDatabase, out_lease: &mut ConnectionLeaseResult<'a>, ) -> PowerSyncResultCode { - let writer = ps_try!(future::block_on(db.writer())); + let writer = ps_try!(future::block_on(db.lease_writer())); out_lease.sqlite3 = unsafe { writer.deref().handle() }; out_lease.lease = Box::into_raw(Box::new(RawConnectionLease { @@ -97,9 +86,7 @@ extern "C" fn powersync_db_return_lease(lease: *mut RawConnectionLease) { #[unsafe(no_mangle)] extern "C" fn powersync_db_free(db: RawPowerSyncDatabase) { - unsafe { - PowerSyncDatabase::drop_raw(db.db.cast()); - } + unsafe { db.free() } } #[unsafe(no_mangle)] diff --git a/libpowersync/src/executor.rs b/libpowersync/src/executor.rs index cc81c63..a1861d4 100644 --- a/libpowersync/src/executor.rs +++ b/libpowersync/src/executor.rs @@ -1,19 +1,20 @@ use async_executor::Executor; -use futures_lite::future; use futures_lite::future::block_on; -use powersync::{InnerPowerSyncState, PowerSyncDatabase}; +use powersync::ffi::RawPowerSyncDatabase; /// Runs asynchronous PowerSync tasks on the current thread. /// /// This blocks the thread until the database is closed. #[unsafe(no_mangle)] -pub extern "C" fn powersync_run_tasks(db: *const InnerPowerSyncState) { - let db = unsafe { PowerSyncDatabase::interpret_raw(db) }; +pub extern "C" fn powersync_run_tasks(db: &RawPowerSyncDatabase) { + let tasks = unsafe { RawPowerSyncDatabase::clone_into_db(db) }.async_tasks(); let executor = Executor::new(); - let downloader = executor.spawn(db.download_actor()); - let uploader = executor.spawn(db.upload_actor()); + let tasks = tasks.spawn_with(|f| executor.spawn(f)); // The actors will run until the source database is closed, so we wait for that to happen. - let future = future::or(downloader, uploader); - block_on(executor.run(future)); + block_on(executor.run(async move { + for task in tasks { + task.await; + } + })); } diff --git a/libpowersync/src/powersync.cpp b/libpowersync/src/powersync.cpp index 5ced8b8..47ef7c4 100644 --- a/libpowersync/src/powersync.cpp +++ b/libpowersync/src/powersync.cpp @@ -96,13 +96,14 @@ namespace powersync { internal::RawPowerSyncDatabase db{}; handle_result(internal::powersync_db_in_memory(helper.to_rust(), &db)); - return Database(db.db); + return Database(db); } void Database::spawn_sync_thread() { - auto raw = this->rust_db; + auto raw = &this->raw; std::thread thread([raw]() { - internal::powersync_run_tasks(static_cast(raw)); + // TODO: There is a race condition here when the database's destructor runs before we reach this call. + internal::powersync_run_tasks(raw); }); this->worker = std::move(thread); @@ -111,19 +112,25 @@ namespace powersync { LeasedConnection Database::reader() const { internal::ConnectionLeaseResult result{}; - auto raw = static_cast(this->rust_db); - handle_result(internal::powersync_db_reader(raw, &result)); + handle_result(internal::powersync_db_reader(&this->raw, &result)); return {result.sqlite3, result.lease}; } LeasedConnection Database::writer() const { internal::ConnectionLeaseResult result{}; - auto raw = static_cast(this->rust_db); - handle_result(internal::powersync_db_writer(raw, &result)); + handle_result(internal::powersync_db_writer(&this->raw, &result)); return {result.sqlite3, result.lease}; } + Database::~Database() { + // First, clear the database client. + internal::powersync_db_free(this->raw); + + // Dropping the client will asynchronously complete sync actors, so join that. + this->worker->join(); + } + LeasedConnection::~LeasedConnection() { internal::powersync_db_return_lease(static_cast(this->raw_lease)); } diff --git a/powersync/src/db/mod.rs b/powersync/src/db/mod.rs index 89a69e5..16f96cc 100644 --- a/powersync/src/db/mod.rs +++ b/powersync/src/db/mod.rs @@ -27,8 +27,8 @@ pub mod watch; #[derive(Clone)] pub struct PowerSyncDatabase { - sync: Arc, - inner: Arc, + pub(crate) sync: Arc, + pub(crate) inner: Arc, } /// An opened database managed by the PowerSync SDK. @@ -162,39 +162,4 @@ impl PowerSyncDatabase { pub async fn writer(&self) -> Result { self.inner.writer().await } - - /* - /// Returns the shared [InnerPowerSyncState] backing this database. - /// - /// This is meant to be used internally to build the PowerSync C++ SDK. - #[cfg(feature = "ffi")] - pub fn into_raw(self) -> *const InnerPowerSyncState { - Arc::into_raw(self.inner) - } - - /// Creates a [Self] from the raw inner pointer. - /// - /// ## Safety - /// - /// The inner pointer must have been obtained from [Self::into_raw] without calling - /// [Self::drop_raw] on it in the meantime. - #[cfg(feature = "ffi")] - pub unsafe fn interpret_raw(inner: *const InnerPowerSyncState) -> Self { - unsafe { Arc::increment_strong_count(inner) }; - Self { - inner: unsafe { Arc::from_raw(inner) }, - } - } - - /// Frees resources from a [Self::into_raw] pointer. - /// - /// ## Safety - /// - /// This method must be called at most once on a pointer previously returned by - /// [Self::into:raw]. - #[cfg(feature = "ffi")] - pub unsafe fn drop_raw(inner: *const InnerPowerSyncState) { - drop(unsafe { Arc::from_raw(inner) }); - } - */ } diff --git a/powersync/src/ffi.rs b/powersync/src/ffi.rs new file mode 100644 index 0000000..7ac49d1 --- /dev/null +++ b/powersync/src/ffi.rs @@ -0,0 +1,72 @@ +use crate::db::internal::InnerPowerSyncState; +use crate::error::PowerSyncError; +use crate::sync::coordinator::SyncCoordinator; +use crate::{LeasedConnection, PowerSyncDatabase}; +use std::ffi::c_void; +use std::sync::Arc; + +/// A [PowerSyncDatabase] reference that can be passed to C. +#[repr(C)] +pub struct RawPowerSyncDatabase { + sync: *mut c_void, // SyncCoordinator + inner: *mut c_void, // InnerPowerSyncState +} + +impl RawPowerSyncDatabase { + /// ## Safety + /// + /// Must be valid pointers, and must at most be called once more than [Self::clone_into_db]. + pub unsafe fn consume_as_db(&self) -> PowerSyncDatabase { + PowerSyncDatabase { + sync: unsafe { Arc::from_raw(self.sync as *const _) }, + inner: unsafe { Arc::from_raw(self.inner as *mut _) }, + } + } + + pub fn clone_into_db(&self) -> PowerSyncDatabase { + unsafe { + Arc::increment_strong_count(self.sync as *const SyncCoordinator); + Arc::increment_strong_count(self.inner as *const InnerPowerSyncState); + + self.consume_as_db() + } + } + + pub async fn lease_reader<'a>(&'a self) -> Result { + let inner: &'a InnerPowerSyncState = unsafe { + // Safety: Valid reference by construction of struct + (self.inner as *const InnerPowerSyncState) + .as_ref() + .unwrap_unchecked() + }; + + inner.reader().await + } + + pub async fn lease_writer<'a>(&'a self) -> Result { + let inner: &'a InnerPowerSyncState = unsafe { + // Safety: Valid reference by construction of struct + (self.inner as *const InnerPowerSyncState) + .as_ref() + .unwrap_unchecked() + }; + + inner.writer().await + } + + /// ## Safety + /// + /// Must only be called once. + pub unsafe fn free(self) { + drop(unsafe { self.consume_as_db() }); + } +} + +impl From for RawPowerSyncDatabase { + fn from(value: PowerSyncDatabase) -> Self { + Self { + sync: Arc::into_raw(value.sync) as *mut c_void, + inner: Arc::into_raw(value.inner) as *mut c_void, + } + } +} diff --git a/powersync/src/lib.rs b/powersync/src/lib.rs index b8fa586..571db8c 100644 --- a/powersync/src/lib.rs +++ b/powersync/src/lib.rs @@ -1,11 +1,13 @@ mod db; pub mod env; +pub mod error; mod sync; mod util; -pub use db::PowerSyncDatabase; #[cfg(feature = "ffi")] -pub use db::internal::InnerPowerSyncState; +pub mod ffi; + +pub use db::PowerSyncDatabase; pub use db::crud::{CrudEntry, CrudTransaction, UpdateType}; pub use db::pool::{ConnectionPool, LeasedConnection}; pub use db::streams::StreamSubscription; @@ -15,7 +17,6 @@ pub use sync::connector::{BackendConnector, PowerSyncCredentials}; pub use sync::options::SyncOptions; pub use sync::status::SyncStatusData; pub use sync::stream_priority::StreamPriority; -pub mod error; pub mod schema { pub use super::db::schema::*; From 0de80c4023a72f0d93b369d590b8a05d2f0065c4 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 20 Oct 2025 17:55:41 +0200 Subject: [PATCH 05/14] Only join when thread is started --- libpowersync/src/powersync.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libpowersync/src/powersync.cpp b/libpowersync/src/powersync.cpp index 47ef7c4..81ce95d 100644 --- a/libpowersync/src/powersync.cpp +++ b/libpowersync/src/powersync.cpp @@ -128,7 +128,9 @@ namespace powersync { internal::powersync_db_free(this->raw); // Dropping the client will asynchronously complete sync actors, so join that. - this->worker->join(); + if (this->worker.has_value()) { + this->worker->join(); + } } LeasedConnection::~LeasedConnection() { From 48e2f9d41fceed4cd26c0d4024a95db0d7c5e8bc Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 21 Oct 2025 14:48:22 +0200 Subject: [PATCH 06/14] Connectors --- CMakeLists.txt | 6 +- Cargo.lock | 1 + examples/cpp_queries.cpp | 58 +++++++++++++- libpowersync/Cargo.toml | 1 + libpowersync/include/powersync.h | 63 +++++++++++++++ libpowersync/src/bindings.h | 25 ++++++ libpowersync/src/completion_handle.rs | 110 ++++++++++++++++++++++++++ libpowersync/src/connector.rs | 79 ++++++++++++++++++ libpowersync/src/database.rs | 12 +++ libpowersync/src/executor.rs | 2 +- libpowersync/src/lib.rs | 2 + libpowersync/src/powersync.cpp | 67 +++++++++++++++- powersync/src/error.rs | 34 ++++++-- powersync/src/ffi.rs | 50 ++++++++---- 14 files changed, 481 insertions(+), 29 deletions(-) create mode 100644 libpowersync/src/completion_handle.rs create mode 100644 libpowersync/src/connector.rs diff --git a/CMakeLists.txt b/CMakeLists.txt index 24e638c..91a55d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,10 @@ cmake_minimum_required(VERSION 4.0) project(powersync) find_package(CURL) +include(FetchContent) +FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz) +FetchContent_MakeAvailable(json) + add_custom_target( powersync_c_rust COMMAND cargo build -p libpowersync @@ -18,5 +22,5 @@ set_target_properties(powersync_c PROPERTIES add_executable(cpp_queries) target_sources(cpp_queries PRIVATE "examples/cpp_queries.cpp") -target_link_libraries(cpp_queries powersync_c) +target_link_libraries(cpp_queries PRIVATE powersync_c nlohmann_json::nlohmann_json) target_compile_features(cpp_queries PRIVATE cxx_std_17) diff --git a/Cargo.lock b/Cargo.lock index db4efc4..17f06c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2863,6 +2863,7 @@ name = "libpowersync" version = "0.1.0" dependencies = [ "async-executor", + "async-oneshot", "cbindgen", "cc", "futures-lite 2.6.1", diff --git a/examples/cpp_queries.cpp b/examples/cpp_queries.cpp index 791ee24..480f17e 100644 --- a/examples/cpp_queries.cpp +++ b/examples/cpp_queries.cpp @@ -1,5 +1,7 @@ #include +#include +#include #include "powersync.h" void check_rc(int rc) { @@ -8,6 +10,51 @@ void check_rc(int rc) { } } +class DemoConnector: public powersync::BackendConnector { +private: + std::shared_ptr database; + + static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) { + auto* response = static_cast(userp); + response->append(static_cast(contents), size * nmemb); + return size * nmemb; + } +public: + explicit DemoConnector(const std::shared_ptr& database): database(database) {} + + void fetch_token(powersync::CompletionHandle completion) override { + std::thread([completion = std::move(completion)]() mutable { + using json = nlohmann::json; + + const auto curl = curl_easy_init(); + std::string response; + + curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:6060/api/auth/token"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + if (auto res = curl_easy_perform(curl); res != CURLE_OK) { + completion.complete_error(res, "CURL request failed"); + return; + } + curl_easy_cleanup(curl); + json parsed_response = response; + + std::string token = parsed_response["token"]; + completion.complete_ok(powersync::PowerSyncCredentials { + .endpoint = "http://localhost:8080/", + .token = token, + }); + }).detach(); + } + + void upload_data(powersync::CompletionHandle completion) override { + completion.complete_ok(std::monostate()); + } + + ~DemoConnector() override = default; +}; + int main() { using namespace powersync; @@ -15,16 +62,19 @@ int main() { schema.tables.emplace_back(Table{"users", { Column::text("name") }}); - auto db = Database::in_memory(schema); - db.spawn_sync_thread(); + + auto db = std::make_shared(std::move(Database::in_memory(schema))); + db->spawn_sync_thread(); + auto connector = std::make_shared(db); + db->connect(connector); { - auto writer = db.writer(); + auto writer = db->writer(); check_rc(sqlite3_exec(writer, "INSERT INTO users (id, name) VALUES (uuid(), 'Simon');", nullptr, nullptr, nullptr)); } { - auto reader = db.reader(); + auto reader = db->reader(); sqlite3_stmt *stmt = nullptr; check_rc(sqlite3_prepare_v2(reader, "SELECT id, name FROM users", -1, &stmt, nullptr)); diff --git a/libpowersync/Cargo.toml b/libpowersync/Cargo.toml index df65df4..b060012 100644 --- a/libpowersync/Cargo.toml +++ b/libpowersync/Cargo.toml @@ -14,6 +14,7 @@ rusqlite = { version = "0.37.0", features = ["load_extension", "bundled"] } async-executor = "1.13.3" futures-lite = "2.6.1" log = "0.4.28" +async-oneshot = "0.5.9" [build-dependencies] cbindgen = "0.29.0" diff --git a/libpowersync/include/powersync.h b/libpowersync/include/powersync.h index 6d75d34..06c8ea7 100644 --- a/libpowersync/include/powersync.h +++ b/libpowersync/include/powersync.h @@ -13,6 +13,17 @@ namespace powersync { void* sync; void* inner; }; + + struct RawCompletionHandle { + void* rust_handle; + + void send_empty(); + void send_credentials(const char* endpoint, const char* token); + void send_error_code(int code); + void send_error_message(int code, const char* message); + + ~RawCompletionHandle(); + }; } enum ColumnType { @@ -68,6 +79,50 @@ struct LeasedConnection { operator sqlite3*() const; }; +template +struct CompletionHandle { +private: + internal::RawCompletionHandle handle; + +public: + explicit CompletionHandle(internal::RawCompletionHandle handle): handle(std::move(handle)) {} + + void complete_ok(T result); + + void complete_error(int code) { + this->handle.send_error_code(code); + } + + void complete_error(int code, const std::string& description) { + this->handle.send_error_message(code, description.c_str()); + } +}; + +template<> +inline void CompletionHandle::complete_ok(std::monostate _result) { + this->handle.send_empty(); +} + +struct PowerSyncCredentials { + const std::string& endpoint; + const std::string& token; +}; + +template<> +inline void CompletionHandle::complete_ok(PowerSyncCredentials credentials) { + this->handle.send_credentials(credentials.endpoint.c_str(), credentials.token.c_str()); +} + +/// Note that methods on this class may be called from multiple threads, or concurrently. Backend connectors must thus +/// be thread-safe. +class BackendConnector { +public: + virtual void fetch_token(CompletionHandle completion) {} + virtual void upload_data(CompletionHandle completion) {} + + virtual ~BackendConnector() = default; +}; + class Database { private: internal::RawPowerSyncDatabase raw; @@ -78,6 +133,14 @@ class Database { // Databases can't be copied, the internal::RawPowerSyncDatabase is an exclusive reference in Rust. Database(const Database&) = delete; public: + Database(Database&& other) noexcept: + raw(other.raw), + worker(std::move(other.worker)) { + other.raw.inner = nullptr; + other.raw.sync = nullptr; + } + + void connect(std::shared_ptr connector); void disconnect(); void spawn_sync_thread(); diff --git a/libpowersync/src/bindings.h b/libpowersync/src/bindings.h index dca126d..a1cb915 100644 --- a/libpowersync/src/bindings.h +++ b/libpowersync/src/bindings.h @@ -19,6 +19,8 @@ enum class PowerSyncResultCode { struct RawConnectionLease; +using CppCompletionHandle = void*; + struct Column { const char *name; ColumnType column_type; @@ -40,6 +42,12 @@ struct RawSchema { uintptr_t tables_len; }; +struct CppConnector { + void (*upload_data)(CppConnector*, CppCompletionHandle); + void (*fetch_credentials)(CppConnector*, CppCompletionHandle); + void (*drop)(CppConnector*); +}; + struct ConnectionLeaseResult { sqlite3 *sqlite3; RawConnectionLease *lease; @@ -47,8 +55,25 @@ struct ConnectionLeaseResult { extern "C" { +void powersync_completion_handle_complete_credentials(CppCompletionHandle *handle, + const char *endpoint, + const char *token); + +void powersync_completion_handle_complete_empty(CppCompletionHandle *handle); + +void powersync_completion_handle_complete_error_code(CppCompletionHandle *handle, int code); + +void powersync_completion_handle_complete_error_msg(CppCompletionHandle *handle, + int code, + const char *msg); + +void powersync_completion_handle_free(CppCompletionHandle *handle); + PowerSyncResultCode powersync_db_in_memory(RawSchema schema, RawPowerSyncDatabase *out_db); +PowerSyncResultCode powersync_db_connect(const RawPowerSyncDatabase *db, + const CppConnector *connector); + PowerSyncResultCode powersync_db_reader(const RawPowerSyncDatabase *db, ConnectionLeaseResult *out_lease); diff --git a/libpowersync/src/completion_handle.rs b/libpowersync/src/completion_handle.rs new file mode 100644 index 0000000..37d63a8 --- /dev/null +++ b/libpowersync/src/completion_handle.rs @@ -0,0 +1,110 @@ +use async_oneshot::{Sender, oneshot}; +use powersync::PowerSyncCredentials; +use powersync::error::PowerSyncError; +use std::ffi::{CStr, c_char, c_int, c_void}; +use std::ptr::null_mut; + +#[repr(transparent)] +pub struct CppCompletionHandle { + sender: *mut c_void, // Sender +} + +impl CppCompletionHandle { + pub fn take_sender(&mut self) -> Option>> { + if self.sender.is_null() { + None + } else { + let prev_sender = self.sender; + self.sender = null_mut(); + Some(unsafe { Box::from_raw(prev_sender as *mut _) }) + } + } +} + +pub type CompletionHandleResult = Result; + +pub enum CompletionHandleValue { + Credentials(PowerSyncCredentials), + Empty, +} + +pub struct RustCompletionHandle { + receiver: async_oneshot::Receiver, +} + +impl RustCompletionHandle { + pub fn new() -> (CppCompletionHandle, RustCompletionHandle) { + let (sender, receiver) = oneshot(); + + let sender = CppCompletionHandle { + sender: Box::into_raw(Box::new(sender)) as *mut _, + }; + let receiver = RustCompletionHandle { receiver }; + (sender, receiver) + } + + pub async fn receive(self) -> CompletionHandleResult { + self.receiver.await.unwrap_or_else(|_| { + Err(PowerSyncError::argument_error( + "Dropped completion handle without completing it.", + )) + }) + } +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_completion_handle_complete_credentials( + handle: &mut CppCompletionHandle, + endpoint: *const c_char, + token: *const c_char, +) { + let endpoint = unsafe { CStr::from_ptr(endpoint) } + .to_str() + .unwrap() + .to_owned(); + let token = unsafe { CStr::from_ptr(token) } + .to_str() + .unwrap() + .to_owned(); + + if let Some(mut sender) = handle.take_sender() { + let _ = sender.send(Ok(CompletionHandleValue::Credentials( + PowerSyncCredentials { endpoint, token }, + ))); + } +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_completion_handle_complete_empty(handle: &mut CppCompletionHandle) { + if let Some(mut sender) = handle.take_sender() { + let _ = sender.send(Ok(CompletionHandleValue::Empty)); + } +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_completion_handle_complete_error_code( + handle: &mut CppCompletionHandle, + code: c_int, +) { + if let Some(mut sender) = handle.take_sender() { + let _ = sender.send(Err(PowerSyncError::user_error(code))); + } +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_completion_handle_complete_error_msg( + handle: &mut CppCompletionHandle, + code: c_int, + msg: *const c_char, +) { + let msg = unsafe { CStr::from_ptr(msg) }.to_str().unwrap().to_owned(); + + if let Some(mut sender) = handle.take_sender() { + let _ = sender.send(Err(PowerSyncError::user_error_with_message(code, msg))); + } +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_completion_handle_free(handle: &mut CppCompletionHandle) { + drop(handle.take_sender()); +} diff --git a/libpowersync/src/connector.rs b/libpowersync/src/connector.rs new file mode 100644 index 0000000..427c6cd --- /dev/null +++ b/libpowersync/src/connector.rs @@ -0,0 +1,79 @@ +use crate::completion_handle::{CompletionHandleValue, CppCompletionHandle, RustCompletionHandle}; +use http_client::async_trait; +use powersync::error::PowerSyncError; +use powersync::{BackendConnector, PowerSyncCredentials}; + +#[repr(C)] +pub struct CppConnector { + upload_data: unsafe extern "C" fn(*mut CppConnector, CppCompletionHandle), + fetch_credentials: unsafe extern "C" fn(*mut CppConnector, CppCompletionHandle), + drop: unsafe extern "C" fn(*mut CppConnector), +} + +struct CppConnectorWrapper(*const CppConnector); + +unsafe impl Send for CppConnectorWrapper { + // SAFETY: Requirement in C++, connectors need to be thread-safe. +} +unsafe impl Sync for CppConnectorWrapper { + // SAFETY: Requirement in C++, connectors need to be thread-safe. +} + +impl AsRef for CppConnectorWrapper { + fn as_ref(&self) -> &CppConnector { + unsafe { &*self.0 } + } +} + +/// ## Safety +/// +/// The connector pointer must point to a valid connector implementation. +pub unsafe fn wrap_cpp_connector( + connector: *const CppConnector, +) -> impl BackendConnector + 'static { + CppConnectorWrapper(connector) +} + +#[async_trait] +impl BackendConnector for CppConnectorWrapper { + async fn fetch_credentials(&self) -> Result { + let (send, recv) = RustCompletionHandle::new(); + let connector: &CppConnector = self.as_ref(); + let handler = connector.fetch_credentials; + unsafe { handler(self.0.cast_mut(), send) }; + + let credentials = recv.receive().await?; + let CompletionHandleValue::Credentials(credentials) = credentials else { + return Err(PowerSyncError::argument_error( + "Expected completion with request.", + )); + }; + Ok(credentials) + } + + async fn upload_data(&self) -> Result<(), PowerSyncError> { + let (send, recv) = RustCompletionHandle::new(); + let connector: &CppConnector = self.as_ref(); + let handler = connector.upload_data; + unsafe { handler(self.0.cast_mut(), send) }; + + let value = recv.receive().await?; + match value { + CompletionHandleValue::Empty => Ok(()), + _ => Err(PowerSyncError::argument_error( + "Expected completion with empty value.", + )), + } + } +} + +impl Drop for CppConnectorWrapper { + fn drop(&mut self) { + let connector: &CppConnector = self.as_ref(); + let handler = connector.drop; + + unsafe { + handler(self.0.cast_mut()); + } + } +} diff --git a/libpowersync/src/database.rs b/libpowersync/src/database.rs index a0f1c38..dca6a1b 100644 --- a/libpowersync/src/database.rs +++ b/libpowersync/src/database.rs @@ -1,3 +1,4 @@ +use crate::connector::{CppConnector, wrap_cpp_connector}; use crate::error::{LAST_ERROR, PowerSyncResultCode}; use crate::schema::RawSchema; use futures_lite::future; @@ -50,6 +51,17 @@ extern "C" fn powersync_db_in_memory( PowerSyncResultCode::OK } +#[unsafe(no_mangle)] +extern "C" fn powersync_db_connect( + db: &RawPowerSyncDatabase, + connector: *const CppConnector, +) -> PowerSyncResultCode { + ps_try!(future::block_on( + db.connect(unsafe { wrap_cpp_connector(connector) }) + )); + PowerSyncResultCode::OK +} + #[unsafe(no_mangle)] extern "C" fn powersync_db_reader<'a>( db: &'a RawPowerSyncDatabase, diff --git a/libpowersync/src/executor.rs b/libpowersync/src/executor.rs index a1861d4..5026010 100644 --- a/libpowersync/src/executor.rs +++ b/libpowersync/src/executor.rs @@ -7,7 +7,7 @@ use powersync::ffi::RawPowerSyncDatabase; /// This blocks the thread until the database is closed. #[unsafe(no_mangle)] pub extern "C" fn powersync_run_tasks(db: &RawPowerSyncDatabase) { - let tasks = unsafe { RawPowerSyncDatabase::clone_into_db(db) }.async_tasks(); + let tasks = RawPowerSyncDatabase::clone_into_db(db).async_tasks(); let executor = Executor::new(); let tasks = tasks.spawn_with(|f| executor.spawn(f)); diff --git a/libpowersync/src/lib.rs b/libpowersync/src/lib.rs index dd2554b..58e3e03 100644 --- a/libpowersync/src/lib.rs +++ b/libpowersync/src/lib.rs @@ -7,6 +7,8 @@ macro_rules! ps_try { }; } +mod completion_handle; +mod connector; mod database; mod error; mod executor; diff --git a/libpowersync/src/powersync.cpp b/libpowersync/src/powersync.cpp index 81ce95d..080a93e 100644 --- a/libpowersync/src/powersync.cpp +++ b/libpowersync/src/powersync.cpp @@ -16,6 +16,27 @@ namespace powersync { } } + void internal::RawCompletionHandle::send_credentials(const char* endpoint, const char* token) { + powersync_completion_handle_complete_credentials(&this->rust_handle, endpoint, token); + } + + void internal::RawCompletionHandle::send_empty() { + powersync_completion_handle_complete_empty(&this->rust_handle); + } + + void internal::RawCompletionHandle::send_error_code(int code) { + powersync_completion_handle_complete_error_code(&this->rust_handle, code); + } + + void internal::RawCompletionHandle::send_error_message(int code, const char* message) { + powersync_completion_handle_complete_error_msg(&this->rust_handle, code, message); + } + + internal::RawCompletionHandle::~RawCompletionHandle() { + powersync_completion_handle_free(&this->rust_handle); + } + + const char * Exception::what() const noexcept { if (this->msg) { return this->msg; @@ -99,6 +120,45 @@ namespace powersync { return Database(db); } + void Database::connect(std::shared_ptr connector) { + struct RawConnector: internal::CppConnector { + std::shared_ptr connector; + + RawConnector(std::shared_ptr connector) : CppConnector(), connector(std::move(connector)) { + this->upload_data = &upload_data_impl; + this->fetch_credentials = &fetch_credentials_impl; + this->drop = &drop_impl; + } + + static void upload_data_impl(CppConnector* connector, internal::CppCompletionHandle handle) { + auto raw = static_cast(connector); + CompletionHandle wrapped(internal::RawCompletionHandle { + .rust_handle = handle + }); + + raw->connector->upload_data(wrapped); + } + + static void fetch_credentials_impl(CppConnector* connector, internal::CppCompletionHandle handle) { + auto raw = static_cast(connector); + CompletionHandle wrapped(internal::RawCompletionHandle { + .rust_handle = handle + }); + + raw->connector->fetch_token(wrapped); + } + + static void drop_impl(CppConnector* connector) { + auto raw = static_cast(connector); + delete raw; + } + }; + + const auto raw_connector = new RawConnector(connector); + const auto rc = internal::powersync_db_connect(&raw, raw_connector); + handle_result(rc); + } + void Database::spawn_sync_thread() { auto raw = &this->raw; std::thread thread([raw]() { @@ -124,8 +184,10 @@ namespace powersync { } Database::~Database() { - // First, clear the database client. - internal::powersync_db_free(this->raw); + if (this->raw.inner) { + // First, clear the database client (unless resources have been moved out of this class). + internal::powersync_db_free(this->raw); + } // Dropping the client will asynchronously complete sync actors, so join that. if (this->worker.has_value()) { @@ -141,4 +203,3 @@ namespace powersync { return this->db; } } - diff --git a/powersync/src/error.rs b/powersync/src/error.rs index ca91127..cd2ce7e 100644 --- a/powersync/src/error.rs +++ b/powersync/src/error.rs @@ -1,11 +1,11 @@ +use http_client::http_types::StatusCode; +use rusqlite::Error as SqliteError; +use rusqlite::types::FromSqlError; use std::error::Error; +use std::ffi::c_int; use std::io; use std::sync::Arc; use std::{borrow::Cow, fmt::Display}; - -use http_client::http_types::StatusCode; -use rusqlite::Error as SqliteError; -use rusqlite::types::FromSqlError; use thiserror::Error; /// A [RawPowerSyncError], but boxed. @@ -18,9 +18,27 @@ pub struct PowerSyncError { } impl PowerSyncError { - pub(crate) fn argument_error(desc: impl Into>) -> Self { + pub fn argument_error(desc: impl Into>) -> Self { RawPowerSyncError::ArgumentError { desc: desc.into() }.into() } + + #[cfg(feature = "ffi")] + pub fn user_error(code: c_int) -> Self { + RawPowerSyncError::CppUserError { + code, + message: None, + } + .into() + } + + #[cfg(feature = "ffi")] + pub fn user_error_with_message(code: c_int, msg: String) -> Self { + RawPowerSyncError::CppUserError { + code, + message: Some(msg), + } + .into() + } } impl From for PowerSyncError { @@ -98,4 +116,10 @@ pub(crate) enum RawPowerSyncError { InvalidCredentials, #[error("Unexpected HTTP status code from PowerSync service: {code}")] UnexpectedStatusCode { code: StatusCode }, + #[cfg(feature = "ffi")] + #[error("Error returned in C++ callback: code {code}, message: {message:?}")] + CppUserError { + code: c_int, + message: Option, + }, } diff --git a/powersync/src/ffi.rs b/powersync/src/ffi.rs index 7ac49d1..96fa820 100644 --- a/powersync/src/ffi.rs +++ b/powersync/src/ffi.rs @@ -1,7 +1,7 @@ use crate::db::internal::InnerPowerSyncState; use crate::error::PowerSyncError; use crate::sync::coordinator::SyncCoordinator; -use crate::{LeasedConnection, PowerSyncDatabase}; +use crate::{BackendConnector, LeasedConnection, PowerSyncDatabase, SyncOptions}; use std::ffi::c_void; use std::sync::Arc; @@ -12,6 +12,11 @@ pub struct RawPowerSyncDatabase { inner: *mut c_void, // InnerPowerSyncState } +struct RawPowerSyncReference<'a> { + sync: &'a SyncCoordinator, + inner: &'a InnerPowerSyncState, +} + impl RawPowerSyncDatabase { /// ## Safety /// @@ -32,28 +37,43 @@ impl RawPowerSyncDatabase { } } - pub async fn lease_reader<'a>(&'a self) -> Result { - let inner: &'a InnerPowerSyncState = unsafe { - // Safety: Valid reference by construction of struct - (self.inner as *const InnerPowerSyncState) - .as_ref() - .unwrap_unchecked() - }; + fn as_ref(&self) -> RawPowerSyncReference { + RawPowerSyncReference { + sync: unsafe { + // Safety: Valid reference by construction of struct + (self.sync as *const SyncCoordinator) + .as_ref() + .unwrap_unchecked() + }, + inner: unsafe { + // Safety: Valid reference by construction of struct + (self.inner as *const InnerPowerSyncState) + .as_ref() + .unwrap_unchecked() + }, + } + } + pub async fn lease_reader<'a>(&'a self) -> Result { + let RawPowerSyncReference { inner, .. } = self.as_ref(); inner.reader().await } pub async fn lease_writer<'a>(&'a self) -> Result { - let inner: &'a InnerPowerSyncState = unsafe { - // Safety: Valid reference by construction of struct - (self.inner as *const InnerPowerSyncState) - .as_ref() - .unwrap_unchecked() - }; - + let RawPowerSyncReference { inner, .. } = self.as_ref(); inner.writer().await } + pub async fn connect( + &self, + connector: impl BackendConnector + 'static, + ) -> Result<(), PowerSyncError> { + let RawPowerSyncReference { sync, inner } = self.as_ref(); + sync.connect(SyncOptions::new(connector), inner).await; + + Ok(()) + } + /// ## Safety /// /// Must only be called once. From 689fd6424873b60749a87d8de35e7642263224a7 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 21 Oct 2025 15:23:12 +0200 Subject: [PATCH 07/14] Logging --- examples/cpp_queries.cpp | 3 ++ libpowersync/Cargo.toml | 2 +- libpowersync/build.rs | 1 + libpowersync/include/powersync.h | 14 +++++- libpowersync/src/bindings.h | 15 +++++++ libpowersync/src/lib.rs | 1 + libpowersync/src/logger.rs | 76 ++++++++++++++++++++++++++++++++ libpowersync/src/powersync.cpp | 11 +++++ 8 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 libpowersync/src/logger.rs diff --git a/examples/cpp_queries.cpp b/examples/cpp_queries.cpp index 480f17e..664d026 100644 --- a/examples/cpp_queries.cpp +++ b/examples/cpp_queries.cpp @@ -57,6 +57,9 @@ class DemoConnector: public powersync::BackendConnector { int main() { using namespace powersync; + set_logger(LogLevel::Trace, [](LogLevel _, const char* message) { + std::cout << message << std::endl; + }); Schema schema{}; schema.tables.emplace_back(Table{"users", { diff --git a/libpowersync/Cargo.toml b/libpowersync/Cargo.toml index b060012..1e3d0f0 100644 --- a/libpowersync/Cargo.toml +++ b/libpowersync/Cargo.toml @@ -13,7 +13,7 @@ http-client = { version = "6.5.3", features = ["curl_client"], default-features rusqlite = { version = "0.37.0", features = ["load_extension", "bundled"] } async-executor = "1.13.3" futures-lite = "2.6.1" -log = "0.4.28" +log = { version = "0.4.28", features = ["std"] } async-oneshot = "0.5.9" [build-dependencies] diff --git a/libpowersync/build.rs b/libpowersync/build.rs index a22b65a..f594a28 100644 --- a/libpowersync/build.rs +++ b/libpowersync/build.rs @@ -10,6 +10,7 @@ fn main() { .expect("Unable to generate bindings") .write_to_file("src/bindings.h"); + return; cc::Build::new() .cpp(true) .std("c++17") diff --git a/libpowersync/include/powersync.h b/libpowersync/include/powersync.h index 06c8ea7..1efb4d6 100644 --- a/libpowersync/include/powersync.h +++ b/libpowersync/include/powersync.h @@ -26,6 +26,16 @@ namespace powersync { }; } + enum class LogLevel { + Error = 0, + Warn = 1, + Info = 2, + Debug = 3, + Trace = 4, + }; + + void set_logger(LogLevel level, void(*logger)(LogLevel, const char*)); + enum ColumnType { TEXT, INTEGER, @@ -155,9 +165,9 @@ class Database { class Exception final : public std::exception { private: const int rc; - char* msg; + const char* msg; public: - explicit Exception(int rc, char* msg) : rc(rc), msg(msg) {} + explicit Exception(int rc, const char* msg) : rc(rc), msg(msg) {} [[nodiscard]] const char *what() const noexcept override; ~Exception() noexcept override; diff --git a/libpowersync/src/bindings.h b/libpowersync/src/bindings.h index a1cb915..a1d672e 100644 --- a/libpowersync/src/bindings.h +++ b/libpowersync/src/bindings.h @@ -12,6 +12,14 @@ enum class ColumnType { Real = 2, }; +enum class LogLevel { + Error = 0, + Warn = 1, + Info = 2, + Debug = 3, + Trace = 4, +}; + enum class PowerSyncResultCode { OK = 0, ERROR = 1, @@ -53,6 +61,11 @@ struct ConnectionLeaseResult { RawConnectionLease *lease; }; +struct CppLogger { + LogLevel level; + void (*native_log)(LogLevel level, const char *line); +}; + extern "C" { void powersync_completion_handle_complete_credentials(CppCompletionHandle *handle, @@ -93,6 +106,8 @@ void powersync_free_str(char *str); /// This blocks the thread until the database is closed. void powersync_run_tasks(const RawPowerSyncDatabase *db); +int powersync_install_logger(CppLogger logger); + } // extern "C" } // namespace powersync::internal diff --git a/libpowersync/src/lib.rs b/libpowersync/src/lib.rs index 58e3e03..bfe70f5 100644 --- a/libpowersync/src/lib.rs +++ b/libpowersync/src/lib.rs @@ -12,4 +12,5 @@ mod connector; mod database; mod error; mod executor; +mod logger; mod schema; diff --git a/libpowersync/src/logger.rs b/libpowersync/src/logger.rs new file mode 100644 index 0000000..4d6a7e0 --- /dev/null +++ b/libpowersync/src/logger.rs @@ -0,0 +1,76 @@ +use log::{Level, LevelFilter, Metadata, Record}; +use std::ffi::{CString, c_char, c_int}; + +#[unsafe(no_mangle)] +pub extern "C" fn powersync_install_logger(logger: CppLogger) -> c_int { + let level = logger.level.into(); + match log::set_boxed_logger(Box::new(logger)).map(|()| log::set_max_level(level)) { + Ok(_) => 0, + Err(e) => 1, + } +} + +#[derive(PartialOrd, Ord, Eq, PartialEq, Copy, Clone)] +#[repr(C)] +pub enum LogLevel { + Error = 0, + Warn = 1, + Info = 2, + Debug = 3, + Trace = 4, +} + +impl From for LogLevel { + fn from(value: Level) -> Self { + match value { + Level::Error => Self::Error, + Level::Warn => Self::Warn, + Level::Info => Self::Info, + Level::Debug => Self::Debug, + Level::Trace => Self::Trace, + } + } +} + +impl From for LevelFilter { + fn from(value: LogLevel) -> Self { + match value { + LogLevel::Error => Self::Error, + LogLevel::Warn => Self::Warn, + LogLevel::Info => Self::Info, + LogLevel::Debug => Self::Debug, + LogLevel::Trace => Self::Trace, + } + } +} + +#[repr(C)] +pub struct CppLogger { + level: LogLevel, + native_log: extern "C" fn(level: LogLevel, line: *const c_char), +} + +impl log::Log for CppLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + self.level >= metadata.level().into() + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + let line = format!("{}: {}", record.target(), record.args()); + + if let Ok(line) = CString::new(line) { + (self.native_log)(record.level().into(), line.as_ptr()) + } else { + // This would be an error if the log line contained null bytes. We can't really log + // them in that case. + (self.native_log)( + LogLevel::Error, + c"Tried logging a line with a null char.".as_ptr(), + ); + } + } + } + + fn flush(&self) {} +} diff --git a/libpowersync/src/powersync.cpp b/libpowersync/src/powersync.cpp index 080a93e..4299fc4 100644 --- a/libpowersync/src/powersync.cpp +++ b/libpowersync/src/powersync.cpp @@ -9,6 +9,17 @@ namespace powersync::internal { #include "helpers.h" namespace powersync { + void set_logger(LogLevel level, void(*logger)(LogLevel, const char*)) { + auto wrapper = internal::CppLogger { + .level = static_cast(level), + .native_log = reinterpret_cast(logger), + }; + + if (auto rc = internal::powersync_install_logger(wrapper)) { + throw Exception(rc, "Could not set logger"); + } + } + static void handle_result(internal::PowerSyncResultCode rc) { if (rc != internal::PowerSyncResultCode::OK) { auto msg = internal::powersync_last_error_desc(); From 8bb7e1e5ef57106d95ae9c763e5bde59da9fce83 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 21 Oct 2025 18:07:52 +0200 Subject: [PATCH 08/14] Port crud transactions stream to C++ --- examples/cpp_queries.cpp | 29 +++++- libpowersync/build.rs | 1 - libpowersync/include/powersync.h | 61 +++++++++++- libpowersync/src/bindings.h | 43 ++++++++- libpowersync/src/crud.rs | 155 +++++++++++++++++++++++++++++++ libpowersync/src/database.rs | 4 +- libpowersync/src/lib.rs | 2 + libpowersync/src/powersync.cpp | 67 +++++++++++++ libpowersync/src/status.rs | 14 +++ powersync/src/db/crud.rs | 48 +++++++--- powersync/src/db/mod.rs | 2 +- powersync/src/error.rs | 1 + powersync/src/ffi.rs | 31 ++++++- powersync/src/util/mod.rs | 35 +++++++ 14 files changed, 470 insertions(+), 23 deletions(-) create mode 100644 libpowersync/src/crud.rs create mode 100644 libpowersync/src/status.rs diff --git a/examples/cpp_queries.cpp b/examples/cpp_queries.cpp index 664d026..3c148ac 100644 --- a/examples/cpp_queries.cpp +++ b/examples/cpp_queries.cpp @@ -57,7 +57,7 @@ class DemoConnector: public powersync::BackendConnector { int main() { using namespace powersync; - set_logger(LogLevel::Trace, [](LogLevel _, const char* message) { + set_logger(LogLevel::Info, [](LogLevel _, const char* message) { std::cout << message << std::endl; }); @@ -68,14 +68,37 @@ int main() { auto db = std::make_shared(std::move(Database::in_memory(schema))); db->spawn_sync_thread(); - auto connector = std::make_shared(db); - db->connect(connector); + //auto connector = std::make_shared(db); + //db->connect(connector); { auto writer = db->writer(); check_rc(sqlite3_exec(writer, "INSERT INTO users (id, name) VALUES (uuid(), 'Simon');", nullptr, nullptr, nullptr)); } + { + auto stream = db->get_crud_transactions(); + while (stream.advance()) { + auto tx = stream.current(); + std::cout << "Has transaction, id " << *tx.id << std::endl; + for (const auto& item : tx.crud) { + std::cout << "Has item: " << item.table << ": " << item.id << std::endl; + } + + tx.complete(); + } + + std::cout << "Done with first transactions iteration" << std::endl; + } + + // Should have no further transactions now, we've completed them. + { + auto stream = db->get_crud_transactions(); + auto has_tx = stream.advance(); + + std::cout << "Has transaction: " << has_tx << std::endl; + } + { auto reader = db->reader(); sqlite3_stmt *stmt = nullptr; diff --git a/libpowersync/build.rs b/libpowersync/build.rs index f594a28..a22b65a 100644 --- a/libpowersync/build.rs +++ b/libpowersync/build.rs @@ -10,7 +10,6 @@ fn main() { .expect("Unable to generate bindings") .write_to_file("src/bindings.h"); - return; cc::Build::new() .cpp(true) .std("c++17") diff --git a/libpowersync/include/powersync.h b/libpowersync/include/powersync.h index 1efb4d6..d9e5b72 100644 --- a/libpowersync/include/powersync.h +++ b/libpowersync/include/powersync.h @@ -26,6 +26,36 @@ namespace powersync { }; } + enum class UpdateType { + PUT = 1, + PATCH = 2, + DELETE = 3, + }; + + struct CrudEntry { + int64_t client_id; + int64_t transaction_id; + UpdateType update_type; + std::string table; + std::string id; + std::optional metadata; + std::optional data; + std::optional previous_values; + }; + + class Database; + + class CrudTransaction { + public: + const Database& db; + int64_t last_item_id; + std::optional id; + std::vector crud; + + void complete() const; + void complete(std::optional custom_write_checkpoint) const; + }; + enum class LogLevel { Error = 0, Warn = 1, @@ -133,8 +163,30 @@ class BackendConnector { virtual ~BackendConnector() = default; }; +class CrudTransactions { + const Database& db; + void* rust_iterator; + + CrudTransactions(const Database& db, void* rust_iterator): db(db), rust_iterator(rust_iterator) {} + + // The rust iterator can't be copied. + CrudTransactions(const CrudTransactions&) = delete; + + friend class Database; +public: + CrudTransactions(CrudTransactions&& other) noexcept: + db(other.db), + rust_iterator(other.rust_iterator) { + other.rust_iterator = nullptr; + } + + bool advance(); + CrudTransaction current() const; + + ~CrudTransactions(); +}; + class Database { -private: internal::RawPowerSyncDatabase raw; std::optional worker; @@ -142,6 +194,8 @@ class Database { // Databases can't be copied, the internal::RawPowerSyncDatabase is an exclusive reference in Rust. Database(const Database&) = delete; + + friend class CrudTransaction; public: Database(Database&& other) noexcept: raw(other.raw), @@ -154,6 +208,11 @@ class Database { void disconnect(); void spawn_sync_thread(); + /// Returns an iterator of completed crud transactions made against this database. + /// + /// This database must outlive the returned transactions stream. + CrudTransactions get_crud_transactions() const; + ~Database(); [[nodiscard]] LeasedConnection reader() const; diff --git a/libpowersync/src/bindings.h b/libpowersync/src/bindings.h index a1d672e..1c09c15 100644 --- a/libpowersync/src/bindings.h +++ b/libpowersync/src/bindings.h @@ -29,6 +29,32 @@ struct RawConnectionLease; using CppCompletionHandle = void*; +struct RawCrudTransaction { + int64_t id; + int64_t last_item_id; + bool has_id; + intptr_t crud_length; +}; + +struct StringView { + const char *value; + intptr_t length; +}; + +struct RawCrudEntry { + int64_t client_id; + int64_t transaction_id; + int32_t update_type; + StringView table; + StringView id; + StringView metadata; + bool has_metadata; + StringView data; + bool has_data; + StringView previous_values; + bool has_previous_values; +}; + struct Column { const char *name; ColumnType column_type; @@ -82,6 +108,21 @@ void powersync_completion_handle_complete_error_msg(CppCompletionHandle *handle, void powersync_completion_handle_free(CppCompletionHandle *handle); +void *powersync_crud_transactions_new(const RawPowerSyncDatabase *db); + +PowerSyncResultCode powersync_crud_transactions_step(void *stream, bool *has_next); + +RawCrudTransaction powersync_crud_transactions_current(const void *stream); + +RawCrudEntry powersync_crud_transactions_current_crud_item(const void *stream, intptr_t index); + +PowerSyncResultCode powersync_crud_complete(const RawPowerSyncDatabase *db, + int64_t last_item_id, + bool has_checkpoint, + int64_t checkpoint); + +void powersync_crud_transactions_free(void *stream); + PowerSyncResultCode powersync_db_in_memory(RawSchema schema, RawPowerSyncDatabase *out_db); PowerSyncResultCode powersync_db_connect(const RawPowerSyncDatabase *db, @@ -99,7 +140,7 @@ void powersync_db_free(RawPowerSyncDatabase db); char *powersync_last_error_desc(); -void powersync_free_str(char *str); +void powersync_free_str(const char *str); /// Runs asynchronous PowerSync tasks on the current thread. /// diff --git a/libpowersync/src/crud.rs b/libpowersync/src/crud.rs new file mode 100644 index 0000000..4209934 --- /dev/null +++ b/libpowersync/src/crud.rs @@ -0,0 +1,155 @@ +use crate::error::PowerSyncResultCode; +use futures_lite::{Stream, StreamExt}; +use powersync::error::PowerSyncError; +use powersync::ffi::RawPowerSyncDatabase; +use powersync::{CrudTransaction, UpdateType}; +use std::ffi::{c_char, c_void}; +use std::pin::Pin; +use std::ptr::null_mut; + +#[repr(C)] +pub struct RawCrudEntry { + pub client_id: i64, + pub transaction_id: i64, + pub update_type: i32, + pub table: StringView, + pub id: StringView, + pub metadata: StringView, + pub has_metadata: bool, + pub data: StringView, + pub has_data: bool, + pub previous_values: StringView, + pub has_previous_values: bool, +} + +#[repr(C)] +pub struct StringView { + pub value: *const c_char, + pub length: isize, +} + +impl StringView { + pub fn view(source: &str) -> Self { + Self { + value: source.as_ptr().cast(), + length: source.len() as isize, + } + } + + pub fn view_optional(source: Option<&str>) -> Self { + match source { + Some(str) => Self::view(str), + None => Self { + value: null_mut(), + length: 0, + }, + } + } +} + +#[repr(C)] +pub struct RawCrudTransaction { + pub id: i64, + pub last_item_id: i64, + pub has_id: bool, + pub crud_length: isize, +} + +struct RawTransactionStream<'a> { + stream: Pin, PowerSyncError>> + Send + 'a>>, + current: Option>, +} + +#[unsafe(no_mangle)] +pub extern "C" fn powersync_crud_transactions_new(db: &RawPowerSyncDatabase) -> *mut c_void { + let stream = RawTransactionStream { + stream: db.crud_transactions().boxed(), + current: None, + }; + + Box::into_raw(Box::new(stream)) as *mut c_void +} + +#[unsafe(no_mangle)] +pub extern "C" fn powersync_crud_transactions_step( + stream: *mut c_void, + has_next: &mut bool, +) -> PowerSyncResultCode { + let stream = unsafe { &mut *(stream as *mut RawTransactionStream) }; + let result = ps_try!(futures_lite::future::block_on(stream.stream.try_next())); + + match result { + None => *has_next = false, + Some(result) => { + *has_next = true; + stream.current = Some(result); + } + }; + PowerSyncResultCode::OK +} + +#[unsafe(no_mangle)] +pub extern "C" fn powersync_crud_transactions_current(stream: *const c_void) -> RawCrudTransaction { + let stream = unsafe { &*(stream as *const RawTransactionStream) }; + let item = stream.current.as_ref().unwrap(); + + RawCrudTransaction { + id: item.id.unwrap_or_default(), + last_item_id: item.last_item_id, + has_id: item.id.is_some(), + crud_length: item.crud.len() as isize, + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn powersync_crud_transactions_current_crud_item( + stream: *const c_void, + index: isize, +) -> RawCrudEntry { + let stream = unsafe { &*(stream as *const RawTransactionStream) }; + let item = stream.current.as_ref().unwrap(); + let item = &item.crud[index as usize]; + + RawCrudEntry { + client_id: item.client_id, + transaction_id: item.transaction_id, + update_type: match item.update_type { + // Must match enum class UpdateType from include/powersync.h + UpdateType::Put => 1, + UpdateType::Patch => 2, + UpdateType::Delete => 3, + }, + table: StringView::view(&item.table), + id: StringView::view(&item.id), + metadata: StringView::view_optional(item.metadata.as_deref()), + has_metadata: item.metadata.is_some(), + data: StringView::view_optional(item.raw_data.as_deref()), + has_data: item.data.is_some(), + previous_values: StringView::view_optional(item.raw_previous_values.as_deref()), + has_previous_values: item.previous_values.is_some(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn powersync_crud_complete( + db: &RawPowerSyncDatabase, + last_item_id: i64, + has_checkpoint: bool, + checkpoint: i64, +) -> PowerSyncResultCode { + let future = db.complete_crud_items( + last_item_id, + if has_checkpoint { + Some(checkpoint) + } else { + None + }, + ); + ps_try!(futures_lite::future::block_on(future)); + PowerSyncResultCode::OK +} + +#[unsafe(no_mangle)] +pub extern "C" fn powersync_crud_transactions_free(stream: *mut c_void) { + drop(unsafe { Box::from_raw(stream as *mut RawTransactionStream) }) +} diff --git a/libpowersync/src/database.rs b/libpowersync/src/database.rs index dca6a1b..8654348 100644 --- a/libpowersync/src/database.rs +++ b/libpowersync/src/database.rs @@ -112,6 +112,6 @@ extern "C" fn powersync_last_error_desc() -> *mut c_char { } #[unsafe(no_mangle)] -extern "C" fn powersync_free_str(str: *mut c_char) { - drop(unsafe { CString::from_raw(str) }); +extern "C" fn powersync_free_str(str: *const c_char) { + drop(unsafe { CString::from_raw(str.cast_mut()) }); } diff --git a/libpowersync/src/lib.rs b/libpowersync/src/lib.rs index bfe70f5..aeda96e 100644 --- a/libpowersync/src/lib.rs +++ b/libpowersync/src/lib.rs @@ -9,8 +9,10 @@ macro_rules! ps_try { mod completion_handle; mod connector; +mod crud; mod database; mod error; mod executor; mod logger; mod schema; +mod status; diff --git a/libpowersync/src/powersync.cpp b/libpowersync/src/powersync.cpp index 4299fc4..cc7ac16 100644 --- a/libpowersync/src/powersync.cpp +++ b/libpowersync/src/powersync.cpp @@ -27,6 +27,19 @@ namespace powersync { } } + static std::string resolve_string(internal::StringView& source) { + size_t count = source.length; + return {source.value, count}; + } + + static std::optional resolve_string(internal::StringView& source, bool exists) { + if (!exists) { + return std::nullopt; + } + + return resolve_string(source); + } + void internal::RawCompletionHandle::send_credentials(const char* endpoint, const char* token) { powersync_completion_handle_complete_credentials(&this->rust_handle, endpoint, token); } @@ -213,4 +226,58 @@ namespace powersync { LeasedConnection::operator sqlite3 *() const { return this->db; } + + CrudTransactions Database::get_crud_transactions() const { + auto iterator = internal::powersync_crud_transactions_new(&this->raw); + return {*this, iterator}; + } + + bool CrudTransactions::advance() { + bool has_next = false; + handle_result(internal::powersync_crud_transactions_step(this->rust_iterator, &has_next)); + return has_next; + } + + CrudTransaction CrudTransactions::current() const { + auto tx = internal::powersync_crud_transactions_current(this->rust_iterator); + std::vector crud; + + for (intptr_t i = 0; i < tx.crud_length; i++) { + auto entry = internal::powersync_crud_transactions_current_crud_item(this->rust_iterator, i); + + crud.push_back({ + .client_id = entry.client_id, + .transaction_id = entry.transaction_id, + .update_type = static_cast(entry.update_type), + .table = resolve_string(entry.table), + .id = resolve_string(entry.id), + .metadata = resolve_string(entry.metadata, entry.has_metadata), + .data = resolve_string(entry.data, entry.has_data), + .previous_values = resolve_string(entry.previous_values, entry.has_previous_values), + }); + } + + return CrudTransaction { + .db = this->db, + .last_item_id = tx.last_item_id, + .id = tx.has_id ? std::make_optional(tx.id) : std::nullopt, + .crud = crud, + }; + } + + CrudTransactions::~CrudTransactions() { + if (this->rust_iterator) { + internal::powersync_crud_transactions_free(this->rust_iterator); + } + } + + void CrudTransaction::complete() const { + this->complete(std::nullopt); + } + + void CrudTransaction::complete(std::optional custom_write_checkpoint) const { + internal::powersync_crud_complete(&db.raw, this->last_item_id, + custom_write_checkpoint.has_value(), + custom_write_checkpoint.has_value() ? custom_write_checkpoint.value(): 0); + } } diff --git a/libpowersync/src/status.rs b/libpowersync/src/status.rs new file mode 100644 index 0000000..11b4fce --- /dev/null +++ b/libpowersync/src/status.rs @@ -0,0 +1,14 @@ +use powersync::SyncStatusData; +use powersync::ffi::RawPowerSyncDatabase; +use std::ffi::c_void; +use std::sync::Arc; + +#[unsafe(no_mangle)] +pub fn powersync_db_status(db: &RawPowerSyncDatabase) -> *const c_void { + Arc::into_raw(db.sync_status()) as *const c_void +} + +#[unsafe(no_mangle)] +pub fn powersync_status_free(status: *const c_void) { + drop(unsafe { Arc::from_raw(status as *const SyncStatusData) }) +} diff --git a/powersync/src/db/crud.rs b/powersync/src/db/crud.rs index 8a3e7b2..da98405 100644 --- a/powersync/src/db/crud.rs +++ b/powersync/src/db/crud.rs @@ -1,18 +1,18 @@ use std::pin::Pin; use std::task::{Context, Poll}; +use crate::db::internal::InnerPowerSyncState; +use crate::error::{PowerSyncError, RawPowerSyncError}; +use crate::util::SerializedJsonObject; use futures_lite::{FutureExt, Stream, ready}; use pin_project_lite::pin_project; use rusqlite::params; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::{Map, Value}; -use crate::PowerSyncDatabase; -use crate::error::{PowerSyncError, RawPowerSyncError}; - /// All local writes that were made in a single SQLite transaction. pub struct CrudTransaction<'a> { - pub(crate) db: &'a PowerSyncDatabase, + pub(crate) db: &'a InnerPowerSyncState, pub last_item_id: i64, /// Unique transaction id. /// @@ -36,7 +36,6 @@ impl<'a> CrudTransaction<'a> { async fn complete_internal(self, checkpoint: Option) -> Result<(), PowerSyncError> { self.db - .inner .complete_crud_items(self.last_item_id, checkpoint) .await } @@ -72,24 +71,45 @@ pub struct CrudEntry { /// /// For DELETE, this is null. pub data: Option>, + pub raw_data: Option, /// Old values before an update. /// /// This is only tracked for tables for which this has been enabled by setting /// the [Table::track_previous_values]. pub previous_values: Option>, + pub raw_previous_values: Option, } impl CrudEntry { fn parse(id: i64, tx_id: i64, data: &str) -> Result { + struct OptionalObject { + raw: Option, + object: Option>, + } + + impl<'de> Deserialize<'de> for OptionalObject { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let raw: Option<&'de SerializedJsonObject> = Option::deserialize(deserializer)?; + + Ok(Self { + raw: raw.map(|e| e.get().to_owned()), + object: raw.map(|e| e.get_value()), + }) + } + } + #[derive(Deserialize)] struct CrudData { op: UpdateType, #[serde(rename = "type")] table: String, id: String, - data: Option>, + data: OptionalObject, metadata: Option, - old: Option>, + old: OptionalObject, } let data: CrudData = serde_json::from_str(data)?; @@ -101,8 +121,10 @@ impl CrudEntry { table: data.table, id: data.id, metadata: data.metadata, - data: data.data, - previous_values: data.old, + data: data.data.object, + raw_data: data.data.raw, + previous_values: data.old.object, + raw_previous_values: data.old.raw, }) } } @@ -126,14 +148,14 @@ type Boxed<'a, T> = Pin + Send + 'a>>; pin_project! { /// A [Stream] of completed transactions on a database. pub(crate) struct CrudTransactionStream<'a> { - db: &'a PowerSyncDatabase, + db: &'a InnerPowerSyncState, last_item_id: Option, next_tx: Option)>, PowerSyncError>>> } } impl<'a> CrudTransactionStream<'a> { - pub fn new(db: &'a PowerSyncDatabase) -> Self { + pub fn new(db: &'a InnerPowerSyncState) -> Self { Self { db, last_item_id: None, @@ -142,7 +164,7 @@ impl<'a> CrudTransactionStream<'a> { } async fn next_transaction( - db: &'a PowerSyncDatabase, + db: &'a InnerPowerSyncState, last: Option, ) -> Result)>, PowerSyncError> { let last = last.unwrap_or(-1); diff --git a/powersync/src/db/mod.rs b/powersync/src/db/mod.rs index 16f96cc..2628324 100644 --- a/powersync/src/db/mod.rs +++ b/powersync/src/db/mod.rs @@ -112,7 +112,7 @@ impl PowerSyncDatabase { pub fn crud_transactions<'a>( &'a self, ) -> impl Stream, PowerSyncError>> + 'a { - CrudTransactionStream::new(self) + CrudTransactionStream::new(&self.inner) } /// Returns the first transaction that has not been marked as completed. diff --git a/powersync/src/error.rs b/powersync/src/error.rs index cd2ce7e..cdfc384 100644 --- a/powersync/src/error.rs +++ b/powersync/src/error.rs @@ -2,6 +2,7 @@ use http_client::http_types::StatusCode; use rusqlite::Error as SqliteError; use rusqlite::types::FromSqlError; use std::error::Error; +#[cfg(feature = "ffi")] use std::ffi::c_int; use std::io; use std::sync::Arc; diff --git a/powersync/src/ffi.rs b/powersync/src/ffi.rs index 96fa820..bb7cd96 100644 --- a/powersync/src/ffi.rs +++ b/powersync/src/ffi.rs @@ -1,7 +1,12 @@ +use crate::db::crud::CrudTransactionStream; use crate::db::internal::InnerPowerSyncState; use crate::error::PowerSyncError; use crate::sync::coordinator::SyncCoordinator; -use crate::{BackendConnector, LeasedConnection, PowerSyncDatabase, SyncOptions}; +use crate::{ + BackendConnector, CrudTransaction, LeasedConnection, PowerSyncDatabase, SyncOptions, + SyncStatusData, +}; +use futures_lite::Stream; use std::ffi::c_void; use std::sync::Arc; @@ -74,6 +79,30 @@ impl RawPowerSyncDatabase { Ok(()) } + pub fn sync_status(&self) -> Arc { + let RawPowerSyncReference { inner, .. } = self.as_ref(); + inner.status.current_snapshot() + } + + pub fn crud_transactions<'a>( + &'a self, + ) -> impl Stream, PowerSyncError>> + 'a { + let RawPowerSyncReference { inner, .. } = self.as_ref(); + CrudTransactionStream::new(inner) + } + + pub async fn complete_crud_items( + &self, + last_item_id: i64, + write_checkpoint: Option, + ) -> Result<(), PowerSyncError> { + let RawPowerSyncReference { inner, .. } = self.as_ref(); + + inner + .complete_crud_items(last_item_id, write_checkpoint) + .await + } + /// ## Safety /// /// Must only be called once. diff --git a/powersync/src/util/mod.rs b/powersync/src/util/mod.rs index c9621c4..be1a0cd 100644 --- a/powersync/src/util/mod.rs +++ b/powersync/src/util/mod.rs @@ -24,6 +24,14 @@ impl SerializedJsonObject { } } + /// Safety: This must only be called for raw values that are known to be objects. + unsafe fn from_raw_value(raw: &RawValue) -> &Self { + unsafe { + // Safety: Identical representation. + std::mem::transmute(raw) + } + } + /// Serializes the given json object and returns its string representation. pub fn from_value(value: &Map) -> Box { let raw = to_raw_value(value).unwrap(); @@ -33,6 +41,15 @@ impl SerializedJsonObject { Self::from_owned_value(raw) } } + + pub fn get(&self) -> &str { + &self.json + } + + pub fn get_value(&self) -> Map { + // We can unwrap this because we already know this is a complete JSON object. + serde_json::from_str(&self.json).unwrap() + } } impl ToOwned for SerializedJsonObject { @@ -58,6 +75,24 @@ impl Clone for Box { } } +impl<'de> Deserialize<'de> for &'de SerializedJsonObject { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw: &'de RawValue = Deserialize::deserialize(deserializer)?; + if raw.get().starts_with('{') { + Ok(unsafe { + // Safety: We know it's a valid JSON value without padding. Since it starts with {, + // it must be a valid JSON object. + SerializedJsonObject::from_raw_value(raw) + }) + } else { + Err(Error::custom("Expected a JSON object")) + } + } +} + impl<'de> Deserialize<'de> for Box { fn deserialize(deserializer: D) -> Result where From 70ee56fbbbf98891fba3c6455c5e82b92368218a Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 22 Oct 2025 18:12:41 +0200 Subject: [PATCH 09/14] Listeners at last --- examples/cpp_queries.cpp | 23 ++--- libpowersync/include/powersync.h | 38 +++++++++ libpowersync/src/bindings.h | 20 +++++ libpowersync/src/crud.rs | 8 ++ libpowersync/src/database.rs | 59 ++++++++++++- libpowersync/src/powersync.cpp | 55 ++++++++++++ libpowersync/src/status.rs | 38 ++++++++- powersync/src/db/pool.rs | 53 +++++++++--- powersync/src/db/watch.rs | 32 +++++-- powersync/src/ffi.rs | 30 +++++++ powersync/src/lib.rs | 2 + powersync/src/sync/status.rs | 25 ++++-- powersync/src/util/mod.rs | 1 + powersync/src/util/raw_listener.rs | 133 +++++++++++++++++++++++++++++ 14 files changed, 475 insertions(+), 42 deletions(-) create mode 100644 powersync/src/util/raw_listener.rs diff --git a/examples/cpp_queries.cpp b/examples/cpp_queries.cpp index 3c148ac..1e19c11 100644 --- a/examples/cpp_queries.cpp +++ b/examples/cpp_queries.cpp @@ -71,6 +71,18 @@ int main() { //auto connector = std::make_shared(db); //db->connect(connector); + auto watcher = db->watch_tables({"users"}, [db] { + std::cout << "Saw change on users table" << std::endl; + auto reader = db->reader(); + sqlite3_stmt *stmt = nullptr; + check_rc(sqlite3_prepare_v2(reader, "SELECT id, name FROM users", -1, &stmt, nullptr)); + + while (sqlite3_step(stmt) == SQLITE_ROW) { + std::cout << sqlite3_column_text(stmt, 0) << ": " << sqlite3_column_text(stmt, 1) << std::endl; + } + sqlite3_finalize(stmt); + }); + { auto writer = db->writer(); check_rc(sqlite3_exec(writer, "INSERT INTO users (id, name) VALUES (uuid(), 'Simon');", nullptr, nullptr, nullptr)); @@ -98,15 +110,4 @@ int main() { std::cout << "Has transaction: " << has_tx << std::endl; } - - { - auto reader = db->reader(); - sqlite3_stmt *stmt = nullptr; - check_rc(sqlite3_prepare_v2(reader, "SELECT id, name FROM users", -1, &stmt, nullptr)); - - while (sqlite3_step(stmt) == SQLITE_ROW) { - std::cout << sqlite3_column_text(stmt, 0) << ": " << sqlite3_column_text(stmt, 1) << std::endl; - } - sqlite3_finalize(stmt); - } } diff --git a/libpowersync/include/powersync.h b/libpowersync/include/powersync.h index d9e5b72..92fc4f3 100644 --- a/libpowersync/include/powersync.h +++ b/libpowersync/include/powersync.h @@ -186,6 +186,38 @@ class CrudTransactions { ~CrudTransactions(); }; +/// An active watcher on a [Database]. +/// +/// Calling the destructor of watchers will unregister the listener. +class Watcher { + void* rust_status_watcher; + void* rust_table_watcher; + std::function callback; + + friend class Database; + static void dispatch(const void* token); + + public: + explicit Watcher(std::function callback): rust_status_watcher(nullptr), rust_table_watcher(nullptr), callback(callback) {} + + Watcher(const Watcher&) = delete; + Watcher(Watcher&& a) noexcept : rust_status_watcher(a.rust_status_watcher), rust_table_watcher(a.rust_table_watcher), callback(std::move(a.callback)) { + a.rust_status_watcher = nullptr; + a.rust_table_watcher = nullptr; + } + + ~Watcher(); +}; + +class SyncStatus { + void* rust_status; + + explicit SyncStatus(void* rust_status): rust_status(rust_status) {} + friend class Database; +public: + ~SyncStatus(); +}; + class Database { internal::RawPowerSyncDatabase raw; std::optional worker; @@ -213,6 +245,12 @@ class Database { /// This database must outlive the returned transactions stream. CrudTransactions get_crud_transactions() const; + SyncStatus sync_status() const; + + /// The watcher keeps a reference to the current database, which must outlive it. + std::unique_ptr watch_sync_status(std::function callback) const; + std::unique_ptr watch_tables(const std::initializer_list& tables, std::function callback) const; + ~Database(); [[nodiscard]] LeasedConnection reader() const; diff --git a/libpowersync/src/bindings.h b/libpowersync/src/bindings.h index 1c09c15..4e711a5 100644 --- a/libpowersync/src/bindings.h +++ b/libpowersync/src/bindings.h @@ -128,6 +128,8 @@ PowerSyncResultCode powersync_db_in_memory(RawSchema schema, RawPowerSyncDatabas PowerSyncResultCode powersync_db_connect(const RawPowerSyncDatabase *db, const CppConnector *connector); +PowerSyncResultCode powersync_db_disconnect(const RawPowerSyncDatabase *db); + PowerSyncResultCode powersync_db_reader(const RawPowerSyncDatabase *db, ConnectionLeaseResult *out_lease); @@ -136,6 +138,14 @@ PowerSyncResultCode powersync_db_writer(const RawPowerSyncDatabase *db, void powersync_db_return_lease(RawConnectionLease *lease); +void *powersync_db_watch_tables(const RawPowerSyncDatabase *db, + const StringView *tables, + uintptr_t table_count, + void (*listener)(const void*), + const void *token); + +void powersync_db_watch_tables_end(void *watcher); + void powersync_db_free(RawPowerSyncDatabase db); char *powersync_last_error_desc(); @@ -149,6 +159,16 @@ void powersync_run_tasks(const RawPowerSyncDatabase *db); int powersync_install_logger(CppLogger logger); +void *powersync_db_status(const RawPowerSyncDatabase *db); + +void powersync_status_free(const void *status); + +void *powersync_db_status_listener(const RawPowerSyncDatabase *db, + void (*listener)(const void*), + const void *token); + +void powersync_db_status_listener_clear(void *listener); + } // extern "C" } // namespace powersync::internal diff --git a/libpowersync/src/crud.rs b/libpowersync/src/crud.rs index 4209934..c6c362f 100644 --- a/libpowersync/src/crud.rs +++ b/libpowersync/src/crud.rs @@ -47,6 +47,14 @@ impl StringView { } } +impl AsRef for StringView { + fn as_ref(&self) -> &str { + let bytes = + unsafe { std::slice::from_raw_parts(self.value as *const u8, self.length as usize) }; + std::str::from_utf8(bytes).unwrap() + } +} + #[repr(C)] pub struct RawCrudTransaction { pub id: i64, diff --git a/libpowersync/src/database.rs b/libpowersync/src/database.rs index 8654348..15c2ff8 100644 --- a/libpowersync/src/database.rs +++ b/libpowersync/src/database.rs @@ -1,4 +1,5 @@ use crate::connector::{CppConnector, wrap_cpp_connector}; +use crate::crud::StringView; use crate::error::{LAST_ERROR, PowerSyncResultCode}; use crate::schema::RawSchema; use futures_lite::future; @@ -7,10 +8,11 @@ use powersync::env::PowerSyncEnvironment; use powersync::error::PowerSyncError; use powersync::ffi::RawPowerSyncDatabase; use powersync::schema::Schema; -use powersync::{ConnectionPool, LeasedConnection, PowerSyncDatabase}; +use powersync::{CallbackListenerHandle, ConnectionPool, LeasedConnection, PowerSyncDatabase}; use rusqlite::Connection; use rusqlite::ffi::sqlite3; -use std::ffi::{CString, c_char}; +use std::collections::HashSet; +use std::ffi::{CString, c_char, c_void}; use std::ops::Deref; use std::ptr::null; use std::sync::Arc; @@ -62,6 +64,12 @@ extern "C" fn powersync_db_connect( PowerSyncResultCode::OK } +#[unsafe(no_mangle)] +extern "C" fn powersync_db_disconnect(db: &RawPowerSyncDatabase) -> PowerSyncResultCode { + ps_try!(future::block_on(db.disconnect())); + PowerSyncResultCode::OK +} + #[unsafe(no_mangle)] extern "C" fn powersync_db_reader<'a>( db: &'a RawPowerSyncDatabase, @@ -96,6 +104,53 @@ extern "C" fn powersync_db_return_lease(lease: *mut RawConnectionLease) { drop(lease); } +#[unsafe(no_mangle)] +extern "C" fn powersync_db_watch_tables<'a>( + db: &'a RawPowerSyncDatabase, + tables: *const StringView, + table_count: usize, + listener: extern "C" fn(*const c_void), + token: *const c_void, +) -> *mut c_void { + let resolved_tables = { + let mut resolved_tables = HashSet::new(); + let table_names = unsafe { std::slice::from_raw_parts(tables, table_count) }; + + for table in table_names { + let name: &str = table.as_ref(); + resolved_tables.insert(name.to_string()); + resolved_tables.insert(format!("ps_data__{name}")); + resolved_tables.insert(format!("ps_data_local__{name}")); + } + + resolved_tables + }; + + #[derive(Clone)] + struct PendingListener { + listener: extern "C" fn(*const c_void), + token: *const c_void, + } + + // Safety: We require listeners to be thread-safe in C++. + unsafe impl Send for PendingListener {} + unsafe impl Sync for PendingListener {} + + let listener = PendingListener { listener, token }; + let handle: CallbackListenerHandle<'a, HashSet> = + db.install_table_listener(resolved_tables, move || { + let inner = &listener; + (inner.listener)(inner.token); + }); + + Box::into_raw(Box::new(handle)) as *mut c_void +} + +#[unsafe(no_mangle)] +extern "C" fn powersync_db_watch_tables_end(watcher: *mut c_void) { + drop(unsafe { Box::from_raw(watcher as *mut CallbackListenerHandle<'_, HashSet>) }); +} + #[unsafe(no_mangle)] extern "C" fn powersync_db_free(db: RawPowerSyncDatabase) { unsafe { db.free() } diff --git a/libpowersync/src/powersync.cpp b/libpowersync/src/powersync.cpp index cc7ac16..1a7852b 100644 --- a/libpowersync/src/powersync.cpp +++ b/libpowersync/src/powersync.cpp @@ -183,6 +183,40 @@ namespace powersync { handle_result(rc); } + void Database::disconnect() { + handle_result(internal::powersync_db_disconnect(&raw)); + } + + SyncStatus Database::sync_status() const { + return SyncStatus(internal::powersync_db_status(&this->raw)); + } + + std::unique_ptr Database::watch_sync_status(std::function callback) const { + std::unique_ptr watcher; + watcher = std::make_unique([callback = std::move(callback), this]() mutable { + callback(this->sync_status()); + }); + + auto handle = internal::powersync_db_status_listener(&this->raw, Watcher::dispatch, watcher.get()); + watcher->rust_status_watcher = handle; + return watcher; + } + + std::unique_ptr Database::watch_tables(const std::initializer_list& tables, std::function callback) const { + auto watcher = std::make_unique(std::move(callback)); + std::vector table_names; + for (const auto& table : tables) { + table_names.push_back(internal::StringView { + .value = table.data(), + .length = static_cast(table.length()), + }); + } + + auto handle = internal::powersync_db_watch_tables(&this->raw, table_names.data(), table_names.size(), Watcher::dispatch, watcher.get()); + watcher->rust_table_watcher = handle; + return watcher; + } + void Database::spawn_sync_thread() { auto raw = &this->raw; std::thread thread([raw]() { @@ -280,4 +314,25 @@ namespace powersync { custom_write_checkpoint.has_value(), custom_write_checkpoint.has_value() ? custom_write_checkpoint.value(): 0); } + + void Watcher::dispatch(const void* token) { + auto watcher = static_cast(token); + watcher->callback(); + } + + Watcher::~Watcher() { + if (this->rust_status_watcher) { + internal::powersync_db_status_listener_clear(this->rust_status_watcher); + } + + if (this->rust_table_watcher) { + internal::powersync_db_watch_tables_end(this->rust_table_watcher); + } + } + + SyncStatus::~SyncStatus() { + if (this->rust_status) { + internal::powersync_status_free(this->rust_status); + } + } } diff --git a/libpowersync/src/status.rs b/libpowersync/src/status.rs index 11b4fce..9bef4f4 100644 --- a/libpowersync/src/status.rs +++ b/libpowersync/src/status.rs @@ -1,14 +1,44 @@ -use powersync::SyncStatusData; use powersync::ffi::RawPowerSyncDatabase; +use powersync::{CallbackListenerHandle, SyncStatusData}; use std::ffi::c_void; use std::sync::Arc; #[unsafe(no_mangle)] -pub fn powersync_db_status(db: &RawPowerSyncDatabase) -> *const c_void { - Arc::into_raw(db.sync_status()) as *const c_void +pub extern "C" fn powersync_db_status(db: &RawPowerSyncDatabase) -> *mut c_void { + Arc::into_raw(db.sync_status()) as *mut c_void } #[unsafe(no_mangle)] -pub fn powersync_status_free(status: *const c_void) { +pub extern "C" fn powersync_status_free(status: *const c_void) { drop(unsafe { Arc::from_raw(status as *const SyncStatusData) }) } + +#[unsafe(no_mangle)] +pub extern "C" fn powersync_db_status_listener<'a>( + db: &'a RawPowerSyncDatabase, + listener: extern "C" fn(*const c_void), + token: *const c_void, +) -> *mut c_void { + #[derive(Clone)] + struct PendingListener { + listener: extern "C" fn(*const c_void), + token: *const c_void, + } + + // Safety: We require listeners to be thread-safe in C++. + unsafe impl Send for PendingListener {} + unsafe impl Sync for PendingListener {} + + let listener = PendingListener { listener, token }; + let handle: CallbackListenerHandle<'a, ()> = db.install_status_listener(move || { + let inner = &listener; + (inner.listener)(inner.token); + }); + + Box::into_raw(Box::new(handle)) as *mut c_void +} + +#[unsafe(no_mangle)] +pub extern "C" fn powersync_db_status_listener_clear(listener: *mut c_void) { + drop(unsafe { Box::from_raw(listener as *mut CallbackListenerHandle<'_, ()>) }); +} diff --git a/powersync/src/db/pool.rs b/powersync/src/db/pool.rs index f75a4b3..9fee60e 100644 --- a/powersync/src/db/pool.rs +++ b/powersync/src/db/pool.rs @@ -103,7 +103,7 @@ impl ConnectionPool { } else { let guard = self.state.writer.lock_blocking(); LeasedConnectionImpl::Writer(LeasedWriter { - connection: guard, + connection: MaybeUninit::new(guard), pool: self, }) } @@ -119,10 +119,6 @@ impl ConnectionPool { let updates = serde_json::from_str::(&rows) .map_err(|_| Error::InvalidQuery)?; - if !updates.tables.is_empty() { - self.state.table_notifiers.notify_updates(&updates.tables); - } - Ok(updates) } @@ -141,7 +137,7 @@ impl ConnectionPool { } else { let guard = self.state.writer.lock().await; LeasedConnectionImpl::Writer(LeasedWriter { - connection: guard, + connection: MaybeUninit::new(guard), pool: self, }) } @@ -187,14 +183,49 @@ struct PoolReaders { } struct LeasedWriter<'a> { - connection: MutexGuard<'a, Connection>, + connection: MaybeUninit>, pool: &'a ConnectionPool, } +impl<'a> LeasedWriter<'a> { + /// Safety: Must not be called in [Drop]. + pub unsafe fn connection(&'a self) -> &'a Connection { + unsafe { + // Safety: Initialized if we're not in drop, which takes the guard out of the struct. + self.connection.assume_init_ref() + } + } + + pub unsafe fn connection_mut(&mut self) -> &mut Connection { + unsafe { + // Safety: Initialized if we're not in drop, which takes the guard out of the struct. + self.connection.assume_init_mut() + } + } +} + impl Drop for LeasedWriter<'_> { fn drop(&mut self) { // Send update notifications for writes made on this connection while leased. - let _ = self.pool.take_update_notifications(&self.connection); + let updates = self + .pool + .take_update_notifications(unsafe { + // Safety: We did not clear the connection yet. + self.connection() + }) + .unwrap(); + + // Now, drop the connection. + unsafe { self.connection.assume_init_drop() } + + // And only after the connection has been returned, dispatch the update notification. This + // avoids deadlocks when we're polling for connections synchronously in C++. + if !updates.tables.is_empty() { + self.pool + .state + .table_notifiers + .notify_updates(&updates.tables); + } } } @@ -227,7 +258,7 @@ impl<'a> Deref for LeasedConnectionImpl<'a> { fn deref(&self) -> &Self::Target { match self { - LeasedConnectionImpl::Writer(writer) => &writer.connection, + LeasedConnectionImpl::Writer(writer) => unsafe { writer.connection() }, LeasedConnectionImpl::Reader(reader) => unsafe { // safety: This is initialized by default, and only uninitialized on Drop. reader.connection.assume_init_ref() @@ -237,9 +268,9 @@ impl<'a> Deref for LeasedConnectionImpl<'a> { } impl<'a> DerefMut for LeasedConnectionImpl<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { + fn deref_mut<'b>(&'b mut self) -> &'b mut Self::Target { match self { - LeasedConnectionImpl::Writer(writer) => &mut writer.connection, + LeasedConnectionImpl::Writer(writer) => unsafe { writer.connection_mut() }, LeasedConnectionImpl::Reader(reader) => unsafe { // safety: This is initialized by default, and only uninitialized on Drop. reader.connection.assume_init_mut() diff --git a/powersync/src/db/watch.rs b/powersync/src/db/watch.rs index 6520368..d48a0aa 100644 --- a/powersync/src/db/watch.rs +++ b/powersync/src/db/watch.rs @@ -1,3 +1,8 @@ +use crate::CallbackListenerHandle; +use crate::util::raw_listener::CallbackListeners; +use event_listener::{Event, EventListener}; +use futures_lite::{FutureExt, Stream, ready}; +use std::sync::Weak; use std::{ collections::HashSet, pin::Pin, @@ -8,12 +13,10 @@ use std::{ task::{Context, Poll}, }; -use event_listener::{Event, EventListener}; -use futures_lite::{FutureExt, Stream, ready}; - #[derive(Default)] pub struct TableNotifiers { active: Mutex>>, + callback_based: CallbackListeners>, } impl TableNotifiers { @@ -25,6 +28,19 @@ impl TableNotifiers { listener.mark_dirty(); } } + + self.callback_based + .notify_listeners(|filter| filter.intersection(updates).next().is_some()); + } + + /// Invokes [listener] for each reported change on [tables] until the returned + /// [CallbackListenerHandle] is dropped. + pub fn install_callback<'a>( + &'a self, + tables: HashSet, + listener: impl Fn() + Send + Sync + 'a, + ) -> CallbackListenerHandle<'a, HashSet> { + self.callback_based.listen(tables, listener) } /// Returns a [Stream] emitting an empty event every time one of the tables updates. @@ -34,7 +50,7 @@ impl TableNotifiers { tables: HashSet, ) -> impl Stream + 'static { let listener = Arc::new(TableListenerState { - notifiers: self.clone(), + notifiers: Arc::downgrade(self), tables, notifer: Event::new(), dirty: AtomicBool::new(emit_initially), @@ -81,8 +97,10 @@ impl TableNotifiers { impl Drop for PendingListener { fn drop(&mut self) { - let mut guard = self.state.notifiers.active.lock().unwrap(); - guard.retain(|listener| !Arc::ptr_eq(listener, &self.state)); + if let Some(notifiers) = self.state.notifiers.upgrade() { + let mut guard = notifiers.active.lock().unwrap(); + guard.retain(|listener| !Arc::ptr_eq(listener, &self.state)); + } } } @@ -94,7 +112,7 @@ impl TableNotifiers { } pub struct TableListenerState { - notifiers: Arc, + notifiers: Weak, tables: HashSet, notifer: Event, dirty: AtomicBool, diff --git a/powersync/src/ffi.rs b/powersync/src/ffi.rs index bb7cd96..9d77a64 100644 --- a/powersync/src/ffi.rs +++ b/powersync/src/ffi.rs @@ -2,11 +2,13 @@ use crate::db::crud::CrudTransactionStream; use crate::db::internal::InnerPowerSyncState; use crate::error::PowerSyncError; use crate::sync::coordinator::SyncCoordinator; +use crate::util::raw_listener::CallbackListenerHandle; use crate::{ BackendConnector, CrudTransaction, LeasedConnection, PowerSyncDatabase, SyncOptions, SyncStatusData, }; use futures_lite::Stream; +use std::collections::HashSet; use std::ffi::c_void; use std::sync::Arc; @@ -79,11 +81,39 @@ impl RawPowerSyncDatabase { Ok(()) } + pub async fn disconnect(&self) -> Result<(), PowerSyncError> { + let RawPowerSyncReference { sync, inner } = self.as_ref(); + sync.disconnect().await; + + Ok(()) + } + pub fn sync_status(&self) -> Arc { let RawPowerSyncReference { inner, .. } = self.as_ref(); inner.status.current_snapshot() } + pub fn install_status_listener<'a>( + &'a self, + f: impl Fn() + Send + Sync + 'a, + ) -> CallbackListenerHandle<'a, ()> { + let RawPowerSyncReference { inner, .. } = self.as_ref(); + inner.status.listener(f) + } + + pub fn install_table_listener<'a>( + &'a self, + tables: HashSet, + f: impl Fn() + Send + Sync + 'a, + ) -> CallbackListenerHandle<'a, HashSet> { + let RawPowerSyncReference { inner, .. } = self.as_ref(); + inner + .env + .pool + .update_notifiers() + .install_callback(tables, f) + } + pub fn crud_transactions<'a>( &'a self, ) -> impl Stream, PowerSyncError>> + 'a { diff --git a/powersync/src/lib.rs b/powersync/src/lib.rs index 571db8c..7d866b8 100644 --- a/powersync/src/lib.rs +++ b/powersync/src/lib.rs @@ -6,6 +6,8 @@ mod util; #[cfg(feature = "ffi")] pub mod ffi; +#[cfg(feature = "ffi")] +pub use util::raw_listener::CallbackListenerHandle; pub use db::PowerSyncDatabase; pub use db::crud::{CrudEntry, CrudTransaction, UpdateType}; diff --git a/powersync/src/sync/status.rs b/powersync/src/sync/status.rs index 5471a28..57bbf4e 100644 --- a/powersync/src/sync/status.rs +++ b/powersync/src/sync/status.rs @@ -1,3 +1,6 @@ +use event_listener::{Event, EventListener}; +use rusqlite::{Connection, params}; +use std::collections::HashSet; use std::{ fmt::Debug, sync::{ @@ -6,9 +9,7 @@ use std::{ }, }; -use event_listener::{Event, EventListener}; -use rusqlite::{Connection, params}; - +use crate::util::raw_listener::{CallbackListenerHandle, CallbackListeners}; use crate::{ error::PowerSyncError, sync::{ @@ -19,15 +20,22 @@ use crate::{ }; /// An internal struct holding the current sync status, which allows notifying listeners. +#[derive(Default)] pub struct SyncStatus { data: Mutex>, + raw_listeners: CallbackListeners<()>, } impl SyncStatus { pub(crate) fn new() -> Self { - Self { - data: Default::default(), - } + Self::default() + } + + pub(crate) fn listener<'a, F: Fn() + Send + Sync + 'a>( + &'a self, + listener: F, + ) -> CallbackListenerHandle<'a, ()> { + self.raw_listeners.listen((), listener) } pub fn current_snapshot(&self) -> Arc { @@ -41,11 +49,14 @@ impl SyncStatus { let mut new = data.new_revision(); let res = update(&mut new); - // Then notify listeners. + // Notify async listeners. let old_state = std::mem::replace(&mut *data, Arc::new(new)); old_state.is_invalidated.store(true, Ordering::SeqCst); old_state.invalidated.notify(usize::MAX); + // Notify external C++ listeners. + self.raw_listeners.notify_all(); + res } } diff --git a/powersync/src/util/mod.rs b/powersync/src/util/mod.rs index be1a0cd..14e5684 100644 --- a/powersync/src/util/mod.rs +++ b/powersync/src/util/mod.rs @@ -1,4 +1,5 @@ mod bson_split; +pub mod raw_listener; mod shared_future; pub use bson_split::BsonObjects; diff --git a/powersync/src/util/raw_listener.rs b/powersync/src/util/raw_listener.rs new file mode 100644 index 0000000..29c5e36 --- /dev/null +++ b/powersync/src/util/raw_listener.rs @@ -0,0 +1,133 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +#[derive(Default)] +pub struct CallbackListeners { + raw_listeners: Mutex>>, +} + +impl CallbackListeners { + pub fn listen<'a, F: Fn() + Send + Sync + 'a>( + &'a self, + key: K, + callback: F, + ) -> CallbackListenerHandle<'a, K> { + let mut raw_listeners = self.raw_listeners.lock().unwrap(); + let listener = Arc::new(callback); + let listener = unsafe { + // Safety: We fake the 'static lifetime here, the listener will not get invoked + // after the CallbackListenerHandle<'a> completes and that guarantees lifetime 'a + // to be kept alive. + std::mem::transmute::< + Arc, + Arc, + >(listener) + }; + let deactivated = Arc::new(AtomicBool::new(false)); + + raw_listeners.push(CallbackListener { + key, + listener: listener.clone(), + deactivated: deactivated.clone(), + }); + CallbackListenerHandle { + group: self, + listener, + deactivated, + } + } + + pub fn notify_listeners(&self, mut filter: impl FnMut(&K) -> bool) { + let mut raw_listeners = self.raw_listeners.lock().unwrap(); + + raw_listeners.retain(|listener| { + if filter(&listener.key) { + (listener.listener)(); + + // Drop the listener if it has deactivated itself in response to the event. + !listener.deactivated.load(Ordering::SeqCst) + } else { + true + } + }); + } + + pub fn notify_all(&self) { + self.notify_listeners(|_| true) + } +} + +struct CallbackListener { + key: K, + listener: Arc, + deactivated: Arc, +} + +pub struct CallbackListenerHandle<'a, K> { + group: &'a CallbackListeners, + listener: Arc, + deactivated: Arc, +} + +impl Drop for CallbackListenerHandle<'_, K> { + fn drop(&mut self) { + self.deactivated.store(true, Ordering::SeqCst); + + if let Ok(mut raw_listeners) = self.group.raw_listeners.try_lock() { + // Not currently notifying listeners, remove listener from waiters. + raw_listeners.retain(|listener| !Arc::ptr_eq(&listener.listener, &self.listener)) + } + } +} + +#[cfg(test)] +mod test { + use crate::util::raw_listener::CallbackListeners; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + + #[test] + fn notify() { + let events = AtomicUsize::new(0); + let listeners = CallbackListeners::default(); + let listener = listeners.listen((), || { + events.fetch_add(1, Ordering::SeqCst); + }); + + listeners.notify_all(); + assert_eq!(events.load(Ordering::SeqCst), 1); + listeners.notify_all(); + assert_eq!(events.load(Ordering::SeqCst), 2); + + drop(listener); + listeners.notify_all(); + assert_eq!(events.load(Ordering::SeqCst), 2); + } + + #[test] + fn drop_on_notify() { + let events = AtomicUsize::new(0); + let listeners = CallbackListeners::default(); + let listener = Arc::new(Mutex::new(None)); + + { + let mut guard = listener.lock().unwrap(); + let listener = listener.clone(); + let events = &events; + *guard = Some(listeners.listen((), move || { + events.store(1, Ordering::SeqCst); + + let mut listener = listener.lock().unwrap(); + // Drop self + drop(listener.take()); + })); + } + + listeners.notify_all(); + assert_eq!(events.load(Ordering::SeqCst), 1); + + // Notifying should have dropped the listener + listeners.notify_all(); + assert_eq!(events.load(Ordering::SeqCst), 1); + } +} From 219bc24c2388f54354e96c9fd895b7cd51566321 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 23 Oct 2025 15:08:59 +0200 Subject: [PATCH 10/14] Remaining sync status --- examples/cpp_queries.cpp | 5 ++ libpowersync/build.rs | 1 + libpowersync/include/powersync.h | 87 +++++++++++++++++++++-- libpowersync/src/bindings.h | 37 ++++++++++ libpowersync/src/lib.rs | 1 + libpowersync/src/powersync.cpp | 117 ++++++++++++++++++++++++++++--- libpowersync/src/status.rs | 105 +++++++++++++++++++++++++++ libpowersync/src/streams.rs | 13 ++++ powersync/src/db/streams.rs | 20 +++++- powersync/src/db/watch.rs | 3 +- powersync/src/sync/progress.rs | 2 +- powersync/src/sync/status.rs | 23 +++--- 12 files changed, 386 insertions(+), 28 deletions(-) create mode 100644 libpowersync/src/streams.rs diff --git a/examples/cpp_queries.cpp b/examples/cpp_queries.cpp index 1e19c11..54d197f 100644 --- a/examples/cpp_queries.cpp +++ b/examples/cpp_queries.cpp @@ -83,6 +83,11 @@ int main() { sqlite3_finalize(stmt); }); + auto status_watcher = db->watch_sync_status([db]() { + const auto status = db->sync_status(); + std::cout << "Sync status: " << status << std::endl; + }); + { auto writer = db->writer(); check_rc(sqlite3_exec(writer, "INSERT INTO users (id, name) VALUES (uuid(), 'Simon');", nullptr, nullptr, nullptr)); diff --git a/libpowersync/build.rs b/libpowersync/build.rs index a22b65a..996d0d9 100644 --- a/libpowersync/build.rs +++ b/libpowersync/build.rs @@ -18,4 +18,5 @@ fn main() { .file("src/powersync.cpp") .link_lib_modifier("+whole-archive") .compile("powersync_bridge"); + println!("cargo::rerun-if-changed=src/powersync.cpp"); } diff --git a/libpowersync/include/powersync.h b/libpowersync/include/powersync.h index 92fc4f3..fb5fbc6 100644 --- a/libpowersync/include/powersync.h +++ b/libpowersync/include/powersync.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -209,12 +210,69 @@ class Watcher { ~Watcher(); }; +class SyncStream; +class SyncStreamSubscription; + +/// Information about a progressing download. +/// +/// This reports the `total` amount of operations to download, how many of them have already been +/// `downloaded`, and finally a `fraction()` indicating relative progress. +struct ProgressCounters { + /// How many operations need to be downloaded in total for the current download to complete. + int64_t total; + /// How many operations, out of `total`, have already been downloaded. + int64_t downloaded; + + /// The relative amount of `total` to items in `downloaded`, as a number between `0.0` and `1.0` (inclusive). + /// + /// When this number reaches `1.0`, all changes have been received from the sync service. Actually applying these + /// changes happens before the progress is cleared though, so progress can stay at `1.0` for a short while before + /// completing. + float fraction() const { + if (total == 0) { + return 0; + } + return static_cast(downloaded) / static_cast(total); + } +}; + +struct SyncStreamStatus { + std::string name; + std::optional parameters; + + std::optional progress; + bool is_active; + bool is_default; + bool has_explicit_subscription; + std::optional> expires_at; + bool has_synced; + std::optional> last_synced_at; +}; + class SyncStatus { void* rust_status; - explicit SyncStatus(void* rust_status): rust_status(rust_status) {} + void read(); + + explicit SyncStatus(void* rust_status); friend class Database; public: + bool connected; + bool connecting; + bool downloading; + std::optional download_error; + + bool uploading; + std::optional upload_error; + + SyncStatus(const SyncStatus& other); + SyncStatus(SyncStatus&& other) = delete; + + std::optional for_stream(const SyncStream& stream) const; + std::vector all_streams() const; + + friend std::ostream& operator<<(std::ostream& os, const SyncStatus& status); + ~SyncStatus(); }; @@ -248,17 +306,36 @@ class Database { SyncStatus sync_status() const; /// The watcher keeps a reference to the current database, which must outlive it. - std::unique_ptr watch_sync_status(std::function callback) const; - std::unique_ptr watch_tables(const std::initializer_list& tables, std::function callback) const; - - ~Database(); + [[nodiscard]] std::unique_ptr watch_sync_status(std::function callback) const; + [[nodiscard]] std::unique_ptr watch_tables(const std::initializer_list& tables, std::function callback) const; [[nodiscard]] LeasedConnection reader() const; [[nodiscard]] LeasedConnection writer() const; + ~Database(); + static Database in_memory(const Schema& schema); }; +class SyncStream { + const Database& db; +public: + const std::string name; + const std::optional parameters; + + SyncStream(const Database& db, const std::string& name): db(db), name(name) {} + SyncStream(const Database& db, const std::string& name, std::string parameters): db(db), name(name), parameters(parameters) {} + + SyncStreamSubscription subscribe(); +}; + +class SyncStreamSubscription { +private: + void* rust_subscription; +public: + const SyncStream stream; +}; + class Exception final : public std::exception { private: const int rc; diff --git a/libpowersync/src/bindings.h b/libpowersync/src/bindings.h index 4e711a5..2512b7b 100644 --- a/libpowersync/src/bindings.h +++ b/libpowersync/src/bindings.h @@ -92,6 +92,33 @@ struct CppLogger { void (*native_log)(LogLevel level, const char *line); }; +struct RawSyncStatus { + bool connected; + bool connecting; + bool downloading; + StringView download_error; + bool has_download_error; + bool uploading; + StringView upload_error; + bool has_upload_error; +}; + +struct RawSyncStreamStatus { + StringView name; + StringView parameters; + bool has_parameters; + int64_t progress_total; + int64_t progress_done; + bool has_progress; + bool is_active; + bool is_default; + bool has_explicit_subscription; + int64_t expires_at; + bool has_expires_at; + bool has_synced; + int64_t last_synced_at; +}; + extern "C" { void powersync_completion_handle_complete_credentials(CppCompletionHandle *handle, @@ -161,6 +188,16 @@ int powersync_install_logger(CppLogger logger); void *powersync_db_status(const RawPowerSyncDatabase *db); +void powersync_status_inspect(const void *status, + void (*inspect)(void *token, RawSyncStatus status), + void *inspect_token); + +void powersync_status_streams(const void *status, + void (*inspect)(void *token, const RawSyncStreamStatus *status), + void *inspect_token); + +void powersync_status_clone(const void *status); + void powersync_status_free(const void *status); void *powersync_db_status_listener(const RawPowerSyncDatabase *db, diff --git a/libpowersync/src/lib.rs b/libpowersync/src/lib.rs index aeda96e..292171c 100644 --- a/libpowersync/src/lib.rs +++ b/libpowersync/src/lib.rs @@ -16,3 +16,4 @@ mod executor; mod logger; mod schema; mod status; +mod streams; diff --git a/libpowersync/src/powersync.cpp b/libpowersync/src/powersync.cpp index 1a7852b..d2a2e32 100644 --- a/libpowersync/src/powersync.cpp +++ b/libpowersync/src/powersync.cpp @@ -27,12 +27,12 @@ namespace powersync { } } - static std::string resolve_string(internal::StringView& source) { + static std::string resolve_string(const internal::StringView& source) { size_t count = source.length; return {source.value, count}; } - static std::optional resolve_string(internal::StringView& source, bool exists) { + static std::optional resolve_string(const internal::StringView& source, bool exists) { if (!exists) { return std::nullopt; } @@ -191,10 +191,10 @@ namespace powersync { return SyncStatus(internal::powersync_db_status(&this->raw)); } - std::unique_ptr Database::watch_sync_status(std::function callback) const { + std::unique_ptr Database::watch_sync_status(std::function callback) const { std::unique_ptr watcher; - watcher = std::make_unique([callback = std::move(callback), this]() mutable { - callback(this->sync_status()); + watcher = std::make_unique([callback = std::move(callback)]() mutable { + callback(); }); auto handle = internal::powersync_db_status_listener(&this->raw, Watcher::dispatch, watcher.get()); @@ -330,9 +330,110 @@ namespace powersync { } } - SyncStatus::~SyncStatus() { - if (this->rust_status) { - internal::powersync_status_free(this->rust_status); + SyncStatus::SyncStatus(void *rust_status): rust_status(rust_status) { + this->read(); + } + + static void sync_status_inspector(void* target, internal::RawSyncStatus info) { + auto status = static_cast(target); + + status->connected = info.connected; + status->connecting = info.connecting; + status->downloading = info.downloading; + status->download_error = resolve_string(info.download_error, info.has_download_error); + status->uploading = info.uploading; + status->upload_error = resolve_string(info.upload_error, info.has_upload_error); + } + + void SyncStatus::read() { + internal::powersync_status_inspect(rust_status, sync_status_inspector, this); + } + + SyncStatus::SyncStatus(const SyncStatus& other): rust_status(other.rust_status) { + internal::powersync_status_clone(rust_status); + this->read(); + } + + static void sync_stream_status_inspector(void* target, const internal::RawSyncStreamStatus* info) { + using Clock = std::chrono::system_clock; + using TimePoint = std::chrono::time_point; + + auto streams = static_cast*>(target); + + std::optional progress; + std::optional expires_at; + std::optional last_synced_at; + if (info->has_progress) { + progress = { + .total = info->progress_total, + .downloaded = info->progress_done, + }; + } + if (info->has_expires_at) { + expires_at = TimePoint(std::chrono::milliseconds(info->expires_at)); + } + if (info->has_synced) { + last_synced_at = TimePoint(std::chrono::milliseconds(info->last_synced_at)); + } + + SyncStreamStatus status = { + .name = resolve_string(info->name), + .parameters = resolve_string(info->parameters, info->has_parameters), + .progress = progress, + .is_active = info->is_active, + .is_default = info->is_default, + .has_explicit_subscription = info->has_explicit_subscription, + .expires_at = expires_at, + .has_synced = info->has_synced, + .last_synced_at = last_synced_at, + }; + streams->emplace_back(status); + } + + std::vector SyncStatus::all_streams() const { + std::vector streams; + internal::powersync_status_streams(rust_status, sync_stream_status_inspector, &streams); + + return streams; + } + + std::optional SyncStatus::for_stream(const SyncStream& stream) const { + // TODO: Don't recompue vector on every call. + const auto streams = all_streams(); + for (const auto& known: streams) { + if (known.name == stream.name && known.parameters == stream.parameters) { + return known; + } } + + return std::nullopt; + } + + std::ostream& operator<<(std::ostream& os, const SyncStatus& status) { + os << "download status: "; + if (status.downloading) { + os << "downloading"; + } else if (status.connected) { + os << "connected"; + } else if (status.connecting) { + os << "connecting"; + } else { + os << "idle"; + } + os << ", "; + if (status.download_error.has_value()) { + os << "download error: " << *status.download_error << ", "; + } + + os << "uploading: " << status.uploading; + + if (status.upload_error.has_value()) { + os << ", upload error: " << *status.download_error; + } + return os; + } + + SyncStatus::~SyncStatus() { + internal::powersync_status_free(this->rust_status); } } diff --git a/libpowersync/src/status.rs b/libpowersync/src/status.rs index 9bef4f4..f95944d 100644 --- a/libpowersync/src/status.rs +++ b/libpowersync/src/status.rs @@ -1,13 +1,118 @@ +use crate::crud::StringView; use powersync::ffi::RawPowerSyncDatabase; use powersync::{CallbackListenerHandle, SyncStatusData}; use std::ffi::c_void; use std::sync::Arc; +use std::time::UNIX_EPOCH; + +#[repr(C)] +pub struct RawSyncStatus { + pub connected: bool, + pub connecting: bool, + pub downloading: bool, + pub download_error: StringView, + pub has_download_error: bool, + pub uploading: bool, + pub upload_error: StringView, + pub has_upload_error: bool, +} #[unsafe(no_mangle)] pub extern "C" fn powersync_db_status(db: &RawPowerSyncDatabase) -> *mut c_void { Arc::into_raw(db.sync_status()) as *mut c_void } +#[unsafe(no_mangle)] +pub extern "C" fn powersync_status_inspect( + status: *const c_void, + inspect: extern "C" fn(token: *mut c_void, status: RawSyncStatus), + inspect_token: *mut c_void, +) { + let status = unsafe { &*(status as *const SyncStatusData) }; + let download_error = status.download_error().map(|e| e.to_string()); + let upload_error = status.upload_error().map(|e| e.to_string()); + + inspect( + inspect_token, + RawSyncStatus { + connected: status.is_connected(), + connecting: status.is_connecting(), + downloading: status.is_downloading(), + download_error: StringView::view_optional(download_error.as_deref()), + has_download_error: status.download_error().is_some(), + uploading: status.is_uploading(), + upload_error: StringView::view_optional(upload_error.as_deref()), + has_upload_error: status.upload_error().is_some(), + }, + ); +} + +#[repr(C)] +pub struct RawSyncStreamStatus { + pub name: StringView, + pub parameters: StringView, + pub has_parameters: bool, + pub progress_total: i64, + pub progress_done: i64, + pub has_progress: bool, + pub is_active: bool, + pub is_default: bool, + pub has_explicit_subscription: bool, + pub expires_at: i64, + pub has_expires_at: bool, + pub has_synced: bool, + pub last_synced_at: i64, +} + +#[unsafe(no_mangle)] +pub extern "C" fn powersync_status_streams( + status: *const c_void, + inspect: extern "C" fn(token: *mut c_void, status: &RawSyncStreamStatus), + inspect_token: *mut c_void, +) { + let status = unsafe { &*(status as *const SyncStatusData) }; + for stream in status.streams() { + let desc = stream.subscription.description(); + let progress = stream.progress.as_ref(); + + let status = RawSyncStreamStatus { + name: StringView::view(desc.name), + parameters: StringView::view_optional(desc.parameters.map(|p| p.get())), + has_parameters: desc.parameters.is_some(), + progress_total: progress.map(|p| p.total).unwrap_or_default(), + progress_done: progress.map(|p| p.downloaded).unwrap_or_default(), + has_progress: progress.is_some(), + is_active: stream.subscription.is_active(), + is_default: stream.subscription.is_default(), + has_explicit_subscription: stream.subscription.has_explicit_subscription(), + expires_at: stream + .subscription + .expires_at() + .map(|e| { + let time = e.duration_since(UNIX_EPOCH).unwrap(); + time.as_millis() as i64 + }) + .unwrap_or_default(), + has_expires_at: stream.subscription.expires_at().is_some(), + has_synced: stream.subscription.has_synced(), + last_synced_at: stream + .subscription + .last_synced_at() + .map(|e| { + let time = e.duration_since(UNIX_EPOCH).unwrap(); + time.as_millis() as i64 + }) + .unwrap_or_default(), + }; + inspect(inspect_token, &status); + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn powersync_status_clone(status: *const c_void) { + unsafe { Arc::increment_strong_count(status as *const SyncStatusData) }; +} + #[unsafe(no_mangle)] pub extern "C" fn powersync_status_free(status: *const c_void) { drop(unsafe { Arc::from_raw(status as *const SyncStatusData) }) diff --git a/libpowersync/src/streams.rs b/libpowersync/src/streams.rs new file mode 100644 index 0000000..25f100b --- /dev/null +++ b/libpowersync/src/streams.rs @@ -0,0 +1,13 @@ +use powersync::StreamSubscription; +use std::ffi::c_void; +use std::mem::forget; + +pub extern "C" fn powersync_stream_subscription_clone(subscription: *mut c_void) { + let subscription = unsafe { StreamSubscription::from_raw(subscription) }; + forget(subscription.clone()); // Increment refcount + forget(subscription); // Don't decrement refcount from subscription pointer +} + +pub extern "C" fn powersync_stream_subscription_free(subscription: *mut c_void) { + drop(unsafe { StreamSubscription::from_raw(subscription) }); +} diff --git a/powersync/src/db/streams.rs b/powersync/src/db/streams.rs index a4538b5..a3be226 100644 --- a/powersync/src/db/streams.rs +++ b/powersync/src/db/streams.rs @@ -1,3 +1,5 @@ +use rusqlite::params; +use std::ffi::c_void; use std::{ cell::Cell, collections::HashMap, @@ -5,8 +7,6 @@ use std::{ time::Duration, }; -use rusqlite::params; - use crate::{ PowerSyncDatabase, StreamPriority, db::internal::InnerPowerSyncState, @@ -210,6 +210,7 @@ impl Drop for StreamSubscriptionGroup { } } +#[derive(Clone)] pub struct StreamSubscription { group: Arc, } @@ -232,6 +233,21 @@ impl StreamSubscription { pub fn unsubscribe(self) { drop(self); } + + #[cfg(feature = "ffi")] + pub fn into_raw(self) -> *mut c_void { + Arc::into_raw(self.group) as _ + } + + /// ## Safety + /// + /// The given pointer must have been obtained from [Self::into:raw], and is only valid once. + #[cfg(feature = "ffi")] + pub unsafe fn from_raw(ptr: *mut c_void) -> Self { + Self { + group: unsafe { Arc::from_raw(ptr as _) }, + } + } } impl<'a> From<&'a StreamSubscription> for StreamDescription<'a> { diff --git a/powersync/src/db/watch.rs b/powersync/src/db/watch.rs index d48a0aa..13f8ed7 100644 --- a/powersync/src/db/watch.rs +++ b/powersync/src/db/watch.rs @@ -1,5 +1,4 @@ -use crate::CallbackListenerHandle; -use crate::util::raw_listener::CallbackListeners; +use crate::util::raw_listener::{CallbackListenerHandle, CallbackListeners}; use event_listener::{Event, EventListener}; use futures_lite::{FutureExt, Stream, ready}; use std::sync::Weak; diff --git a/powersync/src/sync/progress.rs b/powersync/src/sync/progress.rs index 6ceaae2..2f085d6 100644 --- a/powersync/src/sync/progress.rs +++ b/powersync/src/sync/progress.rs @@ -6,7 +6,7 @@ use serde::Deserialize; /// been [Self::downloaded] and finally a [Self::fraction] indicating relative progress. #[derive(Deserialize, Debug, Clone)] pub struct ProgressCounters { - /// How many operations need to be downloaded in total for the current donwload to complete. + /// How many operations need to be downloaded in total for the current download to complete. pub total: i64, /// How many operations, out of [Self::total], have already been downloaded. pub downloaded: i64, diff --git a/powersync/src/sync/status.rs b/powersync/src/sync/status.rs index 57bbf4e..35acbf0 100644 --- a/powersync/src/sync/status.rs +++ b/powersync/src/sync/status.rs @@ -1,6 +1,5 @@ use event_listener::{Event, EventListener}; use rusqlite::{Connection, params}; -use std::collections::HashSet; use std::{ fmt::Debug, sync::{ @@ -9,6 +8,7 @@ use std::{ }, }; +use crate::sync::streams::StreamKey; use crate::util::raw_listener::{CallbackListenerHandle, CallbackListeners}; use crate::{ error::PowerSyncError, @@ -44,15 +44,18 @@ impl SyncStatus { } pub(crate) fn update(&self, update: impl FnOnce(&mut SyncStatusData) -> T) -> T { - // Update status. - let mut data = self.data.lock().unwrap(); - let mut new = data.new_revision(); - let res = update(&mut new); - - // Notify async listeners. - let old_state = std::mem::replace(&mut *data, Arc::new(new)); - old_state.is_invalidated.store(true, Ordering::SeqCst); - old_state.invalidated.notify(usize::MAX); + let res = { + // Update status. + let mut data = self.data.lock().unwrap(); + let mut new = data.new_revision(); + let res = update(&mut new); + + // Notify async listeners. + let old_state = std::mem::replace(&mut *data, Arc::new(new)); + old_state.is_invalidated.store(true, Ordering::SeqCst); + old_state.invalidated.notify(usize::MAX); + res + }; // Notify external C++ listeners. self.raw_listeners.notify_all(); From 943d0165fd682700c3fadaa7506c625f88f20163 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 23 Oct 2025 16:16:41 +0200 Subject: [PATCH 11/14] Sync streams --- examples/cpp_queries.cpp | 2 ++ libpowersync/include/powersync.h | 11 ++++++++- libpowersync/src/bindings.h | 10 +++++++++ libpowersync/src/powersync.cpp | 38 ++++++++++++++++++++++++++++++++ libpowersync/src/streams.rs | 27 +++++++++++++++++++++++ powersync/src/db/streams.rs | 17 ++++++++++++++ powersync/src/ffi.rs | 14 ++++++++++-- 7 files changed, 116 insertions(+), 3 deletions(-) diff --git a/examples/cpp_queries.cpp b/examples/cpp_queries.cpp index 54d197f..fa0a3dc 100644 --- a/examples/cpp_queries.cpp +++ b/examples/cpp_queries.cpp @@ -93,6 +93,8 @@ int main() { check_rc(sqlite3_exec(writer, "INSERT INTO users (id, name) VALUES (uuid(), 'Simon');", nullptr, nullptr, nullptr)); } + auto subscription = SyncStream(*db, "users").subscribe(); + { auto stream = db->get_crud_transactions(); while (stream.advance()) { diff --git a/libpowersync/include/powersync.h b/libpowersync/include/powersync.h index fb5fbc6..9a9ba91 100644 --- a/libpowersync/include/powersync.h +++ b/libpowersync/include/powersync.h @@ -286,6 +286,7 @@ class Database { Database(const Database&) = delete; friend class CrudTransaction; + friend class SyncStream; public: Database(Database&& other) noexcept: raw(other.raw), @@ -326,14 +327,22 @@ class SyncStream { SyncStream(const Database& db, const std::string& name): db(db), name(name) {} SyncStream(const Database& db, const std::string& name, std::string parameters): db(db), name(name), parameters(parameters) {} - SyncStreamSubscription subscribe(); + SyncStreamSubscription subscribe() const; }; class SyncStreamSubscription { private: void* rust_subscription; + + SyncStreamSubscription(SyncStream stream, void* rust_subscription); + friend SyncStream; public: + SyncStreamSubscription(const SyncStreamSubscription&); + SyncStreamSubscription(SyncStreamSubscription&&) = delete; + const SyncStream stream; + + ~SyncStreamSubscription(); }; class Exception final : public std::exception { diff --git a/libpowersync/src/bindings.h b/libpowersync/src/bindings.h index 2512b7b..627659b 100644 --- a/libpowersync/src/bindings.h +++ b/libpowersync/src/bindings.h @@ -206,6 +206,16 @@ void *powersync_db_status_listener(const RawPowerSyncDatabase *db, void powersync_db_status_listener_clear(void *listener); +PowerSyncResultCode powersync_stream_subscription_create(const RawPowerSyncDatabase *db, + StringView name, + StringView parameters, + bool has_parameters, + void **out_db); + +void powersync_stream_subscription_clone(void *subscription); + +void powersync_stream_subscription_free(void *subscription); + } // extern "C" } // namespace powersync::internal diff --git a/libpowersync/src/powersync.cpp b/libpowersync/src/powersync.cpp index d2a2e32..e2604c4 100644 --- a/libpowersync/src/powersync.cpp +++ b/libpowersync/src/powersync.cpp @@ -436,4 +436,42 @@ namespace powersync { SyncStatus::~SyncStatus() { internal::powersync_status_free(this->rust_status); } + + SyncStreamSubscription SyncStream::subscribe() const { + void* rust_subscription = nullptr; + internal::StringView parameters = {nullptr, 0}; + if (this->parameters.has_value()) { + parameters.value = this->parameters->data(); + parameters.length = static_cast(this->parameters->size()); + } + + internal::powersync_stream_subscription_create( + &this->db.raw, + { + .value = this->name.data(), + .length = static_cast(this->name.size()), + }, + parameters, + this->parameters.has_value(), + &rust_subscription + ); + + return SyncStreamSubscription(*this, rust_subscription); + } + + SyncStreamSubscription::SyncStreamSubscription(SyncStream stream, void *rust_subscription) + : rust_subscription(rust_subscription), + stream(std::move(stream)) {} + + + SyncStreamSubscription::SyncStreamSubscription(const SyncStreamSubscription& other) + :rust_subscription(other.rust_subscription), stream(other.stream) + { + internal::powersync_stream_subscription_clone(rust_subscription); + } + + SyncStreamSubscription::~SyncStreamSubscription() { + internal::powersync_stream_subscription_free(rust_subscription); + } + } diff --git a/libpowersync/src/streams.rs b/libpowersync/src/streams.rs index 25f100b..1907a5d 100644 --- a/libpowersync/src/streams.rs +++ b/libpowersync/src/streams.rs @@ -1,13 +1,40 @@ +use crate::crud::StringView; +use crate::error::PowerSyncResultCode; +use futures_lite::future; use powersync::StreamSubscription; +use powersync::ffi::RawPowerSyncDatabase; use std::ffi::c_void; use std::mem::forget; +#[unsafe(no_mangle)] +pub extern "C" fn powersync_stream_subscription_create( + db: &RawPowerSyncDatabase, + name: StringView, + parameters: StringView, + has_parameters: bool, + out_db: &mut *mut c_void, +) -> PowerSyncResultCode { + let future = db.create_stream_subscription( + name.as_ref(), + if has_parameters { + Some(parameters.as_ref()) + } else { + None + }, + ); + let subscription = ps_try!(future::block_on(future)); + *out_db = subscription.into_raw(); + PowerSyncResultCode::OK +} + +#[unsafe(no_mangle)] pub extern "C" fn powersync_stream_subscription_clone(subscription: *mut c_void) { let subscription = unsafe { StreamSubscription::from_raw(subscription) }; forget(subscription.clone()); // Increment refcount forget(subscription); // Don't decrement refcount from subscription pointer } +#[unsafe(no_mangle)] pub extern "C" fn powersync_stream_subscription_free(subscription: *mut c_void) { drop(unsafe { StreamSubscription::from_raw(subscription) }); } diff --git a/powersync/src/db/streams.rs b/powersync/src/db/streams.rs index a3be226..dcca490 100644 --- a/powersync/src/db/streams.rs +++ b/powersync/src/db/streams.rs @@ -80,6 +80,23 @@ impl<'a> SyncStream<'a> { } } + pub(crate) fn with_raw_parameters( + db: &'a PowerSyncDatabase, + name: &'a str, + parameters: Option<&str>, + ) -> Result { + let parameters: Option> = match parameters { + None => None, + Some(raw) => serde_json::from_str(raw)?, + }; + + Ok(Self { + db, + name, + parameters, + }) + } + async fn subscription_command<'b>( &self, cmd: &SubscriptionChangeRequest<'b>, diff --git a/powersync/src/ffi.rs b/powersync/src/ffi.rs index 9d77a64..857861c 100644 --- a/powersync/src/ffi.rs +++ b/powersync/src/ffi.rs @@ -4,8 +4,8 @@ use crate::error::PowerSyncError; use crate::sync::coordinator::SyncCoordinator; use crate::util::raw_listener::CallbackListenerHandle; use crate::{ - BackendConnector, CrudTransaction, LeasedConnection, PowerSyncDatabase, SyncOptions, - SyncStatusData, + BackendConnector, CrudTransaction, LeasedConnection, PowerSyncDatabase, StreamSubscription, + SyncOptions, SyncStatusData, SyncStream, }; use futures_lite::Stream; use std::collections::HashSet; @@ -133,6 +133,16 @@ impl RawPowerSyncDatabase { .await } + pub async fn create_stream_subscription( + &self, + name: &str, + encoded_params: Option<&str>, + ) -> Result { + let db = self.clone_into_db(); + let stream = SyncStream::with_raw_parameters(&db, name, encoded_params)?; + stream.subscribe().await + } + /// ## Safety /// /// Must only be called once. From ddb99c95f2653c29caabd2879d5c29be5dee4289 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 27 Oct 2025 19:51:35 +0100 Subject: [PATCH 12/14] Get example to connect --- examples/cpp_queries.cpp | 98 ++++++++++++++++++-------------- libpowersync/build.rs | 1 + libpowersync/include/powersync.h | 70 +++++++++++++++++++++-- libpowersync/src/powersync.cpp | 22 +++---- 4 files changed, 134 insertions(+), 57 deletions(-) diff --git a/examples/cpp_queries.cpp b/examples/cpp_queries.cpp index fa0a3dc..5c24a90 100644 --- a/examples/cpp_queries.cpp +++ b/examples/cpp_queries.cpp @@ -11,7 +11,6 @@ void check_rc(int rc) { } class DemoConnector: public powersync::BackendConnector { -private: std::shared_ptr database; static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) { @@ -38,7 +37,7 @@ class DemoConnector: public powersync::BackendConnector { return; } curl_easy_cleanup(curl); - json parsed_response = response; + json parsed_response = json::parse(response); std::string token = parsed_response["token"]; completion.complete_ok(powersync::PowerSyncCredentials { @@ -49,7 +48,26 @@ class DemoConnector: public powersync::BackendConnector { } void upload_data(powersync::CompletionHandle completion) override { - completion.complete_ok(std::monostate()); + const auto db = this->database; + std::thread([db, completion = std::move(completion)]() mutable { + std::cout << "Starting crud uploads" << std::endl; + + auto transactions = db->get_crud_transactions(); + while (transactions.advance()) { + auto tx = transactions.current(); + std::cout << "Has transaction, id " << *tx.id << std::endl; + for (const auto& item : tx.crud) { + std::cout << "Has item: " << item.table << ": " << item.id << std::endl; + } + + // TODO: Upload items to backend + + tx.complete(); + } + + std::cout << "Done with transactions iteration" << std::endl; + completion.complete_ok(std::monostate()); + }).detach(); } ~DemoConnector() override = default; @@ -62,59 +80,55 @@ int main() { }); Schema schema{}; - schema.tables.emplace_back(Table{"users", { - Column::text("name") + schema.tables.emplace_back(Table{"todos", { + Column::text("description"), + Column::integer("completed"), + Column::text("list_id"), + }}); + schema.tables.emplace_back(Table{"lists", { + Column::text("name"), }}); auto db = std::make_shared(std::move(Database::in_memory(schema))); db->spawn_sync_thread(); - //auto connector = std::make_shared(db); - //db->connect(connector); - auto watcher = db->watch_tables({"users"}, [db] { - std::cout << "Saw change on users table" << std::endl; - auto reader = db->reader(); - sqlite3_stmt *stmt = nullptr; - check_rc(sqlite3_prepare_v2(reader, "SELECT id, name FROM users", -1, &stmt, nullptr)); - - while (sqlite3_step(stmt) == SQLITE_ROW) { - std::cout << sqlite3_column_text(stmt, 0) << ": " << sqlite3_column_text(stmt, 1) << std::endl; - } - sqlite3_finalize(stmt); - }); + auto subscription = SyncStream(*db, "lists").subscribe(); - auto status_watcher = db->watch_sync_status([db]() { + auto status_watcher = db->watch_sync_status([db, subscription]() { const auto status = db->sync_status(); std::cout << "Sync status: " << status << std::endl; - }); - { - auto writer = db->writer(); - check_rc(sqlite3_exec(writer, "INSERT INTO users (id, name) VALUES (uuid(), 'Simon');", nullptr, nullptr, nullptr)); - } + const auto stream_status = status.for_stream(subscription.stream); + if (stream_status.has_value()) { + const auto progress = stream_status->progress;; + std::cout << "Download progress: Has synced: " << stream_status->has_synced; + if (progress.has_value()) { + std::cout << ", progress: " << progress->downloaded << " / " << progress->total << std::endl; + } + } + }); - auto subscription = SyncStream(*db, "users").subscribe(); + auto connector = std::make_shared(db); + db->connect(connector); - { - auto stream = db->get_crud_transactions(); - while (stream.advance()) { - auto tx = stream.current(); - std::cout << "Has transaction, id " << *tx.id << std::endl; - for (const auto& item : tx.crud) { - std::cout << "Has item: " << item.table << ": " << item.id << std::endl; - } + auto watcher = db->watch_tables({"lists"}, [db] { + std::cout << "Saw change on lists table" << std::endl; + auto reader = db->reader(); + sqlite3_stmt *stmt = nullptr; + check_rc(sqlite3_prepare_v2(reader, "SELECT id, name FROM lists", -1, &stmt, nullptr)); - tx.complete(); + while (sqlite3_step(stmt) == SQLITE_ROW) { + std::cout << sqlite3_column_text(stmt, 0) << ": " << sqlite3_column_text(stmt, 1) << std::endl; } + sqlite3_finalize(stmt); + }); - std::cout << "Done with first transactions iteration" << std::endl; + for (std::string line; std::getline(std::cin, line);) { + // TODO: Handle adding lists } - // Should have no further transactions now, we've completed them. - { - auto stream = db->get_crud_transactions(); - auto has_tx = stream.advance(); - - std::cout << "Has transaction: " << has_tx << std::endl; - } + //{ + // auto writer = db->writer(); + // check_rc(sqlite3_exec(writer, "INSERT INTO users (id, name) VALUES (uuid(), 'Simon');", nullptr, nullptr, nullptr)); + //} } diff --git a/libpowersync/build.rs b/libpowersync/build.rs index 996d0d9..229f0af 100644 --- a/libpowersync/build.rs +++ b/libpowersync/build.rs @@ -18,5 +18,6 @@ fn main() { .file("src/powersync.cpp") .link_lib_modifier("+whole-archive") .compile("powersync_bridge"); + println!("cargo::rerun-if-changed=include/powersync.h"); println!("cargo::rerun-if-changed=src/powersync.cpp"); } diff --git a/libpowersync/include/powersync.h b/libpowersync/include/powersync.h index 9a9ba91..06e9c71 100644 --- a/libpowersync/include/powersync.h +++ b/libpowersync/include/powersync.h @@ -9,15 +9,24 @@ #include namespace powersync { + /// Internal definitions used in PowerSync classes. These are not supposed to be used directly. namespace internal { struct RawPowerSyncDatabase { void* sync; void* inner; }; + /// A completion handle received from Rust. + /// + /// Completion handles are used to bridge asynchronous Rust and C++. Each handle is conceptually an oneshot channel + /// in Rust. Sending a message here or dropping the handle sends a message to an asynchronous Rust function. struct RawCompletionHandle { void* rust_handle; + RawCompletionHandle(void* handle) : rust_handle(handle) {} + RawCompletionHandle(const RawCompletionHandle& other) = delete; + RawCompletionHandle(RawCompletionHandle&& other) noexcept; + void send_empty(); void send_credentials(const char* endpoint, const char* token); void send_error_code(int code); @@ -27,33 +36,66 @@ namespace powersync { }; } + /// Type of a local write. enum class UpdateType { + /// Insert or replace a row. All non-null columns are included in CrudEntry#data. PUT = 1, + /// Update a row if it exists. All updated columns are included in CrudEntry#data. PATCH = 2, + /// Delete a row if it exists. DELETE = 3, }; + /// A single client-side change. struct CrudEntry { + /// Auto-incrementing client-side id. + /// + /// Reset whenever the database is re-created. int64_t client_id; + /// Auto-incrementing transaction id. This is the same for all operations within the same transaction. int64_t transaction_id; + /// Type of change. UpdateType update_type; + /// Table that contained the change. std::string table; + /// ID of the changed row. std::string id; + /// An optional metadata string attached to this entry at the time the write has been issued. + /// + /// For tables where Table#track_metadata is enabled, a hidden `_metadata` column is added to this table that can be + /// used during updates to attach a hint to the update that is preserved here. std::optional metadata; + /// Data associated with the change, rendered as a serialized JSON object. + /// + /// - For PUT, this contains all non-null columns of the row. + /// - For PATCH, this contains the columns that changed. + /// - For DELETE, this is a `std::nullopt`. std::optional data; + /// Old values before an update. + /// + /// This si only tracked for tables for which this has been enabled by setting Table#track_previous_values. std::optional previous_values; }; class Database; + /// A transaction containing all local writes made in a SQLite transaction. class CrudTransaction { public: + /// The database on which this transaction has been recorded. const Database& db; + /// The CrudEntry#client_id of the last write made as part of the transaction. int64_t last_item_id; + /// The id of the transaction. std::optional id; + /// All CRUD entries that have been recorded in this transaction. std::vector crud; + /// Marks the transaction as completed. This should be called after uploading the writes to your server. void complete() const; + /// Marks the transaction as completed with a custom write checkpoint. + /// + /// For details, see [custom write checkpoints](https://docs.powersync.com/usage/use-case-examples/custom-write-checkpoints). void complete(std::optional custom_write_checkpoint) const; }; @@ -65,16 +107,26 @@ namespace powersync { Trace = 4, }; + /// Installs a logger (via a static callback function) that PowerSync will use to emit internal log lines. + /// + /// Additionally, the LogLevel parameter can be used to control the minimum severity for which PowerSync will emit + /// log items. + /// + /// This function may only be called once. void set_logger(LogLevel level, void(*logger)(LogLevel, const char*)); + /// The column types supported by PowerSync. enum ColumnType { TEXT, INTEGER, REAL }; + /// A column as part of a \ref Table managed by PowerSync. struct Column { + /// The name of the column in SQL. std::string name; + /// The supported column type. ColumnType type; static Column text(std::string name) { @@ -88,8 +140,13 @@ namespace powersync { } }; + /// A table as part of the \ref Schema managed by PowerSync. struct Table { + /// The name of the table in SQL. std::string name; + /// All data columns that are part of the table. + /// + /// With PowerSync, each table also has an implicit `id TEXT NOT NULL` column std::vector columns; //std::vector indices = {}; bool local_only = false; @@ -123,25 +180,28 @@ struct LeasedConnection { template struct CompletionHandle { private: - internal::RawCompletionHandle handle; + // Always present unless the handle has been moved out. + std::optional handle; public: explicit CompletionHandle(internal::RawCompletionHandle handle): handle(std::move(handle)) {} + CompletionHandle(const CompletionHandle&) = delete; + CompletionHandle(CompletionHandle&& other) noexcept : handle(std::move(other.handle)) {} void complete_ok(T result); void complete_error(int code) { - this->handle.send_error_code(code); + this->handle->send_error_code(code); } void complete_error(int code, const std::string& description) { - this->handle.send_error_message(code, description.c_str()); + this->handle->send_error_message(code, description.c_str()); } }; template<> inline void CompletionHandle::complete_ok(std::monostate _result) { - this->handle.send_empty(); + this->handle->send_empty(); } struct PowerSyncCredentials { @@ -151,7 +211,7 @@ struct PowerSyncCredentials { template<> inline void CompletionHandle::complete_ok(PowerSyncCredentials credentials) { - this->handle.send_credentials(credentials.endpoint.c_str(), credentials.token.c_str()); + this->handle->send_credentials(credentials.endpoint.c_str(), credentials.token.c_str()); } /// Note that methods on this class may be called from multiple threads, or concurrently. Backend connectors must thus diff --git a/libpowersync/src/powersync.cpp b/libpowersync/src/powersync.cpp index e2604c4..d8d42d8 100644 --- a/libpowersync/src/powersync.cpp +++ b/libpowersync/src/powersync.cpp @@ -40,6 +40,10 @@ namespace powersync { return resolve_string(source); } + internal::RawCompletionHandle::RawCompletionHandle(RawCompletionHandle &&other) noexcept: rust_handle(other.rust_handle) { + other.rust_handle = nullptr; + } + void internal::RawCompletionHandle::send_credentials(const char* endpoint, const char* token) { powersync_completion_handle_complete_credentials(&this->rust_handle, endpoint, token); } @@ -57,7 +61,9 @@ namespace powersync { } internal::RawCompletionHandle::~RawCompletionHandle() { - powersync_completion_handle_free(&this->rust_handle); + if (this->rust_handle) { + powersync_completion_handle_free(&this->rust_handle); + } } @@ -156,20 +162,16 @@ namespace powersync { static void upload_data_impl(CppConnector* connector, internal::CppCompletionHandle handle) { auto raw = static_cast(connector); - CompletionHandle wrapped(internal::RawCompletionHandle { - .rust_handle = handle - }); + internal::RawCompletionHandle raw_handle(handle); - raw->connector->upload_data(wrapped); + CompletionHandle wrapped(handle); + raw->connector->upload_data(std::move(wrapped)); } static void fetch_credentials_impl(CppConnector* connector, internal::CppCompletionHandle handle) { auto raw = static_cast(connector); - CompletionHandle wrapped(internal::RawCompletionHandle { - .rust_handle = handle - }); - - raw->connector->fetch_token(wrapped); + CompletionHandle wrapped(handle); + raw->connector->fetch_token(std::move(wrapped)); } static void drop_impl(CppConnector* connector) { From 7abda990fe84ecacec7be9f9f9ef30f71f63b76d Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 9 Nov 2025 10:29:49 -0800 Subject: [PATCH 13/14] Demo improvements --- examples/cpp_queries.cpp | 66 +++++++++++++++++++++++--- examples/egui_todolist/src/database.rs | 4 +- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/examples/cpp_queries.cpp b/examples/cpp_queries.cpp index 5c24a90..431a119 100644 --- a/examples/cpp_queries.cpp +++ b/examples/cpp_queries.cpp @@ -54,14 +54,63 @@ class DemoConnector: public powersync::BackendConnector { auto transactions = db->get_crud_transactions(); while (transactions.advance()) { + using json = nlohmann::json; + auto tx = transactions.current(); + json entries = json::array({}); + std::cout << "Has transaction, id " << *tx.id << std::endl; for (const auto& item : tx.crud) { std::cout << "Has item: " << item.table << ": " << item.id << std::endl; + + json entry; + switch (item.update_type) { + case powersync::UpdateType::PUT: + entry["op"] = "PUT"; + break; + case powersync::UpdateType::PATCH: + entry["op"] = "PATCH"; + break; + case powersync::UpdateType::DELETE: + entry["op"] = "DELETE"; + break; + } + + entry["table"] = item.table; + entry["id"] = item.id; + if (item.data.has_value()) { + entry["data"] = json::parse(item.data.value()); + } + entries.push_back(entry); } - // TODO: Upload items to backend + const auto curl = curl_easy_init(); + std::string response; + const auto headers = curl_slist_append(nullptr, "Content-Type: application/json"); + + auto request_body = json::object({"batch", entries}); + auto serialized_body = request_body.dump(); + + curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:6060/api/data"); + curl_easy_setopt(curl, CURLOPT_POST, 1); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, serialized_body.c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + if (auto res = curl_easy_perform(curl); res != CURLE_OK) { + completion.complete_error(res, "CURL request failed"); + return; + } + long code; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); + if (code != 200) { + completion.complete_error(static_cast(code), "Unexpected response code, body was: " + response); + } + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); tx.complete(); } @@ -125,10 +174,15 @@ int main() { for (std::string line; std::getline(std::cin, line);) { // TODO: Handle adding lists - } + auto writer = db->writer(); + sqlite3_stmt *stmt; - //{ - // auto writer = db->writer(); - // check_rc(sqlite3_exec(writer, "INSERT INTO users (id, name) VALUES (uuid(), 'Simon');", nullptr, nullptr, nullptr)); - //} + check_rc(sqlite3_prepare_v3(writer, "INSERT INTO lists (id, name) VALUES (uuid(), ?)", -1, 0, &stmt, nullptr)); + check_rc(sqlite3_bind_text(stmt, 1, line.c_str(), static_cast(line.length()), SQLITE_TRANSIENT)); + + const auto rc = sqlite3_step(stmt); + if (rc != SQLITE_DONE) { + throw std::runtime_error("SQLite error: " + std::string(sqlite3_errstr(rc))); + } + } } diff --git a/examples/egui_todolist/src/database.rs b/examples/egui_todolist/src/database.rs index 1da3376..82bebb1 100644 --- a/examples/egui_todolist/src/database.rs +++ b/examples/egui_todolist/src/database.rs @@ -40,7 +40,7 @@ impl TodoEntry { } pub fn fetch_in_list(conn: &Connection, list_id: &str) -> Result, PowerSyncError> { - let mut stmt = conn.prepare("SELECT * FROM todos WHERE list_id = ?")?; + let mut stmt = conn.prepare("SELECT * FROM todos WHERE list_id = ? ORDER BY id")?; let mut rows = stmt.query(params![list_id])?; let mut results = vec![]; @@ -68,7 +68,7 @@ impl TodoList { } pub fn fetch_all(conn: &Connection) -> Result, PowerSyncError> { - let mut stmt = conn.prepare("SELECT * FROM lists")?; + let mut stmt = conn.prepare("SELECT * FROM lists ORDER BY id")?; let mut rows = stmt.query(params![])?; let mut results = vec![]; From 518db9aa950ceee85358361fbf2ec0b49c364dd1 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 9 Nov 2025 10:52:27 -0800 Subject: [PATCH 14/14] Fix crash after uploading data --- examples/cpp_queries.cpp | 9 ++++++--- libpowersync/src/powersync.cpp | 2 -- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/cpp_queries.cpp b/examples/cpp_queries.cpp index 431a119..4597cc2 100644 --- a/examples/cpp_queries.cpp +++ b/examples/cpp_queries.cpp @@ -88,7 +88,7 @@ class DemoConnector: public powersync::BackendConnector { std::string response; const auto headers = curl_slist_append(nullptr, "Content-Type: application/json"); - auto request_body = json::object({"batch", entries}); + auto request_body = json::object({{"batch", entries}}); auto serialized_body = request_body.dump(); curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:6060/api/data"); @@ -107,6 +107,7 @@ class DemoConnector: public powersync::BackendConnector { curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); if (code != 200) { completion.complete_error(static_cast(code), "Unexpected response code, body was: " + response); + return; } curl_easy_cleanup(curl); @@ -136,6 +137,7 @@ int main() { }}); schema.tables.emplace_back(Table{"lists", { Column::text("name"), + Column::text("owner_id"), }}); auto db = std::make_shared(std::move(Database::in_memory(schema))); @@ -152,8 +154,9 @@ int main() { const auto progress = stream_status->progress;; std::cout << "Download progress: Has synced: " << stream_status->has_synced; if (progress.has_value()) { - std::cout << ", progress: " << progress->downloaded << " / " << progress->total << std::endl; + std::cout << ", progress: " << progress->downloaded << " / " << progress->total; } + std::cout << std::endl; } }); @@ -177,7 +180,7 @@ int main() { auto writer = db->writer(); sqlite3_stmt *stmt; - check_rc(sqlite3_prepare_v3(writer, "INSERT INTO lists (id, name) VALUES (uuid(), ?)", -1, 0, &stmt, nullptr)); + check_rc(sqlite3_prepare_v3(writer, "INSERT INTO lists (id, owner_id, name) VALUES (uuid(), uuid(), ?)", -1, 0, &stmt, nullptr)); check_rc(sqlite3_bind_text(stmt, 1, line.c_str(), static_cast(line.length()), SQLITE_TRANSIENT)); const auto rc = sqlite3_step(stmt); diff --git a/libpowersync/src/powersync.cpp b/libpowersync/src/powersync.cpp index d8d42d8..e273283 100644 --- a/libpowersync/src/powersync.cpp +++ b/libpowersync/src/powersync.cpp @@ -66,7 +66,6 @@ namespace powersync { } } - const char * Exception::what() const noexcept { if (this->msg) { return this->msg; @@ -162,7 +161,6 @@ namespace powersync { static void upload_data_impl(CppConnector* connector, internal::CppCompletionHandle handle) { auto raw = static_cast(connector); - internal::RawCompletionHandle raw_handle(handle); CompletionHandle wrapped(handle); raw->connector->upload_data(std::move(wrapped));