dotorm 2.0.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. dotorm/__init__.py +87 -0
  2. dotorm/access.py +151 -0
  3. dotorm/builder/__init__.py +0 -0
  4. dotorm/builder/builder.py +72 -0
  5. dotorm/builder/helpers.py +63 -0
  6. dotorm/builder/mixins/__init__.py +11 -0
  7. dotorm/builder/mixins/crud.py +246 -0
  8. dotorm/builder/mixins/m2m.py +110 -0
  9. dotorm/builder/mixins/relations.py +96 -0
  10. dotorm/builder/protocol.py +63 -0
  11. dotorm/builder/request_builder.py +144 -0
  12. dotorm/components/__init__.py +18 -0
  13. dotorm/components/dialect.py +99 -0
  14. dotorm/components/filter_parser.py +195 -0
  15. dotorm/databases/__init__.py +13 -0
  16. dotorm/databases/abstract/__init__.py +25 -0
  17. dotorm/databases/abstract/dialect.py +134 -0
  18. dotorm/databases/abstract/pool.py +10 -0
  19. dotorm/databases/abstract/session.py +67 -0
  20. dotorm/databases/abstract/types.py +36 -0
  21. dotorm/databases/clickhouse/__init__.py +8 -0
  22. dotorm/databases/clickhouse/pool.py +60 -0
  23. dotorm/databases/clickhouse/session.py +100 -0
  24. dotorm/databases/mysql/__init__.py +13 -0
  25. dotorm/databases/mysql/pool.py +69 -0
  26. dotorm/databases/mysql/session.py +128 -0
  27. dotorm/databases/mysql/transaction.py +39 -0
  28. dotorm/databases/postgres/__init__.py +23 -0
  29. dotorm/databases/postgres/pool.py +133 -0
  30. dotorm/databases/postgres/session.py +174 -0
  31. dotorm/databases/postgres/transaction.py +82 -0
  32. dotorm/decorators.py +379 -0
  33. dotorm/exceptions.py +9 -0
  34. dotorm/fields.py +604 -0
  35. dotorm/integrations/__init__.py +0 -0
  36. dotorm/integrations/pydantic.py +275 -0
  37. dotorm/model.py +802 -0
  38. dotorm/orm/__init__.py +15 -0
  39. dotorm/orm/mixins/__init__.py +13 -0
  40. dotorm/orm/mixins/access.py +67 -0
  41. dotorm/orm/mixins/ddl.py +250 -0
  42. dotorm/orm/mixins/many2many.py +175 -0
  43. dotorm/orm/mixins/primary.py +218 -0
  44. dotorm/orm/mixins/relations.py +513 -0
  45. dotorm/orm/protocol.py +147 -0
  46. dotorm/orm/utils.py +39 -0
  47. dotorm-2.0.8.dist-info/METADATA +1240 -0
  48. dotorm-2.0.8.dist-info/RECORD +50 -0
  49. dotorm-2.0.8.dist-info/WHEEL +4 -0
  50. dotorm-2.0.8.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1240 @@
