Skip to main content

A SQL-first Python library for querying and mapping data across multiple database backends.

Project description

RowQuery

PyPI version Python Versions License: MIT Tests codecov Code style: ruff

A SQL-first Python library for querying and mapping data across multiple database backends.

Features

  • Multi-Database Support: SQLite, PostgreSQL, MySQL, Oracle with unified interface
  • SQL-First Design: Load queries from .sql files organized in namespaces, or pass inline SQL directly
  • Inline SQL: Execute raw SQL strings alongside registry keys — no registry required for ad-hoc queries
  • SQL Sanitization: Configurable sanitizer strips comments, blocks statement stacking, and restricts SQL verbs
  • Flexible Mapping: Map results to dataclasses, Pydantic models, or plain classes
  • Aggregate Mapping: Reconstruct complex object graphs from joined queries (single-pass O(n))
  • Transaction Management: Context manager support with automatic rollback
  • Migration Management: Version-controlled database migrations
  • Repository Pattern: Optional DDD-style repository base classes
  • Async Support: Full async/await support for all operations
  • Type Safe: Fully typed with mypy strict mode

Installation

Core (SQLite support included)

pip install rowquery

With Database Drivers

pip install rowquery[postgres]    # PostgreSQL
pip install rowquery[mysql]       # MySQL
pip install rowquery[oracle]      # Oracle
pip install rowquery[all]         # All drivers

Quick Start

1. Organize Your SQL Files

sql/
  user/
    get_by_id.sql
    list_active.sql
  order/
    create.sql

2. Execute Queries

from row_query import Engine, ConnectionConfig, SQLRegistry

config = ConnectionConfig(driver="sqlite", database="app.db")
registry = SQLRegistry("sql/")
engine = Engine.from_config(config, registry)

# Registry key lookup (dot-separated namespace)
user = engine.fetch_one("user.get_by_id", {"id": 1})
users = engine.fetch_all("user.list_active")
count = engine.fetch_scalar("user.count")

# Inline SQL — pass a raw SQL string directly
user = engine.fetch_one("SELECT * FROM users WHERE id = ?", 1)
users = engine.fetch_all("SELECT * FROM users WHERE active = ?", True)
count = engine.fetch_scalar("SELECT COUNT(*) FROM users")
rows = engine.fetch_all("SELECT * FROM users WHERE id IN (?, ?)", [1, 2])

3. Map to Models

from dataclasses import dataclass
from row_query.mapping import ModelMapper

@dataclass
class User:
    id: int
    name: str
    email: str

mapper = ModelMapper(User)
user = engine.fetch_one("user.get_by_id", {"id": 1}, mapper=mapper)
# Returns: User(id=1, name="Alice", email="alice@example.com")

4. Aggregate Mapping (Reconstruct Object Graphs)

from row_query.mapping import aggregate, AggregateMapper
from dataclasses import dataclass

@dataclass
class Order:
    id: int
    total: float

@dataclass
class UserWithOrders:
    id: int
    name: str
    email: str
    orders: list[Order]

# Build mapping plan for complex object graph
plan = (
    aggregate(UserWithOrders, prefix="user__")
    .key("id")
    .auto_fields()
    .collection("orders", Order, prefix="order__", key="id")
    .build()
)

# Execute joined query and map in single pass
users = engine.fetch_all("user.with_orders", mapper=AggregateMapper(plan))

5. Transactions

# Use context manager for automatic rollback on error
with engine.transaction() as tx:
    tx.execute("user.create", {"name": "Alice", "email": "alice@example.com"})
    tx.execute("audit.log", {"action": "user_created"})
    # Commits on exit, rolls back on exception

6. SQL Sanitization

from row_query import Engine, SQLSanitizer

# Configure what inline SQL is permitted
sanitizer = SQLSanitizer(
    strip_comments=True,           # Remove -- and /* */ comments (default: True)
    block_multiple_statements=True, # Reject "SELECT 1; DROP TABLE t" (default: True)
    allowed_verbs=frozenset({"SELECT"}),  # Only allow SELECT statements (default: None = any)
)

engine = Engine.from_config(config, registry, sanitizer=sanitizer)

# Inline SQL is sanitized before execution
users = engine.fetch_all("SELECT * FROM users -- get all")  # comment stripped
engine.execute("DROP TABLE users")  # raises SQLSanitizationError (verb not allowed)
engine.execute("SELECT 1; DROP TABLE t")  # raises SQLSanitizationError (multiple statements)

# Registry queries are always trusted and never sanitized
users = engine.fetch_all("user.list_active")  # no sanitization applied

7. Async Support

from row_query import AsyncEngine, ConnectionConfig

config = ConnectionConfig(driver="sqlite", database="app.db")
engine = AsyncEngine.from_config(config, registry)

async def fetch_users():
    # Registry key or inline SQL — both work
    users = await engine.fetch_all("user.list_active")
    users = await engine.fetch_all("SELECT * FROM users WHERE active = ?", True)
    return users

# Async transactions
async with engine.transaction() as tx:
    await tx.execute("user.create", {"name": "Bob"})
    await tx.execute("INSERT INTO audit (action) VALUES (?)", "user_created")

Documentation

Development

This project uses uv for package management.

# Install dependencies
uv sync --extra all --extra dev

# Run tests
uv run pytest

# Run tests with coverage
uv run pytest --cov=row_query --cov-report=html

# Lint and format
uv run ruff check row_query/ tests/
uv run ruff format row_query/ tests/

# Type check
uv run mypy row_query/

License

MIT License - see LICENSE for details.

Contributing

Contributions welcome! Please read CONTRIBUTING.md first.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rowquery-0.1.2.tar.gz (57.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rowquery-0.1.2-py3-none-any.whl (32.9 kB view details)

Uploaded Python 3

File details

Details for the file rowquery-0.1.2.tar.gz.

File metadata

  • Download URL: rowquery-0.1.2.tar.gz
  • Upload date:
  • Size: 57.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for rowquery-0.1.2.tar.gz
Algorithm Hash digest
SHA256 7e5e31161b652631e15556da3b29ded45b3f5dd8f28e22081a81970f89e97b7e
MD5 739db1b6c07528e5680872fd4116edb0
BLAKE2b-256 18815b8d5ea4c3f2aac2a71946eb73b873eaac5a334ade2b5da65ca11227898b

See more details on using hashes here.

Provenance

The following attestation bundles were made for rowquery-0.1.2.tar.gz:

Publisher: release.yml on MaksimShevtsov/RowQuery

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rowquery-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: rowquery-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 32.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for rowquery-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 4c2936819ad5d55cfa6537d9ae253b267bc88755ebc9685e38ed1587224b518a
MD5 88c56d34fefb09ad99de15db2eb66929
BLAKE2b-256 ca57d0829a70baf2a018e3542b93041bf9a46954151cb01d92f60bacb1412407

See more details on using hashes here.

Provenance

The following attestation bundles were made for rowquery-0.1.2-py3-none-any.whl:

Publisher: release.yml on MaksimShevtsov/RowQuery

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page