SQL Query Builder
The Storage module in ErisPulse provides a chain-call style generic SQL query builder, supporting custom table creation, querying, updating, and deleting operations.
Architecture Design
Bases/storage.py Core/storage.py
┌─────────────────────┐ ┌──────────────────────────┐
│ BaseStorage (ABC) │◄────────────│ StorageManager │
│ BaseQueryBuilder │ │ (SQLite concrete impl) │
│ (ABC) │ │ │
└─────────────────────┘ │ SQLiteQueryBuilder │
│ AlterTableBuilder │
└──────────────────────────┘
BaseStorage/BaseQueryBuilderare abstract base classes that define a unified interface, supporting future expansion to other storage media (Redis, MySQL, etc.)StorageManageris the current SQLite concrete implementation, fully backward compatible.
Importing
from ErisPulse import sdk
# or
from ErisPulse.Core import storage
# ABC base classes (for type annotation or custom implementation)
from ErisPulse.Core.Bases.storage import BaseStorage, BaseQueryBuilder
Table Management
Creating a Table
sdk.storage.CreateTable("users", {
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
"name": "TEXT NOT NULL",
"age": "INTEGER DEFAULT 0",
"email": "TEXT"
})
Checking if a Table Exists
if sdk.storage.HasTable("users"):
print("users table exists")
Dropping a Table
sdk.storage.DropTable("users")
Modifying Table Structure
# Adding a column
sdk.storage.AlterTable("users").AddColumn("email", "TEXT").Execute()
# Renaming a table
sdk.storage.AlterTable("users").RenameTo("members").Execute()
# Chaining multiple operations
sdk.storage.AlterTable("users") \
.AddColumn("phone", "TEXT") \
.AddColumn("address", "TEXT") \
.Execute()
Chainable Queries
Inserting Data
# Single row insertion (passing a dictionary)
sdk.storage.Table("users").Insert({"name": "Alice", "age": 30}).Execute()
# Batch insertion (passing a list of dictionaries)
sdk.storage.Table("users").InsertMulti([
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 35},
{"name": "Dave", "age": 40}
]).Execute()
Querying Data
Important:
Select()returns alist[tuple](list of tuples), not a dictionary. You need to access values by column index.
# Select all columns
rows = sdk.storage.Table("users").Select().Execute()
# rows: [(1, "Alice", 30), (2, "Bob", 25), ...]
# Select specific columns
rows = sdk.storage.Table("users").Select("name", "age").Execute()
# rows: [("Alice", 30), ("Bob", 25), ...]
# Access values by index
for row in rows:
name = row[0] # "Alice"
age = row[1] # 30
Converting Tuples to Dictionaries
It is recommended to call ToDict() directly on the chain; the SELECT result will automatically be returned as a dictionary (column name → value):
# ToDict chain: result is list[dict], column names are automatically taken from query metadata (SELECT * is also supported)
rows = sdk.storage.Table("users").Select("name", "age").ToDict().Execute()
# rows: [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}, ...]
for row in rows:
print(row["name"], row["age"])
# ExecuteOne also works
row = sdk.storage.Table("users").Select("name", "age") \
.Where("id = ?", 1) \
.ToDict() \
.ExecuteOne()
# row: {"name": "Alice", "age": 30} or None
ToDict()is a chainable marker (returns self): chains without calling it retain the originallist[tuple]behavior, fully backward compatible;copy()will preserve this flag.
Manual zip method (equivalent to ToDict, suitable for scenarios where the chain cannot be modified):
columns = ["id", "name", "age"]
rows = sdk.storage.Table("users").Select(*columns).Execute()
# Method 1: zip in loop
for row in rows:
record = dict(zip(columns, row))
print(record["name"], record["age"])
# Method 2: convert to list of dictionaries at once
records = [dict(zip(columns, row)) for row in rows]
Getting a Single Record
row = sdk.storage.Table("users").Select("name", "age") \
.Where("id = ?", 1) \
.ExecuteOne()
# row is a tuple or None
if row is not None:
name = row[0] # "Alice"
age = row[1] # 30
Filtering Conditions
Where(condition, *params)supports passing multiple parameters, corresponding to multiple?placeholders.
# Single condition (one placeholder, one parameter)
rows = sdk.storage.Table("users").Select("name") \
.Where("age > ?", 18) \
.Execute()
# Using multiple placeholders in one Where
rows = sdk.storage.Table("users").Select("name") \
.Where("age > ? AND age < ?", 20, 40) \
.Execute()
# Multiple Where calls (AND connected)
rows = sdk.storage.Table("users").Select("name") \
.Where("age > ?", 20) \
.Where("age < ?", 40) \
.Execute()
Sorting and Pagination
# Ascending order
rows = sdk.storage.Table("users").Select("name", "age") \
.OrderBy("name") \
.Execute()
# Descending order
rows = sdk.storage.Table("users").Select("name") \
.OrderBy("age", desc=True) \
.Execute()
# Pagination
rows = sdk.storage.Table("users").Select("name") \
.OrderBy("id") \
.Limit(10) \
.Offset(20) \
.Execute()
Updating Data
# Conditional update
sdk.storage.Table("users") \
.Update({"age": 31}) \
.Where("name = ?", "Alice") \
.Execute()
# Full update
sdk.storage.Table("users") \
.Update({"status": "active"}) \
.Execute()
Deleting Data
# Conditional delete
sdk.storage.Table("users") \
.Delete() \
.Where("name = ?", "Bob") \
.Execute()
# Full delete
sdk.storage.Table("users").Delete().Execute()
Counting and Existence Check
# Count
count = sdk.storage.Table("users").Count()
count = sdk.storage.Table("users").Where("age > ?", 18).Count()
# Existence check
exists = sdk.storage.Table("users").Where("name = ?", "Alice").Exists()
Reusing Query Conditions
Use copy() to deep copy the builder and reuse base conditions:
base = sdk.storage.Table("users").Where("age > ?", 20)
# Query based on the same condition
rows = base.copy().Select("name").OrderBy("name").Limit(5).Execute()
# Count based on the same condition
count = base.copy().Count()
# Existence check based on the same condition
exists = base.copy().Where("name = ?", "Alice").Exists()
Resetting the Builder
builder = sdk.storage.Table("users").Select("name").Where("age > ?", 18)
builder.clear()
# Rebuild the query
builder.Select("name", "age").Where("name = ?", "Alice")
rows = builder.Execute()
Using in Transactions
Chainable operations fully support transactions:
# Commit transaction
with sdk.storage.transaction():
sdk.storage.Table("users").Insert({"name": "Eve", "age": 22}).Execute()
sdk.storage.Table("users").Update({"age": 23}).Where("name = ?", "Eve").Execute()
# Rollback example
try:
with sdk.storage.transaction():
sdk.storage.Table("users").Delete().Where("name = ?", "Alice").Execute()
raise Exception("force rollback")
except Exception:
pass
# Alice's record still exists
Asynchronous Native API
Starting from version 2.8.0, the storage layer uses asynchronous operations as the native primary interface. All terminating methods have corresponding asynchronous versions with an a prefix. It is recommended to use these in asynchronous handlers (to avoid temporary blocking of the event loop due to synchronous compatibility layers):
# Asynchronous transaction
async with sdk.storage.atransaction():
await sdk.storage.aset("key1", "value1")
await sdk.storage.aset("key2", {"nested": True})
# Asynchronous chainable query
rows = await sdk.storage.Table("users").Select("name", "age").ToDict().aExecute()
row = await sdk.storage.Table("users").Select("*").Where("id = ?", 1).aExecuteOne()
total = await sdk.storage.Table("users").Where("age > ?", 18).aCount()
exists = await sdk.storage.Table("users").Where("name = ?", "Alice").aExists()
# Asynchronous KV operations
await sdk.storage.aset("app.name", "MyApp")
value = await sdk.storage.aget("app.name")
keys = await sdk.storage.aget_all_keys()
| Synchronous (Compatibility Layer) | Asynchronous Native |
|---|---|
get / set / delete |
aget / aset / adelete |
get_all_keys / clear |
aget_all_keys / aclear |
get_multi / set_multi / delete_multi |
aget_multi / aset_multi / adelete_multi |
transaction() |
atransaction() |
CreateTable / DropTable / HasTable |
aCreateTable / aDropTable / aHasTable |
Execute / ExecuteOne / Count / Exists |
aExecute / aExecuteOne / aCount / aExists |
Return Value Description
| Operation | Return Type | Description |
|---|---|---|
Select().Execute() |
list[tuple] |
List of tuples, ordered by column |
Select().ExecuteOne() |
tuple | None |
Single tuple or None |
Insert().Execute() |
int |
Number of affected rows |
InsertMulti().Execute() |
int |
Number of inserted rows |
Update().Execute() |
int |
Number of affected rows |
Delete().Execute() |
int |
Number of affected rows |
Count() |
int |
Number of matching rows |
Exists() |
bool |
Whether exists |
Example of Return Value Handling
# Select returns tuples, access by index
rows = sdk.storage.Table("users").Select("name", "age").Execute()
first_name = rows[0][0] # First row, first column (name)
first_age = rows[0][1] # First row, second column (age)
# Recommended: Use column name list + zip to convert to dictionary, code is more readable
cols = ["name", "age"]
rows = sdk.storage.Table("users").Select(*cols).Execute()
for row in rows:
d = dict(zip(cols, row))
print(d["name"], d["age"])
# ExecuteOne returns a single tuple or None
row = sdk.storage.Table("users").Select("name").Where("id = ?", 1).ExecuteOne()
name = row[0] if row else None
# Insert/Update/Delete returns number of affected rows
affected = sdk.storage.Table("users").Delete().Where("age < ?", 18).Execute()
print(f"Deleted {affected} records")
Parameterized Queries
All WHERE parameters use ? placeholders, and parameters are passed as subsequent arguments to Where() (not as a tuple or list):
# Correct ✓ — multiple parameters passed individually
sdk.storage.Table("users").Where("age > ? AND name = ?", 18, "Alice").Execute()
# Correct ✓ — multiple Where calls
sdk.storage.Table("users").Where("age > ?", 18).Where("name = ?", "Alice").Execute()
# Incorrect ✗ — do not pass a tuple
sdk.storage.Table("users").Where("age > ? AND name = ?", (18, "Alice")).Execute()
# This will treat the entire tuple as the value for the first placeholder
# Incorrect ✗ — SQL injection risk exists
sdk.storage.Table("users").Where(f"name = '{user_input}'").Execute()
Where Parameter Passing Rules
# Where(condition: str, *params: Any)
# params are variable arguments, passed one by one
# Single parameter
.Where("name = ?", "Alice")
# Multiple parameters
.Where("age > ? AND age < ?", 18, 60)
# LIKE query
.Where("name LIKE ?", "A%")
# IN query (need to manually construct placeholders)
.Where("name IN (?, ?, ?)", "Alice", "Bob", "Charlie")
Custom Storage Backend
Starting from version 2.8.0, the abstract layer uses asynchronous methods as the native contract: inherit BaseStorage and implement asynchronous abstract methods. Synchronous get/set/Execute and others are provided automatically by the base class bridge:
from ErisPulse.Core.Bases.storage import BaseStorage, BaseQueryBuilder
class MyQueryBuilder(BaseQueryBuilder):
async def aExecute(self):
# Implement specific execution logic
...
async def aExecuteOne(self):
...
async def aCount(self):
...
async def aExists(self):
...
class MyStorage(BaseStorage):
async def aget(self, key, default=None):
...
async def aset(self, key, value):
...
# Implement other asynchronous abstract methods and transaction connection hook ...
def Table(self, table_name):
return MyQueryBuilder(self, table_name)
Tip
If you do not want to implement transaction connection routing (conn keyword argument), keep the class attribute
_SUPPORTS_CONN_ROUTING = False (default), and transaction functionality is still available (with limited isolation).
Pure SQL backends can directly inherit Core/Bases/sql_base.py's SQLStorageBase +
SQLQueryBuilder, requiring only connection management and dialect execution funnel implementation, see
Storage Backends.
Related Documents
- Storage Backends - sqlite / mysql / postgres backend selection and configuration
- Core Module API - Complete Storage module API
- Storage Base Class API - BaseStorage/BaseQueryBuilder abstract interface
- Message Builder - MessageBuilder chain-call style reference