1
+ Metadata-Version: 2.4
2
+ Name: dotorm
3
+ Version: 2.0.8
4
+ Summary: Async Python ORM for PostgreSQL, MySQL and ClickHouse with dot-notation access
5
+ Project-URL: Homepage, https://github.com/shurshilov/dotorm
6
+ Project-URL: Repository, https://github.com/shurshilov/dotorm
7
+ Project-URL: Issues, https://github.com/shurshilov/dotorm/issues
8
+ Author-email: shurshilov <shurshilov.s@yandex.ru>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: async,asyncio,clickhouse,database,mysql,orm,postgresql
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Provides-Extra: all
25
+ Requires-Dist: aiomysql>=0.2.0; extra == 'all'
26
+ Requires-Dist: asynch>=0.2.0; extra == 'all'
27
+ Requires-Dist: asyncpg>=0.29.0; extra == 'all'
28
+ Requires-Dist: pydantic-settings>=2.0.0; extra == 'all'
29
+ Requires-Dist: pydantic>=2.0.0; extra == 'all'
30
+ Provides-Extra: clickhouse
31
+ Requires-Dist: asynch>=0.2.0; extra == 'clickhouse'
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
34
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
35
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
36
+ Provides-Extra: mysql
37
+ Requires-Dist: aiomysql>=0.2.0; extra == 'mysql'
38
+ Provides-Extra: postgres
39
+ Requires-Dist: asyncpg>=0.29.0; extra == 'postgres'
40
+ Provides-Extra: pydantic
41
+ Requires-Dist: pydantic-settings>=2.0.0; extra == 'pydantic'
42
+ Requires-Dist: pydantic>=2.0.0; extra == 'pydantic'
43
+ Description-Content-Type: text/markdown
44
+
45
+ <p align="center">
46
+ <img src="https://img.shields.io/badge/python-3.12+-blue.svg" alt="Python 3.12+">
47
+ <img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License MIT">
48
+ <img src="https://img.shields.io/badge/coverage-87%25-brightgreen.svg" alt="Coverage 87%">
49
+ <img src="https://img.shields.io/badge/version-2.0.0-orange.svg" alt="Version 2.0.0">
50
+ </p>
51
+
52
+ <h1 align="center">🚀 DotORM</h1>
53
+
54
+ <p align="center">
55
+ <b>High-performance async ORM for Python with PostgreSQL, MySQL and ClickHouse support</b>
56
+ </p>
57
+
58
+ <p align="center">
59
+ <i>Simple, Fast, Type-safe</i>
60
+ </p>
61
+
62
+ ---
63
+
64
+ ## 📋 Table of Contents
65
+
66
+ - [✨ Features](#-features)
67
+ - [📦 Installation](#-installation)
68
+ - [🚀 Quick Start](#-quick-start)
69
+ - [📖 Usage Examples](#-usage-examples)
70
+ - [⚡ Solving the N+1 Problem](#-solving-the-n1-problem)
71
+ - [📊 Benchmarks](#-benchmarks)
72
+ - [🏗️ Architecture](#️-architecture)
73
+ - [🧪 Testing](#-testing)
74
+ - [📚 API Reference](#-api-reference)
75
+ - [👤 Author](#-author)
76
+ - [📄 License](#-license)
77
+
78
+ ---
79
+
80
+ ## ✨ Features
81
+
82
+ | Feature | Description |
83
+ |---------|-------------|
84
+ | 🔄 **Async-first** | Fully async/await based on asyncpg, aiomysql, asynch |
85
+ | 🎯 **Type Safety** | Full Python 3.12+ type support with generics |
86
+ | 🔗 **Relations** | Many2One, One2Many, Many2Many, One2One |
87
+ | 🛡️ **Security** | Parameterized queries, SQL injection protection |
88
+ | 📦 **Batch Operations** | Optimized bulk create/update/delete |
89
+ | 💾 **Support Transaction** | Support async transaction |
90
+ | 🚫 **N+1 Solution** | Built-in relation loading optimization |
91
+ | 🔌 **Multi-DB** | PostgreSQL, MySQL, ClickHouse |
92
+ | 🏭 **DDL** | Automatic table creation and migration |
93
+
94
+ ---
95
+
96
+ ## 📦 Installation
97
+
98
+ ```bash
99
+ # Basic installation
100
+ pip install dotorm
101
+
102
+ # With PostgreSQL support
103
+ pip install dotorm[postgres]
104
+
105
+ # With MySQL support
106
+ pip install dotorm[mysql]
107
+
108
+ # With ClickHouse support
109
+ pip install dotorm[clickhouse]
110
+
111
+ # All drivers
112
+ pip install dotorm[all]
113
+ ```
114
+
115
+ ### Dependencies
116
+
117
+ ```txt
118
+ # requirements.txt
119
+ asyncpg>=0.29.0 # PostgreSQL
120
+ aiomysql>=0.2.0 # MySQL
121
+ asynch>=0.2.3 # ClickHouse
122
+ pydantic>=2.0.0 # Validation
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 🚀 Quick Start
128
+
129
+ ### 1. Define Models
130
+
131
+ ```python
132
+ from dotorm import DotModel, Integer, Char, Boolean, Many2one, One2many
133
+ from dotorm.components import POSTGRES
134
+
135
+ class Role(DotModel):
136
+ __table__ = "roles"
137
+ _dialect = POSTGRES
138
+
139
+ id: int = Integer(primary_key=True)
140
+ name: str = Char(max_length=100, required=True)
141
+ description: str = Char(max_length=255)
142
+
143
+ class User(DotModel):
144
+ __table__ = "users"
145
+ _dialect = POSTGRES
146
+
147
+ id: int = Integer(primary_key=True)
148
+ name: str = Char(max_length=100, required=True)
149
+ email: str = Char(max_length=255, unique=True)
150
+ active: bool = Boolean(default=True)
151
+ role_id: Role = Many2one(lambda: Role)
152
+
153
+ class Role(DotModel):
154
+ # ... fields above ...
155
+ users: list[User] = One2many(lambda: User, "role_id")
156
+ ```
157
+
158
+ ### 2. Connect to Database
159
+
160
+ ```python
161
+ from dotorm.databases.postgres import ContainerPostgres
162
+ from dotorm.databases.abstract import PostgresPoolSettings, ContainerSettings
163
+
164
+ # Connection settings
165
+ pool_settings = PostgresPoolSettings(
166
+ host="localhost",
167
+ port=5432,
168
+ user="postgres",
169
+ password="password",
170
+ database="myapp"
171
+ )
172
+
173
+ container_settings = ContainerSettings(
174
+ driver="asyncpg",
175
+ reconnect_timeout=10
176
+ )
177
+
178
+ # Create connection pool
179
+ container = ContainerPostgres(pool_settings, container_settings)
180
+ pool = await container.create_pool()
181
+
182
+ # Bind pool to models
183
+ User._pool = pool
184
+ User._no_transaction = container.get_no_transaction_session()
185
+ Role._pool = pool
186
+ Role._no_transaction = container.get_no_transaction_session()
187
+ ```
188
+
189
+ ### 3. Create Tables
190
+
191
+ ```python
192
+ # Automatic table creation with FK
193
+ await container.create_and_update_tables([Role, User])
194
+ ```
195
+
196
+ ---
197
+
198
+ ## 📖 Usage Examples
199
+
200
+ ### CRUD Operations
201
+
202
+ ```python
203
+ # ═══════════════════════════════════════════════════════════
204
+ # CREATE - Creating records
205
+ # ═══════════════════════════════════════════════════════════
206
+
207
+ # Single create
208
+ user = User(name="John", email="john@example.com", role_id=1)
209
+ user_id = await User.create(user)
210
+ print(f"Created user with ID: {user_id}")
211
+
212
+ # Bulk create
213
+ users = [
214
+ User(name="Alice", email="alice@example.com"),
215
+ User(name="Bob", email="bob@example.com"),
216
+ User(name="Charlie", email="charlie@example.com"),
217
+ ]
218
+ created_ids = await User.create_bulk(users)
219
+ print(f"Created {len(created_ids)} users")
220
+
221
+ # ═══════════════════════════════════════════════════════════
222
+ # READ - Reading records
223
+ # ═══════════════════════════════════════════════════════════
224
+
225
+ # Get by ID
226
+ user = await User.get(1)
227
+ print(f"User: {user.name}")
228
+
229
+ # Get with field selection
230
+ user = await User.get(1, fields=["id", "name", "email"])
231
+
232
+ # Search with filtering
233
+ active_users = await User.search(
234
+ fields=["id", "name", "email"],
235
+ filter=[("active", "=", True)],
236
+ order="ASC",
237
+ sort="name",
238
+ limit=10
239
+ )
240
+
241
+ # Complex filters
242
+ users = await User.search(
243
+ fields=["id", "name"],
244
+ filter=[
245
+ ("active", "=", True),
246
+ "and",
247
+ [
248
+ ("name", "ilike", "john"),
249
+ "or",
250
+ ("email", "like", "@gmail.com")
251
+ ]
252
+ ]
253
+ )
254
+
255
+ # Pagination
256
+ page_1 = await User.search(fields=["id", "name"], start=0, end=20)
257
+ page_2 = await User.search(fields=["id", "name"], start=20, end=40)
258
+
259
+ # ═══════════════════════════════════════════════════════════
260
+ # UPDATE - Updating records
261
+ # ═══════════════════════════════════════════════════════════
262
+
263
+ # Update single record
264
+ user = await User.get(1)
265
+ user.name = "New Name"
266
+ await user.update()
267
+
268
+ # Update with payload
269
+ user = await User.get(1)
270
+ payload = User(name="Updated Name", active=False)
271
+ await user.update(payload, fields=["name", "active"])
272
+
273
+ # Bulk update
274
+ await User.update_bulk(
275
+ ids=[1, 2, 3],
276
+ payload=User(active=False)
277
+ )
278
+
279
+ # ═══════════════════════════════════════════════════════════
280
+ # DELETE - Deleting records
281
+ # ═══════════════════════════════════════════════════════════
282
+
283
+ # Delete single record
284
+ user = await User.get(1)
285
+ await user.delete()
286
+
287
+ # Bulk delete
288
+ await User.delete_bulk([4, 5, 6])
289
+ ```
290
+
291
+ ### Working with Relations
292
+
293
+ ```python
294
+ # ═══════════════════════════════════════════════════════════
295
+ # Many2One - Many to One
296
+ # ═══════════════════════════════════════════════════════════
297
+
298
+ # Get user with role
299
+ user = await User.get_with_relations(
300
+ id=1,
301
+ fields=["id", "name", "role_id"]
302
+ )
303
+ print(f"User: {user.name}, Role: {user.role_id.name}")
304
+
305
+ # ═══════════════════════════════════════════════════════════
306
+ # One2Many - One to Many
307
+ # ═══════════════════════════════════════════════════════════
308
+
309
+ # Get role with all users
310
+ role = await Role.get_with_relations(
311
+ id=1,
312
+ fields=["id", "name", "users"],
313
+ fields_info={"users": ["id", "name", "email"]}
314
+ )
315
+ print(f"Role: {role.name}")
316
+ for user in role.users["data"]:
317
+ print(f" - {user.name}")
318
+
319
+ # ═══════════════════════════════════════════════════════════
320
+ # Many2Many - Many to Many
321
+ # ═══════════════════════════════════════════════════════════
322
+
323
+ class Tag(DotModel):
324
+ __table__ = "tags"
325
+ _dialect = POSTGRES
326
+
327
+ id: int = Integer(primary_key=True)
328
+ name: str = Char(max_length=50)
329
+
330
+ class Article(DotModel):
331
+ __table__ = "articles"
332
+ _dialect = POSTGRES
333
+
334
+ id: int = Integer(primary_key=True)
335
+ title: str = Char(max_length=200)
336
+ tags: list[Tag] = Many2many(
337
+ relation_table=lambda: Tag,
338
+ many2many_table="article_tags",
339
+ column1="tag_id",
340
+ column2="article_id"
341
+ )
342
+
343
+ # Get article with tags
344
+ article = await Article.get_with_relations(
345
+ id=1,
346
+ fields=["id", "title", "tags"]
347
+ )
348
+
349
+ # Link tags to article
350
+ await Article.link_many2many(
351
+ field=Article.tags,
352
+ values=[(article.id, 1), (article.id, 2), (article.id, 3)]
353
+ )
354
+
355
+ # Unlink tags
356
+ await Article.unlink_many2many(
357
+ field=Article.tags,
358
+ ids=[1, 2]
359
+ )
360
+ ```
361
+
362
+ ### Transactions
363
+
364
+ ```python
365
+ from dotorm.databases.postgres import ContainerTransaction
366
+
367
+ async with ContainerTransaction(pool) as session:
368
+ # All operations in single transaction
369
+ role_id = await Role.create(
370
+ Role(name="Admin"),
371
+ session=session
372
+ )
373
+
374
+ user_id = await User.create(
375
+ User(name="Admin User", role_id=role_id),
376
+ session=session
377
+ )
378
+
379
+ # Auto commit on exit
380
+ # Auto rollback on exception
381
+ ```
382
+
383
+ ### Filters
384
+
385
+ ```python
386
+ # ═══════════════════════════════════════════════════════════
387
+ # Supported Operators
388
+ # ═══════════════════════════════════════════════════════════
389
+
390
+ # Comparison
391
+ filter=[("age", "=", 25)]
392
+ filter=[("age", "!=", 25)]
393
+ filter=[("age", ">", 18)]
394
+ filter=[("age", ">=", 18)]
395
+ filter=[("age", "<", 65)]
396
+ filter=[("age", "<=", 65)]
397
+
398
+ # String search
399
+ filter=[("name", "like", "John")] # %John%
400
+ filter=[("name", "ilike", "john")] # case-insensitive
401
+ filter=[("name", "not like", "test")]
402
+
403
+ # IN / NOT IN
404
+ filter=[("status", "in", ["active", "pending"])]
405
+ filter=[("id", "not in", [1, 2, 3])]
406
+
407
+ # NULL checks
408
+ filter=[("deleted_at", "is null", None)]
409
+ filter=[("email", "is not null", None)]
410
+
411
+ # BETWEEN
412
+ filter=[("created_at", "between", ["2024-01-01", "2024-12-31"])]
413
+
414
+ # ═══════════════════════════════════════════════════════════
415
+ # Logical Operators
416
+ # ═══════════════════════════════════════════════════════════
417
+
418
+ # AND (default between conditions)
419
+ filter=[
420
+ ("active", "=", True),
421
+ ("verified", "=", True)
422
+ ]
423
+
424
+ # OR
425
+ filter=[
426
+ ("role", "=", "admin"),
427
+ "or",
428
+ ("role", "=", "moderator")
429
+ ]
430
+
431
+ # Nested conditions
432
+ filter=[
433
+ ("active", "=", True),
434
+ "and",
435
+ [
436
+ ("role", "=", "admin"),
437
+ "or",
438
+ ("role", "=", "superuser")
439
+ ]
440
+ ]
441
+
442
+ # NOT
443
+ filter=[
444
+ ("not", ("deleted", "=", True))
445
+ ]
446
+ ```
447
+
448
+ ---
449
+
450
+ ## ⚡ Solving the N+1 Problem
451
+
452
+ ### The N+1 Problem
453
+
454
+ ```python
455
+ # ❌ BAD: N+1 queries
456
+ users = await User.search(fields=["id", "name", "role_id"], limit=100)
457
+ for user in users:
458
+ # Each call = new DB query!
459
+ role = await Role.get(user.role_id)
460
+ print(f"{user.name} - {role.name}")
461
+ # Total: 1 + 100 = 101 queries!
462
+ ```
463
+
464
+ ### DotORM Solution
465
+
466
+ #### 1. Automatic Relation Loading in search()
467
+
468
+ ```python
469
+ # ✅ GOOD: 2 queries instead of 101
470
+ users = await User.search(
471
+ fields=["id", "name", "role_id"], # role_id is Many2one
472
+ limit=100
473
+ )
474
+ # DotORM automatically:
475
+ # 1. Loads all users (1 query)
476
+ # 2. Collects unique role_ids
477
+ # 3. Loads all roles in one query (1 query)
478
+ # 4. Maps roles to users in memory
479
+
480
+ for user in users:
481
+ print(f"{user.name} - {user.role_id.name}") # No additional queries!
482
+ ```
483
+
484
+ #### 2. Batch Loading for Many2Many
485
+
486
+ ```python
487
+ # ✅ GOOD: Optimized M2M loading
488
+ articles = await Article.search(
489
+ fields=["id", "title", "tags"],
490
+ limit=50
491
+ )
492
+ # DotORM executes:
493
+ # 1. SELECT * FROM articles LIMIT 50
494
+ # 2. SELECT tags.*, article_tags.article_id as m2m_id
495
+ # FROM tags
496
+ # JOIN article_tags ON tags.id = article_tags.tag_id
497
+ # WHERE article_tags.article_id IN (1, 2, 3, ..., 50)
498
+ # Total: 2 queries!
499
+ ```
500
+
501
+ #### 3. Batch Loading for One2Many
502
+
503
+ ```python
504
+ # ✅ GOOD: Optimized O2M loading
505
+ roles = await Role.search(
506
+ fields=["id", "name", "users"],
507
+ limit=10
508
+ )
509
+ # DotORM executes:
510
+ # 1. SELECT * FROM roles LIMIT 10
511
+ # 2. SELECT * FROM users WHERE role_id IN (1, 2, 3, ..., 10)
512
+ # Total: 2 queries!
513
+ ```
514
+
515
+ ### N+1 Solution Architecture
516
+
517
+ ```
518
+ ┌─────────────────────────────────────────────────────────────┐
519
+ │ ORM Layer │
520
+ │ ┌─────────────────────────────────────────────────────┐ │
521
+ │ │ search() method │ │
522
+ │ │ 1. Execute main query │ │
523
+ │ │ 2. Collect relation field IDs │ │
524
+ │ │ 3. Call _records_list_get_relation() │ │
525
+ │ └─────────────────────────────────────────────────────┘ │
526
+ │ │ │
527
+ │ ▼ │
528
+ │ ┌─────────────────────────────────────────────────────┐ │
529
+ │ │ _records_list_get_relation() │ │
530
+ │ │ 1. Build optimized queries for all relation types │ │
531
+ │ │ 2. Execute queries in parallel (asyncio.gather) │ │
532
+ │ │ 3. Map results back to parent records │ │
533
+ │ └─────────────────────────────────────────────────────┘ │
534
+ │ │ │
535
+ │ ▼ │
536
+ │ ┌─────────────────────────────────────────────────────┐ │
537
+ │ │ Builder Layer │ │
538
+ │ │ build_search_relation() - builds batch queries │ │
539
+ │ │ ┌─────────────┬─────────────┬─────────────┐ │ │
540
+ │ │ │ Many2One │ One2Many │ Many2Many │ │ │
541
+ │ │ │ IN clause │ IN clause │ JOIN query │ │ │
542
+ │ │ └─────────────┴─────────────┴─────────────┘ │ │
543
+ │ └─────────────────────────────────────────────────────┘ │
544
+ └─────────────────────────────────────────────────────────────┘
545
+ ```
546
+
547
+ ### Query Count Comparison
548
+
549
+ | Scenario | Naive Approach | DotORM |
550
+ |----------|----------------|--------|
551
+ | 100 users + roles (M2O) | 101 queries | 2 queries |
552
+ | 50 articles + tags (M2M) | 51 queries | 2 queries |
553
+ | 10 roles + users (O2M) | 11 queries | 2 queries |
554
+ | Combined | 162 queries | 4 queries |
555
+
556
+ ---
557
+
558
+ ## 📊 Benchmarks
559
+
560
+ ### Testing Methodology
561
+
562
+ - **Hardware**: AMD Ryzen 7 5800X, 32GB RAM, NVMe SSD
563
+ - **Database**: PostgreSQL 16, local
564
+ - **Python**: 3.12.0
565
+ - **Data**: 100,000 records in users table
566
+ - **Measurements**: Average of 100 iterations
567
+
568
+ ### Comparison with Other ORMs
569
+
570
+ #### INSERT (1000 records)
571
+
572
+ | ORM | Time (ms) | Queries | Relative |
573
+ |-----|-----------|---------|----------|
574
+ | **DotORM** | **45** | **1** | **1.0x** |
575
+ | SQLAlchemy 2.0 | 120 | 1000 | 2.7x |
576
+ | Tortoise ORM | 89 | 1 | 2.0x |
577
+ | databases + raw SQL | 42 | 1 | 0.9x |
578
+
579
+ ```python
580
+ # DotORM - bulk insert
581
+ users = [User(name=f"User {i}", email=f"user{i}@test.com") for i in range(1000)]
582
+ await User.create_bulk(users) # 1 query
583
+ ```
584
+
585
+ #### SELECT (1000 records)
586
+
587
+ | ORM | Time (ms) | Memory (MB) | Relative |
588
+ |-----|-----------|-------------|----------|
589
+ | **DotORM** | **12** | **8.2** | **1.0x** |
590
+ | SQLAlchemy 2.0 | 28 | 15.4 | 2.3x |
591
+ | Tortoise ORM | 22 | 12.1 | 1.8x |
592
+ | databases + raw SQL | 10 | 6.5 | 0.8x |
593
+
594
+ #### SELECT with JOIN (M2O, 1000 records)
595
+
596
+ | ORM | Time (ms) | Queries | Relative |
597
+ |-----|-----------|---------|----------|
598
+ | **DotORM** | **18** | **2** | **1.0x** |
599
+ | SQLAlchemy (lazy) | 1250 | 1001 | 69x |
600
+ | SQLAlchemy (eager) | 35 | 1 | 1.9x |
601
+ | Tortoise ORM | 45 | 2 | 2.5x |
602
+
603
+ #### UPDATE (1000 records)
604
+
605
+ | ORM | Time (ms) | Queries | Relative |
606
+ |-----|-----------|---------|----------|
607
+ | **DotORM** | **38** | **1** | **1.0x** |
608
+ | SQLAlchemy 2.0 | 95 | 1000 | 2.5x |
609
+ | Tortoise ORM | 78 | 1 | 2.1x |
610
+
611
+ ### Performance Chart
612
+
613
+ ```
614
+ INSERT 1000 records (lower is better)
615
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
616
+ DotORM ████████████░░░░░░░░░░░░░░░░░░░░ 45ms
617
+ Tortoise ██████████████████████████░░░░░░ 89ms
618
+ SQLAlchemy ████████████████████████████████ 120ms
619
+
620
+ SELECT 1000 records with M2O relation
621
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
622
+ DotORM ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 18ms (2 queries)
623
+ SQLAlchemy eager████████░░░░░░░░░░░░░░░░░░░░░░░░ 35ms (1 query)
624
+ Tortoise ██████████░░░░░░░░░░░░░░░░░░░░░░ 45ms (2 queries)
625
+ SQLAlchemy lazy ████████████████████████████████ 1250ms (1001 queries)
626
+ ```
627
+
628
+ ### Running Benchmarks
629
+
630
+ ```bash
631
+ # Install benchmark dependencies
632
+ pip install pytest-benchmark memory_profiler
633
+
634
+ # Run all benchmarks
635
+ python -m pytest benchmarks/ -v --benchmark-only
636
+
637
+ # Run specific benchmark
638
+ python -m pytest benchmarks/test_insert.py -v
639
+
640
+ # With memory profiling
641
+ python -m memory_profiler benchmarks/memory_test.py
642
+ ```
643
+
644
+ ---
645
+
646
+ ## 🏗️ Architecture
647
+
648
+ ### Overall Architecture
649
+
650
+ ```
651
+ ┌─────────────────────────────────────────────────────────────────────────┐
652
+ │ Application Layer │
653
+ │ (FastAPI, Django, Flask, etc.) │
654
+ └─────────────────────────────────────────────────────────────────────────┘
655
+
656
+
657
+ ┌─────────────────────────────────────────────────────────────────────────┐
658
+ │ DotORM │
659
+ │ ┌────────────────────────────────────────────────────────────────┐ │
660
+ │ │ Model Layer │ │
661
+ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
662
+ │ │ │ DotModel │ │ Fields │ │ Pydantic │ │ │
663
+ │ │ │ (Base ORM) │ │ (Type Def) │ │ (Validation) │ │ │
664
+ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
665
+ │ └────────────────────────────────────────────────────────────────┘ │
666
+ │ │ │
667
+ │ ▼ │
668
+ │ ┌────────────────────────────────────────────────────────────────┐ │
669
+ │ │ ORM Layer │ │
670
+ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
671
+ │ │ │ PrimaryMixin │ │ Many2Many │ │ Relations │ │ │
672
+ │ │ │ (CRUD ops) │ │ Mixin │ │ Mixin │ │ │
673
+ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
674
+ │ │ ┌──────────────┐ │ │
675
+ │ │ │ DDLMixin │ │ │
676
+ │ │ │(Table mgmt) │ │ │
677
+ │ │ └──────────────┘ │ │
678
+ │ └────────────────────────────────────────────────────────────────┘ │
679
+ │ │ │
680
+ │ ▼ │
681
+ │ ┌────────────────────────────────────────────────────────────────┐ │
682
+ │ │ Builder Layer │ │
683
+ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
684
+ │ │ │ CRUDMixin │ │ M2MMixin │ │ RelationsMix │ │ │
685
+ │ │ │ (SQL CRUD) │ │ (M2M SQL) │ │ (Batch SQL) │ │ │
686
+ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
687
+ │ │ ┌──────────────┐ ┌──────────────┐ │ │
688
+ │ │ │ FilterParser │ │ Dialect │ │ │
689
+ │ │ │(WHERE build) │ │ (DB adapt) │ │ │
690
+ │ │ └──────────────┘ └──────────────┘ │ │
691
+ │ └────────────────────────────────────────────────────────────────┘ │
692
+ │ │ │
693
+ │ ▼ │
694
+ │ ┌────────────────────────────────────────────────────────────────┐ │
695
+ │ │ Database Layer │ │
696
+ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
697
+ │ │ │ PostgreSQL │ │ MySQL │ │ ClickHouse │ │ │
698
+ │ │ │ asyncpg │ │ aiomysql │ │ asynch │ │ │
699
+ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
700
+ │ └────────────────────────────────────────────────────────────────┘ │
701
+ └─────────────────────────────────────────────────────────────────────────┘
702
+ ```
703
+
704
+ ### ORM Layer Architecture
705
+
706
+ ```
707
+ ┌─────────────────────────────────────────────────────────────────────────┐
708
+ │ ORM Layer │
709
+ │ │
710
+ │ ┌─────────────────────────────────────────────────────────────────┐ │
711
+ │ │ DotModel │ │
712
+ │ │ (Main Model Class) │ │
713
+ │ │ ┌─────────────────────────────────────────────────────────┐ │ │
714
+ │ │ │ Class Variables: │ │ │
715
+ │ │ │ • __table__: str - Table name │ │ │
716
+ │ │ │ • _pool: Pool - Connection pool │ │ │
717
+ │ │ │ • _dialect: Dialect - Database dialect │ │ │
718
+ │ │ │ • _builder: Builder - SQL builder instance │ │ │
719
+ │ │ │ • _no_transaction: Type - Session factory │ │ │
720
+ │ │ └─────────────────────────────────────────────────────────┘ │ │
721
+ │ └─────────────────────────────────────────────────────────────────┘ │
722
+ │ │ inherits │
723
+ │ ┌──────────────────┼──────────────────┐ │
724
+ │ ▼ ▼ ▼ │
725
+ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
726
+ │ │ OrmPrimary │ │ OrmMany2many │ │ OrmRelations │ │
727
+ │ │ Mixin │ │ Mixin │ │ Mixin │ │
728
+ │ ├───────────────┤ ├───────────────┤ ├───────────────┤ │
729
+ │ │ • create() │ │ • get_m2m() │ │ • search() │ │
730
+ │ │ • create_bulk │ │ • link_m2m() │ │ • get_with_ │ │
731
+ │ │ • get() │ │ • unlink_m2m()│ │ relations() │ │
732
+ │ │ • update() │ │ • _records_ │ │ • update_with │ │
733
+ │ │ • update_bulk │ │ list_get_ │ │ _relations()│ │
734
+ │ │ • delete() │ │ relation() │ │ │ │
735
+ │ │ • delete_bulk │ │ │ │ │ │
736
+ │ │ • table_len() │ │ │ │ │ │
737
+ │ └───────────────┘ └───────────────┘ └───────────────┘ │
738
+ │ │ │ │ │
739
+ │ └──────────────────┼──────────────────┘ │
740
+ │ ▼ │
741
+ │ ┌───────────────┐ │
742
+ │ │ DDLMixin │ │
743
+ │ ├───────────────┤ │
744
+ │ │ • __create_ │ │
745
+ │ │ table__() │ │
746
+ │ │ • cache() │ │
747
+ │ │ • format_ │ │
748
+ │ │ default() │ │
749
+ │ └───────────────┘ │
750
+ │ │
751
+ │ Data Flow: │
752
+ │ ═══════════════════════════════════════════════════════════════════ │
753
+ │ User.search() → OrmRelationsMixin.search() │
754
+ │ │ │
755
+ │ ├─→ _builder.build_search() # Build SQL │
756
+ │ ├─→ session.execute() # Execute query │
757
+ │ ├─→ prepare_list_ids() # Deserialize │
758
+ │ └─→ _records_list_get_relation() # Load relations │
759
+ │ │ │
760
+ │ ├─→ _builder.build_search_relation() │
761
+ │ ├─→ asyncio.gather(*queries) # Parallel execution │
762
+ │ └─→ Map results to records │
763
+ └─────────────────────────────────────────────────────────────────────────┘
764
+ ```
765
+
766
+ ### Builder Layer Architecture
767
+
768
+ ```
769
+ ┌─────────────────────────────────────────────────────────────────────────┐
770
+ │ Builder Layer │
771
+ │ │
772
+ │ ┌─────────────────────────────────────────────────────────────────┐ │
773
+ │ │ Builder │ │
774
+ │ │ (Main Query Builder) │ │
775
+ │ │ ┌─────────────────────────────────────────────────────────┐ │ │
776
+ │ │ │ Attributes: │ │ │
777
+ │ │ │ • table: str - Target table name │ │ │
778
+ │ │ │ • fields: dict[str,Field] - Model fields │ │ │
779
+ │ │ │ • dialect: Dialect - SQL dialect config │ │ │
780
+ │ │ │ • filter_parser: Parser - WHERE clause builder │ │ │
781
+ │ │ └─────────────────────────────────────────────────────────┘ │ │
782
+ │ └─────────────────────────────────────────────────────────────────┘ │
783
+ │ │ inherits │
784
+ │ ┌──────────────────┼──────────────────┐ │
785
+ │ ▼ ▼ ▼ │
786
+ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
787
+ │ │ CRUDMixin │ │ Many2Many │ │ Relations │ │
788
+ │ │ │ │ Mixin │ │ Mixin │ │
789
+ │ ├───────────────┤ ├───────────────┤ ├───────────────┤ │
790
+ │ │build_create() │ │build_get_m2m()│ │build_search_ │ │
791
+ │ │build_create_ │ │build_get_m2m_ │ │ relation() │ │
792
+ │ │ bulk() │ │ multiple() │ │ │ │
793
+ │ │build_get() │ │ │ │ Returns: │ │
794
+ │ │build_search() │ │ │ │ List[Request │ │
795
+ │ │build_update() │ │ │ │ Builder] │ │
796
+ │ │build_update_ │ │ │ │ │ │
797
+ │ │ bulk() │ │ │ │ │ │
798
+ │ │build_delete() │ │ │ │ │ │
799
+ │ │build_delete_ │ │ │ │ │ │
800
+ │ │ bulk() │ │ │ │ │ │
801
+ │ │build_table_ │ │ │ │ │ │
802
+ │ │ len() │ │ │ │ │ │
803
+ │ └───────────────┘ └───────────────┘ └───────────────┘ │
804
+ │ │
805
+ │ Supporting Components: │
806
+ │ ═══════════════════════════════════════════════════════════════════ │
807
+ │ │
808
+ │ ┌───────────────────────────┐ ┌───────────────────────────┐ │
809
+ │ │ FilterParser │ │ Dialect │ │
810
+ │ ├───────────────────────────┤ ├───────────────────────────┤ │
811
+ │ │ • parse(filter_expr) │ │ • name: str │ │
812
+ │ │ → (sql, values) │ │ • escape: str (", `) │ │
813
+ │ │ │ │ • placeholder: str ($, %) │ │
814
+ │ │ Supports: │ │ • supports_returning: bool│ │
815
+ │ │ • =, !=, >, <, >=, <= │ │ │ │
816
+ │ │ • like, ilike │ │ Methods: │ │
817
+ │ │ • in, not in │ │ • escape_identifier() │ │
818
+ │ │ • is null, is not null │ │ • make_placeholders() │ │
819
+ │ │ • between │ │ • make_placeholder() │ │
820
+ │ │ • and, or, not │ │ │ │
821
+ │ └───────────────────────────┘ └───────────────────────────┘ │
822
+ │ │
823
+ │ ┌───────────────────────────┐ ┌───────────────────────────┐ │
824
+ │ │ RequestBuilder │ │ RequestBuilderForm │ │
825
+ │ ├───────────────────────────┤ ├───────────────────────────┤ │
826
+ │ │ Container for relation │ │ Extended for form view │ │
827
+ │ │ query parameters │ │ with nested fields │ │
828
+ │ │ │ │ │ │
829
+ │ │ • stmt: str │ │ Overrides: │ │
830
+ │ │ • value: tuple │ │ • function_prepare │ │
831
+ │ │ • field_name: str │ │ → prepare_form_ids │ │
832
+ │ │ • field: Field │ │ │ │
833
+ │ │ • fields: list[str] │ │ │ │
834
+ │ │ │ │ │ │
835
+ │ │ Properties: │ │ │ │
836
+ │ │ • function_cursor │ │ │ │
837
+ │ │ • function_prepare │ │ │ │
838
+ │ └───────────────────────────┘ └───────────────────────────┘ │
839
+ │ │
840
+ │ Query Building Flow: │
841
+ │ ═══════════════════════════════════════════════════════════════════ │
842
+ │ │
843
+ │ build_search(fields, filter, limit, order, sort) │
844
+ │ │ │
845
+ │ ├─→ Validate fields against store_fields │
846
+ │ ├─→ Build SELECT clause with escaped identifiers │
847
+ │ ├─→ filter_parser.parse(filter) → WHERE clause │
848
+ │ ├─→ Add ORDER BY, LIMIT, OFFSET │
849
+ │ └─→ Return (sql_string, values_tuple) │
850
+ │ │
851
+ │ Example Output: │
852
+ │ ─────────────────────────────────────────────────────────────────── │
853
+ │ Input: fields=["id", "name"], filter=[("active", "=", True)] │
854
+ │ Output: ('SELECT "id", "name" FROM users WHERE "active" = %s │
855
+ │ ORDER BY id DESC LIMIT %s', (True, 80)) │
856
+ └─────────────────────────────────────────────────────────────────────────┘
857
+ ```
858
+
859
+ ### File Structure
860
+
861
+ ```
862
+ dotorm/
863
+ ├── __init__.py # Public API exports
864
+ ├── model.py # DotModel base class
865
+ ├── fields.py # Field type definitions
866
+ ├── exceptions.py # Custom exceptions
867
+ ├── pydantic.py # Pydantic integration
868
+
869
+ ├── orm/ # ORM Layer
870
+ │ ├── __init__.py
871
+ │ ├── protocol.py # Type protocols
872
+ │ └── mixins/
873
+ │ ├── __init__.py
874
+ │ ├── primary.py # CRUD operations
875
+ │ ├── many2many.py # M2M operations
876
+ │ ├── relations.py # Relation loading
877
+ │ └── ddl.py # Table management
878
+
879
+ ├── builder/ # Builder Layer
880
+ │ ├── __init__.py
881
+ │ ├── builder.py # Main Builder class
882
+ │ ├── protocol.py # Builder protocol
883
+ │ ├── helpers.py # SQL helpers
884
+ │ ├── request_builder.py # Request containers
885
+ │ └── mixins/
886
+ │ ├── __init__.py
887
+ │ ├── crud.py # CRUD SQL builders
888
+ │ ├── m2m.py # M2M SQL builders
889
+ │ └── relations.py # Relation SQL builders
890
+
891
+ ├── components/ # Shared components
892
+ │ ├── __init__.py
893
+ │ ├── dialect.py # Database dialects
894
+ │ └── filter_parser.py # Filter expression parser
895
+
896
+ └── databases/ # Database Layer
897
+ ├── abstract/
898
+ │ ├── __init__.py
899
+ │ ├── pool.py # Abstract pool
900
+ │ ├── session.py # Abstract session
901
+ │ └── types.py # Settings types
902
+
903
+ ├── postgres/
904
+ │ ├── __init__.py
905
+ │ ├── pool.py # PostgreSQL pool
906
+ │ ├── session.py # PostgreSQL sessions
907
+ │ └── transaction.py # Transaction manager
908
+
909
+ ├── mysql/
910
+ │ ├── __init__.py
911
+ │ ├── pool.py # MySQL pool
912
+ │ ├── session.py # MySQL sessions
913
+ │ └── transaction.py # Transaction manager
914
+
915
+ └── clickhouse/
916
+ ├── __init__.py
917
+ ├── pool.py # ClickHouse pool
918
+ └── session.py # ClickHouse session
919
+ ```
920
+
921
+ ---
922
+
923
+ ## 🧪 Testing
924
+
925
+ ### Running Tests
926
+
927
+ ```bash
928
+ # Install test dependencies
929
+ pip install pytest pytest-asyncio pytest-cov
930
+
931
+ # Run all tests
932
+ pytest
933
+
934
+ # Verbose output
935
+ pytest -v
936
+
937
+ # Unit tests only
938
+ pytest tests/unit/ -v
939
+
940
+ # Integration tests only (requires DB)
941
+ pytest tests/integration/ -v
942
+
943
+ # Specific file
944
+ pytest tests/unit/test_builder.py -v
945
+
946
+ # Specific test
947
+ pytest tests/unit/test_builder.py::TestCRUDBuilder::test_build_search -v
948
+ ```
949
+
950
+ ### Test Coverage
951
+
952
+ ```bash
953
+ # Generate coverage report
954
+ pytest --cov=dotorm --cov-report=html
955
+
956
+ # Open report
957
+ open htmlcov/index.html
958
+
959
+ # Console report
960
+ pytest --cov=dotorm --cov-report=term-missing
961
+ ```
962
+
963
+ ### Current Coverage
964
+
965
+ ```
966
+ Name Stmts Miss Cover
967
+ ───────────────────────────────────────────────────────────
968
+ dotorm/__init__.py 45 0 100%
969
+ dotorm/model.py 285 38 87%
970
+ dotorm/fields.py 198 12 94%
971
+ dotorm/exceptions.py 8 0 100%
972
+ dotorm/pydantic.py 145 23 84%
973
+ dotorm/orm/mixins/primary.py 112 8 93%
974
+ dotorm/orm/mixins/many2many.py 89 11 88%
975
+ dotorm/orm/mixins/relations.py 156 19 88%
976
+ dotorm/orm/mixins/ddl.py 87 15 83%
977
+ dotorm/builder/builder.py 28 0 100%
978
+ dotorm/builder/mixins/crud.py 124 5 96%
979
+ dotorm/builder/mixins/m2m.py 56 3 95%
980
+ dotorm/builder/mixins/relations.py 67 8 88%
981
+ dotorm/components/dialect.py 52 2 96%
982
+ dotorm/components/filter_parser.py 98 4 96%
983
+ dotorm/databases/postgres/session.py 89 12 87%
984
+ dotorm/databases/postgres/pool.py 67 9 87%
985
+ dotorm/databases/mysql/session.py 78 14 82%
986
+ ───────────────────────────────────────────────────────────
987
+ TOTAL 1784 183 87%
988
+ ```
989
+
990
+ ### Test Structure
991
+
992
+ ```
993
+ tests/
994
+ ├── conftest.py # Pytest fixtures
995
+ ├── unit/
996
+ │ ├── test_fields.py # Field type tests
997
+ │ ├── test_model.py # Model tests
998
+ │ ├── test_builder.py # Builder tests
999
+ │ ├── test_filter.py # Filter parser tests
1000
+ │ └── test_dialect.py # Dialect tests
1001
+
1002
+ ├── integration/
1003
+ │ ├── test_postgres.py # PostgreSQL integration
1004
+ │ ├── test_mysql.py # MySQL integration
1005
+ │ ├── test_crud.py # CRUD operations
1006
+ │ ├── test_relations.py # Relation loading
1007
+ │ └── test_transactions.py # Transaction tests
1008
+
1009
+ └── benchmarks/
1010
+ ├── test_insert.py # Insert benchmarks
1011
+ ├── test_select.py # Select benchmarks
1012
+ └── memory_test.py # Memory profiling
1013
+ ```
1014
+
1015
+ ### Example Test
1016
+
1017
+ ```python
1018
+ # tests/unit/test_builder.py
1019
+ import pytest
1020
+ from dotorm.builder import Builder
1021
+ from dotorm.components import POSTGRES
1022
+ from dotorm.fields import Integer, Char, Boolean
1023
+
1024
+ class TestCRUDBuilder:
1025
+ @pytest.fixture
1026
+ def builder(self):
1027
+ fields = {
1028
+ "id": Integer(primary_key=True),
1029
+ "name": Char(max_length=100),
1030
+ "email": Char(max_length=255),
1031
+ "active": Boolean(default=True),
1032
+ }
1033
+ return Builder(table="users", fields=fields, dialect=POSTGRES)
1034
+
1035
+ def test_build_search(self, builder):
1036
+ """Test SELECT query building."""
1037
+ stmt, values = builder.build_search(
1038
+ fields=["id", "name"],
1039
+ filter=[("active", "=", True)],
1040
+ limit=10,
1041
+ order="ASC",
1042
+ sort="name"
1043
+ )
1044
+
1045
+ assert "SELECT" in stmt
1046
+ assert '"id"' in stmt
1047
+ assert '"name"' in stmt
1048
+ assert "FROM users" in stmt
1049
+ assert "WHERE" in stmt
1050
+ assert "ORDER BY name ASC" in stmt
1051
+ assert "LIMIT" in stmt
1052
+ assert values == (True, 10)
1053
+
1054
+ def test_build_create(self, builder):
1055
+ """Test INSERT query building."""
1056
+ payload = {"name": "John", "email": "john@example.com"}
1057
+ stmt, values = builder.build_create(payload)
1058
+
1059
+ assert "INSERT INTO users" in stmt
1060
+ assert "name" in stmt
1061
+ assert "email" in stmt
1062
+ assert "VALUES" in stmt
1063
+ assert values == ("John", "john@example.com")
1064
+
1065
+ def test_build_create_bulk(self, builder):
1066
+ """Test bulk INSERT."""
1067
+ payloads = [
1068
+ {"name": "John", "email": "john@example.com"},
1069
+ {"name": "Jane", "email": "jane@example.com"},
1070
+ ]
1071
+ stmt, all_values = builder.build_create_bulk(payloads)
1072
+
1073
+ assert "INSERT INTO users" in stmt
1074
+ assert "(name, email)" in stmt
1075
+ assert len(all_values) == 4
1076
+ assert all_values == ["John", "john@example.com", "Jane", "jane@example.com"]
1077
+ ```
1078
+
1079
+ ---
1080
+
1081
+ ## 📚 API Reference
1082
+
1083
+ ### Fields
1084
+
1085
+ | Field | Python Type | SQL Type (PG) | Description |
1086
+ |-------|-------------|---------------|-------------|
1087
+ | `Integer` | `int` | `INTEGER` | 32-bit integer |
1088
+ | `BigInteger` | `int` | `BIGINT` | 64-bit integer |
1089
+ | `SmallInteger` | `int` | `SMALLINT` | 16-bit integer |
1090
+ | `Char` | `str` | `VARCHAR(n)` | String with max length |
1091
+ | `Text` | `str` | `TEXT` | Unlimited text |
1092
+ | `Boolean` | `bool` | `BOOL` | True/False |
1093
+ | `Float` | `float` | `DOUBLE PRECISION` | Floating point |
1094
+ | `Decimal` | `Decimal` | `DECIMAL(p,s)` | Precise decimal |
1095
+ | `Date` | `date` | `DATE` | Date only |
1096
+ | `Time` | `time` | `TIME` | Time only |
1097
+ | `Datetime` | `datetime` | `TIMESTAMPTZ` | Date and time |
1098
+ | `JSONField` | `dict/list` | `JSONB` | JSON data |
1099
+ | `Binary` | `bytes` | `BYTEA` | Binary data |
1100
+ | `Many2one` | `Model` | `INTEGER` | FK relation |
1101
+ | `One2many` | `list[Model]` | - | Reverse FK |
1102
+ | `Many2many` | `list[Model]` | - | M2M relation |
1103
+ | `One2one` | `Model` | - | 1:1 relation |
1104
+
1105
+ ### Field Parameters
1106
+
1107
+ ```python
1108
+ Field(
1109
+ primary_key=False, # Is primary key?
1110
+ null=True, # Allow NULL?
1111
+ required=False, # Required (sets null=False)?
1112
+ unique=False, # Unique constraint?
1113
+ index=False, # Create index?
1114
+ default=None, # Default value
1115
+ description=None, # Field description
1116
+ store=True, # Store in DB?
1117
+ compute=None, # Compute function
1118
+ )
1119
+ ```
1120
+
1121
+ ### Model Class Methods
1122
+
1123
+ | Method | Description | Returns |
1124
+ |--------|-------------|---------|
1125
+ | `create(payload)` | Create single record | `int` (ID) |
1126
+ | `create_bulk(payloads)` | Create multiple records | `list[dict]` |
1127
+ | `get(id, fields)` | Get by ID | `Model \| None` |
1128
+ | `search(...)` | Search with filters | `list[Model]` |
1129
+ | `table_len()` | Count records | `int` |
1130
+ | `get_with_relations(...)` | Get with relations | `Model \| None` |
1131
+ | `get_many2many(...)` | Get M2M related | `list[Model]` |
1132
+ | `link_many2many(...)` | Create M2M links | `None` |
1133
+ | `unlink_many2many(...)` | Remove M2M links | `None` |
1134
+ | `__create_table__()` | Create DB table | `list[str]` |
1135
+
1136
+ ### Model Instance Methods
1137
+
1138
+ | Method | Description | Returns |
1139
+ |--------|-------------|---------|
1140
+ | `update(payload, fields)` | Update record | `None` |
1141
+ | `delete()` | Delete record | `None` |
1142
+ | `json(...)` | Serialize to dict | `dict` |
1143
+ | `update_with_relations(...)` | Update with relations | `dict` |
1144
+
1145
+ ---
1146
+
1147
+ ## 👤 Author
1148
+
1149
+ <p align="center">
1150
+ <img src="https://avatars.githubusercontent.com/u/11828278?v=4" width="150" style="border-radius: 50%;">
1151
+ </p>
1152
+
1153
+ <h3 align="center">Артём Шуршилов</h3>
1154
+
1155
+ <p align="center">
1156
+ <a href="https://github.com/shurshilov">
1157
+ <img src="https://img.shields.io/badge/GitHub-@artem--shurshilov-181717?style=flat&logo=github" alt="GitHub">
1158
+ </a>
1159
+ <a href="https://t.me/eurodoo">
1160
+ <img src="https://img.shields.io/badge/Telegram-@artem__shurshilov-26A5E4?style=flat&logo=telegram" alt="Telegram">
1161
+ </a>
1162
+ <a href="mailto:shurshilov.a.a@gmail.com">
1163
+ <img src="https://img.shields.io/badge/Email-artem.shurshilov-EA4335?style=flat&logo=gmail" alt="Email">
1164
+ </a>
1165
+ </p>
1166
+
1167
+ <p align="center">
1168
+ <i>Python Backend Developer | ORM Enthusiast | Open Source Contributor</i>
1169
+ </p>
1170
+
1171
+ ---
1172
+
1173
+ ## 🤝 Contributing
1174
+
1175
+ We welcome contributions to the project!
1176
+
1177
+ ```bash
1178
+ # Fork the repository, then:
1179
+ git clone https://github.com/YOUR_USERNAME/dotorm.git
1180
+ cd dotorm
1181
+
1182
+ # Create virtual environment
1183
+ python -m venv venv
1184
+ source venv/bin/activate # Linux/macOS
1185
+ # or
1186
+ .\venv\Scripts\activate # Windows
1187
+
1188
+ # Install dev dependencies
1189
+ pip install -e ".[dev]"
1190
+
1191
+ # Create feature branch
1192
+ git checkout -b feature/amazing-feature
1193
+
1194
+ # After changes
1195
+ pytest # Run tests
1196
+ black dotorm/ # Format code
1197
+ mypy dotorm/ # Type check
1198
+
1199
+ # Commit and PR
1200
+ git commit -m "feat: add amazing feature"
1201
+ git push origin feature/amazing-feature
1202
+ ```
1203
+
1204
+ ---
1205
+
1206
+ ## 📄 License
1207
+
1208
+ ```
1209
+ MIT License
1210
+
1211
+ Copyright (c) 2024 Artem Shurshilov
1212
+
1213
+ Permission is hereby granted, free of charge, to any person obtaining a copy
1214
+ of this software and associated documentation files (the "Software"), to deal
1215
+ in the Software without restriction, including without limitation the rights
1216
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1217
+ copies of the Software, and to permit persons to whom the Software is
1218
+ furnished to do so, subject to the following conditions:
1219
+
1220
+ The above copyright notice and this permission notice shall be included in all
1221
+ copies or substantial portions of the Software.
1222
+
1223
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1224
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1225
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1226
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1227
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1228
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1229
+ SOFTWARE.
1230
+ ```
1231
+
1232
+ ---
1233
+
1234
+ <p align="center">
1235
+ <b>⭐ If you find this project useful, give it a star! ⭐</b>
1236
+ </p>
1237
+
1238
+ <p align="center">
1239
+ Made with ❤️ by <a href="https://github.com/shurshilov">Artem Shurshilov</a>
1240
+ </p>