π DotORM
High-performance async ORM for Python with PostgreSQL, MySQL and ClickHouse support
Simple, Fast, Type-safe
π Table of Contents
β¨ Features
| π 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 |
π¦ Installation
pip install dotorm
pip install dotorm[postgres]
pip install dotorm[mysql]
pip install dotorm[clickhouse]
pip install dotorm[all]
Dependencies
# requirements.txt
asyncpg>=0.29.0 # PostgreSQL
aiomysql>=0.2.0 # MySQL
asynch>=0.2.3 # ClickHouse
pydantic>=2.0.0 # Validation
π Quick Start
1. Define Models
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):
users: list[User] = One2many(lambda: User, "role_id")
2. Connect to Database
from dotorm.databases.postgres import ContainerPostgres
from dotorm.databases.abstract import PostgresPoolSettings, ContainerSettings
pool_settings = PostgresPoolSettings(
host="localhost",
port=5432,
user="postgres",
password="password",
database="myapp"
)
container_settings = ContainerSettings(
driver="asyncpg",
reconnect_timeout=10
)
container = ContainerPostgres(pool_settings, container_settings)
pool = await container.create_pool()
User._pool = pool
User._no_transaction = container.get_no_transaction_session()
Role._pool = pool
Role._no_transaction = container.get_no_transaction_session()
3. Create Tables
await container.create_and_update_tables([Role, User])
π Usage Examples
CRUD Operations
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}")
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")
user = await User.get(1)
print(f"User: {user.name}")
user = await User.get(1, fields=["id", "name", "email"])
active_users = await User.search(
fields=["id", "name", "email"],
filter=[("active", "=", True)],
order="ASC",
sort="name",
limit=10
)
users = await User.search(
fields=["id", "name"],
filter=[
("active", "=", True),
"and",
[
("name", "ilike", "john"),
"or",
("email", "like", "@gmail.com")
]
]
)
page_1 = await User.search(fields=["id", "name"], start=0, end=20)
page_2 = await User.search(fields=["id", "name"], start=20, end=40)
user = await User.get(1)
user.name = "New Name"
await user.update()
user = await User.get(1)
payload = User(name="Updated Name", active=False)
await user.update(payload, fields=["name", "active"])
await User.update_bulk(
ids=[1, 2, 3],
payload=User(active=False)
)
user = await User.get(1)
await user.delete()
await User.delete_bulk([4, 5, 6])
Working with Relations
user = await User.get_with_relations(
id=1,
fields=["id", "name", "role_id"]
)
print(f"User: {user.name}, Role: {user.role_id.name}")
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}")
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"
)
article = await Article.get_with_relations(
id=1,
fields=["id", "title", "tags"]
)
await Article.link_many2many(
field=Article.tags,
values=[(article.id, 1), (article.id, 2), (article.id, 3)]
)
await Article.unlink_many2many(
field=Article.tags,
ids=[1, 2]
)
Transactions
from dotorm.databases.postgres import ContainerTransaction
async with ContainerTransaction(pool) as session:
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
)
Filters
filter=[("age", "=", 25)]
filter=[("age", "!=", 25)]
filter=[("age", ">", 18)]
filter=[("age", ">=", 18)]
filter=[("age", "<", 65)]
filter=[("age", "<=", 65)]
filter=[("name", "like", "John")]
filter=[("name", "ilike", "john")]
filter=[("name", "not like", "test")]
filter=[("status", "in", ["active", "pending"])]
filter=[("id", "not in", [1, 2, 3])]
filter=[("deleted_at", "is null", None)]
filter=[("email", "is not null", None)]
filter=[("created_at", "between", ["2024-01-01", "2024-12-31"])]
filter=[
("active", "=", True),
("verified", "=", True)
]
filter=[
("role", "=", "admin"),
"or",
("role", "=", "moderator")
]
filter=[
("active", "=", True),
"and",
[
("role", "=", "admin"),
"or",
("role", "=", "superuser")
]
]
filter=[
("not", ("deleted", "=", True))
]
β‘ Solving the N+1 Problem
The N+1 Problem
users = await User.search(fields=["id", "name", "role_id"], limit=100)
for user in users:
role = await Role.get(user.role_id)
print(f"{user.name} - {role.name}")
DotORM Solution
1. Automatic Relation Loading in search()
users = await User.search(
fields=["id", "name", "role_id"],
limit=100
)
for user in users:
print(f"{user.name} - {user.role_id.name}")
2. Batch Loading for Many2Many
articles = await Article.search(
fields=["id", "title", "tags"],
limit=50
)
3. Batch Loading for One2Many
roles = await Role.search(
fields=["id", "name", "users"],
limit=10
)
N+1 Solution Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 β β β
β β βββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Query Count Comparison
| 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 |
π Benchmarks
Testing Methodology
- Hardware: AMD Ryzen 7 5800X, 32GB RAM, NVMe SSD
- Database: PostgreSQL 16, local
- Python: 3.12.0
- Data: 100,000 records in users table
- Measurements: Average of 100 iterations
Comparison with Other ORMs
INSERT (1000 records)
| 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 |
users = [User(name=f"User {i}", email=f"user{i}@test.com") for i in range(1000)]
await User.create_bulk(users)
SELECT (1000 records)
| 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 |
SELECT with JOIN (M2O, 1000 records)
| DotORM | 18 | 2 | 1.0x |
| SQLAlchemy (lazy) | 1250 | 1001 | 69x |
| SQLAlchemy (eager) | 35 | 1 | 1.9x |
| Tortoise ORM | 45 | 2 | 2.5x |
UPDATE (1000 records)
| DotORM | 38 | 1 | 1.0x |
| SQLAlchemy 2.0 | 95 | 1000 | 2.5x |
| Tortoise ORM | 78 | 1 | 2.1x |
Performance Chart
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)
Running Benchmarks
pip install pytest-benchmark memory_profiler
python -m pytest benchmarks/ -v --benchmark-only
python -m pytest benchmarks/test_insert.py -v
python -m memory_profiler benchmarks/memory_test.py
ποΈ Architecture
Overall Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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 Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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)) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
File Structure
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
π§ͺ Testing
Running Tests
pip install pytest pytest-asyncio pytest-cov
pytest
pytest -v
pytest tests/unit/ -v
pytest tests/integration/ -v
pytest tests/unit/test_builder.py -v
pytest tests/unit/test_builder.py::TestCRUDBuilder::test_build_search -v
Test Coverage
pytest --cov=dotorm --cov-report=html
open htmlcov/index.html
pytest --cov=dotorm --cov-report=term-missing
Current Coverage
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%
Test Structure
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
Example Test
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"]
π API Reference
Fields
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 Parameters
Field(
primary_key=False,
null=True,
required=False,
unique=False,
index=False,
default=None,
description=None,
store=True,
compute=None,
)
Model Class Methods
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] |
Model Instance Methods
update(payload, fields) | Update record | None |
delete() | Delete record | None |
json(...) | Serialize to dict | dict |
update_with_relations(...) | Update with relations | dict |
π€ Author
ΠΡΡΡΠΌ Π¨ΡΡΡΠΈΠ»ΠΎΠ²
Python Backend Developer | ORM Enthusiast | Open Source Contributor
π€ Contributing
We welcome contributions to the project!
git clone https://github.com/YOUR_USERNAME/dotorm.git
cd dotorm
python -m venv venv
source venv/bin/activate
.\venv\Scripts\activate
pip install -e ".[dev]"
git checkout -b feature/amazing-feature
pytest
black dotorm/
mypy dotorm/
git commit -m "feat: add amazing feature"
git push origin feature/amazing-feature
π License
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