Database Backends
Overview
OpenViper exposes database extensibility through a DatabaseBackend API
that wraps the framework’s existing SQLAlchemy-based connection, execution,
features, operations, introspection, and creation behaviour.
This feature does not replace SQLAlchemy. SQLAlchemy remains the default SQL dialect and execution foundation. The backend API exposes OpenViper integration points around SQLAlchemy so developers can customize, extend, and instrument database behaviour.
How OpenViper Database Backends Work
A DatabaseBackend is an OpenViper class that controls how a configured
SQL database alias creates engines, connections, transactions, and executes
statements. It also exposes feature flags, operations, execution hooks,
introspection, test database creation, and optional client helpers.
Each configured database alias (default, replica, etc.) is backed by
exactly one DatabaseBackend instance.
Note
DatabaseBackend controls the core SQL database layer.
VirtualBackend controls per-model custom data sources (REST APIs,
in-memory stores, etc.). If model._meta.virtual is True, use
VirtualBackend routing, not DatabaseBackend routing.
DATABASES Configuration
The DATABASES setting is a dictionary mapping alias names to configuration
dictionaries. See Installation for the full configuration reference
including pool options, URL formats, and the nested vs flat config format.
A minimal configuration requires only a URL:
DATABASES = {
"default": {
"OPTIONS": {
"URL": "postgresql://user:pass@primary-db/app",
},
},
}
Multi-database setups with read replicas are covered in Database Routing and Read/Write Replicas.
BACKEND is Optional
The BACKEND key is optional when using the built-in SQLAlchemy backend.
When omitted, OpenViper uses DefaultDatabaseBackend (SQLAlchemy async
engine). Use the short name "sqlalchemy" or the full dotted path
"openviper.db.backends.DefaultDatabaseBackend" for the built-in backend.
# Omit BACKEND (uses DefaultDatabaseBackend):
DATABASES = {
"default": {
"OPTIONS": {
"URL": "postgresql+asyncpg://user:pass@localhost/app",
"POOL_SIZE": 20,
},
},
}
# Short name (equivalent to omitting BACKEND):
DATABASES = {
"default": {
"BACKEND": "sqlalchemy",
"OPTIONS": {
"URL": "postgresql+asyncpg://user:pass@localhost/app",
},
},
}
# Custom backend (BACKEND is required):
DATABASES = {
"default": {
"BACKEND": "myproject.db.backends.MetricsDatabaseBackend",
"OPTIONS": {
"URL": "postgresql+asyncpg://user:pass@localhost/app",
},
},
}
An empty BACKEND string raises DatabaseConfigurationError. A
non-string BACKEND value also raises DatabaseConfigurationError.
Custom backend import paths must be under openviper.db.backends. or
openviper.contrib. - other paths are rejected for security.
See Installation for pool options, URL formats, and the nested vs flat config format.
DatabaseBackend
- class DatabaseBackend(alias, config)
Abstract base class for all database backends.
- vendor
Short vendor name (e.g.
"postgresql","mysql","sqlite").
- display_name
Human-readable backend name.
- features
DatabaseFeaturesinstance for this backend.
- operations
DatabaseOperationsinstance for this backend.
- execution
DatabaseExecutioninstance for this backend.
- introspection
DatabaseIntrospectioninstance for this backend.
- creation
DatabaseCreationinstance for this backend.
- client
DatabaseClientinstance for this backend.
- create_engine()
Create and return the async SQLAlchemy engine for this alias.
- connect()
Return an async database connection.
- disconnect()
Dispose backend resources.
- execute(statement, parameters=None)
Execute a SQLAlchemy statement through the execution hooks.
- transaction(using=None)
Return a transaction context manager.
- url
The configured database URL for this alias. Reads from
OPTIONS.URLfirst (nested config format), then falls back toURLdirectly in the config dict (flat format).
- is_read_only
Whether this alias is configured as read-only. Reads from
OPTIONS.READ_ONLYfirst, thenREAD_ONLYdirectly.
- role
The configured role (
"primary"or"replica"). Reads fromOPTIONS.ROLEfirst, thenROLEdirectly.
- get_option(key, default=None)
Resolve a configuration key from
OPTIONSfirst (nested format), then from the alias config directly (flat format). Use this in custom backends to read pool options, driver settings, or any user-supplied configuration.pool_size = backend.get_option("POOL_SIZE", 20) echo = backend.get_option("ECHO", False)
DatabaseFeatures
- class DatabaseFeatures
Declares database capabilities for a configured backend alias.
- supports_transactions
- supports_savepoints
- supports_json
- supports_uuid
- supports_returning
- supports_bulk_insert
- supports_foreign_keys
- supports_indexes
- supports_partial_indexes
- supports_check_constraints
- supports_schema_comments
- supports_read_only_connections
Feature flags by vendor
Each dialect ships its own feature class under
openviper.db.backends.features (PostgreSQLFeatures,
MariaDBFeatures, SQLiteFeatures, MSSQLFeatures,
OracleFeatures). get_features_for_vendor() resolves the
vendor name extracted from the configured URL to the appropriate
feature instance. The following table shows which features are enabled
for each supported vendor:
Vendor |
trans |
savept |
json |
uuid |
ret |
bulk |
fk |
idx |
partial |
check |
comments |
|---|---|---|---|---|---|---|---|---|---|---|---|
postgresql |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
mysql |
✓ |
✓ |
✓ |
✗ |
✗ |
✓ |
✓ |
✓ |
✗ |
✓ |
✓ |
sqlite |
✓ |
✓ |
✓ |
✗ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✗ |
mssql |
✓ |
✓ |
✓ |
✗ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
oracle |
✓ |
✓ |
✓ |
✗ |
✓ |
✓ |
✓ |
✓ |
✗ |
✓ |
✓ |
DatabaseOperations
- class DatabaseOperations
Provides OpenViper-level database-specific behaviour above SQLAlchemy. This does not replace SQLAlchemy’s compiler.
- normalize_url(url)
Translate a synchronous database URL to its async driver equivalent.
- extract_vendor(url)
Return a short vendor name derived from the database URL.
- quote_identifier(name)
Quote a SQL identifier if it contains special characters.
- adapt_value(value)
Adapt a Python value before execution.
DatabaseExecution
- class DatabaseExecution
Execution hook layer for instrumentation, logging, retries, and tracing.
- pre_execute(statement, parameters=None)
Called before a statement is executed.
- post_execute(statement, parameters=None, duration=None)
Called after a statement completes successfully.
- on_error(statement, parameters=None, error=None)
Called when statement execution raises an exception.
- execute(connection, statement, parameters=None)
Execute a SQLAlchemy statement through the hook lifecycle.
DatabaseIntrospection
DatabaseCreation
Default SQLAlchemy Backend
The built-in DefaultDatabaseBackend wraps OpenViper’s existing
SQLAlchemy async engine behaviour. It is the default when BACKEND is
omitted.
It supports all pool options (POOL_SIZE, MAX_OVERFLOW,
POOL_RECYCLE, POOL_TIMEOUT, PREPARED_STMT_CACHE, ECHO),
automatic async driver detection, SQLite in-memory handling, and
per-request connection pinning.
Creating a Custom Backend
The most common way to extend the database layer is to subclass
DefaultDatabaseBackend and override only the methods you need. This
preserves all built-in engine creation, pool configuration, and connection
management while letting you add instrumentation, retry logic, or
dialect-specific behaviour.
from openviper.db.backends.sqlalchemy import DefaultDatabaseBackend
from openviper.db.backends.execution import DatabaseExecution
from collections.abc import Mapping
from typing import Any
class MetricsExecution(DatabaseExecution):
"""Execution hooks that record query timing."""
async def pre_execute(self, statement, parameters=None):
# Start a metrics timer
pass
async def post_execute(self, statement, parameters=None, duration=None):
# Record query duration
pass
class MetricsDatabaseBackend(DefaultDatabaseBackend):
"""Backend that adds metrics instrumentation on top of SQLAlchemy."""
display_name = "SQLAlchemy with Metrics"
def create_execution(self):
return MetricsExecution()
# Register it in settings:
DATABASES = {
"default": {
"BACKEND": "myproject.db.backends.MetricsDatabaseBackend",
"OPTIONS": {
"URL": "postgresql+asyncpg://user:pass@localhost/app",
"POOL_SIZE": 20,
},
},
}
For advanced use cases where you need full control over engine creation,
subclass DatabaseBackend directly. You must implement all abstract
methods:
from openviper.db.backends.database import DatabaseBackend
from collections.abc import Mapping
from typing import Any
import sqlalchemy as sa
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncConnection
from contextlib import asynccontextmanager
from collections.abc import AsyncGenerator
class CustomDatabaseBackend(DatabaseBackend):
vendor = "custom"
display_name = "Custom Database"
async def create_engine(self) -> AsyncEngine:
url = self.url
echo = bool(self.get_option("ECHO", False))
# Build engine with custom pool or driver settings
return sa.ext.asyncio.create_async_engine(url, echo=echo)
async def connect(self) -> AsyncConnection:
engine = await self.create_engine()
return engine.connect()
async def disconnect(self) -> None:
# Dispose engine and clean up resources
pass
async def execute(self, statement, parameters=None):
engine = await self.create_engine()
async with engine.connect() as conn:
return await self.execution.execute(conn, statement, parameters)
def transaction(self, using=None):
return self.atomic()
@asynccontextmanager
async def atomic(self) -> AsyncGenerator[AsyncConnection, None]:
engine = await self.create_engine()
async with engine.begin() as conn:
yield conn
# Register it:
DATABASES = {
"default": {
"BACKEND": "myproject.db.backends.CustomDatabaseBackend",
"OPTIONS": {
"URL": "custom://host/db",
},
},
}
Config access in backends
Use DatabaseBackend.get_option() to read configuration values from
the OPTIONS dict (nested format) or directly from the alias config
(flat format). It resolves OPTIONS.<key> first, then falls back to
the top-level key:
class MyBackend(DefaultDatabaseBackend):
async def create_engine(self):
# Reads from OPTIONS.POOL_SIZE, then POOL_SIZE, then default 20
pool_size = int(self.get_option("POOL_SIZE", 20))
echo = bool(self.get_option("ECHO", False))
...
Properties url, is_read_only, and role also
resolve from OPTIONS first, then from the flat config:
backend = MyBackend("default", {
"OPTIONS": {"URL": "postgresql+asyncpg://user:pass@localhost/db"},
"ROLE": "primary",
})
backend.url # "postgresql+asyncpg://user:pass@localhost/db"
backend.role # "primary"
backend.is_read_only # False
Instrumentation Example
The DatabaseExecution hooks are the primary extension point for
instrumentation. Override pre_execute and post_execute to add
timing, tracing spans, or audit logging. Override on_error to add
error metrics or custom error mapping.
Testing a Backend
Use the DatabaseCreation component to create and destroy test databases
per alias. See Database Routing and Read/Write Replicas for multi-database testing helpers.
Security Notes
Never log full database URLs containing credentials.
Never expose database passwords in error messages.
Prevent writes to
READ_ONLYaliases.Use parameterized SQL execution via SQLAlchemy.
Do not bypass SQLAlchemy safety mechanisms.
Limitations
DatabaseBackenddoes not replace SQLAlchemy’s SQL compiler.Custom backends must still use SQLAlchemy’s async engine API.
The backend API is designed for OpenViper integration points, not for implementing entirely new database protocols.