libsql 0.1.3rc1__tar.gz → 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {libsql-0.1.3rc1 → libsql-0.1.5}/Cargo.lock +21 -14
- {libsql-0.1.3rc1 → libsql-0.1.5}/Cargo.toml +2 -2
- {libsql-0.1.3rc1 → libsql-0.1.5}/PKG-INFO +1 -1
- {libsql-0.1.3rc1 → libsql-0.1.5}/docs/api.md +10 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/pyproject.toml +1 -1
- {libsql-0.1.3rc1 → libsql-0.1.5}/src/lib.rs +101 -23
- {libsql-0.1.3rc1 → libsql-0.1.5}/tests/test_suite.py +222 -10
- {libsql-0.1.3rc1 → libsql-0.1.5}/.github/workflows/CI.yml +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/.github/workflows/pr-tests.yml +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/.gitignore +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/CONTRIBUTING.md +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/LICENSE.md +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/README.md +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/build.rs +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/example.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/batch/.gitignore +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/batch/README.md +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/batch/main.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/encryption/.gitignore +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/encryption/README.md +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/encryption/main.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/execute_script.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/local/.gitignore +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/local/README.md +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/local/main.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/memory/README.md +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/memory/main.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/remote/README.md +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/remote/main.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/remote_connect.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/sqlalchemy/dialect.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/sqlalchemy/example.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/statements.sql +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/sync/.gitignore +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/sync/README.md +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/sync/main.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/sync_write.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/transaction/.gitignore +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/transaction/README.md +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/transaction/main.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/vector/.gitignore +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/vector/README.md +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/vector/main.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/examples/vector.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/perf-libsql.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/perf-sqlite3.py +0 -0
- {libsql-0.1.3rc1 → libsql-0.1.5}/shell.nix +0 -0
@@ -815,8 +815,9 @@ dependencies = [
|
|
815
815
|
|
816
816
|
[[package]]
|
817
817
|
name = "libsql"
|
818
|
-
version = "0.9.
|
819
|
-
source = "
|
818
|
+
version = "0.9.18"
|
819
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
820
|
+
checksum = "acd99c54790a50cffb62b1bf3e82c747fafad296c2fd21acce3154e4d9112241"
|
820
821
|
dependencies = [
|
821
822
|
"anyhow",
|
822
823
|
"async-stream",
|
@@ -854,8 +855,9 @@ dependencies = [
|
|
854
855
|
|
855
856
|
[[package]]
|
856
857
|
name = "libsql-ffi"
|
857
|
-
version = "0.9.
|
858
|
-
source = "
|
858
|
+
version = "0.9.18"
|
859
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
860
|
+
checksum = "f72c71234e1ad056592c977f5353b9ee87a2f9923cc7ac80bce52b7e30dbe702"
|
859
861
|
dependencies = [
|
860
862
|
"bindgen",
|
861
863
|
"cc",
|
@@ -865,8 +867,9 @@ dependencies = [
|
|
865
867
|
|
866
868
|
[[package]]
|
867
869
|
name = "libsql-hrana"
|
868
|
-
version = "0.9.
|
869
|
-
source = "
|
870
|
+
version = "0.9.18"
|
871
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
872
|
+
checksum = "25ce12bd586bde040a4bd33b291df6bf26dc56379629e1cb5d50e08a1bd31d56"
|
870
873
|
dependencies = [
|
871
874
|
"base64 0.21.7",
|
872
875
|
"bytes",
|
@@ -876,8 +879,9 @@ dependencies = [
|
|
876
879
|
|
877
880
|
[[package]]
|
878
881
|
name = "libsql-rusqlite"
|
879
|
-
version = "0.9.
|
880
|
-
source = "
|
882
|
+
version = "0.9.18"
|
883
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
884
|
+
checksum = "dc5d27776273a28e4eb1a63e9abb924f7e0d8a3f26b5b713724826f3e0c8bf6f"
|
881
885
|
dependencies = [
|
882
886
|
"bitflags 2.6.0",
|
883
887
|
"fallible-iterator 0.2.0",
|
@@ -890,7 +894,8 @@ dependencies = [
|
|
890
894
|
[[package]]
|
891
895
|
name = "libsql-sqlite3-parser"
|
892
896
|
version = "0.13.0"
|
893
|
-
source = "
|
897
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
898
|
+
checksum = "15a90128c708356af8f7d767c9ac2946692c9112b4f74f07b99a01a60680e413"
|
894
899
|
dependencies = [
|
895
900
|
"bitflags 2.6.0",
|
896
901
|
"cc",
|
@@ -906,8 +911,9 @@ dependencies = [
|
|
906
911
|
|
907
912
|
[[package]]
|
908
913
|
name = "libsql-sys"
|
909
|
-
version = "0.9.
|
910
|
-
source = "
|
914
|
+
version = "0.9.18"
|
915
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
916
|
+
checksum = "11f4b1c9314de9c3f4d768e3b03d5e2d0b95747423dcb55c69494c49a3630728"
|
911
917
|
dependencies = [
|
912
918
|
"bytes",
|
913
919
|
"libsql-ffi",
|
@@ -919,8 +925,9 @@ dependencies = [
|
|
919
925
|
|
920
926
|
[[package]]
|
921
927
|
name = "libsql_replication"
|
922
|
-
version = "0.9.
|
923
|
-
source = "
|
928
|
+
version = "0.9.18"
|
929
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
930
|
+
checksum = "fc5717bf0fd5ef39a40094bb606332e03a37d19b65d96399506453a8ee132167"
|
924
931
|
dependencies = [
|
925
932
|
"aes",
|
926
933
|
"async-stream",
|
@@ -1230,7 +1237,7 @@ dependencies = [
|
|
1230
1237
|
|
1231
1238
|
[[package]]
|
1232
1239
|
name = "pylibsql"
|
1233
|
-
version = "0.1.
|
1240
|
+
version = "0.1.5"
|
1234
1241
|
dependencies = [
|
1235
1242
|
"libsql",
|
1236
1243
|
"pyo3",
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[package]
|
2
2
|
name = "pylibsql"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.5"
|
4
4
|
edition = "2021"
|
5
5
|
|
6
6
|
[lib]
|
@@ -8,7 +8,7 @@ crate-type = ["cdylib"]
|
|
8
8
|
|
9
9
|
[dependencies]
|
10
10
|
pyo3 = "0.19.0"
|
11
|
-
libsql = {
|
11
|
+
libsql = { version = "0.9.18", features = ["encryption"] }
|
12
12
|
tokio = { version = "1.29.1", features = [ "rt-multi-thread" ] }
|
13
13
|
tracing-subscriber = "0.3"
|
14
14
|
|
@@ -32,6 +32,16 @@ Rolls back the current transaction and starts a new one.
|
|
32
32
|
|
33
33
|
Closes the database connection.
|
34
34
|
|
35
|
+
### `with` statement
|
36
|
+
|
37
|
+
Connection objects can be used as context managers to ensure that transactions are properly committed or rolled back. When entering the context, the connection object is returned. When exiting:
|
38
|
+
- Without exception: automatically commits the transaction
|
39
|
+
- With exception: automatically rolls back the transaction
|
40
|
+
|
41
|
+
This behavior is compatible with Python's `sqlite3` module. Context managers work correctly in both transactional and autocommit modes.
|
42
|
+
|
43
|
+
When mixing manual transaction control with context managers, the context manager's commit/rollback will apply to any active transaction at the time of exit. Manual calls to `commit()` or `rollback()` within the context are allowed and will start a new transaction as usual.
|
44
|
+
|
35
45
|
### execute(sql, parameters=())
|
36
46
|
|
37
47
|
Create a new cursor object and executes the SQL statement.
|
@@ -3,13 +3,23 @@ use pyo3::create_exception;
|
|
3
3
|
use pyo3::exceptions::PyValueError;
|
4
4
|
use pyo3::prelude::*;
|
5
5
|
use pyo3::types::{PyList, PyTuple};
|
6
|
-
use std::cell::
|
6
|
+
use std::cell::RefCell;
|
7
7
|
use std::sync::{Arc, OnceLock};
|
8
8
|
use std::time::Duration;
|
9
9
|
use tokio::runtime::{Handle, Runtime};
|
10
10
|
|
11
11
|
const LEGACY_TRANSACTION_CONTROL: i32 = -1;
|
12
12
|
|
13
|
+
enum ListOrTuple<'py> {
|
14
|
+
List(&'py PyList),
|
15
|
+
Tuple(&'py PyTuple),
|
16
|
+
}
|
17
|
+
|
18
|
+
struct ListOrTupleIterator<'py> {
|
19
|
+
index: usize,
|
20
|
+
inner: &'py ListOrTuple<'py>
|
21
|
+
}
|
22
|
+
|
13
23
|
fn rt() -> Handle {
|
14
24
|
static RT: OnceLock<Runtime> = OnceLock::new();
|
15
25
|
|
@@ -38,14 +48,14 @@ fn is_remote_path(path: &str) -> bool {
|
|
38
48
|
|
39
49
|
#[pyfunction]
|
40
50
|
#[cfg(not(Py_3_12))]
|
41
|
-
#[pyo3(signature = (database, timeout=5.0, isolation_level="DEFERRED".to_string(),
|
51
|
+
#[pyo3(signature = (database, timeout=5.0, isolation_level="DEFERRED".to_string(), _check_same_thread=true, _uri=false, sync_url=None, sync_interval=None, auth_token="", encryption_key=None))]
|
42
52
|
fn connect(
|
43
53
|
py: Python<'_>,
|
44
54
|
database: String,
|
45
55
|
timeout: f64,
|
46
56
|
isolation_level: Option<String>,
|
47
|
-
|
48
|
-
|
57
|
+
_check_same_thread: bool,
|
58
|
+
_uri: bool,
|
49
59
|
sync_url: Option<String>,
|
50
60
|
sync_interval: Option<f64>,
|
51
61
|
auth_token: &str,
|
@@ -56,8 +66,8 @@ fn connect(
|
|
56
66
|
database,
|
57
67
|
timeout,
|
58
68
|
isolation_level,
|
59
|
-
|
60
|
-
|
69
|
+
_check_same_thread,
|
70
|
+
_uri,
|
61
71
|
sync_url,
|
62
72
|
sync_interval,
|
63
73
|
auth_token,
|
@@ -68,14 +78,14 @@ fn connect(
|
|
68
78
|
|
69
79
|
#[pyfunction]
|
70
80
|
#[cfg(Py_3_12)]
|
71
|
-
#[pyo3(signature = (database, timeout=5.0, isolation_level="DEFERRED".to_string(),
|
81
|
+
#[pyo3(signature = (database, timeout=5.0, 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))]
|
72
82
|
fn connect(
|
73
83
|
py: Python<'_>,
|
74
84
|
database: String,
|
75
85
|
timeout: f64,
|
76
86
|
isolation_level: Option<String>,
|
77
|
-
|
78
|
-
|
87
|
+
_check_same_thread: bool,
|
88
|
+
_uri: bool,
|
79
89
|
sync_url: Option<String>,
|
80
90
|
sync_interval: Option<f64>,
|
81
91
|
auth_token: &str,
|
@@ -87,8 +97,8 @@ fn connect(
|
|
87
97
|
database,
|
88
98
|
timeout,
|
89
99
|
isolation_level.clone(),
|
90
|
-
|
91
|
-
|
100
|
+
_check_same_thread,
|
101
|
+
_uri,
|
92
102
|
sync_url,
|
93
103
|
sync_interval,
|
94
104
|
auth_token,
|
@@ -111,8 +121,8 @@ fn _connect_core(
|
|
111
121
|
database: String,
|
112
122
|
timeout: f64,
|
113
123
|
isolation_level: Option<String>,
|
114
|
-
|
115
|
-
|
124
|
+
_check_same_thread: bool,
|
125
|
+
_uri: bool,
|
116
126
|
sync_url: Option<String>,
|
117
127
|
sync_interval: Option<f64>,
|
118
128
|
auth_token: &str,
|
@@ -220,7 +230,7 @@ unsafe impl Send for Connection {}
|
|
220
230
|
|
221
231
|
#[pymethods]
|
222
232
|
impl Connection {
|
223
|
-
fn close(self_: PyRef<'_, Self>,
|
233
|
+
fn close(self_: PyRef<'_, Self>, _py: Python<'_>) -> PyResult<()> {
|
224
234
|
self_.conn.replace(None);
|
225
235
|
Ok(())
|
226
236
|
}
|
@@ -286,7 +296,7 @@ impl Connection {
|
|
286
296
|
fn execute(
|
287
297
|
self_: PyRef<'_, Self>,
|
288
298
|
sql: String,
|
289
|
-
parameters: Option
|
299
|
+
parameters: Option<ListOrTuple>,
|
290
300
|
) -> PyResult<Cursor> {
|
291
301
|
let cursor = Connection::cursor(&self_)?;
|
292
302
|
rt().block_on(async { execute(&cursor, sql, parameters).await })?;
|
@@ -300,7 +310,7 @@ impl Connection {
|
|
300
310
|
) -> PyResult<Cursor> {
|
301
311
|
let cursor = Connection::cursor(&self_)?;
|
302
312
|
for parameters in parameters.unwrap().iter() {
|
303
|
-
let parameters = parameters.extract
|
313
|
+
let parameters = parameters.extract::<ListOrTuple>()?;
|
304
314
|
rt().block_on(async { execute(&cursor, sql.clone(), Some(parameters)).await })?;
|
305
315
|
}
|
306
316
|
Ok(cursor)
|
@@ -330,11 +340,14 @@ impl Connection {
|
|
330
340
|
fn in_transaction(self_: PyRef<'_, Self>) -> PyResult<bool> {
|
331
341
|
#[cfg(Py_3_12)]
|
332
342
|
{
|
333
|
-
|
343
|
+
Ok(
|
334
344
|
!self_.conn.borrow().as_ref().unwrap().is_autocommit() || self_.autocommit == 0
|
335
|
-
)
|
345
|
+
)
|
346
|
+
}
|
347
|
+
#[cfg(not(Py_3_12))]
|
348
|
+
{
|
349
|
+
Ok(!self_.conn.borrow().as_ref().unwrap().is_autocommit())
|
336
350
|
}
|
337
|
-
Ok(!self_.conn.borrow().as_ref().unwrap().is_autocommit())
|
338
351
|
}
|
339
352
|
|
340
353
|
#[getter]
|
@@ -354,6 +367,26 @@ impl Connection {
|
|
354
367
|
self_.autocommit = autocommit;
|
355
368
|
Ok(())
|
356
369
|
}
|
370
|
+
|
371
|
+
fn __enter__(slf: PyRef<'_, Self>) -> PyResult<PyRef<'_, Self>> {
|
372
|
+
Ok(slf)
|
373
|
+
}
|
374
|
+
|
375
|
+
fn __exit__(
|
376
|
+
self_: PyRef<'_, Self>,
|
377
|
+
exc_type: Option<&PyAny>,
|
378
|
+
_exc_val: Option<&PyAny>,
|
379
|
+
_exc_tb: Option<&PyAny>,
|
380
|
+
) -> PyResult<bool> {
|
381
|
+
if exc_type.is_none() {
|
382
|
+
// Commit on clean exit
|
383
|
+
Connection::commit(self_)?;
|
384
|
+
} else {
|
385
|
+
// Rollback on error
|
386
|
+
Connection::rollback(self_)?;
|
387
|
+
}
|
388
|
+
Ok(false) // Always propagate exceptions
|
389
|
+
}
|
357
390
|
}
|
358
391
|
|
359
392
|
#[pyclass]
|
@@ -396,7 +429,7 @@ impl Cursor {
|
|
396
429
|
fn execute<'a>(
|
397
430
|
self_: PyRef<'a, Self>,
|
398
431
|
sql: String,
|
399
|
-
parameters: Option
|
432
|
+
parameters: Option<ListOrTuple>,
|
400
433
|
) -> PyResult<pyo3::PyRef<'a, Self>> {
|
401
434
|
rt().block_on(async { execute(&self_, sql, parameters).await })?;
|
402
435
|
Ok(self_)
|
@@ -408,7 +441,7 @@ impl Cursor {
|
|
408
441
|
parameters: Option<&PyList>,
|
409
442
|
) -> PyResult<pyo3::PyRef<'a, Cursor>> {
|
410
443
|
for parameters in parameters.unwrap().iter() {
|
411
|
-
let parameters = parameters.extract
|
444
|
+
let parameters = parameters.extract::<ListOrTuple>()?;
|
412
445
|
rt().block_on(async { execute(&self_, sql.clone(), Some(parameters)).await })?;
|
413
446
|
}
|
414
447
|
Ok(self_)
|
@@ -552,7 +585,11 @@ async fn begin_transaction(conn: &libsql_core::Connection) -> PyResult<()> {
|
|
552
585
|
Ok(())
|
553
586
|
}
|
554
587
|
|
555
|
-
async fn execute
|
588
|
+
async fn execute<'py>(
|
589
|
+
cursor: &Cursor,
|
590
|
+
sql: String,
|
591
|
+
parameters: Option<ListOrTuple<'py>>,
|
592
|
+
) -> PyResult<()> {
|
556
593
|
if cursor.conn.borrow().as_ref().is_none() {
|
557
594
|
return Err(PyValueError::new_err("Connection already closed"));
|
558
595
|
}
|
@@ -576,7 +613,10 @@ async fn execute(cursor: &Cursor, sql: String, parameters: Option<&PyTuple>) ->
|
|
576
613
|
} else if let Ok(value) = param.extract::<&[u8]>() {
|
577
614
|
libsql_core::Value::Blob(value.to_vec())
|
578
615
|
} else {
|
579
|
-
return Err(PyValueError::new_err(
|
616
|
+
return Err(PyValueError::new_err(format!(
|
617
|
+
"Unsupported parameter type {}",
|
618
|
+
param.to_string()
|
619
|
+
)));
|
580
620
|
};
|
581
621
|
params.push(param);
|
582
622
|
}
|
@@ -653,6 +693,44 @@ fn convert_row(py: Python, row: libsql_core::Row, column_count: i32) -> PyResult
|
|
653
693
|
|
654
694
|
create_exception!(libsql, Error, pyo3::exceptions::PyException);
|
655
695
|
|
696
|
+
impl<'py> FromPyObject<'py> for ListOrTuple<'py> {
|
697
|
+
fn extract(ob: &'py PyAny) -> PyResult<Self> {
|
698
|
+
if let Ok(list) = ob.downcast::<PyList>() {
|
699
|
+
Ok(ListOrTuple::List(list))
|
700
|
+
} else if let Ok(tuple) = ob.downcast::<PyTuple>() {
|
701
|
+
Ok(ListOrTuple::Tuple(tuple))
|
702
|
+
} else {
|
703
|
+
Err(PyValueError::new_err(
|
704
|
+
"Expected a list or tuple for parameters",
|
705
|
+
))
|
706
|
+
}
|
707
|
+
}
|
708
|
+
}
|
709
|
+
|
710
|
+
impl<'py> ListOrTuple<'py> {
|
711
|
+
pub fn iter(&self) -> ListOrTupleIterator {
|
712
|
+
ListOrTupleIterator{
|
713
|
+
index: 0,
|
714
|
+
inner: self,
|
715
|
+
}
|
716
|
+
}
|
717
|
+
}
|
718
|
+
|
719
|
+
impl<'py> Iterator for ListOrTupleIterator<'py> {
|
720
|
+
type Item = &'py PyAny;
|
721
|
+
|
722
|
+
fn next(&mut self) -> Option<Self::Item> {
|
723
|
+
let rv = match self.inner {
|
724
|
+
ListOrTuple::List(list) => list.get_item(self.index),
|
725
|
+
ListOrTuple::Tuple(tuple) => tuple.get_item(self.index),
|
726
|
+
};
|
727
|
+
|
728
|
+
rv.ok().map(|item| {
|
729
|
+
self.index += 1;
|
730
|
+
item
|
731
|
+
})
|
732
|
+
}
|
733
|
+
}
|
656
734
|
#[pymodule]
|
657
735
|
fn libsql(py: Python, m: &PyModule) -> PyResult<()> {
|
658
736
|
let _ = tracing_subscriber::fmt::try_init();
|
@@ -6,16 +6,19 @@ import libsql
|
|
6
6
|
import pytest
|
7
7
|
import tempfile
|
8
8
|
|
9
|
+
|
9
10
|
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
10
11
|
def test_connection_timeout(provider):
|
11
12
|
conn = connect(provider, ":memory:", timeout=1.0)
|
12
13
|
conn.close()
|
13
14
|
|
15
|
+
|
14
16
|
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
15
17
|
def test_connection_close(provider):
|
16
18
|
conn = connect(provider, ":memory:")
|
17
19
|
conn.close()
|
18
20
|
|
21
|
+
|
19
22
|
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
20
23
|
def test_execute(provider):
|
21
24
|
conn = connect(provider, ":memory:")
|
@@ -23,6 +26,9 @@ def test_execute(provider):
|
|
23
26
|
conn.execute("INSERT INTO users VALUES (1, 'alice@example.com')")
|
24
27
|
res = conn.execute("SELECT * FROM users")
|
25
28
|
assert (1, "alice@example.com") == res.fetchone()
|
29
|
+
# allow lists for parameters as well
|
30
|
+
res = conn.execute("SELECT * FROM users WHERE id = ?", [1])
|
31
|
+
assert (1, "alice@example.com") == res.fetchone()
|
26
32
|
|
27
33
|
|
28
34
|
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
@@ -34,6 +40,7 @@ def test_cursor_execute(provider):
|
|
34
40
|
res = cur.execute("SELECT * FROM users")
|
35
41
|
assert (1, "alice@example.com") == res.fetchone()
|
36
42
|
|
43
|
+
|
37
44
|
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
38
45
|
def test_cursor_close(provider):
|
39
46
|
conn = connect(provider, ":memory:")
|
@@ -47,6 +54,7 @@ def test_cursor_close(provider):
|
|
47
54
|
with pytest.raises(Exception):
|
48
55
|
cur.execute("SELECT * FROM users")
|
49
56
|
|
57
|
+
|
50
58
|
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
51
59
|
def test_executemany(provider):
|
52
60
|
conn = connect(provider, ":memory:")
|
@@ -198,7 +206,9 @@ def test_connection_autocommit(provider):
|
|
198
206
|
res = cur.execute("SELECT * FROM users")
|
199
207
|
assert (1, "alice@example.com") == res.fetchone()
|
200
208
|
|
201
|
-
conn = connect(
|
209
|
+
conn = connect(
|
210
|
+
provider, ":memory:", timeout=5, isolation_level="DEFERRED", autocommit=-1
|
211
|
+
)
|
202
212
|
assert conn.isolation_level == "DEFERRED"
|
203
213
|
assert conn.autocommit == -1
|
204
214
|
cur = conn.cursor()
|
@@ -210,7 +220,9 @@ def test_connection_autocommit(provider):
|
|
210
220
|
assert (1, "alice@example.com") == res.fetchone()
|
211
221
|
|
212
222
|
# Test autocommit Enabled (True)
|
213
|
-
conn = connect(
|
223
|
+
conn = connect(
|
224
|
+
provider, ":memory:", timeout=5, isolation_level=None, autocommit=True
|
225
|
+
)
|
214
226
|
assert conn.isolation_level == None
|
215
227
|
assert conn.autocommit == True
|
216
228
|
cur = conn.cursor()
|
@@ -221,7 +233,9 @@ def test_connection_autocommit(provider):
|
|
221
233
|
res = cur.execute("SELECT * FROM users")
|
222
234
|
assert (1, "bob@example.com") == res.fetchone()
|
223
235
|
|
224
|
-
conn = connect(
|
236
|
+
conn = connect(
|
237
|
+
provider, ":memory:", timeout=5, isolation_level="DEFERRED", autocommit=True
|
238
|
+
)
|
225
239
|
assert conn.isolation_level == "DEFERRED"
|
226
240
|
assert conn.autocommit == True
|
227
241
|
cur = conn.cursor()
|
@@ -233,7 +247,9 @@ def test_connection_autocommit(provider):
|
|
233
247
|
assert (1, "bob@example.com") == res.fetchone()
|
234
248
|
|
235
249
|
# Test autocommit Disabled (False)
|
236
|
-
conn = connect(
|
250
|
+
conn = connect(
|
251
|
+
provider, ":memory:", timeout=5, isolation_level="DEFERRED", autocommit=False
|
252
|
+
)
|
237
253
|
assert conn.isolation_level == "DEFERRED"
|
238
254
|
assert conn.autocommit == False
|
239
255
|
cur = conn.cursor()
|
@@ -260,6 +276,7 @@ def test_params(provider):
|
|
260
276
|
res = cur.execute("SELECT * FROM users")
|
261
277
|
assert (1, "alice@example.com") == res.fetchone()
|
262
278
|
|
279
|
+
|
263
280
|
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
264
281
|
def test_none_param(provider):
|
265
282
|
conn = connect(provider, ":memory:")
|
@@ -272,6 +289,7 @@ def test_none_param(provider):
|
|
272
289
|
assert results[0] == (1, None)
|
273
290
|
assert results[1] == (2, "alice@example.com")
|
274
291
|
|
292
|
+
|
275
293
|
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
276
294
|
def test_fetchmany(provider):
|
277
295
|
conn = connect(provider, ":memory:")
|
@@ -321,6 +339,194 @@ def test_int64(provider):
|
|
321
339
|
assert [(1, 1099511627776)] == res.fetchall()
|
322
340
|
|
323
341
|
|
342
|
+
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
343
|
+
def test_context_manager_commit(provider):
|
344
|
+
"""Test that context manager commits on clean exit"""
|
345
|
+
conn = connect(provider, ":memory:")
|
346
|
+
with conn as c:
|
347
|
+
c.execute("CREATE TABLE t(x)")
|
348
|
+
c.execute("INSERT INTO t VALUES (1)")
|
349
|
+
# Changes should be committed
|
350
|
+
cur = conn.cursor()
|
351
|
+
cur.execute("SELECT COUNT(*) FROM t")
|
352
|
+
assert cur.fetchone()[0] == 1
|
353
|
+
|
354
|
+
|
355
|
+
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
356
|
+
def test_context_manager_rollback(provider):
|
357
|
+
"""Test that context manager rolls back on exception"""
|
358
|
+
conn = connect(provider, ":memory:")
|
359
|
+
try:
|
360
|
+
with conn as c:
|
361
|
+
c.execute("CREATE TABLE t(x)")
|
362
|
+
c.execute("INSERT INTO t VALUES (1)")
|
363
|
+
raise ValueError("Test exception")
|
364
|
+
except ValueError:
|
365
|
+
pass
|
366
|
+
# Changes should be rolled back
|
367
|
+
cur = conn.cursor()
|
368
|
+
try:
|
369
|
+
cur.execute("SELECT COUNT(*) FROM t")
|
370
|
+
# If we get here, the table exists (rollback didn't work)
|
371
|
+
assert False, "Table should not exist after rollback"
|
372
|
+
except Exception:
|
373
|
+
# Table doesn't exist, which is what we expect after rollback
|
374
|
+
pass
|
375
|
+
|
376
|
+
|
377
|
+
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
378
|
+
def test_context_manager_autocommit(provider):
|
379
|
+
"""Test that context manager works correctly with autocommit mode"""
|
380
|
+
conn = connect(provider, ":memory:", isolation_level=None) # autocommit mode
|
381
|
+
with conn as c:
|
382
|
+
c.execute("CREATE TABLE t(x)")
|
383
|
+
c.execute("INSERT INTO t VALUES (1)")
|
384
|
+
# In autocommit mode, changes are committed immediately
|
385
|
+
cur = conn.cursor()
|
386
|
+
cur.execute("SELECT COUNT(*) FROM t")
|
387
|
+
assert cur.fetchone()[0] == 1
|
388
|
+
|
389
|
+
|
390
|
+
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
391
|
+
def test_context_manager_nested(provider):
|
392
|
+
"""Test nested context managers"""
|
393
|
+
conn = connect(provider, ":memory:")
|
394
|
+
with conn as c1:
|
395
|
+
c1.execute("CREATE TABLE t(x)")
|
396
|
+
c1.execute("INSERT INTO t VALUES (1)")
|
397
|
+
with conn as c2:
|
398
|
+
c2.execute("INSERT INTO t VALUES (2)")
|
399
|
+
# Inner context commits
|
400
|
+
cur = conn.cursor()
|
401
|
+
cur.execute("SELECT COUNT(*) FROM t")
|
402
|
+
assert cur.fetchone()[0] == 2
|
403
|
+
# Outer context also commits
|
404
|
+
cur = conn.cursor()
|
405
|
+
cur.execute("SELECT COUNT(*) FROM t")
|
406
|
+
assert cur.fetchone()[0] == 2
|
407
|
+
|
408
|
+
|
409
|
+
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
410
|
+
def test_context_manager_connection_reuse(provider):
|
411
|
+
"""Test that connection remains usable after context manager exit"""
|
412
|
+
conn = connect(provider, ":memory:")
|
413
|
+
|
414
|
+
# First use with context manager
|
415
|
+
with conn as c:
|
416
|
+
c.execute("CREATE TABLE t(x)")
|
417
|
+
c.execute("INSERT INTO t VALUES (1)")
|
418
|
+
|
419
|
+
# Connection should still be valid
|
420
|
+
cur = conn.cursor()
|
421
|
+
cur.execute("INSERT INTO t VALUES (2)")
|
422
|
+
conn.commit()
|
423
|
+
|
424
|
+
# Verify both inserts worked
|
425
|
+
cur.execute("SELECT COUNT(*) FROM t")
|
426
|
+
assert cur.fetchone()[0] == 2
|
427
|
+
|
428
|
+
# Use context manager again
|
429
|
+
with conn as c:
|
430
|
+
c.execute("INSERT INTO t VALUES (3)")
|
431
|
+
|
432
|
+
# Final verification
|
433
|
+
cur.execute("SELECT COUNT(*) FROM t")
|
434
|
+
assert cur.fetchone()[0] == 3
|
435
|
+
|
436
|
+
conn.close()
|
437
|
+
|
438
|
+
|
439
|
+
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
440
|
+
def test_context_manager_nested_exception(provider):
|
441
|
+
"""Test exception handling in nested context managers"""
|
442
|
+
conn = connect(provider, ":memory:")
|
443
|
+
|
444
|
+
# Create table outside context
|
445
|
+
conn.execute("CREATE TABLE t(x)")
|
446
|
+
conn.commit()
|
447
|
+
|
448
|
+
# Test that nested context managers share the same transaction
|
449
|
+
# An exception in an inner context will roll back the entire transaction
|
450
|
+
try:
|
451
|
+
with conn as c1:
|
452
|
+
c1.execute("INSERT INTO t VALUES (1)")
|
453
|
+
try:
|
454
|
+
with conn as c2:
|
455
|
+
c2.execute("INSERT INTO t VALUES (2)")
|
456
|
+
raise ValueError("Inner exception")
|
457
|
+
except ValueError:
|
458
|
+
pass
|
459
|
+
# The inner rollback affects the entire transaction
|
460
|
+
# So value 1 is also rolled back
|
461
|
+
c1.execute("INSERT INTO t VALUES (3)")
|
462
|
+
except:
|
463
|
+
pass
|
464
|
+
|
465
|
+
# Only value 3 should be committed (1 and 2 were rolled back together)
|
466
|
+
cur = conn.cursor()
|
467
|
+
cur.execute("SELECT x FROM t ORDER BY x")
|
468
|
+
results = cur.fetchall()
|
469
|
+
assert results == [(3,)]
|
470
|
+
|
471
|
+
# Test outer exception after nested context commits
|
472
|
+
conn.execute("DROP TABLE t")
|
473
|
+
conn.execute("CREATE TABLE t(x)")
|
474
|
+
conn.commit()
|
475
|
+
|
476
|
+
try:
|
477
|
+
with conn as c1:
|
478
|
+
c1.execute("INSERT INTO t VALUES (10)")
|
479
|
+
with conn as c2:
|
480
|
+
c2.execute("INSERT INTO t VALUES (20)")
|
481
|
+
# Inner context will commit both values
|
482
|
+
# This will cause outer rollback but values are already committed
|
483
|
+
raise RuntimeError("Outer exception")
|
484
|
+
except RuntimeError:
|
485
|
+
pass
|
486
|
+
|
487
|
+
# Values 10 and 20 should be committed by inner context
|
488
|
+
cur.execute("SELECT COUNT(*) FROM t")
|
489
|
+
assert cur.fetchone()[0] == 2
|
490
|
+
|
491
|
+
|
492
|
+
@pytest.mark.parametrize("provider", ["libsql", "sqlite"])
|
493
|
+
def test_context_manager_manual_transaction_control(provider):
|
494
|
+
"""Test mixing manual transaction control with context managers"""
|
495
|
+
conn = connect(provider, ":memory:")
|
496
|
+
|
497
|
+
with conn as c:
|
498
|
+
c.execute("CREATE TABLE t(x)")
|
499
|
+
c.execute("INSERT INTO t VALUES (1)")
|
500
|
+
|
501
|
+
# Manual commit within context
|
502
|
+
c.commit()
|
503
|
+
|
504
|
+
# Start new transaction
|
505
|
+
c.execute("INSERT INTO t VALUES (2)")
|
506
|
+
# This will be committed by context manager
|
507
|
+
|
508
|
+
# Both values should be present
|
509
|
+
cur = conn.cursor()
|
510
|
+
cur.execute("SELECT COUNT(*) FROM t")
|
511
|
+
assert cur.fetchone()[0] == 2
|
512
|
+
|
513
|
+
# Test manual rollback within context
|
514
|
+
with conn as c:
|
515
|
+
c.execute("INSERT INTO t VALUES (3)")
|
516
|
+
|
517
|
+
# Manual rollback
|
518
|
+
c.rollback()
|
519
|
+
|
520
|
+
# New transaction
|
521
|
+
c.execute("INSERT INTO t VALUES (4)")
|
522
|
+
# This will be committed by context manager
|
523
|
+
|
524
|
+
# Should have values 1, 2, and 4 (not 3)
|
525
|
+
cur.execute("SELECT x FROM t ORDER BY x")
|
526
|
+
results = cur.fetchall()
|
527
|
+
assert results == [(1,), (2,), (4,)]
|
528
|
+
|
529
|
+
|
324
530
|
def connect(provider, database, timeout=5, isolation_level="DEFERRED", autocommit=-1):
|
325
531
|
if provider == "libsql-remote":
|
326
532
|
from urllib import request
|
@@ -331,9 +537,7 @@ def connect(provider, database, timeout=5, isolation_level="DEFERRED", autocommi
|
|
331
537
|
raise Exception("libsql-remote server is not running")
|
332
538
|
if res.getcode() != 200:
|
333
539
|
raise Exception("libsql-remote server is not running")
|
334
|
-
return libsql.connect(
|
335
|
-
database, sync_url="http://localhost:8080", auth_token=""
|
336
|
-
)
|
540
|
+
return libsql.connect(database, sync_url="http://localhost:8080", auth_token="")
|
337
541
|
if provider == "libsql":
|
338
542
|
if sys.version_info < (3, 12):
|
339
543
|
return libsql.connect(
|
@@ -343,15 +547,23 @@ def connect(provider, database, timeout=5, isolation_level="DEFERRED", autocommi
|
|
343
547
|
if autocommit == -1:
|
344
548
|
autocommit = libsql.LEGACY_TRANSACTION_CONTROL
|
345
549
|
return libsql.connect(
|
346
|
-
database,
|
550
|
+
database,
|
551
|
+
timeout=timeout,
|
552
|
+
isolation_level=isolation_level,
|
553
|
+
autocommit=autocommit,
|
347
554
|
)
|
348
555
|
if provider == "sqlite":
|
349
556
|
if sys.version_info < (3, 12):
|
350
|
-
return sqlite3.connect(
|
557
|
+
return sqlite3.connect(
|
558
|
+
database, timeout=timeout, isolation_level=isolation_level
|
559
|
+
)
|
351
560
|
else:
|
352
561
|
if autocommit == -1:
|
353
562
|
autocommit = sqlite3.LEGACY_TRANSACTION_CONTROL
|
354
563
|
return sqlite3.connect(
|
355
|
-
database,
|
564
|
+
database,
|
565
|
+
timeout=timeout,
|
566
|
+
isolation_level=isolation_level,
|
567
|
+
autocommit=autocommit,
|
356
568
|
)
|
357
569
|
raise Exception(f"Provider `{provider}` is not supported")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|