Skip to content

Commit 5cdf4b3

Browse files
authored
Merge pull request #71 from JeanArhancet/feat/autocommit-3-12
feat: add autocommit python3.12
2 parents f11fe04 + 4b1d175 commit 5cdf4b3

File tree

7 files changed

+287
-64
lines changed

7 files changed

+287
-64
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22
target
33
*.db
44
client_wal_index
5-
tests/__pycache__
5+
tests/__pycache__
6+
venv
7+
env

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+5
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ pyo3 = "0.19.0"
1212
libsql = { git = "https://github.com/tursodatabase/libsql/", rev = "21f405b087b210734367fb1343ed436249c8dc10", features = ["encryption"] }
1313
tokio = { version = "1.29.1", features = [ "rt-multi-thread" ] }
1414
tracing-subscriber = "0.3"
15+
16+
[build-dependencies]
17+
version_check = "0.9.5"
18+
# used where logic has to be version/distribution specific, e.g. pypy
19+
pyo3-build-config = { version = "0.19.0" }

build.rs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub fn main() {
2+
pyo3_build_config::use_pyo3_cfgs();
3+
}

docs/api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ Unimplemented.
123123

124124
### autocommit
125125

126-
The driver only supports the legacy `isolation_level`-based transaction mode control and does not implement the `autocommit` property yet (see issue [#30](https://github.com/libsql/libsql-experimental-python/issues/30) for details).
126+
Starting from Python 3.12, the autocommit property is supported, controlling PEP 249 transaction handling behavior. By default, autocommit is set to LEGACY_TRANSACTION_CONTROL, but this will change to False in a future Python release. For more details, refer to [Connection.autocommit](https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.autocommit) in the official Python documentation.
127127

128128
### in_transaction
129129

src/lib.rs

+117-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use pyo3::types::{PyList, PyTuple};
66
use std::cell::RefCell;
77
use std::sync::Arc;
88

9+
const LEGACY_TRANSACTION_CONTROL: i32 = -1;
10+
911
fn to_py_err(error: libsql_core::errors::Error) -> PyErr {
1012
let msg = match error {
1113
libsql::Error::SqliteFailure(_, err) => err,
@@ -19,6 +21,7 @@ fn is_remote_path(path: &str) -> bool {
1921
}
2022

2123
#[pyfunction]
24+
#[cfg(not(Py_3_12))]
2225
#[pyo3(signature = (database, isolation_level="DEFERRED".to_string(), check_same_thread=true, uri=false, sync_url=None, sync_interval=None, auth_token="", encryption_key=None))]
2326
fn connect(
2427
py: Python<'_>,
@@ -30,6 +33,69 @@ fn connect(
3033
sync_interval: Option<f64>,
3134
auth_token: &str,
3235
encryption_key: Option<String>,
36+
) -> PyResult<Connection> {
37+
let conn = _connect_core(
38+
py,
39+
database,
40+
isolation_level,
41+
check_same_thread,
42+
uri,
43+
sync_url,
44+
sync_interval,
45+
auth_token,
46+
encryption_key,
47+
)?;
48+
Ok(conn)
49+
}
50+
51+
#[pyfunction]
52+
#[cfg(Py_3_12)]
53+
#[pyo3(signature = (database, isolation_level="DEFERRED".to_string(), check_same_thread=true, uri=false, sync_url=None, sync_interval=None, auth_token="", encryption_key=None, autocommit = LEGACY_TRANSACTION_CONTROL))]
54+
fn connect(
55+
py: Python<'_>,
56+
database: String,
57+
isolation_level: Option<String>,
58+
check_same_thread: bool,
59+
uri: bool,
60+
sync_url: Option<String>,
61+
sync_interval: Option<f64>,
62+
auth_token: &str,
63+
encryption_key: Option<String>,
64+
autocommit: i32,
65+
) -> PyResult<Connection> {
66+
let mut conn = _connect_core(
67+
py,
68+
database,
69+
isolation_level.clone(),
70+
check_same_thread,
71+
uri,
72+
sync_url,
73+
sync_interval,
74+
auth_token,
75+
encryption_key,
76+
)?;
77+
78+
conn.autocommit =
79+
if autocommit == LEGACY_TRANSACTION_CONTROL || autocommit == 1 || autocommit == 0 {
80+
autocommit
81+
} else {
82+
return Err(PyValueError::new_err(
83+
"autocommit must be True, False, or sqlite3.LEGACY_TRANSACTION_CONTROL",
84+
));
85+
};
86+
Ok(conn)
87+
}
88+
89+
fn _connect_core(
90+
py: Python<'_>,
91+
database: String,
92+
isolation_level: Option<String>,
93+
check_same_thread: bool,
94+
uri: bool,
95+
sync_url: Option<String>,
96+
sync_interval: Option<f64>,
97+
auth_token: &str,
98+
encryption_key: Option<String>,
3399
) -> PyResult<Connection> {
34100
let ver = env!("CARGO_PKG_VERSION");
35101
let ver = format!("libsql-python-rpc-{ver}");
@@ -74,7 +140,8 @@ fn connect(
74140
}
75141
}
76142
};
77-
let autocommit = isolation_level.is_none();
143+
144+
let autocommit = isolation_level.is_none() as i32;
78145
let conn = db.connect().map_err(to_py_err)?;
79146
Ok(Connection {
80147
db,
@@ -121,7 +188,7 @@ pub struct Connection {
121188
conn: Arc<ConnectionGuard>,
122189
rt: tokio::runtime::Runtime,
123190
isolation_level: Option<String>,
124-
autocommit: bool,
191+
autocommit: i32,
125192
}
126193

127194
// SAFETY: The libsql crate guarantees that `Connection` is thread-safe.
@@ -138,6 +205,7 @@ impl Connection {
138205
rows: RefCell::new(None),
139206
rowcount: RefCell::new(0),
140207
autocommit: self.autocommit,
208+
isolation_level: self.isolation_level.clone(),
141209
done: RefCell::new(false),
142210
})
143211
}
@@ -218,8 +286,30 @@ impl Connection {
218286

219287
#[getter]
220288
fn in_transaction(self_: PyRef<'_, Self>) -> PyResult<bool> {
289+
#[cfg(Py_3_12)]
290+
{
291+
return Ok(!self_.conn.is_autocommit() || self_.autocommit == 0);
292+
}
221293
Ok(!self_.conn.is_autocommit())
222294
}
295+
296+
#[getter]
297+
#[cfg(Py_3_12)]
298+
fn get_autocommit(self_: PyRef<'_, Self>) -> PyResult<i32> {
299+
Ok(self_.autocommit)
300+
}
301+
302+
#[setter]
303+
#[cfg(Py_3_12)]
304+
fn set_autocommit(mut self_: PyRefMut<'_, Self>, autocommit: i32) -> PyResult<()> {
305+
if autocommit != LEGACY_TRANSACTION_CONTROL && autocommit != 1 && autocommit != 0 {
306+
return Err(PyValueError::new_err(
307+
"autocommit must be True, False, or sqlite3.LEGACY_TRANSACTION_CONTROL",
308+
));
309+
}
310+
self_.autocommit = autocommit;
311+
Ok(())
312+
}
223313
}
224314

225315
#[pyclass]
@@ -232,7 +322,8 @@ pub struct Cursor {
232322
rows: RefCell<Option<libsql_core::Rows>>,
233323
rowcount: RefCell<i64>,
234324
done: RefCell<bool>,
235-
autocommit: bool,
325+
isolation_level: Option<String>,
326+
autocommit: i32,
236327
}
237328

238329
// SAFETY: The libsql crate guarantees that `Connection` is thread-safe.
@@ -265,12 +356,13 @@ impl Cursor {
265356
Ok(self_)
266357
}
267358

268-
fn executescript<'a>(self_: PyRef<'a, Self>, script: String) -> PyResult<pyo3::PyRef<'a, Self>> {
359+
fn executescript<'a>(
360+
self_: PyRef<'a, Self>,
361+
script: String,
362+
) -> PyResult<pyo3::PyRef<'a, Self>> {
269363
self_
270364
.rt
271-
.block_on(async {
272-
self_.conn.execute_batch(&script).await
273-
})
365+
.block_on(async { self_.conn.execute_batch(&script).await })
274366
.map_err(to_py_err)?;
275367
Ok(self_)
276368
}
@@ -403,7 +495,8 @@ async fn begin_transaction(conn: &libsql_core::Connection) -> PyResult<()> {
403495

404496
async fn execute(cursor: &Cursor, sql: String, parameters: Option<&PyTuple>) -> PyResult<()> {
405497
let stmt_is_dml = stmt_is_dml(&sql);
406-
if !cursor.autocommit && stmt_is_dml && cursor.conn.is_autocommit() {
498+
let autocommit = determine_autocommit(cursor);
499+
if !autocommit && stmt_is_dml && cursor.conn.is_autocommit() {
407500
begin_transaction(&cursor.conn).await?;
408501
}
409502
let params = match parameters {
@@ -440,6 +533,21 @@ async fn execute(cursor: &Cursor, sql: String, parameters: Option<&PyTuple>) ->
440533
Ok(())
441534
}
442535

536+
fn determine_autocommit(cursor: &Cursor) -> bool {
537+
#[cfg(Py_3_12)]
538+
{
539+
match cursor.autocommit {
540+
LEGACY_TRANSACTION_CONTROL => cursor.isolation_level.is_none(),
541+
_ => cursor.autocommit != 0,
542+
}
543+
}
544+
545+
#[cfg(not(Py_3_12))]
546+
{
547+
cursor.isolation_level.is_none()
548+
}
549+
}
550+
443551
fn stmt_is_dml(sql: &str) -> bool {
444552
let sql = sql.trim();
445553
let sql = sql.to_uppercase();
@@ -473,6 +581,7 @@ create_exception!(libsql_experimental, Error, pyo3::exceptions::PyException);
473581
#[pymodule]
474582
fn libsql_experimental(py: Python, m: &PyModule) -> PyResult<()> {
475583
let _ = tracing_subscriber::fmt::try_init();
584+
m.add("LEGACY_TRANSACTION_CONTROL", LEGACY_TRANSACTION_CONTROL)?;
476585
m.add("paramstyle", "qmark")?;
477586
m.add("sqlite_version_info", (3, 42, 0))?;
478587
m.add("Error", py.get_type::<Error>())?;

0 commit comments

Comments
 (0)