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.
Files changed (47) hide show
  1. {libsql-0.1.3rc1 → libsql-0.1.5}/Cargo.lock +21 -14
  2. {libsql-0.1.3rc1 → libsql-0.1.5}/Cargo.toml +2 -2
  3. {libsql-0.1.3rc1 → libsql-0.1.5}/PKG-INFO +1 -1
  4. {libsql-0.1.3rc1 → libsql-0.1.5}/docs/api.md +10 -0
  5. {libsql-0.1.3rc1 → libsql-0.1.5}/pyproject.toml +1 -1
  6. {libsql-0.1.3rc1 → libsql-0.1.5}/src/lib.rs +101 -23
  7. {libsql-0.1.3rc1 → libsql-0.1.5}/tests/test_suite.py +222 -10
  8. {libsql-0.1.3rc1 → libsql-0.1.5}/.github/workflows/CI.yml +0 -0
  9. {libsql-0.1.3rc1 → libsql-0.1.5}/.github/workflows/pr-tests.yml +0 -0
  10. {libsql-0.1.3rc1 → libsql-0.1.5}/.gitignore +0 -0
  11. {libsql-0.1.3rc1 → libsql-0.1.5}/CONTRIBUTING.md +0 -0
  12. {libsql-0.1.3rc1 → libsql-0.1.5}/LICENSE.md +0 -0
  13. {libsql-0.1.3rc1 → libsql-0.1.5}/README.md +0 -0
  14. {libsql-0.1.3rc1 → libsql-0.1.5}/build.rs +0 -0
  15. {libsql-0.1.3rc1 → libsql-0.1.5}/example.py +0 -0
  16. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/batch/.gitignore +0 -0
  17. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/batch/README.md +0 -0
  18. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/batch/main.py +0 -0
  19. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/encryption/.gitignore +0 -0
  20. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/encryption/README.md +0 -0
  21. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/encryption/main.py +0 -0
  22. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/execute_script.py +0 -0
  23. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/local/.gitignore +0 -0
  24. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/local/README.md +0 -0
  25. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/local/main.py +0 -0
  26. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/memory/README.md +0 -0
  27. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/memory/main.py +0 -0
  28. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/remote/README.md +0 -0
  29. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/remote/main.py +0 -0
  30. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/remote_connect.py +0 -0
  31. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/sqlalchemy/dialect.py +0 -0
  32. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/sqlalchemy/example.py +0 -0
  33. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/statements.sql +0 -0
  34. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/sync/.gitignore +0 -0
  35. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/sync/README.md +0 -0
  36. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/sync/main.py +0 -0
  37. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/sync_write.py +0 -0
  38. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/transaction/.gitignore +0 -0
  39. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/transaction/README.md +0 -0
  40. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/transaction/main.py +0 -0
  41. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/vector/.gitignore +0 -0
  42. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/vector/README.md +0 -0
  43. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/vector/main.py +0 -0
  44. {libsql-0.1.3rc1 → libsql-0.1.5}/examples/vector.py +0 -0
  45. {libsql-0.1.3rc1 → libsql-0.1.5}/perf-libsql.py +0 -0
  46. {libsql-0.1.3rc1 → libsql-0.1.5}/perf-sqlite3.py +0 -0
  47. {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.15"
819
- source = "git+https://github.com/tursodatabase/libsql/?rev=57043b60f3db38236187a8905bee62b35bf18afb#57043b60f3db38236187a8905bee62b35bf18afb"
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.15"
858
- source = "git+https://github.com/tursodatabase/libsql/?rev=57043b60f3db38236187a8905bee62b35bf18afb#57043b60f3db38236187a8905bee62b35bf18afb"
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.15"
869
- source = "git+https://github.com/tursodatabase/libsql/?rev=57043b60f3db38236187a8905bee62b35bf18afb#57043b60f3db38236187a8905bee62b35bf18afb"
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.15"
880
- source = "git+https://github.com/tursodatabase/libsql/?rev=57043b60f3db38236187a8905bee62b35bf18afb#57043b60f3db38236187a8905bee62b35bf18afb"
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 = "git+https://github.com/tursodatabase/libsql/?rev=57043b60f3db38236187a8905bee62b35bf18afb#57043b60f3db38236187a8905bee62b35bf18afb"
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.15"
910
- source = "git+https://github.com/tursodatabase/libsql/?rev=57043b60f3db38236187a8905bee62b35bf18afb#57043b60f3db38236187a8905bee62b35bf18afb"
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.15"
923
- source = "git+https://github.com/tursodatabase/libsql/?rev=57043b60f3db38236187a8905bee62b35bf18afb#57043b60f3db38236187a8905bee62b35bf18afb"
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.3-pre.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-pre.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 = { git = "https://github.com/tursodatabase/libsql/", rev = "57043b60f3db38236187a8905bee62b35bf18afb", features = ["encryption"] }
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: libsql
3
- Version: 0.1.3rc1
3
+ Version: 0.1.5
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -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.
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "libsql"
7
- version = "0.1.3-pre.1"
7
+ version = "0.1.5"
8
8
  requires-python = ">=3.7"
9
9
  classifiers = [
10
10
  "Programming Language :: Rust",
@@ -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::{OnceCell, RefCell};
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(), check_same_thread=true, uri=false, sync_url=None, sync_interval=None, auth_token="", encryption_key=None))]
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
- check_same_thread: bool,
48
- uri: bool,
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
- check_same_thread,
60
- uri,
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(), check_same_thread=true, uri=false, sync_url=None, sync_interval=None, auth_token="", encryption_key=None, autocommit = LEGACY_TRANSACTION_CONTROL))]
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
- check_same_thread: bool,
78
- uri: bool,
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
- check_same_thread,
91
- uri,
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
- check_same_thread: bool,
115
- uri: bool,
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>, py: Python<'_>) -> PyResult<()> {
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<&PyTuple>,
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::<&PyTuple>()?;
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
- return Ok(
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<&PyTuple>,
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::<&PyTuple>()?;
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(cursor: &Cursor, sql: String, parameters: Option<&PyTuple>) -> PyResult<()> {
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("Unsupported parameter type"));
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(provider, ":memory:", timeout=5, isolation_level="DEFERRED", autocommit=-1)
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(provider, ":memory:", timeout=5, isolation_level=None, autocommit=True)
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(provider, ":memory:", timeout=5, isolation_level="DEFERRED", autocommit=True)
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(provider, ":memory:", timeout=5, isolation_level="DEFERRED", autocommit=False)
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, timeout=timeout, isolation_level=isolation_level, autocommit=autocommit
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(database, timeout=timeout, isolation_level=isolation_level)
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, timeout=timeout, isolation_level=isolation_level, autocommit=autocommit
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