mirror of
https://github.com/tursodatabase/limbo.git
synced 2025-12-23 08:21:09 +00:00
Merge 'SDK kit' from Nikita Sivukhin
This PR introduces sdk-kit - stable Rust API with C bindings which can be freely used in the SDK development. The differences from the `turso_core` is following: 1. The API surface is smaller 2. The focus is the SDK development - so it has more convenient methods for the SDKs and do not have unnecessary extra methods (like libsql WAL operations, etc) 3. sdk-kit provides C API which can be consumed immediately as shared library The different from the `sqlite3` crate is that sdk-kit for turso implement only necessary methods for turso. In order to prove viability of the idea, Python driver was rewritten with sdk-kit in Rust. The structure of Python driver now looks like this: 1. It exports same API as rsapi of sdk-kit with the help of pyo3 in order to avoid manual objects marshalling and Python integration 2. The driver itself implemented in the pure Python (it was actually vibe-coded, see `py-bindings.mdx` and `py-bindings-tests.mdx` context files for more details) Closes #4048
This commit is contained in:
commit
49b207ce4c
23 changed files with 6324 additions and 406 deletions
127
Cargo.lock
generated
127
Cargo.lock
generated
|
|
@ -404,6 +404,29 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.69.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.10.5",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash 1.1.0",
|
||||
"shlex",
|
||||
"syn 2.0.100",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
|
@ -545,6 +568,15 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
|
|
@ -614,6 +646,17 @@ dependencies = [
|
|||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.47"
|
||||
|
|
@ -2497,6 +2540,12 @@ version = "1.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
|
|
@ -2907,7 +2956,7 @@ dependencies = [
|
|||
"napi-build",
|
||||
"napi-sys",
|
||||
"nohash-hasher",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3407,6 +3456,16 @@ dependencies = [
|
|||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
|
|
@ -3500,18 +3559,17 @@ dependencies = [
|
|||
"anyhow",
|
||||
"pyo3",
|
||||
"pyo3-build-config",
|
||||
"turso_core",
|
||||
"turso_sdk_kit",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.24.1"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17da310086b068fbdcefbba30aeb3721d5bb9af8db4987d6735b2183ca567229"
|
||||
checksum = "37a6df7eab65fc7bee654a421404947e10a0f7085b6951bf2ea395f4659fb0cf"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cfg-if",
|
||||
"indoc",
|
||||
"libc",
|
||||
"memoffset",
|
||||
|
|
@ -3525,19 +3583,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.24.1"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e27165889bd793000a098bb966adc4300c312497ea25cf7a690a9f0ac5aa5fc1"
|
||||
checksum = "f77d387774f6f6eec64a004eac0ed525aab7fa1966d94b42f743797b3e395afb"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.24.1"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05280526e1dbf6b420062f3ef228b78c0c54ba94e157f5cb724a609d0f2faabc"
|
||||
checksum = "2dd13844a4242793e02df3e2ec093f540d948299a6a77ea9ce7afd8623f542be"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
|
|
@ -3545,9 +3602,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.24.1"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c3ce5686aa4d3f63359a5100c62a127c9f15e8398e5fdeb5deef1fed5cd5f44"
|
||||
checksum = "eaf8f9f1108270b90d3676b8679586385430e5c0bb78bb5f043f95499c821a71"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
|
|
@ -3557,9 +3614,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.24.1"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4cf6faa0cbfb0ed08e89beb8103ae9724eb4750e3a78084ba4017cbe94f3855"
|
||||
checksum = "70a3b2274450ba5288bc9b8c1b69ff569d1d61189d4bff38f8d22e03d17f932b"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
|
|
@ -3878,6 +3935,12 @@ version = "0.1.24"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
|
|
@ -4969,7 +5032,7 @@ dependencies = [
|
|||
"roaring",
|
||||
"rstest",
|
||||
"rusqlite",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustix 1.0.7",
|
||||
"ryu",
|
||||
"serde",
|
||||
|
|
@ -5056,6 +5119,28 @@ dependencies = [
|
|||
"turso_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "turso_sdk_kit"
|
||||
version = "0.4.0-pre.3"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"env_logger 0.11.7",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"turso_core",
|
||||
"turso_sdk_kit_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "turso_sdk_kit_macros"
|
||||
version = "0.4.0-pre.3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "turso_sqlite3"
|
||||
version = "0.4.0-pre.3"
|
||||
|
|
@ -5447,6 +5532,18 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "4.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
|
||||
dependencies = [
|
||||
"either",
|
||||
"home",
|
||||
"once_cell",
|
||||
"rustix 0.38.44",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
|
|
|||
10
Cargo.toml
10
Cargo.toml
|
|
@ -32,11 +32,11 @@ members = [
|
|||
"whopper",
|
||||
"perf/throughput/turso",
|
||||
"perf/throughput/rusqlite",
|
||||
"perf/encryption"
|
||||
]
|
||||
exclude = [
|
||||
"perf/latency/limbo",
|
||||
"perf/encryption",
|
||||
"sdk-kit",
|
||||
"sdk-kit-macros",
|
||||
]
|
||||
exclude = ["perf/latency/limbo"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.0-pre.3"
|
||||
|
|
@ -48,6 +48,8 @@ repository = "https://github.com/tursodatabase/turso"
|
|||
[workspace.dependencies]
|
||||
turso = { path = "bindings/rust", version = "0.4.0-pre.3" }
|
||||
turso_node = { path = "bindings/javascript", version = "0.4.0-pre.3" }
|
||||
turso_sdk_kit = { path = "sdk-kit", version = "0.4.0-pre.3" }
|
||||
turso_sdk_kit_macros = { path = "sdk-kit-macros", version = "0.4.0-pre.3" }
|
||||
limbo_completion = { path = "extensions/completion", version = "0.4.0-pre.3" }
|
||||
turso_core = { path = "core", version = "0.4.0-pre.3" }
|
||||
turso_sync_engine = { path = "sync/engine", version = "0.4.0-pre.3" }
|
||||
|
|
|
|||
|
|
@ -14,14 +14,13 @@ crate-type = ["cdylib"]
|
|||
[features]
|
||||
# must be enabled when building with `cargo build`, maturin enables this automatically
|
||||
extension-module = ["pyo3/extension-module"]
|
||||
tracing_release = ["turso_core/tracing_release"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
turso_core = { workspace = true, features = ["io_uring"] }
|
||||
pyo3 = { version = "0.24.1", features = ["anyhow"] }
|
||||
turso_sdk_kit = { workspace = true }
|
||||
pyo3 = { version = "0.27.1", features = ["anyhow"] }
|
||||
|
||||
[build-dependencies]
|
||||
version_check = "0.9.5"
|
||||
# used where logic has to be version/distribution specific, e.g. pypy
|
||||
pyo3-build-config = { version = "0.24.0" }
|
||||
pyo3-build-config = { version = "0.27.0" }
|
||||
|
|
|
|||
59
bindings/python/py-bindings-tests.mdx
Normal file
59
bindings/python/py-bindings-tests.mdx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
name: 2025-11-26-py-bindings-tests
|
||||
---
|
||||
|
||||
<Output path="./tests/test_database.py">
|
||||
|
||||
<Code model="openai/gpt-5" language="python">
|
||||
|
||||
Turso - is the **SQLite compatible** database written in Rust.
|
||||
Your task is to generate tests for Python driver with the API similar to the SQLite DB-api2
|
||||
|
||||
# Rules
|
||||
|
||||
General rules for driver implementation you **MUST** follow and never go against these rules:
|
||||
- Inspect tests in the test_database.py file and ALWAYS append new tests in the end
|
||||
- DO NOT change current content of the test_database.py file - ONLY APPEND new tests
|
||||
- DO NOT duplicate already existing tests
|
||||
- DO NOT test not implemented features
|
||||
- DO COVER all essential methods currently implemented in the driver
|
||||
- FOLLOW programming style of the test_database.py file
|
||||
<File path="./tests/test_database.py" />
|
||||
|
||||
# Test case category 1: DB API2
|
||||
|
||||
Generate tests which will cover API of the driver surface
|
||||
|
||||
Inspect implementaton of the driver here:
|
||||
<File path="./turso/lib.py" />
|
||||
|
||||
# Test case category 2: SQL
|
||||
|
||||
Generate tests which will cover generic use of SQL.
|
||||
**Non exhaustive** list of things to check:
|
||||
- Subqueries
|
||||
- INSERT ... RETURNING ...
|
||||
* Make additional test case for scenario, where multiple values were inserted, but only one row were fetch
|
||||
* Make sure that in this case transaction will be properly commited even when not all rows were consumed
|
||||
- CONFLICT clauses (and how driver inform caller about conflict)
|
||||
- Basic DDL statements (CREATE/DELETE)
|
||||
- More complex DDL statements (ALTER TABLE)
|
||||
- Builtin virtual tables (generate_series)
|
||||
- JOIN
|
||||
- JSON functions
|
||||
|
||||
# Supported functions
|
||||
|
||||
- DRIVER: .rowcount works correctly only for DML statements
|
||||
* DO NOT test it with DQL/DDL statements
|
||||
- DRIVER: .lastrowid is not implemented right now
|
||||
* DO NOT test it at all
|
||||
- SQLITE: generate_series is not enabled by default in the sqlite3 module
|
||||
- TURSO: ORDER BY is not supported for compound SELECTs yet
|
||||
- TURSO: Recursive CTEs are not yet supported
|
||||
- TURSO: Inspect compatibility file in order to understand what subset of SQLite query language is supported by the turso at the moment
|
||||
<File path="../../COMPAT.md" />
|
||||
|
||||
</Code>
|
||||
|
||||
</Output>
|
||||
116
bindings/python/py-bindings.mdx
Normal file
116
bindings/python/py-bindings.mdx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
---
|
||||
name: 2025-11-26-py-bindings
|
||||
---
|
||||
|
||||
<Output path="./turso/lib.py">
|
||||
|
||||
<Code model="openai/gpt-5" language="python">
|
||||
|
||||
Turso - is the SQLite compatible database written in Rust.
|
||||
One of the important features of the Turso - is async IO execution which can be used with modern storage backend like IO uring.
|
||||
|
||||
Your task is to generate Python driver with the API similar to the SQLite DB-api2
|
||||
|
||||
# Rules
|
||||
|
||||
General rules for driver implementation you **MUST** follow and never go against these rules:
|
||||
- STRUCTURE of the implementation
|
||||
* Declaration order of elements and semantic blocks MUST be exsactly the same
|
||||
* (details and full enumerations omited in the example for brevity but you must generate full code)
|
||||
```py
|
||||
# all imports must be at the beginning - no imports in the middle of function
|
||||
from typing import ...
|
||||
from ._turso import ( ... )
|
||||
|
||||
|
||||
# DB-API 2.0 module attributes
|
||||
apilevel = "2.0"
|
||||
...
|
||||
|
||||
# Exception hierarchy following DB-API 2.0
|
||||
class Warning(Exception): ... # more
|
||||
...
|
||||
|
||||
def _map_turso_exception(exc: Exception) -> Exception:
|
||||
"""Maps Turso-specific exceptions to DB-API 2.0 exception hierarchy"""
|
||||
if isinstance(exc, Busy): ...
|
||||
...
|
||||
|
||||
# Connection goes FIRST
|
||||
class Connection: ...
|
||||
# Cursor goes SECOND
|
||||
class Cursor: ...
|
||||
# Row goes THIRD
|
||||
class Row: ...
|
||||
|
||||
def connect(
|
||||
database: str,
|
||||
*,
|
||||
experimental_features: Optional[str] = None,
|
||||
isolation_level: Optional[str] = "DEFERRED",
|
||||
): ...
|
||||
|
||||
# Make it easy to enable logging with native `logging` Python module
|
||||
def setup_logging(level: int = logging.INFO) -> None: ...
|
||||
```
|
||||
- AVOID unnecessary FFI calls as their cost is non zero
|
||||
- AVOID unnecessary strings transformations - replace them with more efficient alternatives if possible
|
||||
- DO NOT ever mix `PyTursoStatement::execute` and `PyTursoStatement::step` methods: every statement must be either "stepped" or "executed"
|
||||
* This is because `execute` ignores all rows
|
||||
- NEVER put import in the middle of a function - always put all necessary immports at the beginning of the file
|
||||
- SQL query can be arbitrary, be very careful writing the code which relies on properties derived from the simple string analysis
|
||||
* ONLY ANALYZE SQL statement to detect DML statement and open implicit transaction
|
||||
* DO NOT check for any symbols to detect multi statements, named parameters, etc - this is error prone. Use provided methods and avoid certain checks if they are impossible with current API provided from the Rust
|
||||
- FOCUS on code readability: if possible extract helper function but make sure that it will be used more than once and that it really contribute to the code readability
|
||||
- WATCH OUT for variables scopes and do not use variables which are no longer accessible
|
||||
- DO NOT TRACK transaction state manually and use `get_auto_commit` method - otherwise it can be hard to properly implement implicit transaction rules of DB API2
|
||||
- USE forward reference string in when return method type depends on its class:
|
||||
```py
|
||||
class T:
|
||||
def f(self) -> 'T':
|
||||
```
|
||||
|
||||
# Implementation
|
||||
|
||||
- Put compact citations from the official DB-API doc if this is helpful
|
||||
- Driver must implement context API for Python to be used like `with ... as conn:` ...
|
||||
- Driver implementation must be type-friendly - emit types everywhere at API boundary (public methods, class fields, etc)
|
||||
* DO NOT forget that constructor of Row must have following signature:
|
||||
```py
|
||||
class Row(Sequence[Any]):
|
||||
def __new__(cls, cursor: Cursor, data: tuple[Any, ...], /) -> Self: ...
|
||||
```
|
||||
* Make typings compatible with official types:
|
||||
<Link url="https://raw.githubusercontent.com/python/typeshed/refs/heads/main/stdlib/sqlite3/__init__.pyi" />
|
||||
|
||||
|
||||
# Bindings
|
||||
|
||||
You must use bindings in the lib.rs written with `pyo3` library which has certain conventions.
|
||||
|
||||
<File path="./src/lib.rs" />
|
||||
<File path="./turso/__init__.py" />
|
||||
|
||||
Remember, that it can accept `py: Python` argument which will be passed implicitly and exported bindings will not have this extra arg
|
||||
|
||||
# SQLite-like DB API
|
||||
|
||||
Make driver API similar to the SQLite DB-API2 for the python.
|
||||
|
||||
Pay additional attention to the following aspects:
|
||||
* SQLite DB-API2 implementation implicitly opens transaction for DML queries in the execute(...) method:
|
||||
> If autocommit is LEGACY_TRANSACTION_CONTROL, isolation_level is not None, sql is an INSERT, UPDATE, DELETE, or REPLACE statement, and there is no open transaction, a transaction is implicitly opened before executing sql.
|
||||
- MAKE SURE this logic implemented properly
|
||||
* Implement .rowcount property correctly, be careful with `executemany(...)` methods as it must return rowcount of all executed statements (not just last statement)
|
||||
* Convert exceptions from rust layer to appropriate exceptions documented in the sqlite3 db-api2 docs
|
||||
* BE CAREFUL with implementation of transaction control. Make sure that in LEGACY_TRANSACTION_CONTROL mode implicit transaction will be properly commited in case of cursor close
|
||||
|
||||
<Shell
|
||||
cmd="curl https://docs.python.org/3/library/sqlite3.html | pandoc --from html --to markdown"
|
||||
selector="~Connection objects|~Cursor objects|~Row objects|~Exceptions|~Transaction"
|
||||
ext=".md"
|
||||
/>
|
||||
|
||||
</Code>
|
||||
|
||||
</Output>
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
use pyo3::create_exception;
|
||||
use pyo3::exceptions::PyException;
|
||||
|
||||
create_exception!(
|
||||
limbo,
|
||||
Warning,
|
||||
PyException,
|
||||
"Exception raised for important warnings like data truncations while inserting."
|
||||
);
|
||||
create_exception!(limbo, Error, PyException, "Base class for all other error exceptions. Catch all database-related errors using this class.");
|
||||
|
||||
create_exception!(
|
||||
limbo,
|
||||
InterfaceError,
|
||||
Error,
|
||||
"Raised for errors related to the database interface rather than the database itself."
|
||||
);
|
||||
create_exception!(
|
||||
limbo,
|
||||
DatabaseError,
|
||||
Error,
|
||||
"Raised for errors that are related to the database."
|
||||
);
|
||||
|
||||
create_exception!(limbo, DataError, DatabaseError, "Raised for errors due to problems with the processed data like division by zero, numeric value out of range, etc.");
|
||||
create_exception!(limbo, OperationalError, DatabaseError, "Raised for errors related to the database’s operation, not necessarily under the programmer's control.");
|
||||
create_exception!(limbo, IntegrityError, DatabaseError, "Raised when the relational integrity of the database is affected, e.g., a foreign key check fails.");
|
||||
create_exception!(limbo, InternalError, DatabaseError, "Raised when the database encounters an internal error, e.g., cursor is not valid anymore, transaction out of sync.");
|
||||
create_exception!(limbo, ProgrammingError, DatabaseError, "Raised for programming errors, e.g., table not found, syntax error in SQL, wrong number of parameters specified.");
|
||||
create_exception!(
|
||||
limbo,
|
||||
NotSupportedError,
|
||||
DatabaseError,
|
||||
"Raised when a method or database API is used which is not supported by the database."
|
||||
);
|
||||
|
|
@ -1,350 +1,326 @@
|
|||
use anyhow::Result;
|
||||
use errors::*;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyBytes, PyList, PyTuple};
|
||||
use std::cell::RefCell;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::rc::Rc;
|
||||
use pyo3::{
|
||||
prelude::*,
|
||||
types::{PyBytes, PyTuple},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use turso_core::{DatabaseOpts, Value};
|
||||
use turso_sdk_kit::rsapi::{self, TursoError, TursoStatusCode, Value, ValueRef};
|
||||
|
||||
mod errors;
|
||||
use pyo3::create_exception;
|
||||
use pyo3::exceptions::PyException;
|
||||
|
||||
// support equality for status codes
|
||||
#[pyclass(eq, eq_int)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] // Add necessary traits for your use case
|
||||
pub enum PyTursoStatusCode {
|
||||
Ok = 0,
|
||||
Done = 1,
|
||||
Row = 2,
|
||||
Io = 3,
|
||||
}
|
||||
create_exception!(turso, Busy, PyException, "database is busy");
|
||||
create_exception!(turso, Interrupt, PyException, "interrupted");
|
||||
create_exception!(turso, Error, PyException, "generic error");
|
||||
create_exception!(turso, Misuse, PyException, "API misuse");
|
||||
create_exception!(turso, Constraint, PyException, "constraint error");
|
||||
create_exception!(turso, Readonly, PyException, "database is readonly");
|
||||
create_exception!(turso, DatabaseFull, PyException, "database is full");
|
||||
create_exception!(turso, NotAdb, PyException, "not a database`");
|
||||
create_exception!(turso, Corrupt, PyException, "database corrupted");
|
||||
|
||||
fn turso_error_to_py_err(err: TursoError) -> PyErr {
|
||||
match err.code {
|
||||
rsapi::TursoStatusCode::Busy => Busy::new_err(err.message),
|
||||
rsapi::TursoStatusCode::Interrupt => Interrupt::new_err(err.message),
|
||||
rsapi::TursoStatusCode::Error => Error::new_err(err.message),
|
||||
rsapi::TursoStatusCode::Misuse => Misuse::new_err(err.message),
|
||||
rsapi::TursoStatusCode::Constraint => Constraint::new_err(err.message),
|
||||
rsapi::TursoStatusCode::Readonly => Readonly::new_err(err.message),
|
||||
rsapi::TursoStatusCode::DatabaseFull => DatabaseFull::new_err(err.message),
|
||||
rsapi::TursoStatusCode::NotAdb => NotAdb::new_err(err.message),
|
||||
rsapi::TursoStatusCode::Corrupt => Corrupt::new_err(err.message),
|
||||
_ => Error::new_err("unexpected status from the sdk-kit".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn turso_status_to_py(status: TursoStatusCode) -> PyTursoStatusCode {
|
||||
match status {
|
||||
TursoStatusCode::Ok => PyTursoStatusCode::Ok,
|
||||
TursoStatusCode::Done => PyTursoStatusCode::Done,
|
||||
TursoStatusCode::Row => PyTursoStatusCode::Row,
|
||||
TursoStatusCode::Io => PyTursoStatusCode::Io,
|
||||
_ => panic!("unexpected status code: {status:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Clone, Debug)]
|
||||
struct Description {
|
||||
pub struct PyTursoExecutionResult {
|
||||
#[pyo3(get)]
|
||||
name: String,
|
||||
pub status: PyTursoStatusCode,
|
||||
#[pyo3(get)]
|
||||
type_code: String,
|
||||
#[pyo3(get)]
|
||||
display_size: Option<String>,
|
||||
#[pyo3(get)]
|
||||
internal_size: Option<String>,
|
||||
#[pyo3(get)]
|
||||
precision: Option<String>,
|
||||
#[pyo3(get)]
|
||||
scale: Option<String>,
|
||||
#[pyo3(get)]
|
||||
null_ok: Option<String>,
|
||||
pub rows_changed: u64,
|
||||
}
|
||||
|
||||
#[pyclass(unsendable)]
|
||||
pub struct Cursor {
|
||||
/// This read/write attribute specifies the number of rows to fetch at a time with `.fetchmany()`.
|
||||
/// It defaults to `1`, meaning it fetches a single row at a time.
|
||||
#[pyclass]
|
||||
pub struct PyTursoLog {
|
||||
#[pyo3(get)]
|
||||
arraysize: i64,
|
||||
|
||||
conn: Connection,
|
||||
|
||||
/// The `.description` attribute is a read-only sequence of 7-item, each describing a column in the result set:
|
||||
///
|
||||
/// - `name`: The column's name (always present).
|
||||
/// - `type_code`: The data type code (always present).
|
||||
/// - `display_size`: Column's display size (optional).
|
||||
/// - `internal_size`: Column's internal size (optional).
|
||||
/// - `precision`: Numeric precision (optional).
|
||||
/// - `scale`: Numeric scale (optional).
|
||||
/// - `null_ok`: Indicates if null values are allowed (optional).
|
||||
///
|
||||
/// The `name` and `type_code` fields are mandatory; others default to `None` if not applicable.
|
||||
///
|
||||
/// This attribute is `None` for operations that do not return rows or if no `.execute*()` method has been invoked.
|
||||
pub message: String,
|
||||
#[pyo3(get)]
|
||||
description: Option<Description>,
|
||||
|
||||
/// Read-only attribute that provides the number of modified rows for `INSERT`, `UPDATE`, `DELETE`,
|
||||
/// and `REPLACE` statements; it is `-1` for other statements, including CTE queries.
|
||||
/// It is only updated by the `execute()` and `executemany()` methods after the statement has run to completion.
|
||||
/// This means any resulting rows must be fetched for `rowcount` to be updated.
|
||||
pub target: String,
|
||||
#[pyo3(get)]
|
||||
rowcount: i64,
|
||||
|
||||
smt: Option<Rc<RefCell<turso_core::Statement>>>,
|
||||
pub file: String,
|
||||
#[pyo3(get)]
|
||||
pub timestamp: u64,
|
||||
#[pyo3(get)]
|
||||
pub line: usize,
|
||||
#[pyo3(get)]
|
||||
pub level: String,
|
||||
}
|
||||
|
||||
#[allow(unused_variables, clippy::arc_with_non_send_sync)]
|
||||
#[pymethods]
|
||||
impl Cursor {
|
||||
#[pyo3(signature = (sql, parameters=None))]
|
||||
pub fn execute(&mut self, sql: &str, parameters: Option<Py<PyTuple>>) -> Result<Self> {
|
||||
let stmt_is_dml = stmt_is_dml(sql);
|
||||
let stmt_is_ddl = stmt_is_ddl(sql);
|
||||
let stmt_is_tx = stmt_is_tx(sql);
|
||||
|
||||
let statement = self.conn.conn.prepare(sql).map_err(|e| {
|
||||
PyErr::new::<ProgrammingError, _>(format!("Failed to prepare statement: {e:?}"))
|
||||
})?;
|
||||
|
||||
let stmt = Rc::new(RefCell::new(statement));
|
||||
|
||||
Python::with_gil(|py| {
|
||||
if let Some(params) = parameters {
|
||||
let obj = params.into_bound(py);
|
||||
|
||||
for (i, elem) in obj.iter().enumerate() {
|
||||
let value = py_to_db_value(&elem)?;
|
||||
stmt.borrow_mut()
|
||||
.bind_at(NonZeroUsize::new(i + 1).unwrap(), value);
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})?;
|
||||
|
||||
if stmt_is_dml && self.conn.conn.get_auto_commit() {
|
||||
self.conn.conn.execute("BEGIN").map_err(|e| {
|
||||
PyErr::new::<OperationalError, _>(format!(
|
||||
"Failed to start transaction after DDL: {e:?}"
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
// For DDL and DML statements,
|
||||
// we need to execute the statement immediately
|
||||
if stmt_is_ddl || stmt_is_dml || stmt_is_tx {
|
||||
let mut stmt = stmt.borrow_mut();
|
||||
while let turso_core::StepResult::IO = stmt
|
||||
.step()
|
||||
.map_err(|e| PyErr::new::<OperationalError, _>(format!("Step error: {e:?}")))?
|
||||
{
|
||||
stmt.run_once()
|
||||
.map_err(|e| PyErr::new::<OperationalError, _>(format!("IO error: {e:?}")))?;
|
||||
}
|
||||
}
|
||||
|
||||
self.smt = Some(stmt);
|
||||
|
||||
Ok(Cursor {
|
||||
smt: self.smt.clone(),
|
||||
conn: self.conn.clone(),
|
||||
description: self.description.clone(),
|
||||
rowcount: self.rowcount,
|
||||
arraysize: self.arraysize,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fetchone(&mut self, py: Python) -> Result<Option<PyObject>> {
|
||||
if let Some(smt) = &self.smt {
|
||||
loop {
|
||||
let mut stmt = smt.borrow_mut();
|
||||
match stmt
|
||||
.step()
|
||||
.map_err(|e| PyErr::new::<OperationalError, _>(format!("Step error: {e:?}")))?
|
||||
{
|
||||
turso_core::StepResult::Row => {
|
||||
let row = stmt.row().unwrap();
|
||||
let py_row = row_to_py(py, row)?;
|
||||
return Ok(Some(py_row));
|
||||
}
|
||||
turso_core::StepResult::IO => {
|
||||
stmt.run_once().map_err(|e| {
|
||||
PyErr::new::<OperationalError, _>(format!("IO error: {e:?}"))
|
||||
})?;
|
||||
}
|
||||
turso_core::StepResult::Interrupt => {
|
||||
return Ok(None);
|
||||
}
|
||||
turso_core::StepResult::Done => {
|
||||
return Ok(None);
|
||||
}
|
||||
turso_core::StepResult::Busy => {
|
||||
return Err(
|
||||
PyErr::new::<OperationalError, _>("Busy error".to_string()).into()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(PyErr::new::<ProgrammingError, _>("No statement prepared for execution").into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetchall(&mut self, py: Python) -> Result<Vec<PyObject>> {
|
||||
let mut results = Vec::new();
|
||||
if let Some(smt) = &self.smt {
|
||||
loop {
|
||||
let mut stmt = smt.borrow_mut();
|
||||
match stmt
|
||||
.step()
|
||||
.map_err(|e| PyErr::new::<OperationalError, _>(format!("Step error: {e:?}")))?
|
||||
{
|
||||
turso_core::StepResult::Row => {
|
||||
let row = stmt.row().unwrap();
|
||||
let py_row = row_to_py(py, row)?;
|
||||
results.push(py_row);
|
||||
}
|
||||
turso_core::StepResult::IO => {
|
||||
stmt.run_once().map_err(|e| {
|
||||
PyErr::new::<OperationalError, _>(format!("IO error: {e:?}"))
|
||||
})?;
|
||||
}
|
||||
turso_core::StepResult::Interrupt => {
|
||||
return Ok(results);
|
||||
}
|
||||
turso_core::StepResult::Done => {
|
||||
return Ok(results);
|
||||
}
|
||||
turso_core::StepResult::Busy => {
|
||||
return Err(
|
||||
PyErr::new::<OperationalError, _>("Busy error".to_string()).into()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(PyErr::new::<ProgrammingError, _>("No statement prepared for execution").into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(&self) -> PyResult<()> {
|
||||
self.conn.close()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pyo3(signature = (sql, parameters=None))]
|
||||
pub fn executemany(&self, sql: &str, parameters: Option<Py<PyList>>) -> PyResult<()> {
|
||||
Err(PyErr::new::<NotSupportedError, _>(
|
||||
"executemany() is not supported in this version",
|
||||
))
|
||||
}
|
||||
|
||||
#[pyo3(signature = (size=None))]
|
||||
pub fn fetchmany(&self, size: Option<i64>) -> PyResult<Option<Vec<PyObject>>> {
|
||||
Err(PyErr::new::<NotSupportedError, _>(
|
||||
"fetchmany() is not supported in this version",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn stmt_is_dml(sql: &str) -> bool {
|
||||
let sql = sql.trim();
|
||||
let sql = sql.to_uppercase();
|
||||
sql.starts_with("INSERT") || sql.starts_with("UPDATE") || sql.starts_with("DELETE")
|
||||
}
|
||||
|
||||
fn stmt_is_ddl(sql: &str) -> bool {
|
||||
let sql = sql.trim();
|
||||
let sql = sql.to_uppercase();
|
||||
sql.starts_with("CREATE") || sql.starts_with("ALTER") || sql.starts_with("DROP")
|
||||
}
|
||||
|
||||
fn stmt_is_tx(sql: &str) -> bool {
|
||||
let sql = sql.trim();
|
||||
let sql = sql.to_uppercase();
|
||||
sql.starts_with("BEGIN") || sql.starts_with("COMMIT") || sql.starts_with("ROLLBACK")
|
||||
}
|
||||
|
||||
#[pyclass(unsendable)]
|
||||
#[derive(Clone)]
|
||||
pub struct Connection {
|
||||
conn: Arc<turso_core::Connection>,
|
||||
_io: Arc<dyn turso_core::IO>,
|
||||
#[pyclass]
|
||||
pub struct PyTursoSetupConfig {
|
||||
pub logger: Option<Py<PyAny>>,
|
||||
pub log_level: Option<String>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl Connection {
|
||||
pub fn cursor(&self) -> Result<Cursor> {
|
||||
Ok(Cursor {
|
||||
arraysize: 1,
|
||||
conn: self.clone(),
|
||||
description: None,
|
||||
rowcount: -1,
|
||||
smt: None,
|
||||
impl PyTursoSetupConfig {
|
||||
#[new]
|
||||
#[pyo3(signature = (logger, log_level))]
|
||||
fn new(logger: Option<Py<PyAny>>, log_level: Option<String>) -> Self {
|
||||
Self { logger, log_level }
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
pub struct PyTursoDatabaseConfig {
|
||||
pub path: String,
|
||||
|
||||
/// comma-separated list of experimental features to enable
|
||||
/// this field is intentionally just a string in order to make enablement of experimental features as flexible as possible
|
||||
pub experimental_features: Option<String>,
|
||||
|
||||
/// if true, library methods will return Io status code and delegate Io loop to the caller
|
||||
/// if false, library will spin IO itself in case of Io status code and never return it to the caller
|
||||
pub async_io: bool,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTursoDatabaseConfig {
|
||||
#[new]
|
||||
#[pyo3(signature = (path, experimental_features=None, async_io=false))]
|
||||
fn new(path: String, experimental_features: Option<String>, async_io: bool) -> Self {
|
||||
Self {
|
||||
path,
|
||||
experimental_features,
|
||||
async_io,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
pub struct PyTursoDatabase {
|
||||
database: Arc<rsapi::TursoDatabase>,
|
||||
}
|
||||
|
||||
/// Setup logging for the turso globally
|
||||
/// Only first invocation has effect - all subsequent updates will be ignored
|
||||
#[pyfunction]
|
||||
pub fn py_turso_setup(py: Python, config: &PyTursoSetupConfig) -> PyResult<()> {
|
||||
rsapi::turso_setup(rsapi::TursoSetupConfig {
|
||||
logger: if let Some(logger) = &config.logger {
|
||||
let logger = logger.clone_ref(py);
|
||||
Some(Box::new(move |log| {
|
||||
Python::attach(|py| {
|
||||
let py_log = PyTursoLog {
|
||||
message: log.message.to_string(),
|
||||
target: log.target.to_string(),
|
||||
file: log.file.to_string(),
|
||||
timestamp: log.timestamp,
|
||||
line: log.line,
|
||||
level: log.level.to_string(),
|
||||
};
|
||||
logger.call1(py, (py_log,)).unwrap();
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
log_level: config.log_level.clone(),
|
||||
})
|
||||
.map_err(turso_error_to_py_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open the database
|
||||
#[pyfunction]
|
||||
pub fn py_turso_database_open(config: &PyTursoDatabaseConfig) -> PyResult<PyTursoDatabase> {
|
||||
let database = rsapi::TursoDatabase::create(rsapi::TursoDatabaseConfig {
|
||||
path: config.path.clone(),
|
||||
experimental_features: config.experimental_features.clone(),
|
||||
async_io: config.async_io,
|
||||
io: None,
|
||||
});
|
||||
database.open().map_err(turso_error_to_py_err)?;
|
||||
Ok(PyTursoDatabase { database })
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTursoDatabase {
|
||||
pub fn connect(&self) -> PyResult<PyTursoConnection> {
|
||||
Ok(PyTursoConnection {
|
||||
connection: self.database.connect().map_err(turso_error_to_py_err)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
pub struct PyTursoConnection {
|
||||
connection: Arc<rsapi::TursoConnection>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTursoConnection {
|
||||
/// prepare single statement from the string
|
||||
pub fn prepare_single(&self, sql: &str) -> PyResult<PyTursoStatement> {
|
||||
Ok(PyTursoStatement {
|
||||
statement: self
|
||||
.connection
|
||||
.prepare_single(sql)
|
||||
.map_err(turso_error_to_py_err)?,
|
||||
})
|
||||
}
|
||||
/// prepare first statement from the string which can have multiple statements separated by semicolon
|
||||
/// returns None if string has no statements
|
||||
/// returns Some with prepared statement and position in the string right after the prepared statement end
|
||||
pub fn prepare_first(&self, sql: &str) -> PyResult<Option<(PyTursoStatement, usize)>> {
|
||||
match self
|
||||
.connection
|
||||
.prepare_first(sql)
|
||||
.map_err(turso_error_to_py_err)?
|
||||
{
|
||||
Some((statement, tail_idx)) => Ok(Some((PyTursoStatement { statement }, tail_idx))),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
/// Get the auto_commmit mode for the connection
|
||||
pub fn get_auto_commit(&self) -> PyResult<bool> {
|
||||
Ok(self.connection.get_auto_commit())
|
||||
}
|
||||
/// Close the connection
|
||||
/// (caller must ensure that no operations over connection or derived statements will happen after the call)
|
||||
pub fn close(&self) -> PyResult<()> {
|
||||
self.conn.close().map_err(|e| {
|
||||
PyErr::new::<OperationalError, _>(format!("Failed to close connection: {e:?}"))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn commit(&self) -> PyResult<()> {
|
||||
if !self.conn.get_auto_commit() {
|
||||
self.conn.execute("COMMIT").map_err(|e| {
|
||||
PyErr::new::<OperationalError, _>(format!("Failed to commit: {e:?}"))
|
||||
})?;
|
||||
|
||||
self.conn.execute("BEGIN").map_err(|e| {
|
||||
PyErr::new::<OperationalError, _>(format!("Failed to commit: {e:?}"))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rollback(&self) -> PyResult<()> {
|
||||
if !self.conn.get_auto_commit() {
|
||||
self.conn.execute("ROLLBACK").map_err(|e| {
|
||||
PyErr::new::<OperationalError, _>(format!("Failed to commit: {e:?}"))
|
||||
})?;
|
||||
|
||||
self.conn.execute("BEGIN").map_err(|e| {
|
||||
PyErr::new::<OperationalError, _>(format!("Failed to commit: {e:?}"))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn __enter__(&self) -> PyResult<Self> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
|
||||
fn __exit__(
|
||||
&self,
|
||||
_exc_type: Option<&Bound<'_, PyAny>>,
|
||||
_exc_val: Option<&Bound<'_, PyAny>>,
|
||||
_exc_tb: Option<&Bound<'_, PyAny>>,
|
||||
) -> PyResult<()> {
|
||||
self.close()
|
||||
self.connection.close().map_err(turso_error_to_py_err)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Connection {
|
||||
fn drop(&mut self) {
|
||||
if Arc::strong_count(&self.conn) == 1 {
|
||||
self.conn
|
||||
.close()
|
||||
.expect("Failed to drop (close) connection");
|
||||
}
|
||||
}
|
||||
#[pyclass]
|
||||
pub struct PyTursoStatement {
|
||||
statement: Box<rsapi::TursoStatement>,
|
||||
}
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
#[pyfunction(signature = (path))]
|
||||
pub fn connect(path: &str) -> Result<Connection> {
|
||||
match turso_core::Connection::from_uri(path, DatabaseOpts::default()) {
|
||||
Ok((io, conn)) => Ok(Connection { conn, _io: io }),
|
||||
Err(e) => Err(PyErr::new::<ProgrammingError, _>(format!(
|
||||
"Failed to create connection: {e:?}"
|
||||
#[pymethods]
|
||||
impl PyTursoStatement {
|
||||
/// binds positional parameters to the statement
|
||||
pub fn bind(&mut self, parameters: Bound<PyTuple>) -> PyResult<()> {
|
||||
let len = parameters.len();
|
||||
for i in 0..len {
|
||||
let parameter = parameters.get_item(i)?;
|
||||
self.statement
|
||||
.bind_positional(i + 1, py_to_db_value(parameter)?)
|
||||
.map_err(turso_error_to_py_err)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// step one iteration of the statement execution
|
||||
/// Returns [PyTursoStatusCode::Done] when execution is finished
|
||||
/// Returns [PyTursoStatusCode::Row] when execution generated a row which can be consumed with [Self::row] method
|
||||
/// Returns [PyTursoStatusCode::Io] when async_io is set and execution needs IO in order to make progress
|
||||
///
|
||||
/// The caller must always either use [Self::step] or [Self::execute] methods for single statement - but never mix them together
|
||||
pub fn step(&mut self) -> PyResult<PyTursoStatusCode> {
|
||||
Ok(turso_status_to_py(
|
||||
self.statement.step().map_err(turso_error_to_py_err)?,
|
||||
))
|
||||
.into()),
|
||||
}
|
||||
|
||||
/// execute statement and ignore all rows generated by it
|
||||
/// Returns [PyTursoStatusCode::Done] when execution is finished
|
||||
/// Returns [PyTursoStatusCode::Io] when async_io is set and execution needs IO in order to make progress
|
||||
///
|
||||
/// Note, that execute never returns Row status code
|
||||
///
|
||||
/// The caller must always either use [Self::step] or [Self::execute] methods for single statement - but never mix them together
|
||||
pub fn execute(&mut self) -> PyResult<PyTursoExecutionResult> {
|
||||
let result = self.statement.execute().map_err(turso_error_to_py_err)?;
|
||||
Ok(PyTursoExecutionResult {
|
||||
status: turso_status_to_py(result.status),
|
||||
rows_changed: result.rows_changed,
|
||||
})
|
||||
}
|
||||
/// Run one iteration of IO backend
|
||||
pub fn run_io(&self) -> PyResult<()> {
|
||||
self.statement.run_io().map_err(turso_error_to_py_err)?;
|
||||
Ok(())
|
||||
}
|
||||
/// Get column names of the statement
|
||||
pub fn columns(&self, py: Python) -> PyResult<Py<PyTuple>> {
|
||||
let columns_count = self.statement.column_count();
|
||||
let mut columns = Vec::with_capacity(columns_count);
|
||||
for i in 0..columns_count {
|
||||
columns.push(
|
||||
self.statement
|
||||
.column_name(i)
|
||||
.map_err(turso_error_to_py_err)?
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(PyTuple::new(py, columns.into_iter())?.unbind())
|
||||
}
|
||||
/// Get tuple with current row values
|
||||
/// This method is only valid to call after [Self::step] returned [PyTursoStatusCode::Row] status code
|
||||
pub fn row(&self, py: Python) -> PyResult<Py<PyTuple>> {
|
||||
let columns_count = self.statement.column_count();
|
||||
let mut py_values = Vec::with_capacity(columns_count);
|
||||
for i in 0..columns_count {
|
||||
py_values.push(db_value_to_py(
|
||||
py,
|
||||
self.statement.row_value(i).map_err(turso_error_to_py_err)?,
|
||||
)?);
|
||||
}
|
||||
Ok(PyTuple::new(py, &py_values)?.into_pyobject(py)?.into())
|
||||
}
|
||||
/// Finalize statement execution
|
||||
/// This method must be called when statement is no longer need
|
||||
/// It will perform necessary cleanup and run any unfinished statement operations to completion
|
||||
/// (for example, in `INSERT INTO ... RETURNING ...` query, finalize is essential as it will make sure that all inserts will be completed, even if only few first rows were consumed by the caller)
|
||||
///
|
||||
/// Note, that if statement wasn't started (no step / execute methods was called) - finalize will not execute the statement
|
||||
pub fn finalize(&mut self) -> PyResult<PyTursoStatusCode> {
|
||||
Ok(turso_status_to_py(
|
||||
self.statement.finalize().map_err(turso_error_to_py_err)?,
|
||||
))
|
||||
}
|
||||
/// Reset the statement by clearing bindings and reclaiming memory of the program from previous run
|
||||
/// This will also abort last operation if any was unfinished (but if transaction was opened before this statement - its state will be untouched, reset will only affect operation within current statement)
|
||||
pub fn reset(&mut self) -> PyResult<()> {
|
||||
self.statement.reset().map_err(turso_error_to_py_err)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn row_to_py(py: Python, row: &turso_core::Row) -> Result<PyObject> {
|
||||
let mut py_values = Vec::new();
|
||||
for value in row.get_values() {
|
||||
match value {
|
||||
turso_core::Value::Null => py_values.push(py.None()),
|
||||
turso_core::Value::Integer(i) => py_values.push(i.into_pyobject(py)?.into()),
|
||||
turso_core::Value::Float(f) => py_values.push(f.into_pyobject(py)?.into()),
|
||||
turso_core::Value::Text(s) => py_values.push(s.as_str().into_pyobject(py)?.into()),
|
||||
turso_core::Value::Blob(b) => py_values.push(PyBytes::new(py, b.as_slice()).into()),
|
||||
}
|
||||
fn db_value_to_py(py: Python, value: rsapi::ValueRef) -> PyResult<Py<PyAny>> {
|
||||
match value {
|
||||
ValueRef::Null => Ok(py.None()),
|
||||
ValueRef::Integer(i) => Ok(i.into_pyobject(py)?.into()),
|
||||
ValueRef::Float(f) => Ok(f.into_pyobject(py)?.into()),
|
||||
ValueRef::Text(s) => Ok(s.as_str().into_pyobject(py)?.into()),
|
||||
ValueRef::Blob(b) => Ok(PyBytes::new(py, b).into()),
|
||||
}
|
||||
Ok(PyTuple::new(py, &py_values)
|
||||
.unwrap()
|
||||
.into_pyobject(py)?
|
||||
.into())
|
||||
}
|
||||
|
||||
/// Converts a Python object to a Turso Value
|
||||
fn py_to_db_value(obj: &Bound<PyAny>) -> Result<turso_core::Value> {
|
||||
fn py_to_db_value(obj: Bound<PyAny>) -> PyResult<Value> {
|
||||
if obj.is_none() {
|
||||
Ok(Value::Null)
|
||||
} else if let Ok(integer) = obj.extract::<i64>() {
|
||||
|
|
@ -353,32 +329,38 @@ fn py_to_db_value(obj: &Bound<PyAny>) -> Result<turso_core::Value> {
|
|||
Ok(Value::Float(float))
|
||||
} else if let Ok(string) = obj.extract::<String>() {
|
||||
Ok(Value::Text(string.into()))
|
||||
} else if let Ok(bytes) = obj.downcast::<PyBytes>() {
|
||||
} else if let Ok(bytes) = obj.cast::<PyBytes>() {
|
||||
Ok(Value::Blob(bytes.as_bytes().to_vec()))
|
||||
} else {
|
||||
return Err(PyErr::new::<ProgrammingError, _>(format!(
|
||||
"Unsupported Python type: {}",
|
||||
obj.get_type().name()?
|
||||
Err(Error::new_err(
|
||||
"unexpected parameter value, only None, numbers, strings and bytes are supported"
|
||||
.to_string(),
|
||||
))
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn _turso(m: &Bound<PyModule>) -> PyResult<()> {
|
||||
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
|
||||
m.add_class::<Connection>()?;
|
||||
m.add_class::<Cursor>()?;
|
||||
m.add_function(wrap_pyfunction!(connect, m)?)?;
|
||||
m.add("Warning", m.py().get_type::<Warning>())?;
|
||||
m.add_function(wrap_pyfunction!(py_turso_setup, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(py_turso_database_open, m)?)?;
|
||||
m.add_class::<PyTursoStatusCode>()?;
|
||||
m.add_class::<PyTursoExecutionResult>()?;
|
||||
m.add_class::<PyTursoLog>()?;
|
||||
m.add_class::<PyTursoSetupConfig>()?;
|
||||
m.add_class::<PyTursoDatabaseConfig>()?;
|
||||
m.add_class::<PyTursoDatabase>()?;
|
||||
m.add_class::<PyTursoConnection>()?;
|
||||
m.add_class::<PyTursoStatement>()?;
|
||||
|
||||
m.add("Busy", m.py().get_type::<Busy>())?;
|
||||
m.add("Interrupt", m.py().get_type::<Interrupt>())?;
|
||||
m.add("Error", m.py().get_type::<Error>())?;
|
||||
m.add("InterfaceError", m.py().get_type::<InterfaceError>())?;
|
||||
m.add("DatabaseError", m.py().get_type::<DatabaseError>())?;
|
||||
m.add("DataError", m.py().get_type::<DataError>())?;
|
||||
m.add("OperationalError", m.py().get_type::<OperationalError>())?;
|
||||
m.add("IntegrityError", m.py().get_type::<IntegrityError>())?;
|
||||
m.add("InternalError", m.py().get_type::<InternalError>())?;
|
||||
m.add("ProgrammingError", m.py().get_type::<ProgrammingError>())?;
|
||||
m.add("NotSupportedError", m.py().get_type::<NotSupportedError>())?;
|
||||
m.add("Misuse", m.py().get_type::<Misuse>())?;
|
||||
m.add("Constraint", m.py().get_type::<Constraint>())?;
|
||||
m.add("Readonly", m.py().get_type::<Readonly>())?;
|
||||
m.add("DatabaseFull", m.py().get_type::<DatabaseFull>())?;
|
||||
m.add("NotAdb", m.py().get_type::<NotAdb>())?;
|
||||
m.add("Corrupt", m.py().get_type::<Corrupt>())?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,29 +1,41 @@
|
|||
from ._turso import (
|
||||
from .lib import (
|
||||
Connection,
|
||||
Cursor,
|
||||
DatabaseError,
|
||||
DataError,
|
||||
Error,
|
||||
IntegrityError,
|
||||
InterfaceError,
|
||||
InternalError,
|
||||
NotSupportedError,
|
||||
OperationalError,
|
||||
ProgrammingError,
|
||||
__version__,
|
||||
Row,
|
||||
Warning,
|
||||
apilevel,
|
||||
connect,
|
||||
paramstyle,
|
||||
setup_logging,
|
||||
threadsafety,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
"Connection",
|
||||
"Cursor",
|
||||
"InterfaceError",
|
||||
"Row",
|
||||
"connect",
|
||||
"setup_logging",
|
||||
"Warning",
|
||||
"DatabaseError",
|
||||
"DataError",
|
||||
"OperationalError",
|
||||
"Error",
|
||||
"IntegrityError",
|
||||
"InterfaceError",
|
||||
"InternalError",
|
||||
"ProgrammingError",
|
||||
"NotSupportedError",
|
||||
"connect",
|
||||
"OperationalError",
|
||||
"ProgrammingError",
|
||||
"apilevel",
|
||||
"paramstyle",
|
||||
"threadsafety",
|
||||
]
|
||||
|
|
|
|||
909
bindings/python/turso/lib.py
Normal file
909
bindings/python/turso/lib.py
Normal file
|
|
@ -0,0 +1,909 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from types import TracebackType
|
||||
from typing import Any, Callable, Optional, TypeVar
|
||||
|
||||
from ._turso import (
|
||||
Busy,
|
||||
Constraint,
|
||||
Corrupt,
|
||||
DatabaseFull,
|
||||
Interrupt,
|
||||
Misuse,
|
||||
NotAdb,
|
||||
PyTursoConnection,
|
||||
PyTursoDatabase,
|
||||
PyTursoDatabaseConfig,
|
||||
PyTursoExecutionResult,
|
||||
PyTursoLog,
|
||||
PyTursoSetupConfig,
|
||||
PyTursoStatement,
|
||||
PyTursoStatusCode,
|
||||
py_turso_database_open,
|
||||
py_turso_setup,
|
||||
)
|
||||
from ._turso import (
|
||||
Error as TursoError,
|
||||
)
|
||||
from ._turso import (
|
||||
PyTursoStatusCode as Status,
|
||||
)
|
||||
|
||||
# DB-API 2.0 module attributes
|
||||
apilevel = "2.0"
|
||||
threadsafety = 1 # 1 means: Threads may share the module, but not connections.
|
||||
paramstyle = "qmark" # Only positional parameters are supported.
|
||||
|
||||
|
||||
# Exception hierarchy following DB-API 2.0
|
||||
class Warning(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InterfaceError(Error):
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseError(Error):
|
||||
pass
|
||||
|
||||
|
||||
class DataError(DatabaseError):
|
||||
pass
|
||||
|
||||
|
||||
class OperationalError(DatabaseError):
|
||||
pass
|
||||
|
||||
|
||||
class IntegrityError(DatabaseError):
|
||||
pass
|
||||
|
||||
|
||||
class InternalError(DatabaseError):
|
||||
pass
|
||||
|
||||
|
||||
class ProgrammingError(DatabaseError):
|
||||
pass
|
||||
|
||||
|
||||
class NotSupportedError(DatabaseError):
|
||||
pass
|
||||
|
||||
|
||||
def _map_turso_exception(exc: Exception) -> Exception:
|
||||
"""Maps Turso-specific exceptions to DB-API 2.0 exception hierarchy"""
|
||||
if isinstance(exc, Busy):
|
||||
return OperationalError(str(exc))
|
||||
if isinstance(exc, Interrupt):
|
||||
return OperationalError(str(exc))
|
||||
if isinstance(exc, Misuse):
|
||||
return InterfaceError(str(exc))
|
||||
if isinstance(exc, Constraint):
|
||||
return IntegrityError(str(exc))
|
||||
if isinstance(exc, TursoError):
|
||||
# Generic Turso error -> DatabaseError
|
||||
return DatabaseError(str(exc))
|
||||
if isinstance(exc, DatabaseFull):
|
||||
return OperationalError(str(exc))
|
||||
if isinstance(exc, NotAdb):
|
||||
return DatabaseError(str(exc))
|
||||
if isinstance(exc, Corrupt):
|
||||
return DatabaseError(str(exc))
|
||||
return exc
|
||||
|
||||
|
||||
# Internal helpers
|
||||
|
||||
_DBCursorT = TypeVar("_DBCursorT", bound="Cursor")
|
||||
|
||||
|
||||
def _first_keyword(sql: str) -> str:
|
||||
"""
|
||||
Return the first SQL keyword (uppercased) ignoring leading whitespace
|
||||
and single-line and multi-line comments.
|
||||
|
||||
This is intentionally minimal and only used to detect DML for implicit
|
||||
transaction handling. It may not handle all edge cases (e.g. complex WITH).
|
||||
"""
|
||||
i = 0
|
||||
n = len(sql)
|
||||
while i < n:
|
||||
c = sql[i]
|
||||
if c.isspace():
|
||||
i += 1
|
||||
continue
|
||||
if c == "-" and i + 1 < n and sql[i + 1] == "-":
|
||||
# line comment
|
||||
i += 2
|
||||
while i < n and sql[i] not in ("\r", "\n"):
|
||||
i += 1
|
||||
continue
|
||||
if c == "/" and i + 1 < n and sql[i + 1] == "*":
|
||||
# block comment
|
||||
i += 2
|
||||
while i + 1 < n and not (sql[i] == "*" and sql[i + 1] == "/"):
|
||||
i += 1
|
||||
i = min(i + 2, n)
|
||||
continue
|
||||
break
|
||||
# read token
|
||||
j = i
|
||||
while j < n and (sql[j].isalpha() or sql[j] == "_"):
|
||||
j += 1
|
||||
return sql[i:j].upper()
|
||||
|
||||
|
||||
def _is_dml(sql: str) -> bool:
|
||||
kw = _first_keyword(sql)
|
||||
if kw in ("INSERT", "UPDATE", "DELETE", "REPLACE"):
|
||||
return True
|
||||
# "WITH" can also prefix DML, but we conservatively skip it to avoid false positives.
|
||||
return False
|
||||
|
||||
|
||||
def _is_insert_or_replace(sql: str) -> bool:
|
||||
kw = _first_keyword(sql)
|
||||
return kw in ("INSERT", "REPLACE")
|
||||
|
||||
|
||||
def _run_execute_with_io(stmt: PyTursoStatement) -> PyTursoExecutionResult:
|
||||
"""
|
||||
Run PyTursoStatement.execute() handling potential async IO loops.
|
||||
"""
|
||||
while True:
|
||||
result = stmt.execute()
|
||||
status = result.status
|
||||
if status == Status.Io:
|
||||
# Drive IO loop; repeat.
|
||||
stmt.run_io()
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
def _step_once_with_io(stmt: PyTursoStatement) -> PyTursoStatusCode:
|
||||
"""
|
||||
Run PyTursoStatement.step() once handling potential async IO loops.
|
||||
"""
|
||||
while True:
|
||||
status = stmt.step()
|
||||
if status == Status.Io:
|
||||
stmt.run_io()
|
||||
continue
|
||||
return status
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Prepared:
|
||||
stmt: PyTursoStatement
|
||||
tail_index: int
|
||||
has_columns: bool
|
||||
column_names: tuple[str, ...]
|
||||
|
||||
|
||||
# Connection goes FIRST
|
||||
class Connection:
|
||||
"""
|
||||
A connection to a Turso (SQLite-compatible) database.
|
||||
|
||||
Similar to sqlite3.Connection with a subset of features focusing on DB-API 2.0.
|
||||
"""
|
||||
|
||||
# Expose exception classes as attributes like sqlite3.Connection does
|
||||
@property
|
||||
def DataError(self) -> type[DataError]:
|
||||
return DataError
|
||||
|
||||
@property
|
||||
def DatabaseError(self) -> type[DatabaseError]:
|
||||
return DatabaseError
|
||||
|
||||
@property
|
||||
def Error(self) -> type[Error]:
|
||||
return Error
|
||||
|
||||
@property
|
||||
def IntegrityError(self) -> type[IntegrityError]:
|
||||
return IntegrityError
|
||||
|
||||
@property
|
||||
def InterfaceError(self) -> type[InterfaceError]:
|
||||
return InterfaceError
|
||||
|
||||
@property
|
||||
def InternalError(self) -> type[InternalError]:
|
||||
return InternalError
|
||||
|
||||
@property
|
||||
def NotSupportedError(self) -> type[NotSupportedError]:
|
||||
return NotSupportedError
|
||||
|
||||
@property
|
||||
def OperationalError(self) -> type[OperationalError]:
|
||||
return OperationalError
|
||||
|
||||
@property
|
||||
def ProgrammingError(self) -> type[ProgrammingError]:
|
||||
return ProgrammingError
|
||||
|
||||
@property
|
||||
def Warning(self) -> type[Warning]:
|
||||
return Warning
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
conn: PyTursoConnection,
|
||||
*,
|
||||
isolation_level: Optional[str] = "DEFERRED",
|
||||
) -> None:
|
||||
self._conn: PyTursoConnection = conn
|
||||
# autocommit behavior:
|
||||
# - True: SQLite autocommit mode; commit/rollback are no-ops.
|
||||
# - False: PEP 249 compliant: ensure a transaction is always open.
|
||||
# We'll use BEGIN DEFERRED after commit/rollback.
|
||||
# - "LEGACY": implicit transactions on DML when isolation_level is not None.
|
||||
self._autocommit_mode: object | bool = "LEGACY"
|
||||
self.isolation_level: Optional[str] = isolation_level
|
||||
self.row_factory: Callable[[Cursor, Row], object] | type[Row] | None = None
|
||||
self.text_factory: Any = str
|
||||
|
||||
# If autocommit is False, ensure a transaction is open
|
||||
if self._autocommit_mode is False:
|
||||
self._ensure_transaction_open()
|
||||
|
||||
def _ensure_transaction_open(self) -> None:
|
||||
"""
|
||||
Ensure a transaction is open when autocommit is False.
|
||||
"""
|
||||
try:
|
||||
if self._conn.get_auto_commit():
|
||||
# No transaction active -> open new one according to isolation_level (default to DEFERRED)
|
||||
level = self.isolation_level or "DEFERRED"
|
||||
self._exec_ddl_only(f"BEGIN {level}")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
def _exec_ddl_only(self, sql: str) -> None:
|
||||
"""
|
||||
Execute a SQL statement that does not produce rows and ignore any result rows.
|
||||
"""
|
||||
try:
|
||||
stmt = self._conn.prepare_single(sql)
|
||||
_run_execute_with_io(stmt)
|
||||
# finalize to ensure completion; finalize never mixes with execute
|
||||
stmt.finalize()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
def _prepare_first(self, sql: str) -> _Prepared:
|
||||
"""
|
||||
Prepare the first statement in the given SQL string and return metadata.
|
||||
"""
|
||||
try:
|
||||
opt = self._conn.prepare_first(sql)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
if opt is None:
|
||||
raise ProgrammingError("no SQL statements to execute")
|
||||
|
||||
stmt, tail_idx = opt
|
||||
# Determine whether statement returns columns (rows)
|
||||
try:
|
||||
columns = tuple(stmt.columns())
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# Clean up statement before re-raising
|
||||
try:
|
||||
stmt.finalize()
|
||||
except Exception:
|
||||
pass
|
||||
raise _map_turso_exception(exc)
|
||||
has_cols = len(columns) > 0
|
||||
return _Prepared(stmt=stmt, tail_index=tail_idx, has_columns=has_cols, column_names=columns)
|
||||
|
||||
def _raise_if_multiple_statements(self, sql: str, tail_index: int) -> None:
|
||||
"""
|
||||
Ensure there is no second statement after the first one; otherwise raise ProgrammingError.
|
||||
"""
|
||||
# Skip any trailing whitespace/comments after tail_index, and check if another statement exists.
|
||||
rest = sql[tail_index:]
|
||||
try:
|
||||
nxt = self._conn.prepare_first(rest)
|
||||
if nxt is not None:
|
||||
# Clean-up the prepared second statement immediately
|
||||
second_stmt, _ = nxt
|
||||
try:
|
||||
second_stmt.finalize()
|
||||
except Exception:
|
||||
pass
|
||||
raise ProgrammingError("You can only execute one statement at a time")
|
||||
except ProgrammingError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
@property
|
||||
def in_transaction(self) -> bool:
|
||||
try:
|
||||
return not self._conn.get_auto_commit()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
# Provide autocommit property for sqlite3-like API (optional)
|
||||
@property
|
||||
def autocommit(self) -> object | bool:
|
||||
return self._autocommit_mode
|
||||
|
||||
@autocommit.setter
|
||||
def autocommit(self, val: object | bool) -> None:
|
||||
# Accept True, False, or "LEGACY"
|
||||
if val not in (True, False, "LEGACY"):
|
||||
raise ProgrammingError("autocommit must be True, False, or 'LEGACY'")
|
||||
self._autocommit_mode = val
|
||||
# If switching to False, ensure a transaction is open
|
||||
if val is False:
|
||||
self._ensure_transaction_open()
|
||||
# If switching to True or LEGACY, nothing else to do immediately.
|
||||
|
||||
def close(self) -> None:
|
||||
# In sqlite3: If autocommit is False, pending transaction is implicitly rolled back.
|
||||
try:
|
||||
if self._autocommit_mode is False and self.in_transaction:
|
||||
try:
|
||||
self._exec_ddl_only("ROLLBACK")
|
||||
except Exception:
|
||||
# As sqlite3 does, ignore rollback failure on close
|
||||
pass
|
||||
self._conn.close()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
def commit(self) -> None:
|
||||
try:
|
||||
if self._autocommit_mode is True:
|
||||
# No-op in SQLite autocommit mode
|
||||
return
|
||||
if self.in_transaction:
|
||||
self._exec_ddl_only("COMMIT")
|
||||
if self._autocommit_mode is False:
|
||||
# Re-open a transaction to maintain PEP 249 behavior
|
||||
self._ensure_transaction_open()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
def rollback(self) -> None:
|
||||
try:
|
||||
if self._autocommit_mode is True:
|
||||
# No-op in SQLite autocommit mode
|
||||
return
|
||||
if self.in_transaction:
|
||||
self._exec_ddl_only("ROLLBACK")
|
||||
if self._autocommit_mode is False:
|
||||
# Re-open a transaction to maintain PEP 249 behavior
|
||||
self._ensure_transaction_open()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
def _maybe_implicit_begin(self, sql: str) -> None:
|
||||
"""
|
||||
Implement sqlite3 legacy implicit transaction behavior:
|
||||
|
||||
If autocommit is LEGACY_TRANSACTION_CONTROL, isolation_level is not None, sql is a DML
|
||||
(INSERT/UPDATE/DELETE/REPLACE), and there is no open transaction, issue:
|
||||
BEGIN <isolation_level>
|
||||
"""
|
||||
if self._autocommit_mode == "LEGACY" and self.isolation_level is not None:
|
||||
if not self.in_transaction and _is_dml(sql):
|
||||
level = self.isolation_level or "DEFERRED"
|
||||
self._exec_ddl_only(f"BEGIN {level}")
|
||||
|
||||
def cursor(self, factory: Optional[Callable[[Connection], _DBCursorT]] = None) -> _DBCursorT | Cursor:
|
||||
if factory is None:
|
||||
return Cursor(self)
|
||||
return factory(self)
|
||||
|
||||
def execute(self, sql: str, parameters: Sequence[Any] | Mapping[str, Any] = ()) -> Cursor:
|
||||
cur = self.cursor()
|
||||
cur.execute(sql, parameters)
|
||||
return cur
|
||||
|
||||
def executemany(self, sql: str, parameters: Iterable[Sequence[Any] | Mapping[str, Any]]) -> Cursor:
|
||||
cur = self.cursor()
|
||||
cur.executemany(sql, parameters)
|
||||
return cur
|
||||
|
||||
def executescript(self, sql_script: str) -> Cursor:
|
||||
cur = self.cursor()
|
||||
cur.executescript(sql_script)
|
||||
return cur
|
||||
|
||||
def __call__(self, sql: str) -> PyTursoStatement:
|
||||
# Shortcut to prepare a single statement
|
||||
try:
|
||||
return self._conn.prepare_single(sql)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
def __enter__(self) -> "Connection":
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
type: type[BaseException] | None,
|
||||
value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> bool:
|
||||
# sqlite3 behavior: In context manager, if no exception -> commit, else rollback (legacy and PEP 249 modes)
|
||||
try:
|
||||
if type is None:
|
||||
self.commit()
|
||||
else:
|
||||
self.rollback()
|
||||
finally:
|
||||
# Always propagate exceptions (returning False)
|
||||
return False
|
||||
|
||||
|
||||
# Cursor goes SECOND
|
||||
class Cursor:
|
||||
arraysize: int
|
||||
|
||||
def __init__(self, connection: Connection, /) -> None:
|
||||
self._connection: Connection = connection
|
||||
self.arraysize = 1
|
||||
self.row_factory: Callable[[Cursor, Row], object] | type[Row] | None = connection.row_factory
|
||||
|
||||
# State for the last executed statement
|
||||
self._active_stmt: Optional[PyTursoStatement] = None
|
||||
self._active_has_rows: bool = False
|
||||
self._description: Optional[tuple[tuple[str, None, None, None, None, None, None], ...]] = None
|
||||
self._lastrowid: Optional[int] = None
|
||||
self._rowcount: int = -1
|
||||
self._closed: bool = False
|
||||
|
||||
@property
|
||||
def connection(self) -> Connection:
|
||||
return self._connection
|
||||
|
||||
def close(self) -> None:
|
||||
if self._closed:
|
||||
return
|
||||
try:
|
||||
# Finalize any active statement to ensure completion.
|
||||
if self._active_stmt is not None:
|
||||
try:
|
||||
self._active_stmt.finalize()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._active_stmt = None
|
||||
self._active_has_rows = False
|
||||
self._closed = True
|
||||
|
||||
def _ensure_open(self) -> None:
|
||||
if self._closed:
|
||||
raise ProgrammingError("Cannot operate on a closed cursor")
|
||||
|
||||
@property
|
||||
def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...] | None:
|
||||
return self._description
|
||||
|
||||
@property
|
||||
def lastrowid(self) -> int | None:
|
||||
return self._lastrowid
|
||||
|
||||
@property
|
||||
def rowcount(self) -> int:
|
||||
return self._rowcount
|
||||
|
||||
def _reset_last_result(self) -> None:
|
||||
# Ensure any previous statement is finalized to not leak resources
|
||||
if self._active_stmt is not None:
|
||||
try:
|
||||
self._active_stmt.finalize()
|
||||
except Exception:
|
||||
pass
|
||||
self._active_stmt = None
|
||||
self._active_has_rows = False
|
||||
self._description = None
|
||||
self._rowcount = -1
|
||||
# Do not reset lastrowid here; sqlite3 preserves lastrowid until next insert.
|
||||
|
||||
@staticmethod
|
||||
def _to_positional_params(parameters: Sequence[Any] | Mapping[str, Any]) -> tuple[Any, ...]:
|
||||
if isinstance(parameters, Mapping):
|
||||
# Named placeholders are not supported
|
||||
raise ProgrammingError("Named parameters are not supported; use positional parameters with '?'")
|
||||
if parameters is None:
|
||||
return ()
|
||||
if isinstance(parameters, tuple):
|
||||
return parameters
|
||||
# Convert arbitrary sequences to tuple efficiently
|
||||
return tuple(parameters)
|
||||
|
||||
def _maybe_implicit_begin(self, sql: str) -> None:
|
||||
self._connection._maybe_implicit_begin(sql)
|
||||
|
||||
def _prepare_single_statement(self, sql: str) -> _Prepared:
|
||||
prepared = self._connection._prepare_first(sql)
|
||||
# Ensure there are no further statements
|
||||
self._connection._raise_if_multiple_statements(sql, prepared.tail_index)
|
||||
return prepared
|
||||
|
||||
def execute(self, sql: str, parameters: Sequence[Any] | Mapping[str, Any] = ()) -> "Cursor":
|
||||
self._ensure_open()
|
||||
self._reset_last_result()
|
||||
|
||||
# Implement legacy implicit transactions if needed
|
||||
self._maybe_implicit_begin(sql)
|
||||
|
||||
# Prepare exactly one statement
|
||||
prepared = self._prepare_single_statement(sql)
|
||||
|
||||
stmt = prepared.stmt
|
||||
try:
|
||||
# Bind positional parameters
|
||||
params = self._to_positional_params(parameters)
|
||||
if params:
|
||||
stmt.bind(params)
|
||||
|
||||
if prepared.has_columns:
|
||||
# Stepped statement (e.g., SELECT or DML with RETURNING)
|
||||
self._active_stmt = stmt
|
||||
self._active_has_rows = True
|
||||
# Set description immediately (even if there are no rows)
|
||||
self._description = tuple((name, None, None, None, None, None, None) for name in prepared.column_names)
|
||||
# For statements that return rows, DB-API specifies rowcount is -1
|
||||
self._rowcount = -1
|
||||
# Do not compute lastrowid here
|
||||
else:
|
||||
# Executed statement (no rows returned)
|
||||
result = _run_execute_with_io(stmt)
|
||||
# rows_changed from execution result
|
||||
self._rowcount = int(result.rows_changed)
|
||||
# Set description to None
|
||||
self._description = None
|
||||
# Set lastrowid for INSERT/REPLACE (best-effort)
|
||||
self._lastrowid = self._fetch_last_insert_rowid_if_needed(sql, result.rows_changed)
|
||||
# Finalize the statement to release resources
|
||||
stmt.finalize()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# Ensure cleanup on error
|
||||
try:
|
||||
stmt.finalize()
|
||||
except Exception:
|
||||
pass
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
return self
|
||||
|
||||
def _fetch_last_insert_rowid_if_needed(self, sql: str, rows_changed: int) -> Optional[int]:
|
||||
if rows_changed <= 0 or not _is_insert_or_replace(sql):
|
||||
return self._lastrowid
|
||||
# Query last_insert_rowid(); this is connection-scoped and cheap
|
||||
try:
|
||||
q = self._connection._conn.prepare_single("SELECT last_insert_rowid()")
|
||||
# No parameters; this produces a single-row single-column result
|
||||
# Use stepping to fetch the row
|
||||
status = _step_once_with_io(q)
|
||||
if status == Status.Row:
|
||||
py_row = q.row()
|
||||
# row() returns a Python tuple with one element
|
||||
# We avoid complex conversions: take first item
|
||||
value = tuple(py_row)[0] # type: ignore[call-arg]
|
||||
# Finalize to complete
|
||||
q.finalize()
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
try:
|
||||
return int(value)
|
||||
except Exception:
|
||||
return self._lastrowid
|
||||
# Finalize anyway
|
||||
q.finalize()
|
||||
except Exception:
|
||||
# Ignore errors; lastrowid remains unchanged on failure
|
||||
pass
|
||||
return self._lastrowid
|
||||
|
||||
def executemany(self, sql: str, seq_of_parameters: Iterable[Sequence[Any] | Mapping[str, Any]]) -> "Cursor":
|
||||
self._ensure_open()
|
||||
self._reset_last_result()
|
||||
|
||||
# executemany only accepts DML; enforce this to match sqlite3 semantics
|
||||
if not _is_dml(sql):
|
||||
raise ProgrammingError("executemany() requires a single DML (INSERT/UPDATE/DELETE/REPLACE) statement")
|
||||
|
||||
# Implement legacy implicit transaction: same as execute()
|
||||
self._maybe_implicit_begin(sql)
|
||||
|
||||
prepared = self._prepare_single_statement(sql)
|
||||
stmt = prepared.stmt
|
||||
try:
|
||||
# For executemany, discard any rows produced (even if RETURNING was used)
|
||||
# Therefore we ALWAYS use execute() path per-iteration.
|
||||
for parameters in seq_of_parameters:
|
||||
# Reset previous bindings and program memory before reusing
|
||||
stmt.reset()
|
||||
params = self._to_positional_params(parameters)
|
||||
if params:
|
||||
stmt.bind(params)
|
||||
result = _run_execute_with_io(stmt)
|
||||
# rowcount is "the number of modified rows" for the LAST executed statement only
|
||||
self._rowcount = int(result.rows_changed) + (self._rowcount if self._rowcount != -1 else 0)
|
||||
# After loop, finalize statement
|
||||
stmt.finalize()
|
||||
# Cursor description is None for DML executed via executemany()
|
||||
self._description = None
|
||||
# sqlite3 leaves lastrowid unchanged for executemany
|
||||
except Exception as exc: # noqa: BLE001
|
||||
try:
|
||||
stmt.finalize()
|
||||
except Exception:
|
||||
pass
|
||||
raise _map_turso_exception(exc)
|
||||
return self
|
||||
|
||||
def executescript(self, sql_script: str) -> "Cursor":
|
||||
self._ensure_open()
|
||||
self._reset_last_result()
|
||||
|
||||
# sqlite3 behavior: If autocommit is LEGACY and there is a pending transaction, implicitly COMMIT first
|
||||
if self._connection._autocommit_mode == "LEGACY" and self._connection.in_transaction:
|
||||
try:
|
||||
self._connection._exec_ddl_only("COMMIT")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
# Iterate over statements in the script and execute them, discarding rows
|
||||
sql = sql_script
|
||||
total_rowcount = -1
|
||||
try:
|
||||
offset = 0
|
||||
while True:
|
||||
opt = self._connection._conn.prepare_first(sql[offset:])
|
||||
if opt is None:
|
||||
break
|
||||
stmt, tail = opt
|
||||
# Note: per DB-API, any resulting rows are discarded
|
||||
result = _run_execute_with_io(stmt)
|
||||
total_rowcount = int(result.rows_changed) if result.rows_changed > 0 else total_rowcount
|
||||
# finalize to ensure completion
|
||||
stmt.finalize()
|
||||
offset += tail
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
self._description = None
|
||||
self._rowcount = total_rowcount
|
||||
return self
|
||||
|
||||
def _fetchone_tuple(self) -> Optional[tuple[Any, ...]]:
|
||||
"""
|
||||
Fetch one row as a plain Python tuple, or return None if no more rows.
|
||||
"""
|
||||
if not self._active_has_rows or self._active_stmt is None:
|
||||
return None
|
||||
try:
|
||||
status = _step_once_with_io(self._active_stmt)
|
||||
if status == Status.Row:
|
||||
row_tuple = tuple(self._active_stmt.row()) # type: ignore[call-arg]
|
||||
return row_tuple
|
||||
# status == Done: finalize and clean up
|
||||
self._active_stmt.finalize()
|
||||
self._active_stmt = None
|
||||
self._active_has_rows = False
|
||||
return None
|
||||
except Exception as exc: # noqa: BLE001
|
||||
# Finalize and clean up on error
|
||||
try:
|
||||
if self._active_stmt is not None:
|
||||
self._active_stmt.finalize()
|
||||
except Exception:
|
||||
pass
|
||||
self._active_stmt = None
|
||||
self._active_has_rows = False
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
def _apply_row_factory(self, row_values: tuple[Any, ...]) -> Any:
|
||||
rf = self.row_factory
|
||||
if rf is None:
|
||||
return row_values
|
||||
if isinstance(rf, type) and issubclass(rf, Row):
|
||||
return rf(self, Row(self, row_values)) # type: ignore[call-arg]
|
||||
if callable(rf):
|
||||
return rf(self, Row(self, row_values)) # type: ignore[misc]
|
||||
# Fallback: return tuple
|
||||
return row_values
|
||||
|
||||
def fetchone(self) -> Any:
|
||||
self._ensure_open()
|
||||
row = self._fetchone_tuple()
|
||||
if row is None:
|
||||
return None
|
||||
return self._apply_row_factory(row)
|
||||
|
||||
def fetchmany(self, size: Optional[int] = None) -> list[Any]:
|
||||
self._ensure_open()
|
||||
if size is None:
|
||||
size = self.arraysize
|
||||
if size < 0:
|
||||
raise ValueError("size must be non-negative")
|
||||
result: list[Any] = []
|
||||
for _ in range(size):
|
||||
row = self._fetchone_tuple()
|
||||
if row is None:
|
||||
break
|
||||
result.append(self._apply_row_factory(row))
|
||||
return result
|
||||
|
||||
def fetchall(self) -> list[Any]:
|
||||
self._ensure_open()
|
||||
result: list[Any] = []
|
||||
while True:
|
||||
row = self._fetchone_tuple()
|
||||
if row is None:
|
||||
break
|
||||
result.append(self._apply_row_factory(row))
|
||||
return result
|
||||
|
||||
def setinputsizes(self, sizes: Any, /) -> None:
|
||||
# No-op for DB-API compliance
|
||||
return None
|
||||
|
||||
def setoutputsize(self, size: Any, column: Any = None, /) -> None:
|
||||
# No-op for DB-API compliance
|
||||
return None
|
||||
|
||||
def __iter__(self) -> "Cursor":
|
||||
return self
|
||||
|
||||
def __next__(self) -> Any:
|
||||
row = self.fetchone()
|
||||
if row is None:
|
||||
raise StopIteration
|
||||
return row
|
||||
|
||||
|
||||
# Row goes THIRD
|
||||
class Row(Sequence[Any]):
|
||||
"""
|
||||
sqlite3.Row-like container supporting index and name-based access.
|
||||
"""
|
||||
|
||||
def __new__(cls, cursor: Cursor, data: tuple[Any, ...], /) -> "Row":
|
||||
obj = super().__new__(cls)
|
||||
# Attach metadata
|
||||
obj._cursor = cursor
|
||||
obj._data = data
|
||||
# Build mapping from column name to index
|
||||
desc = cursor.description or ()
|
||||
obj._keys = tuple(col[0] for col in desc)
|
||||
obj._index = {name: idx for idx, name in enumerate(obj._keys)}
|
||||
return obj
|
||||
|
||||
def keys(self) -> list[str]:
|
||||
return list(self._keys)
|
||||
|
||||
def __getitem__(self, key: int | str | slice, /) -> Any:
|
||||
if isinstance(key, slice):
|
||||
return self._data[key]
|
||||
if isinstance(key, int):
|
||||
return self._data[key]
|
||||
# key is column name
|
||||
idx = self._index.get(key)
|
||||
if idx is None:
|
||||
raise KeyError(key)
|
||||
return self._data[idx]
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self._keys, self._data))
|
||||
|
||||
def __iter__(self) -> Iterator[Any]:
|
||||
return iter(self._data)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._data)
|
||||
|
||||
def __eq__(self, value: object, /) -> bool:
|
||||
if not isinstance(value, Row):
|
||||
return NotImplemented # type: ignore[return-value]
|
||||
return self._keys == value._keys and self._data == value._data
|
||||
|
||||
def __ne__(self, value: object, /) -> bool:
|
||||
if not isinstance(value, Row):
|
||||
return NotImplemented # type: ignore[return-value]
|
||||
return not self.__eq__(value)
|
||||
|
||||
# The rest return NotImplemented for non-Row comparisons
|
||||
def __lt__(self, value: object, /) -> bool:
|
||||
if not isinstance(value, Row):
|
||||
return NotImplemented # type: ignore[return-value]
|
||||
return (self._keys, self._data) < (value._keys, value._data)
|
||||
|
||||
def __le__(self, value: object, /) -> bool:
|
||||
if not isinstance(value, Row):
|
||||
return NotImplemented # type: ignore[return-value]
|
||||
return (self._keys, self._data) <= (value._keys, value._data)
|
||||
|
||||
def __gt__(self, value: object, /) -> bool:
|
||||
if not isinstance(value, Row):
|
||||
return NotImplemented # type: ignore[return-value]
|
||||
return (self._keys, self._data) > (value._keys, value._data)
|
||||
|
||||
def __ge__(self, value: object, /) -> bool:
|
||||
if not isinstance(value, Row):
|
||||
return NotImplemented # type: ignore[return-value]
|
||||
return (self._keys, self._data) >= (value._keys, value._data)
|
||||
|
||||
|
||||
def connect(
|
||||
database: str,
|
||||
*,
|
||||
experimental_features: Optional[str] = None,
|
||||
isolation_level: Optional[str] = "DEFERRED",
|
||||
) -> Connection:
|
||||
"""
|
||||
Open a Turso (SQLite-compatible) database and return a Connection.
|
||||
|
||||
Parameters:
|
||||
- database: path or identifier of the database.
|
||||
- experimental_features: comma-separated list of features to enable.
|
||||
- isolation_level: one of "DEFERRED" (default), "IMMEDIATE", "EXCLUSIVE", or None.
|
||||
"""
|
||||
try:
|
||||
cfg = PyTursoDatabaseConfig(
|
||||
path=database,
|
||||
experimental_features=experimental_features,
|
||||
async_io=False, # Let the Rust layer drive IO internally by default
|
||||
)
|
||||
db: PyTursoDatabase = py_turso_database_open(cfg)
|
||||
conn: PyTursoConnection = db.connect()
|
||||
return Connection(conn, isolation_level=isolation_level)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
|
||||
|
||||
# Make it easy to enable logging with native `logging` Python module
|
||||
def setup_logging(level: int = logging.INFO) -> None:
|
||||
"""
|
||||
Setup Turso logging to integrate with Python's logging module.
|
||||
|
||||
Usage:
|
||||
import turso
|
||||
turso.setup_logging(logging.DEBUG)
|
||||
"""
|
||||
logger = logging.getLogger("turso")
|
||||
logger.setLevel(level)
|
||||
|
||||
def _py_logger(log: PyTursoLog) -> None:
|
||||
# Map Rust/Turso log level strings to Python logging levels (best-effort)
|
||||
lvl_map = {
|
||||
"ERROR": logging.ERROR,
|
||||
"WARN": logging.WARNING,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
"DEBUG": logging.DEBUG,
|
||||
"TRACE": logging.DEBUG,
|
||||
}
|
||||
py_level = lvl_map.get(log.level.upper(), level)
|
||||
logger.log(
|
||||
py_level,
|
||||
"%s [%s:%s] %s",
|
||||
log.target,
|
||||
log.file,
|
||||
log.line,
|
||||
log.message,
|
||||
)
|
||||
|
||||
try:
|
||||
py_turso_setup(PyTursoSetupConfig(logger=_py_logger, log_level=None))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise _map_turso_exception(exc)
|
||||
|
|
@ -1555,15 +1555,18 @@ impl Connection {
|
|||
}
|
||||
|
||||
#[instrument(skip_all, level = Level::INFO)]
|
||||
pub fn consume_stmt(self: &Arc<Connection>, sql: &str) -> Result<Option<(Statement, usize)>> {
|
||||
let mut parser = Parser::new(sql.as_bytes());
|
||||
pub fn consume_stmt(
|
||||
self: &Arc<Connection>,
|
||||
sql: impl AsRef<str>,
|
||||
) -> Result<Option<(Statement, usize)>> {
|
||||
let mut parser = Parser::new(sql.as_ref().as_bytes());
|
||||
let Some(cmd) = parser.next_cmd()? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let syms = self.syms.read();
|
||||
let pager = self.pager.load().clone();
|
||||
let byte_offset_end = parser.offset();
|
||||
let input = str::from_utf8(&sql.as_bytes()[..byte_offset_end])
|
||||
let input = str::from_utf8(&sql.as_ref().as_bytes()[..byte_offset_end])
|
||||
.unwrap()
|
||||
.trim();
|
||||
let mode = QueryMode::new(&cmd);
|
||||
|
|
|
|||
|
|
@ -65,7 +65,11 @@ impl Parameters {
|
|||
.copied()
|
||||
}
|
||||
|
||||
pub fn next_index(&mut self) -> NonZero<usize> {
|
||||
pub fn next_index(&self) -> NonZero<usize> {
|
||||
self.next_index
|
||||
}
|
||||
|
||||
fn allocate_new_index(&mut self) -> NonZero<usize> {
|
||||
let index = self.next_index;
|
||||
self.next_index = self.next_index.checked_add(1).unwrap();
|
||||
index
|
||||
|
|
@ -86,7 +90,7 @@ impl Parameters {
|
|||
index
|
||||
}
|
||||
None => {
|
||||
let index = self.next_index();
|
||||
let index = self.allocate_new_index();
|
||||
self.list.push(Parameter::Named(name.to_owned(), index));
|
||||
tracing::trace!("named parameter at {index} as {name}");
|
||||
index
|
||||
|
|
|
|||
15
sdk-kit-macros/Cargo.toml
Normal file
15
sdk-kit-macros/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "turso_sdk_kit_macros"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2.0", features = ["full"] }
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
28
sdk-kit-macros/src/lib.rs
Normal file
28
sdk-kit-macros/src/lib.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use std::iter::repeat_n;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
/// macros that checks that bindings signature matches with implementation signature
|
||||
#[proc_macro_attribute]
|
||||
pub fn signature(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let path = parse_macro_input!(attr as syn::Path);
|
||||
let input = parse_macro_input!(item as syn::ItemFn);
|
||||
|
||||
let fn_name = &input.sig.ident;
|
||||
let arg_count = input.sig.inputs.len();
|
||||
|
||||
let args = repeat_n(quote!(_), arg_count);
|
||||
|
||||
quote! {
|
||||
const _:() = {
|
||||
let _: [unsafe extern "C" fn(#(#args),*) -> _; 2] = [
|
||||
#path::#fn_name,
|
||||
#fn_name,
|
||||
];
|
||||
};
|
||||
|
||||
#input
|
||||
}
|
||||
.into()
|
||||
}
|
||||
22
sdk-kit/Cargo.toml
Normal file
22
sdk-kit/Cargo.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "turso_sdk_kit"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lib]
|
||||
doc = false
|
||||
crate-type = ["lib", "cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
env_logger = { workspace = true, default-features = false }
|
||||
turso_core = { workspace = true, features = ["conn_raw_api"] }
|
||||
turso_sdk_kit_macros = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.69.5"
|
||||
191
sdk-kit/README.md
Normal file
191
sdk-kit/README.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# Turso SDK kit
|
||||
|
||||
Low-level, SQLite-compatible Turso API to make building SDKs in any language simple and predictable. The core logic is implemented in Rust and exposed through a small, portable C ABI (turso.h), so you can generate language bindings (e.g. via rust-bindgen) without re-implementing database semantics.
|
||||
|
||||
Key ideas:
|
||||
- Async I/O: opt-in caller-driven I/O (ideal for event loops or io_uring), or library-driven I/O.
|
||||
- Clear status codes: Done/Row/Io/Error/etc. No hidden blocking or exceptions.
|
||||
- Minimal surface: open database, connect, prepare, bind, step/execute, read rows, finalize.
|
||||
- Ownership model and lifetimes are explicit for C consumers.
|
||||
|
||||
Note: turso.h is the single C header exported by the crate and can be translated to bindings.rs with rust-bindgen.
|
||||
|
||||
## Rust example
|
||||
|
||||
```rust
|
||||
use turso_sdk_kit::rsapi::{
|
||||
TursoDatabase, TursoDatabaseConfig, TursoStatusCode, Value, ValueRef,
|
||||
};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create the database holder (not opened yet).
|
||||
let db = TursoDatabase::create(TursoDatabaseConfig {
|
||||
path: ":memory:".to_string(),
|
||||
experimental_features: None,
|
||||
io: None,
|
||||
// When true, step/execute may return Io and you should call run_io() to progress.
|
||||
async_io: true,
|
||||
});
|
||||
|
||||
// Open and connect.
|
||||
db.open()?;
|
||||
let conn = db.connect()?;
|
||||
|
||||
// Prepare, bind, and step a simple query.
|
||||
let mut stmt = conn.prepare_single("SELECT :greet || ' Turso'")?;
|
||||
stmt.bind_named("greet", Value::Text("Hello".into()))?;
|
||||
|
||||
loop {
|
||||
match stmt.step()? {
|
||||
TursoStatusCode::Row => {
|
||||
// Read current row value. Valid until next step/reset/finalize.
|
||||
match stmt.row_value(0)? {
|
||||
ValueRef::Text(t) => println!("{}", t.as_str()),
|
||||
other => println!("row[0] = {:?}", other),
|
||||
}
|
||||
}
|
||||
TursoStatusCode::Io => {
|
||||
// Drive one iteration of the I/O backend.
|
||||
stmt.run_io()?;
|
||||
}
|
||||
TursoStatusCode::Done => break,
|
||||
_ => unreachable!("unexpected status"),
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize to complete the statement cleanly (may also return Io).
|
||||
match stmt.finalize()? {
|
||||
TursoStatusCode::Io => {
|
||||
// If needed, drive IO and finalize again.
|
||||
stmt.run_io()?;
|
||||
let _ = stmt.finalize()?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## C example
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include "turso.h"
|
||||
|
||||
static void fail_and_cleanup(const char *msg, turso_status_t st) {
|
||||
fprintf(stderr, "%s: %s\n", msg, st.error ? st.error : "(no message)");
|
||||
if (st.error) turso_status_deinit(st);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
// Optional: initialize logging (stdout example) and/or set log level via environment.
|
||||
// turso_setup((turso_config_t){ .logger = NULL, .log_level = "info" });
|
||||
|
||||
// Create database holder (not opened yet). Enable caller-driven async I/O.
|
||||
turso_database_create_result_t db_res = turso_database_create(
|
||||
(turso_database_config_t){
|
||||
.path = ":memory:",
|
||||
.experimental_features = NULL,
|
||||
.async_io = true,
|
||||
}
|
||||
);
|
||||
if (db_res.status.code != TURSO_OK) {
|
||||
fail_and_cleanup("create database failed", db_res.status);
|
||||
return 1;
|
||||
}
|
||||
|
||||
turso_status_t st = turso_database_open(db_res.database);
|
||||
if (st.code != TURSO_OK) {
|
||||
fail_and_cleanup("open failed", st);
|
||||
turso_database_deinit(db_res.database);
|
||||
return 1;
|
||||
}
|
||||
|
||||
turso_database_connect_result_t conn_res = turso_database_connect(db_res.database);
|
||||
if (conn_res.status.code != TURSO_OK) {
|
||||
fail_and_cleanup("connect failed", conn_res.status);
|
||||
turso_database_deinit(db_res.database);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *sql = "SELECT :greet || ' Turso'";
|
||||
turso_connection_prepare_single_t prep =
|
||||
turso_connection_prepare_single(
|
||||
conn_res.connection,
|
||||
(turso_slice_ref_t){ .ptr = (const void*)sql, .len = strlen(sql) });
|
||||
|
||||
if (prep.status.code != TURSO_OK) {
|
||||
fail_and_cleanup("prepare failed", prep.status);
|
||||
turso_connection_deinit(conn_res.connection);
|
||||
turso_database_deinit(db_res.database);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Bind named parameter (omit the leading ':' in the name).
|
||||
const char *name = "greet";
|
||||
const char *val = "Hello";
|
||||
st = turso_statement_bind_named(
|
||||
prep.statement,
|
||||
(turso_slice_ref_t){ .ptr = (const void*)name, .len = strlen(name) },
|
||||
turso_text(val, strlen(val))
|
||||
);
|
||||
if (st.code != TURSO_OK) {
|
||||
fail_and_cleanup("bind failed", st);
|
||||
turso_statement_deinit(prep.statement);
|
||||
turso_connection_deinit(conn_res.connection);
|
||||
turso_database_deinit(db_res.database);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Drive the statement: handle ROW/IO/DONE.
|
||||
for (;;) {
|
||||
turso_status_t step = turso_statement_step(prep.statement);
|
||||
if (step.code == TURSO_IO) {
|
||||
// Run one iteration of the IO backend and continue.
|
||||
turso_status_t io = turso_statement_run_io(prep.statement);
|
||||
if (io.code != TURSO_OK) {
|
||||
fail_and_cleanup("run_io failed", io);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (step.code == TURSO_ROW) {
|
||||
turso_statement_row_value_t rv = turso_statement_row_value(prep.statement, 0);
|
||||
if (rv.status.code == TURSO_OK && rv.value.type == TURSO_TYPE_TEXT) {
|
||||
const turso_slice_ref_t s = rv.value.value.text;
|
||||
fwrite(s.ptr, 1, s.len, stdout);
|
||||
fputc('\n', stdout);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (step.code == TURSO_DONE) {
|
||||
break;
|
||||
}
|
||||
// Any other status is an error.
|
||||
fail_and_cleanup("step failed", step);
|
||||
break;
|
||||
}
|
||||
|
||||
// Finalize; if it returns TURSO_IO, drive IO until OK.
|
||||
for (;;) {
|
||||
turso_status_t fin = turso_statement_finalize(prep.statement);
|
||||
if (fin.code == TURSO_OK) break;
|
||||
if (fin.code == TURSO_IO) {
|
||||
turso_status_t io = turso_statement_run_io(prep.statement);
|
||||
if (io.code != TURSO_OK) { fail_and_cleanup("finalize run_io failed", io); break; }
|
||||
continue;
|
||||
}
|
||||
fail_and_cleanup("finalize failed", fin);
|
||||
break;
|
||||
}
|
||||
|
||||
// Clean up handles.
|
||||
turso_statement_deinit(prep.statement);
|
||||
turso_connection_deinit(conn_res.connection);
|
||||
turso_database_deinit(db_res.database);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
11
sdk-kit/bindgen.sh
Normal file
11
sdk-kit/bindgen.sh
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
set -xe
|
||||
|
||||
bindgen turso.h -o src/bindings.rs \
|
||||
--with-derive-default \
|
||||
--allowlist-type "turso_.*_t" \
|
||||
--allowlist-function "turso_.*" \
|
||||
--rustified-enum "turso_type_t" \
|
||||
--rustified-enum "turso_tracing_level_t" \
|
||||
--rustified-enum "turso_status_code_t"
|
||||
40
sdk-kit/readme-sdk-kit.mdx
Normal file
40
sdk-kit/readme-sdk-kit.mdx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
name: 2025-11-28-readme-sdk-kit
|
||||
---
|
||||
|
||||
<Output path="README.md">
|
||||
|
||||
<Readme model="openai/gpt-5">
|
||||
|
||||
Turso - is the SQLite compatible database written in Rust.
|
||||
One of the important features of the Turso - is async IO execution which can be used with modern storage backend like IO uring.
|
||||
|
||||
Generate high level readme for low level Turso API written for the purpose of ease SDK development.
|
||||
The main logic implemented in the Rust layer but it also propagate to the C API in order to simplify consumption of the bindings from different languages.
|
||||
|
||||
Use following simple structure - README must be lightweight:
|
||||
|
||||
```md
|
||||
# Turso SDK kit
|
||||
|
||||
// Write brief description and motiviation here
|
||||
|
||||
## Rust example
|
||||
|
||||
// Add simple example of usage of Rust API
|
||||
|
||||
## C example
|
||||
|
||||
// Add simple example of usage of C API
|
||||
|
||||
```
|
||||
|
||||
turso.h headers will be translated to the bindings.rs with rust-bindgen`
|
||||
<File path="./turso.h"/>
|
||||
|
||||
<File path="./src/rsapi.rs"/>
|
||||
<File path="./src/capi.rs"/>
|
||||
|
||||
</Readme>
|
||||
|
||||
</Output>
|
||||
699
sdk-kit/src/bindings.rs
Normal file
699
sdk-kit/src/bindings.rs
Normal file
|
|
@ -0,0 +1,699 @@
|
|||
/* automatically generated by rust-bindgen 0.71.1 */
|
||||
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
||||
pub enum turso_status_code_t {
|
||||
TURSO_OK = 0,
|
||||
TURSO_DONE = 1,
|
||||
TURSO_ROW = 2,
|
||||
TURSO_IO = 3,
|
||||
TURSO_BUSY = 4,
|
||||
TURSO_INTERRUPT = 5,
|
||||
TURSO_ERROR = 127,
|
||||
TURSO_MISUSE = 128,
|
||||
TURSO_CONSTRAINT = 129,
|
||||
TURSO_READONLY = 130,
|
||||
TURSO_DATABASE_FULL = 131,
|
||||
TURSO_NOTADB = 132,
|
||||
TURSO_CORRUPT = 133,
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_status_t {
|
||||
pub code: turso_status_code_t,
|
||||
pub error: *const ::std::os::raw::c_char,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_status_t"][::std::mem::size_of::<turso_status_t>() - 16usize];
|
||||
["Alignment of turso_status_t"][::std::mem::align_of::<turso_status_t>() - 8usize];
|
||||
["Offset of field: turso_status_t::code"]
|
||||
[::std::mem::offset_of!(turso_status_t, code) - 0usize];
|
||||
["Offset of field: turso_status_t::error"]
|
||||
[::std::mem::offset_of!(turso_status_t, error) - 8usize];
|
||||
};
|
||||
impl Default for turso_status_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
||||
pub enum turso_type_t {
|
||||
TURSO_TYPE_INTEGER = 1,
|
||||
TURSO_TYPE_REAL = 2,
|
||||
TURSO_TYPE_TEXT = 3,
|
||||
TURSO_TYPE_BLOB = 4,
|
||||
TURSO_TYPE_NULL = 5,
|
||||
}
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
||||
pub enum turso_tracing_level_t {
|
||||
TURSO_TRACING_LEVEL_ERROR = 1,
|
||||
TURSO_TRACING_LEVEL_WARN = 2,
|
||||
TURSO_TRACING_LEVEL_INFO = 3,
|
||||
TURSO_TRACING_LEVEL_DEBUG = 4,
|
||||
TURSO_TRACING_LEVEL_TRACE = 5,
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_log_t {
|
||||
pub message: *const ::std::os::raw::c_char,
|
||||
pub target: *const ::std::os::raw::c_char,
|
||||
pub file: *const ::std::os::raw::c_char,
|
||||
pub timestamp: u64,
|
||||
pub line: usize,
|
||||
pub level: turso_tracing_level_t,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_log_t"][::std::mem::size_of::<turso_log_t>() - 48usize];
|
||||
["Alignment of turso_log_t"][::std::mem::align_of::<turso_log_t>() - 8usize];
|
||||
["Offset of field: turso_log_t::message"]
|
||||
[::std::mem::offset_of!(turso_log_t, message) - 0usize];
|
||||
["Offset of field: turso_log_t::target"][::std::mem::offset_of!(turso_log_t, target) - 8usize];
|
||||
["Offset of field: turso_log_t::file"][::std::mem::offset_of!(turso_log_t, file) - 16usize];
|
||||
["Offset of field: turso_log_t::timestamp"]
|
||||
[::std::mem::offset_of!(turso_log_t, timestamp) - 24usize];
|
||||
["Offset of field: turso_log_t::line"][::std::mem::offset_of!(turso_log_t, line) - 32usize];
|
||||
["Offset of field: turso_log_t::level"][::std::mem::offset_of!(turso_log_t, level) - 40usize];
|
||||
};
|
||||
impl Default for turso_log_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
#[doc = " SAFETY: slice with non-null ptr must points to the valid memory range [ptr..ptr + len)\n ownership of the slice is not transferred - so its either caller owns the data or turso\n as the owner doesn't change - there is no method to free the slice reference - because:\n 1. if tursodb owns it - it will clean it in appropriate time\n 2. if caller owns it - it must clean it in appropriate time with appropriate method and tursodb doesn't know how to properly free the data"]
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_slice_ref_t {
|
||||
pub ptr: *const ::std::os::raw::c_void,
|
||||
pub len: usize,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_slice_ref_t"][::std::mem::size_of::<turso_slice_ref_t>() - 16usize];
|
||||
["Alignment of turso_slice_ref_t"][::std::mem::align_of::<turso_slice_ref_t>() - 8usize];
|
||||
["Offset of field: turso_slice_ref_t::ptr"]
|
||||
[::std::mem::offset_of!(turso_slice_ref_t, ptr) - 0usize];
|
||||
["Offset of field: turso_slice_ref_t::len"]
|
||||
[::std::mem::offset_of!(turso_slice_ref_t, len) - 8usize];
|
||||
};
|
||||
impl Default for turso_slice_ref_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_slice_owned_t {
|
||||
pub ptr: *const ::std::os::raw::c_void,
|
||||
pub len: usize,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_slice_owned_t"][::std::mem::size_of::<turso_slice_owned_t>() - 16usize];
|
||||
["Alignment of turso_slice_owned_t"][::std::mem::align_of::<turso_slice_owned_t>() - 8usize];
|
||||
["Offset of field: turso_slice_owned_t::ptr"]
|
||||
[::std::mem::offset_of!(turso_slice_owned_t, ptr) - 0usize];
|
||||
["Offset of field: turso_slice_owned_t::len"]
|
||||
[::std::mem::offset_of!(turso_slice_owned_t, len) - 8usize];
|
||||
};
|
||||
impl Default for turso_slice_owned_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
#[doc = " structure holding opaque pointer to the TursoDatabase instance\n SAFETY: the database must be opened and closed only once but can be used concurrently"]
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_database_t {
|
||||
pub inner: *mut ::std::os::raw::c_void,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_database_t"][::std::mem::size_of::<turso_database_t>() - 8usize];
|
||||
["Alignment of turso_database_t"][::std::mem::align_of::<turso_database_t>() - 8usize];
|
||||
["Offset of field: turso_database_t::inner"]
|
||||
[::std::mem::offset_of!(turso_database_t, inner) - 0usize];
|
||||
};
|
||||
impl Default for turso_database_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
#[doc = " structure holding opaque pointer to the TursoConnection instance\n SAFETY: the connection must be used exclusive and can't be accessed concurrently"]
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_connection_t {
|
||||
pub inner: *mut ::std::os::raw::c_void,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_connection_t"][::std::mem::size_of::<turso_connection_t>() - 8usize];
|
||||
["Alignment of turso_connection_t"][::std::mem::align_of::<turso_connection_t>() - 8usize];
|
||||
["Offset of field: turso_connection_t::inner"]
|
||||
[::std::mem::offset_of!(turso_connection_t, inner) - 0usize];
|
||||
};
|
||||
impl Default for turso_connection_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
#[doc = " structure holding opaque pointer to the TursoStatement instance\n SAFETY: the statement must be used exclusive and can't be accessed concurrently"]
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_statement_t {
|
||||
pub inner: *mut ::std::os::raw::c_void,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_statement_t"][::std::mem::size_of::<turso_statement_t>() - 8usize];
|
||||
["Alignment of turso_statement_t"][::std::mem::align_of::<turso_statement_t>() - 8usize];
|
||||
["Offset of field: turso_statement_t::inner"]
|
||||
[::std::mem::offset_of!(turso_statement_t, inner) - 0usize];
|
||||
};
|
||||
impl Default for turso_statement_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub union turso_value_union_t {
|
||||
pub integer: i64,
|
||||
pub real: f64,
|
||||
pub text: turso_slice_ref_t,
|
||||
pub blob: turso_slice_ref_t,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_value_union_t"][::std::mem::size_of::<turso_value_union_t>() - 16usize];
|
||||
["Alignment of turso_value_union_t"][::std::mem::align_of::<turso_value_union_t>() - 8usize];
|
||||
["Offset of field: turso_value_union_t::integer"]
|
||||
[::std::mem::offset_of!(turso_value_union_t, integer) - 0usize];
|
||||
["Offset of field: turso_value_union_t::real"]
|
||||
[::std::mem::offset_of!(turso_value_union_t, real) - 0usize];
|
||||
["Offset of field: turso_value_union_t::text"]
|
||||
[::std::mem::offset_of!(turso_value_union_t, text) - 0usize];
|
||||
["Offset of field: turso_value_union_t::blob"]
|
||||
[::std::mem::offset_of!(turso_value_union_t, blob) - 0usize];
|
||||
};
|
||||
impl Default for turso_value_union_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct turso_value_t {
|
||||
pub value: turso_value_union_t,
|
||||
pub type_: turso_type_t,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_value_t"][::std::mem::size_of::<turso_value_t>() - 24usize];
|
||||
["Alignment of turso_value_t"][::std::mem::align_of::<turso_value_t>() - 8usize];
|
||||
["Offset of field: turso_value_t::value"]
|
||||
[::std::mem::offset_of!(turso_value_t, value) - 0usize];
|
||||
["Offset of field: turso_value_t::type_"]
|
||||
[::std::mem::offset_of!(turso_value_t, type_) - 16usize];
|
||||
};
|
||||
impl Default for turso_value_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
#[doc = " Database description."]
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_database_config_t {
|
||||
#[doc = " Path to the database file or `:memory:`"]
|
||||
pub path: *const ::std::os::raw::c_char,
|
||||
#[doc = " Optional comma separated list of experimental features to enable"]
|
||||
pub experimental_features: *const ::std::os::raw::c_char,
|
||||
#[doc = " Parameter which defines who drives the IO - callee or the caller"]
|
||||
pub async_io: bool,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_database_config_t"][::std::mem::size_of::<turso_database_config_t>() - 24usize];
|
||||
["Alignment of turso_database_config_t"]
|
||||
[::std::mem::align_of::<turso_database_config_t>() - 8usize];
|
||||
["Offset of field: turso_database_config_t::path"]
|
||||
[::std::mem::offset_of!(turso_database_config_t, path) - 0usize];
|
||||
["Offset of field: turso_database_config_t::experimental_features"]
|
||||
[::std::mem::offset_of!(turso_database_config_t, experimental_features) - 8usize];
|
||||
["Offset of field: turso_database_config_t::async_io"]
|
||||
[::std::mem::offset_of!(turso_database_config_t, async_io) - 16usize];
|
||||
};
|
||||
impl Default for turso_database_config_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_config_t {
|
||||
#[doc = " SAFETY: turso_log_t log argument fields have lifetime scoped to the logger invocation\n caller must ensure that data is properly copied if it wants it to have longer lifetime"]
|
||||
pub logger: ::std::option::Option<unsafe extern "C" fn(log: turso_log_t)>,
|
||||
pub log_level: *const ::std::os::raw::c_char,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_config_t"][::std::mem::size_of::<turso_config_t>() - 16usize];
|
||||
["Alignment of turso_config_t"][::std::mem::align_of::<turso_config_t>() - 8usize];
|
||||
["Offset of field: turso_config_t::logger"]
|
||||
[::std::mem::offset_of!(turso_config_t, logger) - 0usize];
|
||||
["Offset of field: turso_config_t::log_level"]
|
||||
[::std::mem::offset_of!(turso_config_t, log_level) - 8usize];
|
||||
};
|
||||
impl Default for turso_config_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Setup global database info"]
|
||||
pub fn turso_setup(config: turso_config_t) -> turso_status_t;
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_database_create_result_t {
|
||||
pub status: turso_status_t,
|
||||
pub database: turso_database_t,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_database_create_result_t"]
|
||||
[::std::mem::size_of::<turso_database_create_result_t>() - 24usize];
|
||||
["Alignment of turso_database_create_result_t"]
|
||||
[::std::mem::align_of::<turso_database_create_result_t>() - 8usize];
|
||||
["Offset of field: turso_database_create_result_t::status"]
|
||||
[::std::mem::offset_of!(turso_database_create_result_t, status) - 0usize];
|
||||
["Offset of field: turso_database_create_result_t::database"]
|
||||
[::std::mem::offset_of!(turso_database_create_result_t, database) - 16usize];
|
||||
};
|
||||
impl Default for turso_database_create_result_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Create database holder but do not open it"]
|
||||
pub fn turso_database_create(config: turso_database_config_t)
|
||||
-> turso_database_create_result_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Open database"]
|
||||
pub fn turso_database_open(database: turso_database_t) -> turso_status_t;
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_database_connect_result_t {
|
||||
pub status: turso_status_t,
|
||||
pub connection: turso_connection_t,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_database_connect_result_t"]
|
||||
[::std::mem::size_of::<turso_database_connect_result_t>() - 24usize];
|
||||
["Alignment of turso_database_connect_result_t"]
|
||||
[::std::mem::align_of::<turso_database_connect_result_t>() - 8usize];
|
||||
["Offset of field: turso_database_connect_result_t::status"]
|
||||
[::std::mem::offset_of!(turso_database_connect_result_t, status) - 0usize];
|
||||
["Offset of field: turso_database_connect_result_t::connection"]
|
||||
[::std::mem::offset_of!(turso_database_connect_result_t, connection) - 16usize];
|
||||
};
|
||||
impl Default for turso_database_connect_result_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Connect to the database"]
|
||||
pub fn turso_database_connect(self_: turso_database_t) -> turso_database_connect_result_t;
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_connection_get_autocommit_result_t {
|
||||
pub status: turso_status_t,
|
||||
pub auto_commit: bool,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_connection_get_autocommit_result_t"]
|
||||
[::std::mem::size_of::<turso_connection_get_autocommit_result_t>() - 24usize];
|
||||
["Alignment of turso_connection_get_autocommit_result_t"]
|
||||
[::std::mem::align_of::<turso_connection_get_autocommit_result_t>() - 8usize];
|
||||
["Offset of field: turso_connection_get_autocommit_result_t::status"]
|
||||
[::std::mem::offset_of!(turso_connection_get_autocommit_result_t, status) - 0usize];
|
||||
["Offset of field: turso_connection_get_autocommit_result_t::auto_commit"]
|
||||
[::std::mem::offset_of!(turso_connection_get_autocommit_result_t, auto_commit) - 16usize];
|
||||
};
|
||||
impl Default for turso_connection_get_autocommit_result_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Get autocommit state of the connection"]
|
||||
pub fn turso_connection_get_autocommit(
|
||||
self_: turso_connection_t,
|
||||
) -> turso_connection_get_autocommit_result_t;
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_connection_prepare_single_t {
|
||||
pub status: turso_status_t,
|
||||
pub statement: turso_statement_t,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_connection_prepare_single_t"]
|
||||
[::std::mem::size_of::<turso_connection_prepare_single_t>() - 24usize];
|
||||
["Alignment of turso_connection_prepare_single_t"]
|
||||
[::std::mem::align_of::<turso_connection_prepare_single_t>() - 8usize];
|
||||
["Offset of field: turso_connection_prepare_single_t::status"]
|
||||
[::std::mem::offset_of!(turso_connection_prepare_single_t, status) - 0usize];
|
||||
["Offset of field: turso_connection_prepare_single_t::statement"]
|
||||
[::std::mem::offset_of!(turso_connection_prepare_single_t, statement) - 16usize];
|
||||
};
|
||||
impl Default for turso_connection_prepare_single_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Prepare single statement in a connection"]
|
||||
pub fn turso_connection_prepare_single(
|
||||
self_: turso_connection_t,
|
||||
sql: turso_slice_ref_t,
|
||||
) -> turso_connection_prepare_single_t;
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_connection_prepare_first_t {
|
||||
pub status: turso_status_t,
|
||||
pub statement: turso_statement_t,
|
||||
pub tail_idx: usize,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_connection_prepare_first_t"]
|
||||
[::std::mem::size_of::<turso_connection_prepare_first_t>() - 32usize];
|
||||
["Alignment of turso_connection_prepare_first_t"]
|
||||
[::std::mem::align_of::<turso_connection_prepare_first_t>() - 8usize];
|
||||
["Offset of field: turso_connection_prepare_first_t::status"]
|
||||
[::std::mem::offset_of!(turso_connection_prepare_first_t, status) - 0usize];
|
||||
["Offset of field: turso_connection_prepare_first_t::statement"]
|
||||
[::std::mem::offset_of!(turso_connection_prepare_first_t, statement) - 16usize];
|
||||
["Offset of field: turso_connection_prepare_first_t::tail_idx"]
|
||||
[::std::mem::offset_of!(turso_connection_prepare_first_t, tail_idx) - 24usize];
|
||||
};
|
||||
impl Default for turso_connection_prepare_first_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Prepare first statement in a string containing multiple statements in a connection"]
|
||||
pub fn turso_connection_prepare_first(
|
||||
self_: turso_connection_t,
|
||||
sql: turso_slice_ref_t,
|
||||
) -> turso_connection_prepare_first_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " close the connection preventing any further operations executed over it\n caller still need to call deinit method to reclaim memory from the instance holding connection\n SAFETY: caller must guarantee that no ongoing operations are running over connection before calling turso_connection_close(...) method"]
|
||||
pub fn turso_connection_close(self_: turso_connection_t) -> turso_status_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Check if no more statements was parsed after execution of turso_connection_prepare_first method"]
|
||||
pub fn turso_connection_prepare_first_result_empty(
|
||||
result: turso_connection_prepare_first_t,
|
||||
) -> bool;
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_statement_execute_t {
|
||||
pub status: turso_status_t,
|
||||
pub rows_changed: u64,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_statement_execute_t"]
|
||||
[::std::mem::size_of::<turso_statement_execute_t>() - 24usize];
|
||||
["Alignment of turso_statement_execute_t"]
|
||||
[::std::mem::align_of::<turso_statement_execute_t>() - 8usize];
|
||||
["Offset of field: turso_statement_execute_t::status"]
|
||||
[::std::mem::offset_of!(turso_statement_execute_t, status) - 0usize];
|
||||
["Offset of field: turso_statement_execute_t::rows_changed"]
|
||||
[::std::mem::offset_of!(turso_statement_execute_t, rows_changed) - 16usize];
|
||||
};
|
||||
impl Default for turso_statement_execute_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Execute single statement"]
|
||||
pub fn turso_statement_execute(self_: turso_statement_t) -> turso_statement_execute_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Step statement execution once\n Returns TURSO_DONE if execution finished\n Returns TURSO_ROW if execution generated the row (row values can be inspected with corresponding statement methods)\n Returns TURSO_IO if async_io was set and statement needs to execute IO to make progress"]
|
||||
pub fn turso_statement_step(self_: turso_statement_t) -> turso_status_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Execute one iteration of underlying IO backend"]
|
||||
pub fn turso_statement_run_io(self_: turso_statement_t) -> turso_status_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Reset a statement"]
|
||||
pub fn turso_statement_reset(self_: turso_statement_t) -> turso_status_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Finalize a statement\n This method must be called in the end of statement execution (either successfull or not)"]
|
||||
pub fn turso_statement_finalize(self_: turso_statement_t) -> turso_status_t;
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_statement_column_count_result_t {
|
||||
pub status: turso_status_t,
|
||||
pub column_count: usize,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_statement_column_count_result_t"]
|
||||
[::std::mem::size_of::<turso_statement_column_count_result_t>() - 24usize];
|
||||
["Alignment of turso_statement_column_count_result_t"]
|
||||
[::std::mem::align_of::<turso_statement_column_count_result_t>() - 8usize];
|
||||
["Offset of field: turso_statement_column_count_result_t::status"]
|
||||
[::std::mem::offset_of!(turso_statement_column_count_result_t, status) - 0usize];
|
||||
["Offset of field: turso_statement_column_count_result_t::column_count"]
|
||||
[::std::mem::offset_of!(turso_statement_column_count_result_t, column_count) - 16usize];
|
||||
};
|
||||
impl Default for turso_statement_column_count_result_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Get column count"]
|
||||
pub fn turso_statement_column_count(
|
||||
self_: turso_statement_t,
|
||||
) -> turso_statement_column_count_result_t;
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct turso_statement_column_name_result_t {
|
||||
pub status: turso_status_t,
|
||||
pub column_name: *const ::std::os::raw::c_char,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_statement_column_name_result_t"]
|
||||
[::std::mem::size_of::<turso_statement_column_name_result_t>() - 24usize];
|
||||
["Alignment of turso_statement_column_name_result_t"]
|
||||
[::std::mem::align_of::<turso_statement_column_name_result_t>() - 8usize];
|
||||
["Offset of field: turso_statement_column_name_result_t::status"]
|
||||
[::std::mem::offset_of!(turso_statement_column_name_result_t, status) - 0usize];
|
||||
["Offset of field: turso_statement_column_name_result_t::column_name"]
|
||||
[::std::mem::offset_of!(turso_statement_column_name_result_t, column_name) - 16usize];
|
||||
};
|
||||
impl Default for turso_statement_column_name_result_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Get the column name at the index\n C string allocated by Turso must be freed after the usage with corresponding turso_str_deinit(...) method"]
|
||||
pub fn turso_statement_column_name(
|
||||
self_: turso_statement_t,
|
||||
index: usize,
|
||||
) -> turso_statement_column_name_result_t;
|
||||
}
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct turso_statement_row_value_t {
|
||||
pub status: turso_status_t,
|
||||
pub value: turso_value_t,
|
||||
}
|
||||
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
|
||||
const _: () = {
|
||||
["Size of turso_statement_row_value_t"]
|
||||
[::std::mem::size_of::<turso_statement_row_value_t>() - 40usize];
|
||||
["Alignment of turso_statement_row_value_t"]
|
||||
[::std::mem::align_of::<turso_statement_row_value_t>() - 8usize];
|
||||
["Offset of field: turso_statement_row_value_t::status"]
|
||||
[::std::mem::offset_of!(turso_statement_row_value_t, status) - 0usize];
|
||||
["Offset of field: turso_statement_row_value_t::value"]
|
||||
[::std::mem::offset_of!(turso_statement_row_value_t, value) - 16usize];
|
||||
};
|
||||
impl Default for turso_statement_row_value_t {
|
||||
fn default() -> Self {
|
||||
let mut s = ::std::mem::MaybeUninit::<Self>::uninit();
|
||||
unsafe {
|
||||
::std::ptr::write_bytes(s.as_mut_ptr(), 0, 1);
|
||||
s.assume_init()
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Get the row value at the the index for a current statement state\n SAFETY: returned turso_value_t will be valid only until next invocation of statement operation (step, finalize, reset, etc)\n Caller must make sure that any non-owning memory is copied appropriated if it will be used for longer lifetime"]
|
||||
pub fn turso_statement_row_value(
|
||||
self_: turso_statement_t,
|
||||
index: usize,
|
||||
) -> turso_statement_row_value_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Bind a named argument to a statement"]
|
||||
pub fn turso_statement_bind_named(
|
||||
self_: turso_statement_t,
|
||||
name: turso_slice_ref_t,
|
||||
value: turso_value_t,
|
||||
) -> turso_status_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Bind a positional argument to a statement"]
|
||||
pub fn turso_statement_bind_positional(
|
||||
self_: turso_statement_t,
|
||||
position: usize,
|
||||
value: turso_value_t,
|
||||
) -> turso_status_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Create a turso integer value"]
|
||||
pub fn turso_integer(integer: i64) -> turso_value_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Create a turso real value"]
|
||||
pub fn turso_real(real: f64) -> turso_value_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Create a turso text value"]
|
||||
pub fn turso_text(ptr: *const ::std::os::raw::c_char, len: usize) -> turso_value_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Create a turso blob value"]
|
||||
pub fn turso_blob(ptr: *const u8, len: usize) -> turso_value_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Create a turso null value"]
|
||||
pub fn turso_null() -> turso_value_t;
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Deallocate a status"]
|
||||
pub fn turso_status_deinit(self_: turso_status_t);
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Deallocate C string allocated by Turso"]
|
||||
pub fn turso_str_deinit(self_: *const ::std::os::raw::c_char);
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Deallocate and close a database\n SAFETY: caller must ensure that no other code can concurrently or later call methods over deinited database"]
|
||||
pub fn turso_database_deinit(self_: turso_database_t);
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Deallocate and close a connection\n SAFETY: caller must ensure that no other code can concurrently or later call methods over deinited connection"]
|
||||
pub fn turso_connection_deinit(self_: turso_connection_t);
|
||||
}
|
||||
unsafe extern "C" {
|
||||
#[doc = " Deallocate and close a statement\n SAFETY: caller must ensure that no other code can concurrently or later call methods over deinited statement"]
|
||||
pub fn turso_statement_deinit(self_: turso_statement_t);
|
||||
}
|
||||
1302
sdk-kit/src/capi.rs
Normal file
1302
sdk-kit/src/capi.rs
Normal file
File diff suppressed because it is too large
Load diff
66
sdk-kit/src/lib.rs
Normal file
66
sdk-kit/src/lib.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use crate::rsapi::TursoError;
|
||||
|
||||
pub mod capi;
|
||||
pub mod rsapi;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_send {
|
||||
($($ty:ty),+ $(,)?) => {
|
||||
const _: fn() = || {
|
||||
fn check<T: Send>() {}
|
||||
$( check::<$ty>(); )+
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_sync {
|
||||
($($ty:ty),+ $(,)?) => {
|
||||
const _: fn() = || {
|
||||
fn check<T: Sync>() {}
|
||||
$( check::<$ty>(); )+
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/// simple helper which return MISUSE error in case of concurrent access to some operation
|
||||
struct ConcurrentGuard {
|
||||
in_use: AtomicU32,
|
||||
}
|
||||
|
||||
struct ConcurrentGuardToken<'a> {
|
||||
guard: &'a ConcurrentGuard,
|
||||
}
|
||||
|
||||
impl ConcurrentGuard {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
in_use: AtomicU32::new(0),
|
||||
}
|
||||
}
|
||||
pub fn try_use(&self) -> Result<ConcurrentGuardToken<'_>, TursoError> {
|
||||
if self
|
||||
.in_use
|
||||
.compare_exchange(0, 1, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_err()
|
||||
{
|
||||
return Err(TursoError {
|
||||
code: rsapi::TursoStatusCode::Misuse,
|
||||
message: Some("concurrent use forbidden".to_string()),
|
||||
});
|
||||
};
|
||||
Ok(ConcurrentGuardToken { guard: self })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for ConcurrentGuardToken<'a> {
|
||||
fn drop(&mut self) {
|
||||
let before = self.guard.in_use.swap(0, Ordering::SeqCst);
|
||||
assert!(
|
||||
before == 1,
|
||||
"invalid db state: guard wasn't in use while token is active"
|
||||
);
|
||||
}
|
||||
}
|
||||
846
sdk-kit/src/rsapi.rs
Normal file
846
sdk-kit/src/rsapi.rs
Normal file
|
|
@ -0,0 +1,846 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
sync::{Arc, Mutex, Once, RwLock},
|
||||
};
|
||||
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{
|
||||
fmt::{self, format::Writer},
|
||||
layer::{Context, SubscriberExt},
|
||||
util::SubscriberInitExt,
|
||||
EnvFilter, Layer,
|
||||
};
|
||||
use turso_core::{
|
||||
types::AsValueRef, Connection, Database, DatabaseOpts, LimboError, OpenFlags, Statement,
|
||||
StepResult, IO,
|
||||
};
|
||||
|
||||
use crate::{assert_send, assert_sync, capi, ConcurrentGuard};
|
||||
|
||||
assert_send!(TursoDatabase, TursoConnection, TursoStatement);
|
||||
assert_sync!(TursoDatabase);
|
||||
|
||||
pub type Value = turso_core::Value;
|
||||
pub type ValueRef<'a> = turso_core::types::ValueRef<'a>;
|
||||
pub type Text = turso_core::types::Text;
|
||||
pub type TextRef<'a> = turso_core::types::TextRef<'a>;
|
||||
|
||||
pub struct TursoLog<'a> {
|
||||
pub message: &'a str,
|
||||
pub target: &'a str,
|
||||
pub file: &'a str,
|
||||
pub timestamp: u64,
|
||||
pub line: usize,
|
||||
pub level: &'a str,
|
||||
}
|
||||
|
||||
type Logger = dyn Fn(TursoLog) + Send + Sync + 'static;
|
||||
pub struct TursoSetupConfig {
|
||||
pub logger: Option<Box<Logger>>,
|
||||
pub log_level: Option<String>,
|
||||
}
|
||||
|
||||
fn logger_wrap(log: TursoLog<'_>, logger: unsafe extern "C" fn(capi::c::turso_log_t)) {
|
||||
let Ok(message_cstr) = std::ffi::CString::new(log.message) else {
|
||||
return;
|
||||
};
|
||||
let Ok(target_cstr) = std::ffi::CString::new(log.target) else {
|
||||
return;
|
||||
};
|
||||
let Ok(file_cstr) = std::ffi::CString::new(log.file) else {
|
||||
return;
|
||||
};
|
||||
unsafe {
|
||||
logger(capi::c::turso_log_t {
|
||||
message: message_cstr.as_ptr(),
|
||||
target: target_cstr.as_ptr(),
|
||||
file: file_cstr.as_ptr(),
|
||||
timestamp: log.timestamp,
|
||||
line: log.line,
|
||||
level: match log.level {
|
||||
"TRACE" => capi::c::turso_tracing_level_t::TURSO_TRACING_LEVEL_TRACE,
|
||||
"DEBUG" => capi::c::turso_tracing_level_t::TURSO_TRACING_LEVEL_DEBUG,
|
||||
"INFO" => capi::c::turso_tracing_level_t::TURSO_TRACING_LEVEL_INFO,
|
||||
"WARN" => capi::c::turso_tracing_level_t::TURSO_TRACING_LEVEL_WARN,
|
||||
_ => capi::c::turso_tracing_level_t::TURSO_TRACING_LEVEL_ERROR,
|
||||
},
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
impl TursoSetupConfig {
|
||||
pub fn from_capi(value: capi::c::turso_config_t) -> Result<Self, TursoError> {
|
||||
Ok(Self {
|
||||
log_level: if !value.log_level.is_null() {
|
||||
Some(str_from_c_str(value.log_level)?.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
logger: if let Some(logger) = value.logger {
|
||||
Some(Box::new(move |log| logger_wrap(log, logger)))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TursoDatabaseConfig {
|
||||
/// path to the database file or ":memory:" for in-memory connection
|
||||
pub path: String,
|
||||
|
||||
/// comma-separated list of experimental features to enable
|
||||
/// this field is intentionally just a string in order to make enablement of experimental features as flexible as possible
|
||||
pub experimental_features: Option<String>,
|
||||
|
||||
/// optional custom IO provided by the caller
|
||||
pub io: Option<Arc<dyn IO>>,
|
||||
|
||||
/// if true, library methods will return Io status code and delegate Io loop to the caller
|
||||
/// if false, library will spin IO itself in case of Io status code and never return it to the caller
|
||||
pub async_io: bool,
|
||||
}
|
||||
|
||||
pub fn value_from_c_value(value: capi::c::turso_value_t) -> Result<turso_core::Value, TursoError> {
|
||||
match value.type_ {
|
||||
capi::c::turso_type_t::TURSO_TYPE_NULL => Ok(turso_core::Value::Null),
|
||||
capi::c::turso_type_t::TURSO_TYPE_INTEGER => {
|
||||
Ok(turso_core::Value::Integer(unsafe { value.value.integer }))
|
||||
}
|
||||
capi::c::turso_type_t::TURSO_TYPE_REAL => {
|
||||
Ok(turso_core::Value::Float(unsafe { value.value.real }))
|
||||
}
|
||||
capi::c::turso_type_t::TURSO_TYPE_TEXT => {
|
||||
let text = std::str::from_utf8(bytes_from_turso_slice(unsafe { value.value.text })?);
|
||||
let text = match text {
|
||||
Ok(text) => text,
|
||||
Err(err) => {
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some(format!("expected utf-8 string: {err}")),
|
||||
})
|
||||
}
|
||||
};
|
||||
Ok(turso_core::Value::Text(turso_core::types::Text::new(
|
||||
// convert to_string explicitly in order to avoid potential dangling references
|
||||
text.to_string(),
|
||||
)))
|
||||
}
|
||||
capi::c::turso_type_t::TURSO_TYPE_BLOB => {
|
||||
let blob = bytes_from_turso_slice(unsafe { value.value.blob })?;
|
||||
Ok(turso_core::Value::Blob(blob.to_vec()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn turso_slice_from_bytes(bytes: &[u8]) -> capi::c::turso_slice_ref_t {
|
||||
capi::c::turso_slice_ref_t {
|
||||
ptr: bytes.as_ptr() as *const std::ffi::c_void,
|
||||
len: bytes.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// SAFETY: slice must points to the valid memory
|
||||
pub fn str_from_turso_slice<'a>(slice: capi::c::turso_slice_ref_t) -> Result<&'a str, TursoError> {
|
||||
if slice.ptr.is_null() {
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some("expected slice representing utf-8 value, got null".to_string()),
|
||||
});
|
||||
}
|
||||
let s = unsafe { std::slice::from_raw_parts(slice.ptr as *const u8, slice.len) };
|
||||
match std::str::from_utf8(s) {
|
||||
Ok(s) => Ok(s),
|
||||
Err(err) => Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some(format!("expected slice representing utf-8 value: {err}")),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// SAFETY: slice must points to the valid memory
|
||||
pub fn bytes_from_turso_slice<'a>(
|
||||
slice: capi::c::turso_slice_ref_t,
|
||||
) -> Result<&'a [u8], TursoError> {
|
||||
if slice.ptr.is_null() {
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some("expected slice representing utf-8 value, got null".to_string()),
|
||||
});
|
||||
}
|
||||
Ok(unsafe { std::slice::from_raw_parts(slice.ptr as *const u8, slice.len) })
|
||||
}
|
||||
|
||||
pub(crate) fn str_from_c_str<'a>(ptr: *const std::ffi::c_char) -> Result<&'a str, TursoError> {
|
||||
if ptr.is_null() {
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some("expected zero terminated c string, got null pointer".to_string()),
|
||||
});
|
||||
}
|
||||
let c_str = unsafe { std::ffi::CStr::from_ptr(ptr) };
|
||||
match c_str.to_str() {
|
||||
Ok(s) => Ok(s),
|
||||
Err(err) => Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some(format!(
|
||||
"expected zero terminated c-string representing utf-8 value: {err}"
|
||||
)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
impl TursoDatabaseConfig {
|
||||
pub fn from_capi(value: capi::c::turso_database_config_t) -> Result<Self, TursoError> {
|
||||
Ok(Self {
|
||||
path: str_from_c_str(value.path)?.to_string(),
|
||||
experimental_features: if !value.experimental_features.is_null() {
|
||||
Some(str_from_c_str(value.experimental_features)?.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
async_io: value.async_io,
|
||||
io: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TursoDatabase {
|
||||
config: TursoDatabaseConfig,
|
||||
db: Arc<Mutex<Option<Arc<Database>>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
pub enum TursoStatusCode {
|
||||
Ok = 0,
|
||||
Done = 1,
|
||||
Row = 2,
|
||||
Io = 3,
|
||||
Busy = 4,
|
||||
Interrupt = 5,
|
||||
Error = 127,
|
||||
Misuse = 128,
|
||||
Constraint = 129,
|
||||
Readonly = 130,
|
||||
DatabaseFull = 131,
|
||||
NotAdb = 132,
|
||||
Corrupt = 133,
|
||||
}
|
||||
|
||||
impl TursoStatusCode {
|
||||
pub fn to_capi(self) -> capi::c::turso_status_t {
|
||||
capi::c::turso_status_t {
|
||||
code: match self {
|
||||
TursoStatusCode::Ok => capi::c::turso_status_code_t::TURSO_OK,
|
||||
TursoStatusCode::Done => capi::c::turso_status_code_t::TURSO_DONE,
|
||||
TursoStatusCode::Row => capi::c::turso_status_code_t::TURSO_ROW,
|
||||
TursoStatusCode::Io => capi::c::turso_status_code_t::TURSO_IO,
|
||||
TursoStatusCode::Busy => capi::c::turso_status_code_t::TURSO_BUSY,
|
||||
TursoStatusCode::Interrupt => capi::c::turso_status_code_t::TURSO_INTERRUPT,
|
||||
TursoStatusCode::Error => capi::c::turso_status_code_t::TURSO_ERROR,
|
||||
TursoStatusCode::Misuse => capi::c::turso_status_code_t::TURSO_MISUSE,
|
||||
TursoStatusCode::Constraint => capi::c::turso_status_code_t::TURSO_CONSTRAINT,
|
||||
TursoStatusCode::Readonly => capi::c::turso_status_code_t::TURSO_READONLY,
|
||||
TursoStatusCode::DatabaseFull => capi::c::turso_status_code_t::TURSO_DATABASE_FULL,
|
||||
TursoStatusCode::NotAdb => capi::c::turso_status_code_t::TURSO_NOTADB,
|
||||
TursoStatusCode::Corrupt => capi::c::turso_status_code_t::TURSO_CORRUPT,
|
||||
},
|
||||
error: std::ptr::null(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TursoError {
|
||||
pub code: TursoStatusCode,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
pub fn str_to_c_string(message: &str) -> *const std::ffi::c_char {
|
||||
let Ok(message) = std::ffi::CString::new(message) else {
|
||||
return std::ptr::null();
|
||||
};
|
||||
message.into_raw()
|
||||
}
|
||||
|
||||
pub fn c_string_to_str(ptr: *const std::ffi::c_char) -> std::ffi::CString {
|
||||
unsafe { std::ffi::CString::from_raw(ptr as *mut std::ffi::c_char) }
|
||||
}
|
||||
|
||||
impl TursoError {
|
||||
pub fn to_capi(self) -> capi::c::turso_status_t {
|
||||
capi::c::turso_status_t {
|
||||
code: self.code.to_capi().code,
|
||||
error: match self.message {
|
||||
Some(message) => str_to_c_string(&message),
|
||||
None => std::ptr::null(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn turso_error_from_limbo_error(err: LimboError) -> TursoError {
|
||||
TursoError {
|
||||
code: match &err {
|
||||
LimboError::Constraint(_) => TursoStatusCode::Constraint,
|
||||
LimboError::Corrupt(..) => TursoStatusCode::Corrupt,
|
||||
LimboError::NotADB => TursoStatusCode::NotAdb,
|
||||
LimboError::DatabaseFull(_) => TursoStatusCode::DatabaseFull,
|
||||
LimboError::ReadOnly => TursoStatusCode::Readonly,
|
||||
LimboError::Busy => TursoStatusCode::Busy,
|
||||
_ => TursoStatusCode::Error,
|
||||
},
|
||||
message: Some(format!("{err}")),
|
||||
}
|
||||
}
|
||||
static LOGGER: RwLock<Option<Box<Logger>>> = RwLock::new(None);
|
||||
static SETUP: Once = Once::new();
|
||||
|
||||
struct CallbackLayer<F>
|
||||
where
|
||||
F: Fn(TursoLog) + Send + Sync + 'static,
|
||||
{
|
||||
callback: F,
|
||||
}
|
||||
|
||||
impl<S, F> tracing_subscriber::Layer<S> for CallbackLayer<F>
|
||||
where
|
||||
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
|
||||
F: Fn(TursoLog) + Send + Sync + 'static,
|
||||
{
|
||||
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
|
||||
let mut buffer = String::new();
|
||||
let mut visitor = fmt::format::DefaultVisitor::new(Writer::new(&mut buffer), true);
|
||||
|
||||
event.record(&mut visitor);
|
||||
|
||||
let log = TursoLog {
|
||||
level: event.metadata().level().as_str(),
|
||||
target: event.metadata().target(),
|
||||
message: &buffer,
|
||||
timestamp: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|t| t.as_secs())
|
||||
.unwrap_or(0),
|
||||
file: event.metadata().file().unwrap_or(""),
|
||||
line: event.metadata().line().unwrap_or(0) as usize,
|
||||
};
|
||||
|
||||
(self.callback)(log);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn turso_setup(config: TursoSetupConfig) -> Result<(), TursoError> {
|
||||
fn callback(log: TursoLog<'_>) {
|
||||
let Ok(logger) = LOGGER.try_read() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(logger) = logger.as_ref() {
|
||||
logger(log)
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(logger) = config.logger {
|
||||
let mut guard = LOGGER.write().unwrap();
|
||||
*guard = Some(logger);
|
||||
}
|
||||
|
||||
let level_filter = if let Some(log_level) = &config.log_level {
|
||||
match log_level.as_ref() {
|
||||
"error" => Some(LevelFilter::ERROR),
|
||||
"warn" => Some(LevelFilter::WARN),
|
||||
"info" => Some(LevelFilter::INFO),
|
||||
"debug" => Some(LevelFilter::DEBUG),
|
||||
"trace" => Some(LevelFilter::TRACE),
|
||||
_ => {
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Error,
|
||||
message: Some("unknown log level".to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
SETUP.call_once(|| {
|
||||
if let Some(level_filter) = level_filter {
|
||||
tracing_subscriber::registry()
|
||||
.with(CallbackLayer { callback }.with_filter(level_filter))
|
||||
.init();
|
||||
} else {
|
||||
tracing_subscriber::registry()
|
||||
.with(CallbackLayer { callback }.with_filter(EnvFilter::from_default_env()))
|
||||
.init();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl TursoDatabase {
|
||||
/// create database holder struct but do not initialize it yet
|
||||
/// this can be useful for some environments, where IO operations must be executed in certain fashion (and open do IO under the hood)
|
||||
pub fn create(config: TursoDatabaseConfig) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
config,
|
||||
db: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}
|
||||
/// open the database
|
||||
/// this method must be called only once
|
||||
pub fn open(&self) -> Result<(), TursoError> {
|
||||
let mut inner_db = self.db.lock().unwrap();
|
||||
if inner_db.is_some() {
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some("database must be opened only once".to_string()),
|
||||
});
|
||||
}
|
||||
let io: Arc<dyn turso_core::IO> = if let Some(io) = &self.config.io {
|
||||
io.clone()
|
||||
} else {
|
||||
match self.config.path.as_str() {
|
||||
":memory:" => Arc::new(turso_core::MemoryIO::new()),
|
||||
_ => match turso_core::PlatformIO::new() {
|
||||
Ok(io) => Arc::new(io),
|
||||
Err(err) => {
|
||||
return Err(turso_error_from_limbo_error(err));
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
let mut opts = DatabaseOpts::new();
|
||||
if let Some(experimental_features) = &self.config.experimental_features {
|
||||
for features in experimental_features.split(",").map(|s| s.trim()) {
|
||||
opts = match features {
|
||||
"views" => opts.with_views(true),
|
||||
"mvcc" => opts.with_mvcc(true),
|
||||
"index_method" => opts.with_index_method(true),
|
||||
"strict" => opts.with_strict(true),
|
||||
"autovacuum" => opts.with_autovacuum(true),
|
||||
_ => opts,
|
||||
};
|
||||
}
|
||||
}
|
||||
match turso_core::Database::open_file_with_flags(
|
||||
io.clone(),
|
||||
&self.config.path,
|
||||
OpenFlags::default(),
|
||||
opts,
|
||||
None,
|
||||
) {
|
||||
Ok(db) => {
|
||||
*inner_db = Some(db);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(turso_error_from_limbo_error(err)),
|
||||
}
|
||||
}
|
||||
|
||||
/// creates database connection
|
||||
/// database must be already opened with [Self::open] method
|
||||
pub fn connect(&self) -> Result<Arc<TursoConnection>, TursoError> {
|
||||
let inner_db = self.db.lock().unwrap();
|
||||
let Some(db) = inner_db.as_ref() else {
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some("database must be opened first".to_string()),
|
||||
});
|
||||
};
|
||||
let connection = db.connect();
|
||||
|
||||
match connection {
|
||||
Ok(connection) => Ok(Arc::new(TursoConnection {
|
||||
async_io: self.config.async_io,
|
||||
concurrent_guard: Arc::new(ConcurrentGuard::new()),
|
||||
connection,
|
||||
})),
|
||||
Err(err) => Err(turso_error_from_limbo_error(err)),
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method to get C raw container with TursoDatabase instance
|
||||
/// this method is used in the capi wrappers
|
||||
pub fn to_capi(self: &Arc<Self>) -> capi::c::turso_database_t {
|
||||
capi::c::turso_database_t {
|
||||
inner: Arc::into_raw(self.clone()) as *mut std::ffi::c_void,
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method to restore TursoDatabase ref from C raw container
|
||||
/// this method is used in the capi wrappers
|
||||
///
|
||||
/// # Safety
|
||||
/// value must be a pointer returned from [Self::to_capi] method
|
||||
pub unsafe fn ref_from_capi<'a>(
|
||||
value: capi::c::turso_database_t,
|
||||
) -> Result<&'a Self, TursoError> {
|
||||
if value.inner.is_null() {
|
||||
Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some("got null pointer".to_string()),
|
||||
})
|
||||
} else {
|
||||
Ok(&*(value.inner as *const Self))
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method to restore TursoDatabase instance from C raw container
|
||||
/// this method is used in the capi wrappers
|
||||
///
|
||||
/// # Safety
|
||||
/// value must be a pointer returned from [Self::to_capi] method
|
||||
pub unsafe fn arc_from_capi(value: capi::c::turso_database_t) -> Arc<Self> {
|
||||
Arc::from_raw(value.inner as *const Self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TursoConnection {
|
||||
async_io: bool,
|
||||
concurrent_guard: Arc<ConcurrentGuard>,
|
||||
connection: Arc<Connection>,
|
||||
}
|
||||
|
||||
impl TursoConnection {
|
||||
pub fn get_auto_commit(&self) -> bool {
|
||||
self.connection.get_auto_commit()
|
||||
}
|
||||
/// prepares single SQL statement
|
||||
pub fn prepare_single(&self, sql: impl AsRef<str>) -> Result<Box<TursoStatement>, TursoError> {
|
||||
match self.connection.prepare(sql) {
|
||||
Ok(statement) => Ok(Box::new(TursoStatement {
|
||||
concurrent_guard: self.concurrent_guard.clone(),
|
||||
async_io: self.async_io,
|
||||
statement,
|
||||
})),
|
||||
Err(err) => Err(turso_error_from_limbo_error(err)),
|
||||
}
|
||||
}
|
||||
/// prepares first SQL statement from the string and return prepared statement and position after the end of the parsed statement
|
||||
/// this method can be useful if SDK provides an execute(...) method which run all statements from the provided input in sequence
|
||||
pub fn prepare_first(
|
||||
&self,
|
||||
sql: impl AsRef<str>,
|
||||
) -> Result<Option<(Box<TursoStatement>, usize)>, TursoError> {
|
||||
match self.connection.consume_stmt(sql) {
|
||||
Ok(Some((statement, position))) => Ok(Some((
|
||||
Box::new(TursoStatement {
|
||||
async_io: self.async_io,
|
||||
concurrent_guard: Arc::new(ConcurrentGuard::new()),
|
||||
statement,
|
||||
}),
|
||||
position,
|
||||
))),
|
||||
Ok(None) => Ok(None),
|
||||
Err(err) => Err(turso_error_from_limbo_error(err)),
|
||||
}
|
||||
}
|
||||
|
||||
/// close the connection preventing any further operations executed over it
|
||||
/// SAFETY: caller must guarantee that no ongoing operations are running over connection before calling close(...) method
|
||||
pub fn close(&self) -> Result<(), TursoError> {
|
||||
self.connection
|
||||
.close()
|
||||
.map_err(turso_error_from_limbo_error)
|
||||
}
|
||||
|
||||
/// helper method to get C raw container to the TursoConnection instance
|
||||
/// this method is used in the capi wrappers
|
||||
pub fn to_capi(self: &Arc<Self>) -> capi::c::turso_connection_t {
|
||||
capi::c::turso_connection_t {
|
||||
inner: Arc::into_raw(self.clone()) as *mut std::ffi::c_void,
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method to restore TursoConnection ref from C raw container
|
||||
/// this method is used in the capi wrappers
|
||||
///
|
||||
/// # Safety
|
||||
/// value must be a pointer returned from [Self::to_capi] method
|
||||
pub unsafe fn ref_from_capi<'a>(
|
||||
value: capi::c::turso_connection_t,
|
||||
) -> Result<&'a Self, TursoError> {
|
||||
if value.inner.is_null() {
|
||||
Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some("got null pointer".to_string()),
|
||||
})
|
||||
} else {
|
||||
Ok(&*(value.inner as *const Self))
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method to restore TursoConnection instance from C raw container
|
||||
/// this method is used in the capi wrappers
|
||||
///
|
||||
/// # Safety
|
||||
/// value must be a pointer returned from [Self::to_capi] method
|
||||
pub unsafe fn arc_from_capi(value: capi::c::turso_connection_t) -> Arc<Self> {
|
||||
Arc::from_raw(value.inner as *const Self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TursoStatement {
|
||||
async_io: bool,
|
||||
concurrent_guard: Arc<ConcurrentGuard>,
|
||||
statement: Statement,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TursoExecutionResult {
|
||||
pub status: TursoStatusCode,
|
||||
pub rows_changed: u64,
|
||||
}
|
||||
|
||||
impl TursoStatement {
|
||||
/// binds positional parameter at the corresponding index (1-based)
|
||||
pub fn bind_positional(
|
||||
&mut self,
|
||||
index: usize,
|
||||
value: turso_core::Value,
|
||||
) -> Result<(), TursoError> {
|
||||
let Ok(index) = index.try_into() else {
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some("bind index must be non-zero".to_string()),
|
||||
});
|
||||
};
|
||||
// bind_at is safe to call with any index as it will put pair (index, value) into the map
|
||||
self.statement.bind_at(index, value);
|
||||
Ok(())
|
||||
}
|
||||
/// binds named parameter (name MUST omit named-parameter control character, e.g. '@', '$' or ':')
|
||||
pub fn bind_named(
|
||||
&mut self,
|
||||
name: impl AsRef<str>,
|
||||
value: turso_core::Value,
|
||||
) -> Result<(), TursoError> {
|
||||
let parameters = self.statement.parameters();
|
||||
for i in 1..parameters.next_index().get() {
|
||||
// i is positive - so conversion to NonZero<> type will always succeed
|
||||
let index = i.try_into().unwrap();
|
||||
let Some(parameter) = parameters.name(index) else {
|
||||
continue;
|
||||
};
|
||||
if !(parameter.starts_with(":")
|
||||
|| parameter.starts_with("@")
|
||||
|| parameter.starts_with("$")
|
||||
|| parameter.starts_with("?"))
|
||||
{
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Error,
|
||||
message: Some(format!(
|
||||
"internal error: unexpected internal parameter name: {parameter}"
|
||||
)),
|
||||
});
|
||||
}
|
||||
if name.as_ref() == ¶meter[1..] {
|
||||
self.statement.bind_at(index, value);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(TursoError {
|
||||
code: TursoStatusCode::Error,
|
||||
message: Some(format!("named parameter {} not found", name.as_ref())),
|
||||
})
|
||||
}
|
||||
/// make one execution step of the statement
|
||||
/// method returns [TursoStatusCode::Done] if execution is finished
|
||||
/// method returns [TursoStatusCode::Row] if execution generated a row
|
||||
/// method returns [TursoStatusCode::Io] if async_io was set and execution needs IO in order to make progress
|
||||
pub fn step(&mut self) -> Result<TursoStatusCode, TursoError> {
|
||||
let guard = self.concurrent_guard.clone();
|
||||
let _guard = guard.try_use()?;
|
||||
self.step_no_guard()
|
||||
}
|
||||
|
||||
fn step_no_guard(&mut self) -> Result<TursoStatusCode, TursoError> {
|
||||
let async_io = self.async_io;
|
||||
loop {
|
||||
return match self.statement.step() {
|
||||
Ok(StepResult::Done) => Ok(TursoStatusCode::Done),
|
||||
Ok(StepResult::Row) => Ok(TursoStatusCode::Row),
|
||||
Ok(StepResult::Busy) => Err(TursoError {
|
||||
code: TursoStatusCode::Busy,
|
||||
message: None,
|
||||
}),
|
||||
Ok(StepResult::Interrupt) => Err(TursoError {
|
||||
code: TursoStatusCode::Interrupt,
|
||||
message: None,
|
||||
}),
|
||||
Ok(StepResult::IO) => {
|
||||
if async_io {
|
||||
Ok(TursoStatusCode::Io)
|
||||
} else {
|
||||
self.run_io()?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(turso_error_from_limbo_error(err)),
|
||||
};
|
||||
}
|
||||
}
|
||||
/// execute statement to completion
|
||||
/// method returns [TursoStatusCode::Ok] if execution completed
|
||||
/// method returns [TursoStatusCode::Io] if async_io was set and execution needs IO in order to make progress
|
||||
pub fn execute(&mut self) -> Result<TursoExecutionResult, TursoError> {
|
||||
let guard = self.concurrent_guard.clone();
|
||||
let _guard = guard.try_use()?;
|
||||
|
||||
loop {
|
||||
let status = self.step_no_guard()?;
|
||||
if status == TursoStatusCode::Row {
|
||||
continue;
|
||||
} else if status == TursoStatusCode::Io {
|
||||
return Ok(TursoExecutionResult {
|
||||
status,
|
||||
rows_changed: 0,
|
||||
});
|
||||
} else if status == TursoStatusCode::Done {
|
||||
return Ok(TursoExecutionResult {
|
||||
status: TursoStatusCode::Ok,
|
||||
rows_changed: self.statement.n_change() as u64,
|
||||
});
|
||||
}
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Error,
|
||||
message: Some(format!(
|
||||
"internal error: unexpected status code: {status:?}",
|
||||
)),
|
||||
});
|
||||
}
|
||||
}
|
||||
/// run iteration of the IO backend
|
||||
pub fn run_io(&self) -> Result<(), TursoError> {
|
||||
self.statement
|
||||
.run_once()
|
||||
.map_err(turso_error_from_limbo_error)
|
||||
}
|
||||
/// get row value reference currently pointed by the statement
|
||||
/// note, that this row will no longer be valid after execution of methods like [Self::step]/[Self::execute]/[Self::finalize]/[Self::reset]
|
||||
pub fn row_value(&self, index: usize) -> Result<turso_core::ValueRef, TursoError> {
|
||||
let Some(row) = self.statement.row() else {
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some("statement holds no row".to_string()),
|
||||
});
|
||||
};
|
||||
if index >= row.len() {
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some("attempt to access row value out of bounds".to_string()),
|
||||
});
|
||||
}
|
||||
let value = row.get_value(index);
|
||||
Ok(value.as_value_ref())
|
||||
}
|
||||
/// returns column count
|
||||
pub fn column_count(&self) -> usize {
|
||||
self.statement.num_columns()
|
||||
}
|
||||
/// returns column name
|
||||
pub fn column_name(&self, index: usize) -> Result<Cow<'_, str>, TursoError> {
|
||||
if index >= self.column_count() {
|
||||
return Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some("column index out of bounds".to_string()),
|
||||
});
|
||||
}
|
||||
Ok(self.statement.get_column_name(index))
|
||||
}
|
||||
/// finalize statement execution
|
||||
/// this method must be called in the end of statement execution (either successfull or not)
|
||||
pub fn finalize(&mut self) -> Result<TursoStatusCode, TursoError> {
|
||||
let guard = self.concurrent_guard.clone();
|
||||
let _guard = guard.try_use()?;
|
||||
|
||||
while self.statement.execution_state().is_running() {
|
||||
let status = self.step_no_guard()?;
|
||||
if status == TursoStatusCode::Io {
|
||||
return Ok(status);
|
||||
}
|
||||
}
|
||||
Ok(TursoStatusCode::Ok)
|
||||
}
|
||||
/// reset internal statement state and bindings
|
||||
pub fn reset(&mut self) -> Result<(), TursoError> {
|
||||
self.statement.reset();
|
||||
self.statement.clear_bindings();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// helper method to get C raw container to the TursoStatement instance
|
||||
/// this method is used in the capi wrappers
|
||||
pub fn to_capi(self: Box<Self>) -> capi::c::turso_statement_t {
|
||||
capi::c::turso_statement_t {
|
||||
inner: Box::into_raw(self) as *mut std::ffi::c_void,
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method to restore TursoStatement ref from C raw container
|
||||
/// this method is used in the capi wrappers
|
||||
///
|
||||
/// # Safety
|
||||
/// value must be a pointer returned from [Self::to_capi] method
|
||||
pub unsafe fn ref_from_capi<'a>(
|
||||
value: capi::c::turso_statement_t,
|
||||
) -> Result<&'a mut Self, TursoError> {
|
||||
if value.inner.is_null() {
|
||||
Err(TursoError {
|
||||
code: TursoStatusCode::Misuse,
|
||||
message: Some("got null pointer".to_string()),
|
||||
})
|
||||
} else {
|
||||
Ok(&mut *(value.inner as *mut Self))
|
||||
}
|
||||
}
|
||||
|
||||
/// helper method to restore TursoStatement instance from C raw container
|
||||
/// this method is used in the capi wrappers
|
||||
///
|
||||
/// # Safety
|
||||
/// value must be a pointer returned from [Self::to_capi] method
|
||||
pub unsafe fn box_from_capi(value: capi::c::turso_statement_t) -> Box<Self> {
|
||||
Box::from_raw(value.inner as *mut Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::rsapi::{TursoDatabase, TursoDatabaseConfig};
|
||||
|
||||
#[test]
|
||||
pub fn test_db_concurrent_use() {
|
||||
let db = TursoDatabase::create(TursoDatabaseConfig {
|
||||
path: ":memory:".to_string(),
|
||||
experimental_features: None,
|
||||
io: None,
|
||||
async_io: false,
|
||||
});
|
||||
db.open().unwrap();
|
||||
let conn = db.connect().unwrap();
|
||||
let stmt1 = conn
|
||||
.prepare_single("SELECT * FROM generate_series(1, 10000)")
|
||||
.unwrap();
|
||||
let stmt2 = conn
|
||||
.prepare_single("SELECT * FROM generate_series(1, 10000)")
|
||||
.unwrap();
|
||||
|
||||
let mut threads = Vec::new();
|
||||
for mut stmt in [stmt1, stmt2] {
|
||||
let thread = std::thread::spawn(move || stmt.execute());
|
||||
threads.push(thread);
|
||||
}
|
||||
let mut results = Vec::new();
|
||||
for thread in threads {
|
||||
results.push(thread.join().unwrap());
|
||||
}
|
||||
assert!(
|
||||
results[0].is_err() && results[1].is_ok() || results[0].is_ok() && results[1].is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
302
sdk-kit/turso.h
Normal file
302
sdk-kit/turso.h
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
#ifndef TURSO_H
|
||||
#define TURSO_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum
|
||||
{
|
||||
TURSO_OK = 0,
|
||||
TURSO_DONE = 1,
|
||||
TURSO_ROW = 2,
|
||||
TURSO_IO = 3,
|
||||
TURSO_BUSY = 4,
|
||||
TURSO_INTERRUPT = 5,
|
||||
TURSO_ERROR = 127,
|
||||
TURSO_MISUSE = 128,
|
||||
TURSO_CONSTRAINT = 129,
|
||||
TURSO_READONLY = 130,
|
||||
TURSO_DATABASE_FULL = 131,
|
||||
TURSO_NOTADB = 132,
|
||||
TURSO_CORRUPT = 133,
|
||||
} turso_status_code_t;
|
||||
|
||||
// status of the operation
|
||||
// in most cases - this status acts as a signal for the error
|
||||
// in case of execution, status can return non-error code like ROW/DONE/IO which must be handled by the caller accordingly
|
||||
typedef struct turso_status_t
|
||||
{
|
||||
turso_status_code_t code;
|
||||
const char *error;
|
||||
} turso_status_t;
|
||||
|
||||
// enumeration of value types supported by the database
|
||||
typedef enum
|
||||
{
|
||||
TURSO_TYPE_INTEGER = 1,
|
||||
TURSO_TYPE_REAL = 2,
|
||||
TURSO_TYPE_TEXT = 3,
|
||||
TURSO_TYPE_BLOB = 4,
|
||||
TURSO_TYPE_NULL = 5,
|
||||
} turso_type_t;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
TURSO_TRACING_LEVEL_ERROR = 1,
|
||||
TURSO_TRACING_LEVEL_WARN,
|
||||
TURSO_TRACING_LEVEL_INFO,
|
||||
TURSO_TRACING_LEVEL_DEBUG,
|
||||
TURSO_TRACING_LEVEL_TRACE,
|
||||
} turso_tracing_level_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
const char *message;
|
||||
const char *target;
|
||||
const char *file;
|
||||
uint64_t timestamp;
|
||||
size_t line;
|
||||
turso_tracing_level_t level;
|
||||
} turso_log_t;
|
||||
|
||||
/// SAFETY: slice with non-null ptr must points to the valid memory range [ptr..ptr + len)
|
||||
/// ownership of the slice is not transferred - so its either caller owns the data or turso
|
||||
/// as the owner doesn't change - there is no method to free the slice reference - because:
|
||||
/// 1. if tursodb owns it - it will clean it in appropriate time
|
||||
/// 2. if caller owns it - it must clean it in appropriate time with appropriate method and tursodb doesn't know how to properly free the data
|
||||
typedef struct
|
||||
{
|
||||
const void *ptr;
|
||||
size_t len;
|
||||
} turso_slice_ref_t;
|
||||
|
||||
// owned slice - must be freed by the caller with corresponding method
|
||||
typedef struct
|
||||
{
|
||||
const void *ptr;
|
||||
size_t len;
|
||||
} turso_slice_owned_t;
|
||||
|
||||
/// structure holding opaque pointer to the TursoDatabase instance
|
||||
/// SAFETY: the database must be opened and closed only once but can be used concurrently
|
||||
typedef struct
|
||||
{
|
||||
void *inner;
|
||||
} turso_database_t;
|
||||
|
||||
/// structure holding opaque pointer to the TursoConnection instance
|
||||
/// SAFETY: the connection must be used exclusive and can't be accessed concurrently
|
||||
typedef struct
|
||||
{
|
||||
void *inner;
|
||||
} turso_connection_t;
|
||||
|
||||
/// structure holding opaque pointer to the TursoStatement instance
|
||||
/// SAFETY: the statement must be used exclusive and can't be accessed concurrently
|
||||
typedef struct
|
||||
{
|
||||
void *inner;
|
||||
} turso_statement_t;
|
||||
|
||||
// typeless union holding one possible value from the database row
|
||||
typedef union
|
||||
{
|
||||
int64_t integer;
|
||||
double real;
|
||||
turso_slice_ref_t text;
|
||||
turso_slice_ref_t blob;
|
||||
} turso_value_union_t;
|
||||
|
||||
// type-tagged union holding one possible value from the database row
|
||||
typedef struct
|
||||
{
|
||||
turso_value_union_t value;
|
||||
turso_type_t type;
|
||||
} turso_value_t;
|
||||
|
||||
/**
|
||||
* Database description.
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
/** Path to the database file or `:memory:` */
|
||||
const char *path;
|
||||
/** Optional comma separated list of experimental features to enable */
|
||||
const char *experimental_features;
|
||||
/** Parameter which defines who drives the IO - callee or the caller */
|
||||
bool async_io;
|
||||
} turso_database_config_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
/// SAFETY: turso_log_t log argument fields have lifetime scoped to the logger invocation
|
||||
/// caller must ensure that data is properly copied if it wants it to have longer lifetime
|
||||
void (*logger)(turso_log_t log);
|
||||
const char *log_level;
|
||||
} turso_config_t;
|
||||
|
||||
/** Setup global database info */
|
||||
turso_status_t turso_setup(turso_config_t config);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
turso_status_t status;
|
||||
turso_database_t database;
|
||||
} turso_database_create_result_t;
|
||||
|
||||
/** Create database holder but do not open it */
|
||||
turso_database_create_result_t turso_database_create(turso_database_config_t config);
|
||||
|
||||
/** Open database */
|
||||
turso_status_t turso_database_open(turso_database_t database);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
turso_status_t status;
|
||||
turso_connection_t connection;
|
||||
} turso_database_connect_result_t;
|
||||
|
||||
/** Connect to the database */
|
||||
turso_database_connect_result_t turso_database_connect(turso_database_t self);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
turso_status_t status;
|
||||
bool auto_commit;
|
||||
} turso_connection_get_autocommit_result_t;
|
||||
|
||||
/** Get autocommit state of the connection */
|
||||
turso_connection_get_autocommit_result_t
|
||||
turso_connection_get_autocommit(turso_connection_t self);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
turso_status_t status;
|
||||
turso_statement_t statement;
|
||||
} turso_connection_prepare_single_t;
|
||||
|
||||
/** Prepare single statement in a connection */
|
||||
turso_connection_prepare_single_t
|
||||
turso_connection_prepare_single(turso_connection_t self, turso_slice_ref_t sql);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
turso_status_t status;
|
||||
turso_statement_t statement;
|
||||
size_t tail_idx;
|
||||
} turso_connection_prepare_first_t;
|
||||
|
||||
/** Prepare first statement in a string containing multiple statements in a connection */
|
||||
turso_connection_prepare_first_t
|
||||
turso_connection_prepare_first(turso_connection_t self, turso_slice_ref_t sql);
|
||||
|
||||
/** close the connection preventing any further operations executed over it
|
||||
* caller still need to call deinit method to reclaim memory from the instance holding connection
|
||||
* SAFETY: caller must guarantee that no ongoing operations are running over connection before calling turso_connection_close(...) method
|
||||
*/
|
||||
turso_status_t turso_connection_close(turso_connection_t self);
|
||||
|
||||
/** Check if no more statements was parsed after execution of turso_connection_prepare_first method */
|
||||
bool turso_connection_prepare_first_result_empty(turso_connection_prepare_first_t result);
|
||||
|
||||
// result of the statement execution
|
||||
typedef struct
|
||||
{
|
||||
turso_status_t status;
|
||||
uint64_t rows_changed;
|
||||
} turso_statement_execute_t;
|
||||
|
||||
/** Execute single statement */
|
||||
turso_statement_execute_t turso_statement_execute(turso_statement_t self);
|
||||
|
||||
/** Step statement execution once
|
||||
* Returns TURSO_DONE if execution finished
|
||||
* Returns TURSO_ROW if execution generated the row (row values can be inspected with corresponding statement methods)
|
||||
* Returns TURSO_IO if async_io was set and statement needs to execute IO to make progress
|
||||
*/
|
||||
turso_status_t turso_statement_step(turso_statement_t self);
|
||||
|
||||
/** Execute one iteration of underlying IO backend */
|
||||
turso_status_t turso_statement_run_io(turso_statement_t self);
|
||||
|
||||
/** Reset a statement */
|
||||
turso_status_t turso_statement_reset(turso_statement_t self);
|
||||
|
||||
/** Finalize a statement
|
||||
* This method must be called in the end of statement execution (either successfull or not)
|
||||
*/
|
||||
turso_status_t turso_statement_finalize(turso_statement_t self);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
turso_status_t status;
|
||||
size_t column_count;
|
||||
} turso_statement_column_count_result_t;
|
||||
|
||||
/** Get column count */
|
||||
turso_statement_column_count_result_t
|
||||
turso_statement_column_count(turso_statement_t self);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
turso_status_t status;
|
||||
const char *column_name;
|
||||
} turso_statement_column_name_result_t;
|
||||
|
||||
/** Get the column name at the index
|
||||
* C string allocated by Turso must be freed after the usage with corresponding turso_str_deinit(...) method
|
||||
*/
|
||||
turso_statement_column_name_result_t
|
||||
turso_statement_column_name(turso_statement_t self, size_t index);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
turso_status_t status;
|
||||
turso_value_t value;
|
||||
} turso_statement_row_value_t;
|
||||
|
||||
/** Get the row value at the the index for a current statement state
|
||||
* SAFETY: returned turso_value_t will be valid only until next invocation of statement operation (step, finalize, reset, etc)
|
||||
* Caller must make sure that any non-owning memory is copied appropriated if it will be used for longer lifetime
|
||||
*/
|
||||
turso_statement_row_value_t turso_statement_row_value(turso_statement_t self, size_t index);
|
||||
|
||||
/** Bind a named argument to a statement */
|
||||
turso_status_t turso_statement_bind_named(
|
||||
turso_statement_t self,
|
||||
turso_slice_ref_t name,
|
||||
turso_value_t value);
|
||||
/** Bind a positional argument to a statement */
|
||||
turso_status_t
|
||||
turso_statement_bind_positional(turso_statement_t self, size_t position, turso_value_t value);
|
||||
|
||||
/** Create a turso integer value */
|
||||
turso_value_t turso_integer(int64_t integer);
|
||||
/** Create a turso real value */
|
||||
turso_value_t turso_real(double real);
|
||||
/** Create a turso text value */
|
||||
turso_value_t turso_text(const char *ptr, size_t len);
|
||||
/** Create a turso blob value */
|
||||
turso_value_t turso_blob(const uint8_t *ptr, size_t len);
|
||||
/** Create a turso null value */
|
||||
turso_value_t turso_null();
|
||||
|
||||
/** Deallocate a status */
|
||||
void turso_status_deinit(turso_status_t self);
|
||||
/** Deallocate C string allocated by Turso */
|
||||
void turso_str_deinit(const char *self);
|
||||
/** Deallocate and close a database
|
||||
* SAFETY: caller must ensure that no other code can concurrently or later call methods over deinited database
|
||||
*/
|
||||
void turso_database_deinit(turso_database_t self);
|
||||
/** Deallocate and close a connection
|
||||
* SAFETY: caller must ensure that no other code can concurrently or later call methods over deinited connection
|
||||
*/
|
||||
void turso_connection_deinit(turso_connection_t self);
|
||||
/** Deallocate and close a statement
|
||||
* SAFETY: caller must ensure that no other code can concurrently or later call methods over deinited statement
|
||||
*/
|
||||
void turso_statement_deinit(turso_statement_t self);
|
||||
|
||||
#endif /* TURSO_H */
|
||||
Loading…
Add table
Add a link
Reference in a new issue