Storage Backends
ErisPulse comes with three built-in asynchronous native storage backends, switchable with a single configuration change. The API is completely consistent, requiring zero code changes when switching backends.
| Backend | Driver | Installation | Features |
|---|---|---|---|
| SQLite (default) | aiosqlite | Ready to use out-of-the-box | Zero configuration, single-file, WAL concurrency |
| MySQL / MariaDB | aiomysql | pip install ErisPulse[mysql] |
Suitable for existing MySQL infrastructure, shared across multiple instances |
| PostgreSQL | asyncpg | pip install ErisPulse[postgres] |
Strong transaction support, JSONB ecosystem, high concurrency |
{!--< tips >!--}
- Asynchronous operations are the native primary interface (
aget/aset/atransaction/aExecute), while synchronous APIs serve as a compatibility layer. - The framework's own configuration persistence, session inbox, and conversation checkpoints all use the same storage backend—switching the backend results in a full migration. {!--< /tips >!--}
Backend Selection
Configure in config/config.toml:
[ErisPulse.storage]
backend = "sqlite" # "sqlite" (default) / "mysql" / "postgres"
use_global_db = false # Only for SQLite: use the package's global database data/config.db
Environment variables are also supported for overriding (useful for Docker / 12-factor applications):
ERISPULSE_STORAGE_BACKEND=postgres
ERISPULSE_STORAGE_POSTGRES_HOST=db.example.com
ERISPULSE_STORAGE_POSTGRES_PASSWORD=secret
Environment variable naming convention: Configuration paths are uppercase, and dots are replaced with underscores.
(e.g., ErisPulse.storage.postgres.host → ERISPULSE_STORAGE_POSTGRES_HOST).
Connection Parameters
MySQL (ErisPulse.storage.mysql)
[ErisPulse.storage.mysql]
host = "127.0.0.1"
port = 3306
user = "erispulse"
password = ""
database = "erispulse"
charset = "utf8mb4"
pool_min = 1
pool_max = 10
PostgreSQL (ErisPulse.storage.postgres)
[ErisPulse.storage.postgres]
host = "127.0.0.1"
port = 5432
user = "erispulse"
password = ""
database = "erispulse"
pool_min = 1
pool_max = 10
Note
Changes to connection parameters require a framework restart to take effect (a restart reminder log is output during hot configuration updates). Connection pools are lazily created per event loop, and transient connection failures (e.g., network fluctuations or database restart windows) are automatically retried with exponential backoff.
Connection Failure Behavior
The framework's startup and operation do not depend on database reachability—MySQL / PostgreSQL connection failures will not cause the framework to crash or fail to start:
- Instant pool creation failure triggers automatic exponential backoff retry (default 3 attempts)
- After exhausting retries → logs a WARNING (including reason and cooldown duration), the framework starts / continues running as normal, with only storage operations temporarily unavailable
- During the cooldown period (default 30 seconds), subsequent storage operations fail quickly (without blocking or slowing down other functions)
- Automatic reconnection attempt after cooldown ends—once the database recovers, storage becomes available again without requiring a restart
Log example: mysql connection pool creation failed after 3 retries, automatic reconnection in 30 seconds; storage operations will fail quickly during this period, while other framework functions remain unaffected
Asynchronous Native APIs
# KV operations
await sdk.storage.aset("app.name", "MyApp")
value = await sdk.storage.aget("app.name")
keys = await sdk.storage.aget_all_keys()
# Table operations
await sdk.storage.aCreateTable("users", {
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"name": "TEXT NOT NULL",
})
rows = await sdk.storage.Table("users").Select("name").ToDict().aExecute()
# Asynchronous transactions
async with sdk.storage.atransaction():
await sdk.storage.aset("key1", "value1")
await sdk.storage.Table("users").Insert({"name": "Alice"}).aExecute()
Synchronous APIs (get/set/transaction/Table(...).Execute()) are still available, internally executed via the AsyncBridge background event loop bridge. Calling them in asynchronous handlers will briefly block the event loop, and a one-time warning will be output on the first call ("It is recommended to use asynchronous methods with the 'a' prefix"), and asynchronous methods with the 'a' prefix are recommended. Two boundaries must be known:
- Synchronous interfaces cannot be called again within the bridged thread: Synchronous interfaces are executed within the bridged thread, and calling them again will throw a
RuntimeError(reentrancy protection to avoid self-deadlock). - Calling after the bridged target loop is closed will fail: After
uninit(), do not call storage interfaces again.
See SQL Query Builder for a complete method comparison.
Dialect Behavior Differences
All dialect differences are encapsulated within the framework, so calling code does not need to be aware:
| Difference | SQLite | MySQL | PostgreSQL |
|---|---|---|---|
| Placeholder | ? |
%s (automatically translated) |
$1..$n (automatically translated) |
| KV UPSERT | INSERT OR REPLACE |
ON DUPLICATE KEY UPDATE |
ON CONFLICT DO UPDATE |
| KV Value Column Type | TEXT |
LONGTEXT |
TEXT |
| Auto-increment Primary Key | Native support | AUTO_INCREMENT (automatically translated) |
SERIAL (automatically translated) |
Table column types are defined uniformly in SQLite style (e.g., "INTEGER PRIMARY KEY AUTOINCREMENT", "TEXT NOT NULL", "DOUBLE DEFAULT 0.0"), with dialects automatically translating these into equivalent syntax for the target backend.
Custom SQL Backend
For pure SQL backends, you can inherit and share the base class, providing only connection management and dialect execution funnel:
from ErisPulse.Core.Bases.sql_base import SQLDialect, SQLStorageBase
class MyDialect(SQLDialect):
name = "mydb"
# Override: placeholder translation / identifier quoting / UPSERT / type mapping ...
class MyStorage(SQLStorageBase):
dialect = MyDialect()
async def _create_loop_resource(self): ... # Connection pool
async def _destroy_loop_resource(self, r): ...
async def _acquire_resource_conn(self, r): ...
async def _release_resource_conn(self, r, c): ...
async def _open_txn_conn(self): ...
async def _close_txn_conn(self, c): ...
async def _exec_query_on(self, kind, sql, params, conn): ... # Execution funnel
For non-SQL backends (e.g., Redis), inherit BaseStorage to implement asynchronous KV interface, then directly use KVQueryBuilder to gain chainable table query capabilities (in-memory filtering, suitable for small to medium datasets).
Related Documentation
- SQL Query Builder - Chainable query syntax and asynchronous API comparison
- Core Module API - Full API for the Storage module
- Storage Base Class API - Abstract interfaces for SQLStorageBase / SQLDialect