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.0.tar.gz (48.5 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.0-py3-none-any.whl (32.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: rowquery-0.1.0.tar.gz
  • Upload date:
  • Size: 48.5 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.0.tar.gz
Algorithm Hash digest
SHA256 7132b3e4b64ab03b1c63430fa14ae6bdae2d3000b53814e3bfb3b7c54ab5abe3
MD5 df5b6b8c4043fd4c21f7edb3075fc074
BLAKE2b-256 a4ccc5673f461520022337449dc64f7c333f8b65032be0c42013caa58e2befe4

See more details on using hashes here.

Provenance

The following attestation bundles were made for rowquery-0.1.0.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.0-py3-none-any.whl.

File metadata

  • Download URL: rowquery-0.1.0-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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 89214c94a846b7973760181b95d2b444a1a22edda1288096128f4b37aa7190c9
MD5 faacc447ed4e60e2b476f7e13a2a6464
BLAKE2b-256 c9dc5ac0ec394c9a95dde5557c84ff27dfff318bb8da15ff4a66eca831ff00ea

See more details on using hashes here.

Provenance

The following attestation bundles were made for rowquery-0.1.0-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