Requirements
Section titled “Requirements”| Package | Required | Purpose |
|---|---|---|
oridecon | Yes | Core framework |
oridecon-contracts | Yes | Protocol definitions |
asyncpg | Recommended | PostgreSQL async driver |
aiosqlite | Recommended | SQLite async driver |
oridecon-cache | Optional | Query caching |
Overview
Section titled “Overview”oridecon-sql provides async SQL database access for Postgres, MySQL, and SQLite — built on SQLAlchemy 2.0 with repositories, migrations, query building, and unit-of-work.
What it solves: Wrangling async database connections, pooling, migrations, and repository boilerplate is repetitive. oridecon-sql gives you declarative repositories, DI-managed lifecycle, multi-backend support, and a clean query builder — all behind DatabaseProviderProtocol from contracts.
The mental model: A DatabaseProvider manages the connection pool. You interact through GenericRepository[T, TKey] for CRUD, AsyncQueryBuilder for custom queries, and SimpleUnitOfWork for transactions. Everything is registered in the DI container and injectable as DatabaseProviderProtocol.
Core Concepts
Section titled “Core Concepts”DatabaseProvider
Section titled “DatabaseProvider”The DatabaseProvider is the entry point — it registers the connection pool, query logger, migration manager, and unit-of-work factory in the container.
from oridecon import Applicationfrom oridecon.sql import DatabaseProvider
app = Application(name="my-app")app.add_provider(DatabaseProvider(config=DatabaseConfig.from_url("...")))It runs at INFRASTRUCTURE priority (10) — booting before domain services.
DatabaseModule
Section titled “DatabaseModule”The module wrapper provides configure() and stub():
from oridecon.sql import DatabaseModule
app.add_module(DatabaseModule.configure("postgresql+asyncpg://localhost/mydb"))DatabaseConfig
Section titled “DatabaseConfig”Configuration is loaded from the sql section of application.yaml or passed explicitly:
from oridecon.sql.config import DatabaseConfig, DatabaseBackendConfig
config = DatabaseConfig( backend=DatabaseBackendConfig(url="postgresql+asyncpg://user:pass@localhost/db"),)GenericRepository
Section titled “GenericRepository”The primary CRUD abstraction — typed by entity class and key type:
from oridecon.sql import GenericRepository
repo = GenericRepository[User, int]( provider=db_provider, table_name="users", entity_class=User, key_field="id",)Methods: find, find_one, create, update, delete, count, exists.
AsyncQueryBuilder
Section titled “AsyncQueryBuilder”Build SQL queries programmatically:
from oridecon.sql.query import AsyncQueryBuilder, Operator
query = ( AsyncQueryBuilder("users") .where("age", Operator.GTE, 18) .order_by("name", ascending=True) .limit(20) .offset(0))results = await query.fetch(db_provider)Unit of Work
Section titled “Unit of Work”Coordinate multiple repository operations in a single transaction:
from oridecon.sql import SimpleUnitOfWork
class UserService: def __init__(self, uow: SimpleUnitOfWork) -> None: self.uow = uow
async def create_user(self, data: dict) -> Result[User, Error]: async with self.uow: user = await self.uow.users.create(data) await self.uow.audit_log.create({"action": "user_created", "user_id": user.id}) await self.uow.commit() return Ok(user)Multi-Backend Support
Section titled “Multi-Backend Support”Configure multiple named databases:
sql: backends: - name: primary backend: url: postgresql+asyncpg:///primary primary: true - name: analytics backend: url: postgresql+asyncpg:///analytics migration_dir: migrations/analyticsResolve by name: Annotated[DatabaseProviderProtocol, Named("analytics")].
Typical Usage
Section titled “Typical Usage”Application Factory
Section titled “Application Factory”from oridecon import Applicationfrom oridecon.sql import DatabaseModule
def create_app() -> Application: app = Application(name="my-app") app.add_module(DatabaseModule.configure( "postgresql+asyncpg://localhost/mydb", enable_migrations=True, )) return appRepository Service
Section titled “Repository Service”from oridecon.di import singletonfrom oridecon.result import Result, Ok, Errfrom oridecon.contracts.data import DatabaseProviderProtocolfrom oridecon.sql import GenericRepositoryfrom oridecon.sql.exceptions import DatabaseError, RepositoryError
@singletonclass UserRepository: def __init__(self, db: DatabaseProviderProtocol) -> None: self.repo = GenericRepository[User, int]( provider=db, table_name="users", entity_class=User, key_field="id", )
async def find_by_email(self, email: str) -> Result[User, RepositoryError]: try: user = await self.repo.find_one(email=email) if user is None: return Err(RepositoryError("User not found")) return Ok(user) except DatabaseError as e: return Err(RepositoryError(str(e)))Migration Management
Section titled “Migration Management”# Use the CLIuv run oridecon db upgrade
# Or the AlembicManager APIfrom oridecon.sql.migrations.api import AlembicManager
manager = AlembicManager("postgresql+asyncpg://localhost/mydb", "migrations")await manager.upgrade()Common Patterns
Section titled “Common Patterns”Filtering with F expressions
Section titled “Filtering with F expressions”from oridecon.sql import F, Filter
active_users = await repo.find( filters=[ Filter("status", "==", "active"), Filter("age", ">=", 18), ], order_by=[F("created_at").desc()], limit=50,)Type-Safe Identifiers
Section titled “Type-Safe Identifiers”from oridecon.sql import table, column, Table, Column
users = Table("users")Using DatabaseModule.stub() in Tests
Section titled “Using DatabaseModule.stub() in Tests”from oridecon import Applicationfrom oridecon.sql import DatabaseModule
async def test_repository(): async with Application.boot( name="test", modules=[DatabaseModule.stub()], ) as app: db = await app.container.resolve(DatabaseProviderProtocol) repo = GenericRepository[User, int]( provider=db, table_name="users", entity_class=User, key_field="id", ) assert result.is_ok()Row-Level Security
Section titled “Row-Level Security”from oridecon.sql import RowLevelSecurityPolicy, ScopeColumn
policy = RowLevelSecurityPolicy(columns=[ScopeColumn("tenant_id")])repo = GenericRepository[User, int]( provider=db, table_name="users", entity_class=User, key_field="id", rls_policy=policy,)Best Practices
Section titled “Best Practices”- ✅ Use
DatabaseModule.configure()for module-based registration - ✅ Use
DatabaseModule.stub()in unit tests for in-memory operation - ✅ Return
Result[T, RepositoryError]from repository methods - ✅ Use
SimpleUnitOfWorkfor multi-repository transactions - ✅ Use
DatabaseProviderProtocolfrom contracts — never importoridecon-sqldirectly from other extensions - ✅ Pin versions in production
- ❌ Don’t create connection pools manually — always use
DatabaseProvider - ❌ Don’t embed raw SQL strings — use
AsyncQueryBuilderorGenericRepository - ❌ Don’t call
provider.boot()manually — let theApplicationlifecycle handle it - ❌ Don’t add
oridecon-sqlas a dependency of another extension — use the contracts protocol
Next Steps
Section titled “Next Steps”- Architecture — internal design, provider lifecycle, contracts
- Configuration — every config key
- How-Tos — task-oriented recipes
- Troubleshooting — common errors and fixes