
Security News
minimatch Patches 3 High-Severity ReDoS Vulnerabilities
minimatch patched three high-severity ReDoS vulnerabilities that can stall the Node.js event loop, and Socket has released free certified patches.
dotorm
Advanced tools
High-performance async ORM for Python with PostgreSQL, MySQL and ClickHouse support
Simple, Fast, Type-safe
| Feature | Description |
|---|---|
| π Async-first | Fully async/await based on asyncpg, aiomysql, asynch |
| π― Type Safety | Full Python 3.12+ type support with generics |
| π Relations | Many2One, One2Many, Many2Many, One2One |
| π‘οΈ Security | Parameterized queries, SQL injection protection |
| π¦ Batch Operations | Optimized bulk create/update/delete |
| πΎ Support Transaction | Support async transaction |
| π« N+1 Solution | Built-in relation loading optimization |
| π Multi-DB | PostgreSQL, MySQL, ClickHouse |
| π DDL | Automatic table creation and migration |
# Basic installation
pip install dotorm
# With PostgreSQL support
pip install dotorm[postgres]
# With MySQL support
pip install dotorm[mysql]
# With ClickHouse support
pip install dotorm[clickhouse]
# All drivers
pip install dotorm[all]
# requirements.txt
asyncpg>=0.29.0 # PostgreSQL
aiomysql>=0.2.0 # MySQL
asynch>=0.2.3 # ClickHouse
pydantic>=2.0.0 # Validation
from dotorm import DotModel, Integer, Char, Boolean, Many2one, One2many
from dotorm.components import POSTGRES
class Role(DotModel):
__table__ = "roles"
_dialect = POSTGRES
id: int = Integer(primary_key=True)
name: str = Char(max_length=100, required=True)
description: str = Char(max_length=255)
class User(DotModel):
__table__ = "users"
_dialect = POSTGRES
id: int = Integer(primary_key=True)
name: str = Char(max_length=100, required=True)
email: str = Char(max_length=255, unique=True)
active: bool = Boolean(default=True)
role_id: Role = Many2one(lambda: Role)
class Role(DotModel):
# ... fields above ...
users: list[User] = One2many(lambda: User, "role_id")
from dotorm.databases.postgres import ContainerPostgres
from dotorm.databases.abstract import PostgresPoolSettings, ContainerSettings
# Connection settings
pool_settings = PostgresPoolSettings(
host="localhost",
port=5432,
user="postgres",
password="password",
database="myapp"
)
container_settings = ContainerSettings(
driver="asyncpg",
reconnect_timeout=10
)
# Create connection pool
container = ContainerPostgres(pool_settings, container_settings)
pool = await container.create_pool()
# Bind pool to models
User._pool = pool
User._no_transaction = container.get_no_transaction_session()
Role._pool = pool
Role._no_transaction = container.get_no_transaction_session()
# Automatic table creation with FK
await container.create_and_update_tables([Role, User])
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# CREATE - Creating records
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Single create
user = User(name="John", email="john@example.com", role_id=1)
user_id = await User.create(user)
print(f"Created user with ID: {user_id}")
# Bulk create
users = [
User(name="Alice", email="alice@example.com"),
User(name="Bob", email="bob@example.com"),
User(name="Charlie", email="charlie@example.com"),
]
created_ids = await User.create_bulk(users)
print(f"Created {len(created_ids)} users")
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# READ - Reading records
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Get by ID
user = await User.get(1)
print(f"User: {user.name}")
# Get with field selection
user = await User.get(1, fields=["id", "name", "email"])
# Search with filtering
active_users = await User.search(
fields=["id", "name", "email"],
filter=[("active", "=", True)],
order="ASC",
sort="name",
limit=10
)
# Complex filters
users = await User.search(
fields=["id", "name"],
filter=[
("active", "=", True),
"and",
[
("name", "ilike", "john"),
"or",
("email", "like", "@gmail.com")
]
]
)
# Pagination
page_1 = await User.search(fields=["id", "name"], start=0, end=20)
page_2 = await User.search(fields=["id", "name"], start=20, end=40)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# UPDATE - Updating records
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Update single record
user = await User.get(1)
user.name = "New Name"
await user.update()
# Update with payload
user = await User.get(1)
payload = User(name="Updated Name", active=False)
await user.update(payload, fields=["name", "active"])
# Bulk update
await User.update_bulk(
ids=[1, 2, 3],
payload=User(active=False)
)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# DELETE - Deleting records
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Delete single record
user = await User.get(1)
await user.delete()
# Bulk delete
await User.delete_bulk([4, 5, 6])
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Many2One - Many to One
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Get user with role
user = await User.get_with_relations(
id=1,
fields=["id", "name", "role_id"]
)
print(f"User: {user.name}, Role: {user.role_id.name}")
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# One2Many - One to Many
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Get role with all users
role = await Role.get_with_relations(
id=1,
fields=["id", "name", "users"],
fields_info={"users": ["id", "name", "email"]}
)
print(f"Role: {role.name}")
for user in role.users["data"]:
print(f" - {user.name}")
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Many2Many - Many to Many
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class Tag(DotModel):
__table__ = "tags"
_dialect = POSTGRES
id: int = Integer(primary_key=True)
name: str = Char(max_length=50)
class Article(DotModel):
__table__ = "articles"
_dialect = POSTGRES
id: int = Integer(primary_key=True)
title: str = Char(max_length=200)
tags: list[Tag] = Many2many(
relation_table=lambda: Tag,
many2many_table="article_tags",
column1="tag_id",
column2="article_id"
)
# Get article with tags
article = await Article.get_with_relations(
id=1,
fields=["id", "title", "tags"]
)
# Link tags to article
await Article.link_many2many(
field=Article.tags,
values=[(article.id, 1), (article.id, 2), (article.id, 3)]
)
# Unlink tags
await Article.unlink_many2many(
field=Article.tags,
ids=[1, 2]
)
from dotorm.databases.postgres import ContainerTransaction
async with ContainerTransaction(pool) as session:
# All operations in single transaction
role_id = await Role.create(
Role(name="Admin"),
session=session
)
user_id = await User.create(
User(name="Admin User", role_id=role_id),
session=session
)
# Auto commit on exit
# Auto rollback on exception
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Supported Operators
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Comparison
filter=[("age", "=", 25)]
filter=[("age", "!=", 25)]
filter=[("age", ">", 18)]
filter=[("age", ">=", 18)]
filter=[("age", "<", 65)]
filter=[("age", "<=", 65)]
# String search
filter=[("name", "like", "John")] # %John%
filter=[("name", "ilike", "john")] # case-insensitive
filter=[("name", "not like", "test")]
# IN / NOT IN
filter=[("status", "in", ["active", "pending"])]
filter=[("id", "not in", [1, 2, 3])]
# NULL checks
filter=[("deleted_at", "is null", None)]
filter=[("email", "is not null", None)]
# BETWEEN
filter=[("created_at", "between", ["2024-01-01", "2024-12-31"])]
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Logical Operators
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# AND (default between conditions)
filter=[
("active", "=", True),
("verified", "=", True)
]
# OR
filter=[
("role", "=", "admin"),
"or",
("role", "=", "moderator")
]
# Nested conditions
filter=[
("active", "=", True),
"and",
[
("role", "=", "admin"),
"or",
("role", "=", "superuser")
]
]
# NOT
filter=[
("not", ("deleted", "=", True))
]
# β BAD: N+1 queries
users = await User.search(fields=["id", "name", "role_id"], limit=100)
for user in users:
# Each call = new DB query!
role = await Role.get(user.role_id)
print(f"{user.name} - {role.name}")
# Total: 1 + 100 = 101 queries!
# β
GOOD: 2 queries instead of 101
users = await User.search(
fields=["id", "name", "role_id"], # role_id is Many2one
limit=100
)
# DotORM automatically:
# 1. Loads all users (1 query)
# 2. Collects unique role_ids
# 3. Loads all roles in one query (1 query)
# 4. Maps roles to users in memory
for user in users:
print(f"{user.name} - {user.role_id.name}") # No additional queries!
# β
GOOD: Optimized M2M loading
articles = await Article.search(
fields=["id", "title", "tags"],
limit=50
)
# DotORM executes:
# 1. SELECT * FROM articles LIMIT 50
# 2. SELECT tags.*, article_tags.article_id as m2m_id
# FROM tags
# JOIN article_tags ON tags.id = article_tags.tag_id
# WHERE article_tags.article_id IN (1, 2, 3, ..., 50)
# Total: 2 queries!
# β
GOOD: Optimized O2M loading
roles = await Role.search(
fields=["id", "name", "users"],
limit=10
)
# DotORM executes:
# 1. SELECT * FROM roles LIMIT 10
# 2. SELECT * FROM users WHERE role_id IN (1, 2, 3, ..., 10)
# Total: 2 queries!
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ORM Layer β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β search() method β β
β β 1. Execute main query β β
β β 2. Collect relation field IDs β β
β β 3. Call _records_list_get_relation() β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β _records_list_get_relation() β β
β β 1. Build optimized queries for all relation types β β
β β 2. Execute queries in parallel (asyncio.gather) β β
β β 3. Map results back to parent records β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Builder Layer β β
β β build_search_relation() - builds batch queries β β
β β βββββββββββββββ¬ββββββββββββββ¬ββββββββββββββ β β
β β β Many2One β One2Many β Many2Many β β β
β β β IN clause β IN clause β JOIN query β β β
β β βββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Scenario | Naive Approach | DotORM |
|---|---|---|
| 100 users + roles (M2O) | 101 queries | 2 queries |
| 50 articles + tags (M2M) | 51 queries | 2 queries |
| 10 roles + users (O2M) | 11 queries | 2 queries |
| Combined | 162 queries | 4 queries |
| ORM | Time (ms) | Queries | Relative |
|---|---|---|---|
| DotORM | 45 | 1 | 1.0x |
| SQLAlchemy 2.0 | 120 | 1000 | 2.7x |
| Tortoise ORM | 89 | 1 | 2.0x |
| databases + raw SQL | 42 | 1 | 0.9x |
# DotORM - bulk insert
users = [User(name=f"User {i}", email=f"user{i}@test.com") for i in range(1000)]
await User.create_bulk(users) # 1 query
| ORM | Time (ms) | Memory (MB) | Relative |
|---|---|---|---|
| DotORM | 12 | 8.2 | 1.0x |
| SQLAlchemy 2.0 | 28 | 15.4 | 2.3x |
| Tortoise ORM | 22 | 12.1 | 1.8x |
| databases + raw SQL | 10 | 6.5 | 0.8x |
| ORM | Time (ms) | Queries | Relative |
|---|---|---|---|
| DotORM | 18 | 2 | 1.0x |
| SQLAlchemy (lazy) | 1250 | 1001 | 69x |
| SQLAlchemy (eager) | 35 | 1 | 1.9x |
| Tortoise ORM | 45 | 2 | 2.5x |
| ORM | Time (ms) | Queries | Relative |
|---|---|---|---|
| DotORM | 38 | 1 | 1.0x |
| SQLAlchemy 2.0 | 95 | 1000 | 2.5x |
| Tortoise ORM | 78 | 1 | 2.1x |
INSERT 1000 records (lower is better)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
DotORM ββββββββββββββββββββββββββββββββ 45ms
Tortoise ββββββββββββββββββββββββββββββββ 89ms
SQLAlchemy ββββββββββββββββββββββββββββββββ 120ms
SELECT 1000 records with M2O relation
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
DotORM ββββββββββββββββββββββββββββββββ 18ms (2 queries)
SQLAlchemy eagerββββββββββββββββββββββββββββββββ 35ms (1 query)
Tortoise ββββββββββββββββββββββββββββββββ 45ms (2 queries)
SQLAlchemy lazy ββββββββββββββββββββββββββββββββ 1250ms (1001 queries)
# Install benchmark dependencies
pip install pytest-benchmark memory_profiler
# Run all benchmarks
python -m pytest benchmarks/ -v --benchmark-only
# Run specific benchmark
python -m pytest benchmarks/test_insert.py -v
# With memory profiling
python -m memory_profiler benchmarks/memory_test.py
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Application Layer β
β (FastAPI, Django, Flask, etc.) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DotORM β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Model Layer β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β β β DotModel β β Fields β β Pydantic β β β
β β β (Base ORM) β β (Type Def) β β (Validation) β β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β ORM Layer β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β β β PrimaryMixin β β Many2Many β β Relations β β β
β β β (CRUD ops) β β Mixin β β Mixin β β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β β ββββββββββββββββ β β
β β β DDLMixin β β β
β β β(Table mgmt) β β β
β β ββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Builder Layer β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β β β CRUDMixin β β M2MMixin β β RelationsMix β β β
β β β (SQL CRUD) β β (M2M SQL) β β (Batch SQL) β β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β β ββββββββββββββββ ββββββββββββββββ β β
β β β FilterParser β β Dialect β β β
β β β(WHERE build) β β (DB adapt) β β β
β β ββββββββββββββββ ββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Database Layer β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β β β PostgreSQL β β MySQL β β ClickHouse β β β
β β β asyncpg β β aiomysql β β asynch β β β
β β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ORM Layer β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β DotModel β β
β β (Main Model Class) β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β Class Variables: β β β
β β β β’ __table__: str - Table name β β β
β β β β’ _pool: Pool - Connection pool β β β
β β β β’ _dialect: Dialect - Database dialect β β β
β β β β’ _builder: Builder - SQL builder instance β β β
β β β β’ _no_transaction: Type - Session factory β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β inherits β
β ββββββββββββββββββββΌβββββββββββββββββββ β
β βΌ βΌ βΌ β
β βββββββββββββββββ βββββββββββββββββ βββββββββββββββββ β
β β OrmPrimary β β OrmMany2many β β OrmRelations β β
β β Mixin β β Mixin β β Mixin β β
β βββββββββββββββββ€ βββββββββββββββββ€ βββββββββββββββββ€ β
β β β’ create() β β β’ get_m2m() β β β’ search() β β
β β β’ create_bulk β β β’ link_m2m() β β β’ get_with_ β β
β β β’ get() β β β’ unlink_m2m()β β relations() β β
β β β’ update() β β β’ _records_ β β β’ update_with β β
β β β’ update_bulk β β list_get_ β β _relations()β β
β β β’ delete() β β relation() β β β β
β β β’ delete_bulk β β β β β β
β β β’ table_len() β β β β β β
β βββββββββββββββββ βββββββββββββββββ βββββββββββββββββ β
β β β β β
β ββββββββββββββββββββΌβββββββββββββββββββ β
β βΌ β
β βββββββββββββββββ β
β β DDLMixin β β
β βββββββββββββββββ€ β
β β β’ __create_ β β
β β table__() β β
β β β’ cache() β β
β β β’ format_ β β
β β default() β β
β βββββββββββββββββ β
β β
β Data Flow: β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β User.search() β OrmRelationsMixin.search() β
β β β
β βββ _builder.build_search() # Build SQL β
β βββ session.execute() # Execute query β
β βββ prepare_list_ids() # Deserialize β
β βββ _records_list_get_relation() # Load relations β
β β β
β βββ _builder.build_search_relation() β
β βββ asyncio.gather(*queries) # Parallel execution β
β βββ Map results to records β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Builder Layer β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Builder β β
β β (Main Query Builder) β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β Attributes: β β β
β β β β’ table: str - Target table name β β β
β β β β’ fields: dict[str,Field] - Model fields β β β
β β β β’ dialect: Dialect - SQL dialect config β β β
β β β β’ filter_parser: Parser - WHERE clause builder β β β
β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β inherits β
β ββββββββββββββββββββΌβββββββββββββββββββ β
β βΌ βΌ βΌ β
β βββββββββββββββββ βββββββββββββββββ βββββββββββββββββ β
β β CRUDMixin β β Many2Many β β Relations β β
β β β β Mixin β β Mixin β β
β βββββββββββββββββ€ βββββββββββββββββ€ βββββββββββββββββ€ β
β βbuild_create() β βbuild_get_m2m()β βbuild_search_ β β
β βbuild_create_ β βbuild_get_m2m_ β β relation() β β
β β bulk() β β multiple() β β β β
β βbuild_get() β β β β Returns: β β
β βbuild_search() β β β β List[Request β β
β βbuild_update() β β β β Builder] β β
β βbuild_update_ β β β β β β
β β bulk() β β β β β β
β βbuild_delete() β β β β β β
β βbuild_delete_ β β β β β β
β β bulk() β β β β β β
β βbuild_table_ β β β β β β
β β len() β β β β β β
β βββββββββββββββββ βββββββββββββββββ βββββββββββββββββ β
β β
β Supporting Components: β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β βββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββ β
β β FilterParser β β Dialect β β
β βββββββββββββββββββββββββββββ€ βββββββββββββββββββββββββββββ€ β
β β β’ parse(filter_expr) β β β’ name: str β β
β β β (sql, values) β β β’ escape: str (", `) β β
β β β β β’ placeholder: str ($, %) β β
β β Supports: β β β’ supports_returning: boolβ β
β β β’ =, !=, >, <, >=, <= β β β β
β β β’ like, ilike β β Methods: β β
β β β’ in, not in β β β’ escape_identifier() β β
β β β’ is null, is not null β β β’ make_placeholders() β β
β β β’ between β β β’ make_placeholder() β β
β β β’ and, or, not β β β β
β βββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββ β
β β
β βββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββ β
β β RequestBuilder β β RequestBuilderForm β β
β βββββββββββββββββββββββββββββ€ βββββββββββββββββββββββββββββ€ β
β β Container for relation β β Extended for form view β β
β β query parameters β β with nested fields β β
β β β β β β
β β β’ stmt: str β β Overrides: β β
β β β’ value: tuple β β β’ function_prepare β β
β β β’ field_name: str β β β prepare_form_ids β β
β β β’ field: Field β β β β
β β β’ fields: list[str] β β β β
β β β β β β
β β Properties: β β β β
β β β’ function_cursor β β β β
β β β’ function_prepare β β β β
β βββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββ β
β β
β Query Building Flow: β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β build_search(fields, filter, limit, order, sort) β
β β β
β βββ Validate fields against store_fields β
β βββ Build SELECT clause with escaped identifiers β
β βββ filter_parser.parse(filter) β WHERE clause β
β βββ Add ORDER BY, LIMIT, OFFSET β
β βββ Return (sql_string, values_tuple) β
β β
β Example Output: β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β Input: fields=["id", "name"], filter=[("active", "=", True)] β
β Output: ('SELECT "id", "name" FROM users WHERE "active" = %s β
β ORDER BY id DESC LIMIT %s', (True, 80)) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
dotorm/
βββ __init__.py # Public API exports
βββ model.py # DotModel base class
βββ fields.py # Field type definitions
βββ exceptions.py # Custom exceptions
βββ pydantic.py # Pydantic integration
β
βββ orm/ # ORM Layer
β βββ __init__.py
β βββ protocol.py # Type protocols
β βββ mixins/
β βββ __init__.py
β βββ primary.py # CRUD operations
β βββ many2many.py # M2M operations
β βββ relations.py # Relation loading
β βββ ddl.py # Table management
β
βββ builder/ # Builder Layer
β βββ __init__.py
β βββ builder.py # Main Builder class
β βββ protocol.py # Builder protocol
β βββ helpers.py # SQL helpers
β βββ request_builder.py # Request containers
β βββ mixins/
β βββ __init__.py
β βββ crud.py # CRUD SQL builders
β βββ m2m.py # M2M SQL builders
β βββ relations.py # Relation SQL builders
β
βββ components/ # Shared components
β βββ __init__.py
β βββ dialect.py # Database dialects
β βββ filter_parser.py # Filter expression parser
β
βββ databases/ # Database Layer
βββ abstract/
β βββ __init__.py
β βββ pool.py # Abstract pool
β βββ session.py # Abstract session
β βββ types.py # Settings types
β
βββ postgres/
β βββ __init__.py
β βββ pool.py # PostgreSQL pool
β βββ session.py # PostgreSQL sessions
β βββ transaction.py # Transaction manager
β
βββ mysql/
β βββ __init__.py
β βββ pool.py # MySQL pool
β βββ session.py # MySQL sessions
β βββ transaction.py # Transaction manager
β
βββ clickhouse/
βββ __init__.py
βββ pool.py # ClickHouse pool
βββ session.py # ClickHouse session
# Install test dependencies
pip install pytest pytest-asyncio pytest-cov
# Run all tests
pytest
# Verbose output
pytest -v
# Unit tests only
pytest tests/unit/ -v
# Integration tests only (requires DB)
pytest tests/integration/ -v
# Specific file
pytest tests/unit/test_builder.py -v
# Specific test
pytest tests/unit/test_builder.py::TestCRUDBuilder::test_build_search -v
# Generate coverage report
pytest --cov=dotorm --cov-report=html
# Open report
open htmlcov/index.html
# Console report
pytest --cov=dotorm --cov-report=term-missing
Name Stmts Miss Cover
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
dotorm/__init__.py 45 0 100%
dotorm/model.py 285 38 87%
dotorm/fields.py 198 12 94%
dotorm/exceptions.py 8 0 100%
dotorm/pydantic.py 145 23 84%
dotorm/orm/mixins/primary.py 112 8 93%
dotorm/orm/mixins/many2many.py 89 11 88%
dotorm/orm/mixins/relations.py 156 19 88%
dotorm/orm/mixins/ddl.py 87 15 83%
dotorm/builder/builder.py 28 0 100%
dotorm/builder/mixins/crud.py 124 5 96%
dotorm/builder/mixins/m2m.py 56 3 95%
dotorm/builder/mixins/relations.py 67 8 88%
dotorm/components/dialect.py 52 2 96%
dotorm/components/filter_parser.py 98 4 96%
dotorm/databases/postgres/session.py 89 12 87%
dotorm/databases/postgres/pool.py 67 9 87%
dotorm/databases/mysql/session.py 78 14 82%
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
TOTAL 1784 183 87%
tests/
βββ conftest.py # Pytest fixtures
βββ unit/
β βββ test_fields.py # Field type tests
β βββ test_model.py # Model tests
β βββ test_builder.py # Builder tests
β βββ test_filter.py # Filter parser tests
β βββ test_dialect.py # Dialect tests
β
βββ integration/
β βββ test_postgres.py # PostgreSQL integration
β βββ test_mysql.py # MySQL integration
β βββ test_crud.py # CRUD operations
β βββ test_relations.py # Relation loading
β βββ test_transactions.py # Transaction tests
β
βββ benchmarks/
βββ test_insert.py # Insert benchmarks
βββ test_select.py # Select benchmarks
βββ memory_test.py # Memory profiling
# tests/unit/test_builder.py
import pytest
from dotorm.builder import Builder
from dotorm.components import POSTGRES
from dotorm.fields import Integer, Char, Boolean
class TestCRUDBuilder:
@pytest.fixture
def builder(self):
fields = {
"id": Integer(primary_key=True),
"name": Char(max_length=100),
"email": Char(max_length=255),
"active": Boolean(default=True),
}
return Builder(table="users", fields=fields, dialect=POSTGRES)
def test_build_search(self, builder):
"""Test SELECT query building."""
stmt, values = builder.build_search(
fields=["id", "name"],
filter=[("active", "=", True)],
limit=10,
order="ASC",
sort="name"
)
assert "SELECT" in stmt
assert '"id"' in stmt
assert '"name"' in stmt
assert "FROM users" in stmt
assert "WHERE" in stmt
assert "ORDER BY name ASC" in stmt
assert "LIMIT" in stmt
assert values == (True, 10)
def test_build_create(self, builder):
"""Test INSERT query building."""
payload = {"name": "John", "email": "john@example.com"}
stmt, values = builder.build_create(payload)
assert "INSERT INTO users" in stmt
assert "name" in stmt
assert "email" in stmt
assert "VALUES" in stmt
assert values == ("John", "john@example.com")
def test_build_create_bulk(self, builder):
"""Test bulk INSERT."""
payloads = [
{"name": "John", "email": "john@example.com"},
{"name": "Jane", "email": "jane@example.com"},
]
stmt, all_values = builder.build_create_bulk(payloads)
assert "INSERT INTO users" in stmt
assert "(name, email)" in stmt
assert len(all_values) == 4
assert all_values == ["John", "john@example.com", "Jane", "jane@example.com"]
| Field | Python Type | SQL Type (PG) | Description |
|---|---|---|---|
Integer | int | INTEGER | 32-bit integer |
BigInteger | int | BIGINT | 64-bit integer |
SmallInteger | int | SMALLINT | 16-bit integer |
Char | str | VARCHAR(n) | String with max length |
Text | str | TEXT | Unlimited text |
Boolean | bool | BOOL | True/False |
Float | float | DOUBLE PRECISION | Floating point |
Decimal | Decimal | DECIMAL(p,s) | Precise decimal |
Date | date | DATE | Date only |
Time | time | TIME | Time only |
Datetime | datetime | TIMESTAMPTZ | Date and time |
JSONField | dict/list | JSONB | JSON data |
Binary | bytes | BYTEA | Binary data |
Many2one | Model | INTEGER | FK relation |
One2many | list[Model] | - | Reverse FK |
Many2many | list[Model] | - | M2M relation |
One2one | Model | - | 1:1 relation |
Field(
primary_key=False, # Is primary key?
null=True, # Allow NULL?
required=False, # Required (sets null=False)?
unique=False, # Unique constraint?
index=False, # Create index?
default=None, # Default value
description=None, # Field description
store=True, # Store in DB?
compute=None, # Compute function
)
| Method | Description | Returns |
|---|---|---|
create(payload) | Create single record | int (ID) |
create_bulk(payloads) | Create multiple records | list[dict] |
get(id, fields) | Get by ID | Model | None |
search(...) | Search with filters | list[Model] |
table_len() | Count records | int |
get_with_relations(...) | Get with relations | Model | None |
get_many2many(...) | Get M2M related | list[Model] |
link_many2many(...) | Create M2M links | None |
unlink_many2many(...) | Remove M2M links | None |
__create_table__() | Create DB table | list[str] |
| Method | Description | Returns |
|---|---|---|
update(payload, fields) | Update record | None |
delete() | Delete record | None |
json(...) | Serialize to dict | dict |
update_with_relations(...) | Update with relations | dict |
Python Backend Developer | ORM Enthusiast | Open Source Contributor
We welcome contributions to the project!
# Fork the repository, then:
git clone https://github.com/YOUR_USERNAME/dotorm.git
cd dotorm
# Create virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
# or
.\venv\Scripts\activate # Windows
# Install dev dependencies
pip install -e ".[dev]"
# Create feature branch
git checkout -b feature/amazing-feature
# After changes
pytest # Run tests
black dotorm/ # Format code
mypy dotorm/ # Type check
# Commit and PR
git commit -m "feat: add amazing feature"
git push origin feature/amazing-feature
MIT License
Copyright (c) 2024 Artem Shurshilov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
β If you find this project useful, give it a star! β
Made with β€οΈ by Artem Shurshilov
FAQs
Async Python ORM for PostgreSQL, MySQL and ClickHouse with dot-notation access
We found that dotorm demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Β It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
minimatch patched three high-severity ReDoS vulnerabilities that can stall the Node.js event loop, and Socket has released free certified patches.

Research
/Security News
Socket uncovered 26 malicious npm packages tied to North Korea's Contagious Interview campaign, retrieving a live 9-module infostealer and RAT from the adversary's C2.

Research
An impersonated golang.org/x/crypto clone exfiltrates passwords, executes a remote shell stager, and delivers a Rekoobe backdoor on Linux.