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:
Pekka Enberg 2025-12-01 19:48:45 +02:00 committed by GitHub
commit 49b207ce4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 6324 additions and 406 deletions

127
Cargo.lock generated
View file

@ -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"

View file

@ -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" }

View file

@ -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" }

View 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>

View 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>

View file

@ -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 databases 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."
);

View file

@ -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

View file

@ -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",
]

View 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)

View file

@ -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);

View file

@ -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
View 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
View 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
View 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
View 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
View 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"

View 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
View 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

File diff suppressed because it is too large Load diff

66
sdk-kit/src/lib.rs Normal file
View 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
View 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() == &parameter[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
View 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 */