Skip to content

Latest commit

 

History

History
119 lines (93 loc) · 5.36 KB

File metadata and controls

119 lines (93 loc) · 5.36 KB
name
2025-11-26-py-bindings

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)
# 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(
    path: str,
    *,
    experimental_features: Optional[str] = None,
    isolation_level: Optional[str] = "DEFERRED",
    extra_io: Optional[Callable[[], None]] = None # extra IO which must be called after stmt.run_io() invocations in the driver
): ...

# Make it easy to enable logging with native `logging` Python module
# (import logging only inside this function - make an exception here)
def setup_logging(level: Optional[int] = None) -> 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:
class T: 
    def f(self) -> 'T':

Implementation

  • Accept extra_io optional parameter in the driver which will run after stmt.run_io() whenever statement execution returned TURSO_IO status
  • 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:
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.

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