Skip to content

Commit dbf4cbb

Browse files
authored
Don't create transactions automatically (#191)
1 parent f53c9c6 commit dbf4cbb

16 files changed

Lines changed: 182 additions & 140 deletions

crates/core/src/error.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ impl PowerSyncError {
125125
match self.inner.as_ref() {
126126
Sqlite(desc) => desc.code,
127127
ArgumentError { .. } => ResultCode::CONSTRAINT_DATATYPE,
128-
StateError { .. } => ResultCode::MISUSE,
128+
StateError { .. } | MustBeCalledInTransaction { .. } => ResultCode::MISUSE,
129129
MissingClientId
130130
| SyncProtocolError { .. }
131131
| DownMigrationDidNotUpdateVersion { .. }
@@ -193,7 +193,7 @@ impl From<ResultCode> for PowerSyncError {
193193

194194
/// A structured enumeration of possible errors that can occur in the core extension.
195195
#[derive(Error, Debug)]
196-
enum RawPowerSyncError {
196+
pub enum RawPowerSyncError {
197197
/// An internal call to SQLite made by the core extension has failed. We store the original
198198
/// result code and an optional context describing what the core extension was trying to do when
199199
/// the error occurred.
@@ -248,10 +248,12 @@ enum RawPowerSyncError {
248248
libversion_number: c_int,
249249
libversion: &'static str,
250250
},
251+
#[error("This function may only be called in transactions.")]
252+
MustBeCalledInTransaction,
251253
}
252254

253255
#[derive(Debug)]
254-
struct SqliteError {
256+
pub struct SqliteError {
255257
code: ResultCode,
256258
errstr: Option<String>,
257259
context: Option<Cow<'static, str>>,

crates/core/src/macros.rs

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -43,42 +43,3 @@ macro_rules! create_sqlite_optional_text_fn {
4343
}
4444
};
4545
}
46-
47-
// Wrap a function in an auto-transaction.
48-
// Gives the equivalent of SQLite's auto-commit behaviour, except that applies to all statements
49-
// inside the function. Otherwise, each statement inside the function would be a transaction on its
50-
// own if the function itself is not wrapped in a transaction.
51-
#[macro_export]
52-
macro_rules! create_auto_tx_function {
53-
($fn_name:ident, $fn_impl_name:ident) => {
54-
fn $fn_name(
55-
ctx: *mut sqlite::context,
56-
args: &[*mut sqlite::value],
57-
) -> Result<String, PowerSyncError> {
58-
let db = ctx.db_handle();
59-
60-
// Auto-start a transaction if we're not in a transaction
61-
let started_tx = if db.get_autocommit() {
62-
db.exec_safe("BEGIN")?;
63-
true
64-
} else {
65-
false
66-
};
67-
68-
let result = $fn_impl_name(ctx, args);
69-
if result.is_err() {
70-
// Always ROLLBACK, even when we didn't start the transaction.
71-
// Otherwise the user may be able to continue the transaction and end up in an inconsistent state.
72-
// We ignore rollback errors.
73-
if !db.get_autocommit() {
74-
let _ignore = db.exec_safe("ROLLBACK");
75-
}
76-
} else if started_tx {
77-
// Only COMMIT our own transactions.
78-
db.exec_safe("COMMIT")?;
79-
}
80-
81-
result
82-
}
83-
};
84-
}

crates/core/src/schema/management.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ use powersync_sqlite_nostd as sqlite;
1313
use powersync_sqlite_nostd::Context;
1414
use sqlite::{Connection, ResultCode, Value};
1515

16+
use crate::create_sqlite_text_fn;
1617
use crate::error::{PSResult, PowerSyncError};
1718
use crate::ext::ExtendedDatabase;
1819
use crate::schema::inspection::{ExistingTable, ExistingView};
1920
use crate::schema::table_info::Index;
2021
use crate::state::DatabaseState;
21-
use crate::utils::SqlBuffer;
22+
use crate::utils::{SqlBuffer, verify_in_transaction};
2223
use crate::views::{
2324
powersync_trigger_delete_sql, powersync_trigger_insert_sql, powersync_trigger_update_sql,
2425
powersync_view_sql,
2526
};
26-
use crate::{create_auto_tx_function, create_sqlite_text_fn};
2727

2828
use super::Schema;
2929

@@ -265,13 +265,14 @@ fn powersync_replace_schema_impl(
265265
ctx: *mut sqlite::context,
266266
args: &[*mut sqlite::value],
267267
) -> Result<String, PowerSyncError> {
268+
let db = ctx.db_handle();
269+
verify_in_transaction(db)?;
270+
268271
let schema = args[0].text();
269272
let state = unsafe { DatabaseState::from_context(&ctx) };
270273
let parsed_schema =
271274
serde_json::from_str::<Schema>(schema).map_err(PowerSyncError::as_argument_error)?;
272275

273-
let db = ctx.db_handle();
274-
275276
// language=SQLite
276277
db.exec_safe("SELECT powersync_init()").into_db_result(db)?;
277278

@@ -283,10 +284,9 @@ fn powersync_replace_schema_impl(
283284
Ok(String::from(""))
284285
}
285286

286-
create_auto_tx_function!(powersync_replace_schema_tx, powersync_replace_schema_impl);
287287
create_sqlite_text_fn!(
288288
powersync_replace_schema,
289-
powersync_replace_schema_tx,
289+
powersync_replace_schema_impl,
290290
"powersync_replace_schema"
291291
);
292292

crates/core/src/sync/interface.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use serde_json::value::RawValue;
2222
use sqlite::{ResultCode, Value};
2323

2424
use crate::sync::BucketPriority;
25-
use crate::utils::JsonString;
25+
use crate::utils::{JsonString, verify_in_transaction};
2626

2727
/// Payload provided by SDKs when requesting a sync iteration.
2828
#[derive(Deserialize)]
@@ -206,7 +206,7 @@ pub fn register(db: *mut sqlite::sqlite3, state: Rc<DatabaseState>) -> Result<()
206206
) -> () {
207207
let result = (|| -> Result<(), PowerSyncError> {
208208
let db = ctx.db_handle();
209-
debug_assert!(!db.get_autocommit());
209+
verify_in_transaction(db)?;
210210

211211
let state = unsafe { DatabaseState::from_context(&ctx) };
212212
let args = sqlite::args!(argc, argv);

crates/core/src/utils/mod.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,28 @@ mod sql_buffer;
33
use core::{cmp::Ordering, fmt::Display, hash::Hash};
44

55
use alloc::{boxed::Box, string::String};
6-
use powersync_sqlite_nostd::{ColumnType, ManagedStmt};
6+
use powersync_sqlite_nostd::{ColumnType, Connection, ManagedStmt, sqlite3};
77
use serde::Serialize;
88
use serde_json::value::RawValue;
99
pub use sql_buffer::{InsertIntoCrud, SqlBuffer, WriteType};
1010

11-
use crate::error::PowerSyncError;
11+
use crate::error::{PowerSyncError, RawPowerSyncError};
1212
use uuid::Uuid;
1313

14+
#[cold]
15+
fn must_be_in_tx_error() -> PowerSyncError {
16+
return RawPowerSyncError::MustBeCalledInTransaction.into();
17+
}
18+
19+
#[inline]
20+
pub fn verify_in_transaction(db: *mut sqlite3) -> Result<(), PowerSyncError> {
21+
if db.get_autocommit() {
22+
return Err(must_be_in_tx_error());
23+
}
24+
25+
Ok(())
26+
}
27+
1428
/// Calls [read] to read a column if it's not null, otherwise returns [None].
1529
#[inline]
1630
pub fn column_nullable<T, R: FnOnce() -> Result<T, PowerSyncError>>(

crates/core/src/view_admin.rs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ use powersync_sqlite_nostd as sqlite;
1010
use powersync_sqlite_nostd::{Connection, Context};
1111
use sqlite::{ResultCode, Value};
1212

13+
use crate::create_sqlite_text_fn;
1314
use crate::error::{PSResult, PowerSyncError};
1415
use crate::migrations::{LATEST_VERSION, powersync_migrate};
1516
use crate::schema::inspection::ExistingView;
1617
use crate::state::DatabaseState;
17-
use crate::utils::SqlBuffer;
18-
use crate::{create_auto_tx_function, create_sqlite_text_fn};
18+
use crate::utils::{SqlBuffer, verify_in_transaction};
1919

2020
// Used in old down migrations, do not remove.
2121
extern "C" fn powersync_drop_view(
@@ -35,6 +35,8 @@ fn powersync_init_impl(
3535
ctx: *mut sqlite::context,
3636
_args: &[*mut sqlite::value],
3737
) -> Result<String, PowerSyncError> {
38+
let db = ctx.db_handle();
39+
verify_in_transaction(db)?;
3840
powersync_migrate(ctx, LATEST_VERSION)?;
3941

4042
// Register the powersync_internal_close vtab to implement a "pre-close hook".
@@ -45,23 +47,24 @@ fn powersync_init_impl(
4547
Ok(String::from(""))
4648
}
4749

48-
create_auto_tx_function!(powersync_init_tx, powersync_init_impl);
49-
create_sqlite_text_fn!(powersync_init, powersync_init_tx, "powersync_init");
50+
create_sqlite_text_fn!(powersync_init, powersync_init_impl, "powersync_init");
5051

5152
fn powersync_test_migration_impl(
5253
ctx: *mut sqlite::context,
5354
args: &[*mut sqlite::value],
5455
) -> Result<String, PowerSyncError> {
56+
let db = ctx.db_handle();
57+
verify_in_transaction(db)?;
58+
5559
let target_version = args[0].int();
5660
powersync_migrate(ctx, target_version)?;
5761

5862
Ok(String::from(""))
5963
}
6064

61-
create_auto_tx_function!(powersync_test_migration_tx, powersync_test_migration_impl);
6265
create_sqlite_text_fn!(
6366
powersync_test_migration,
64-
powersync_test_migration_tx,
67+
powersync_test_migration_impl,
6568
"powersync_test_migration"
6669
);
6770

@@ -70,6 +73,7 @@ fn powersync_clear_impl(
7073
args: &[*mut sqlite::value],
7174
) -> Result<String, PowerSyncError> {
7275
let local_db = ctx.db_handle();
76+
verify_in_transaction(local_db)?;
7377
let state = unsafe { DatabaseState::from_context(&ctx) };
7478

7579
let flags = PowerSyncClearFlags(args[0].int());
@@ -176,6 +180,8 @@ fn powersync_trigger_resync_impl(
176180
args: &[*mut sqlite::value],
177181
) -> Result<String, PowerSyncError> {
178182
let local_db = ctx.db_handle();
183+
verify_in_transaction(local_db)?;
184+
179185
let state = unsafe { DatabaseState::from_context(&ctx) };
180186
trigger_resync(local_db, state)?;
181187

@@ -187,10 +193,9 @@ fn powersync_trigger_resync_impl(
187193
Ok(Default::default())
188194
}
189195

190-
create_auto_tx_function!(powersync_trigger_resync_tx, powersync_trigger_resync_impl);
191196
create_sqlite_text_fn!(
192197
powersync_trigger_resync,
193-
powersync_trigger_resync_tx,
198+
powersync_trigger_resync_impl,
194199
"powersync_trigger_resync"
195200
);
196201

@@ -210,8 +215,7 @@ impl PowerSyncClearFlags {
210215
}
211216
}
212217

213-
create_auto_tx_function!(powersync_clear_tx, powersync_clear_impl);
214-
create_sqlite_text_fn!(powersync_clear, powersync_clear_tx, "powersync_clear");
218+
create_sqlite_text_fn!(powersync_clear, powersync_clear_impl, "powersync_clear");
215219

216220
pub fn register(db: *mut sqlite::sqlite3, state: Rc<DatabaseState>) -> Result<(), ResultCode> {
217221
// This entire module is just making it easier to edit sqlite_master using queries.

0 commit comments

Comments
 (0)