pgstorm 0.1.0__tar.gz

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 (53) hide show
  1. pgstorm-0.1.0/PKG-INFO +364 -0
  2. pgstorm-0.1.0/README.md +328 -0
  3. pgstorm-0.1.0/pgstorm/__init__.py +77 -0
  4. pgstorm-0.1.0/pgstorm/columns/__init__.py +247 -0
  5. pgstorm-0.1.0/pgstorm/columns/base.py +770 -0
  6. pgstorm-0.1.0/pgstorm/columns/binary.py +25 -0
  7. pgstorm-0.1.0/pgstorm/columns/bit.py +70 -0
  8. pgstorm-0.1.0/pgstorm/columns/boolean.py +40 -0
  9. pgstorm-0.1.0/pgstorm/columns/character.py +134 -0
  10. pgstorm-0.1.0/pgstorm/columns/datetime.py +249 -0
  11. pgstorm-0.1.0/pgstorm/columns/geometric.py +139 -0
  12. pgstorm-0.1.0/pgstorm/columns/json_types.py +47 -0
  13. pgstorm-0.1.0/pgstorm/columns/money.py +25 -0
  14. pgstorm-0.1.0/pgstorm/columns/network.py +86 -0
  15. pgstorm-0.1.0/pgstorm/columns/numeric.py +257 -0
  16. pgstorm-0.1.0/pgstorm/columns/snapshot.py +63 -0
  17. pgstorm-0.1.0/pgstorm/columns/textsearch.py +44 -0
  18. pgstorm-0.1.0/pgstorm/columns/uuid_type.py +41 -0
  19. pgstorm-0.1.0/pgstorm/columns/vector.py +130 -0
  20. pgstorm-0.1.0/pgstorm/columns/xml_type.py +25 -0
  21. pgstorm-0.1.0/pgstorm/engine/__init__.py +49 -0
  22. pgstorm-0.1.0/pgstorm/engine/base.py +265 -0
  23. pgstorm-0.1.0/pgstorm/engine/context.py +15 -0
  24. pgstorm-0.1.0/pgstorm/engine/create.py +77 -0
  25. pgstorm-0.1.0/pgstorm/engine/interface.py +46 -0
  26. pgstorm-0.1.0/pgstorm/engine/interfaces/__init__.py +15 -0
  27. pgstorm-0.1.0/pgstorm/engine/interfaces/asyncpg.py +71 -0
  28. pgstorm-0.1.0/pgstorm/engine/interfaces/psycopg2.py +67 -0
  29. pgstorm-0.1.0/pgstorm/engine/interfaces/psycopg3_async.py +67 -0
  30. pgstorm-0.1.0/pgstorm/engine/interfaces/psycopg3_sync.py +59 -0
  31. pgstorm-0.1.0/pgstorm/engine/query_utils.py +56 -0
  32. pgstorm-0.1.0/pgstorm/functions/__init__.py +0 -0
  33. pgstorm-0.1.0/pgstorm/functions/aggregate.py +61 -0
  34. pgstorm-0.1.0/pgstorm/functions/expression.py +459 -0
  35. pgstorm-0.1.0/pgstorm/functions/func.py +139 -0
  36. pgstorm-0.1.0/pgstorm/models.py +243 -0
  37. pgstorm-0.1.0/pgstorm/observers.py +189 -0
  38. pgstorm-0.1.0/pgstorm/operator.py +22 -0
  39. pgstorm-0.1.0/pgstorm/prefetch.py +29 -0
  40. pgstorm-0.1.0/pgstorm/queryset/__init__.py +0 -0
  41. pgstorm-0.1.0/pgstorm/queryset/base.py +888 -0
  42. pgstorm-0.1.0/pgstorm/queryset/parser.py +1873 -0
  43. pgstorm-0.1.0/pgstorm/queryset/prefetch_impl.py +224 -0
  44. pgstorm-0.1.0/pgstorm/queryset/q.py +0 -0
  45. pgstorm-0.1.0/pgstorm/types.py +91 -0
  46. pgstorm-0.1.0/pgstorm/views.py +46 -0
  47. pgstorm-0.1.0/pgstorm.egg-info/PKG-INFO +364 -0
  48. pgstorm-0.1.0/pgstorm.egg-info/SOURCES.txt +51 -0
  49. pgstorm-0.1.0/pgstorm.egg-info/dependency_links.txt +1 -0
  50. pgstorm-0.1.0/pgstorm.egg-info/requires.txt +21 -0
  51. pgstorm-0.1.0/pgstorm.egg-info/top_level.txt +1 -0
  52. pgstorm-0.1.0/pyproject.toml +52 -0
  53. pgstorm-0.1.0/setup.cfg +4 -0
pgstorm-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,364 @@
1
+ Metadata-Version: 2.4
2
+ Name: pgstorm
3
+ Version: 0.1.0
4
+ Summary: A lightweight PostgreSQL query builder and mini-ORM for Python
5
+ Author: pgstorm contributors
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/your-org/pgstorm
8
+ Project-URL: Documentation, https://github.com/your-org/pgstorm#readme
9
+ Keywords: postgresql,orm,query-builder,async,database
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Database
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: psycopg[binary]>=3.0
22
+ Provides-Extra: psycopg2
23
+ Requires-Dist: psycopg2; extra == "psycopg2"
24
+ Provides-Extra: psycopg2-binary
25
+ Requires-Dist: psycopg2-binary; extra == "psycopg2-binary"
26
+ Provides-Extra: psycopg3
27
+ Requires-Dist: psycopg>=3.0; extra == "psycopg3"
28
+ Provides-Extra: psycopg3-binary
29
+ Requires-Dist: psycopg[binary]>=3.0; extra == "psycopg3-binary"
30
+ Provides-Extra: asyncpg
31
+ Requires-Dist: asyncpg; extra == "asyncpg"
32
+ Provides-Extra: all
33
+ Requires-Dist: psycopg2-binary; extra == "all"
34
+ Requires-Dist: psycopg[binary]>=3.0; extra == "all"
35
+ Requires-Dist: asyncpg; extra == "all"
36
+
37
+ # pgstorm
38
+
39
+ A lightweight PostgreSQL query builder and mini-ORM for Python. Compose type-safe queries that compile to parameterized SQL, then execute them using a pluggable engine (sync or async).
40
+
41
+ ## Features
42
+
43
+ - **Type-safe models** — Define models with type annotations; `__table__` or `__tablename__` for table names
44
+ - **Rich QuerySet API** — `filter`, `exclude`, `order_by`, `limit`, `offset`, `join`, `aggregate`, `annotate`, `alias`
45
+ - **Q objects** — Combine conditions with `|` (OR), `&` (AND), `~` (NOT)
46
+ - **Subqueries** — `Subquery` and `OuterRef` for correlated subqueries
47
+ - **F expressions** — Reference annotations/aliases in filters and `order_by`
48
+ - **SQL functions** — `Concat`, `Coalesce`, `Upper`, `Lower`, `Now`, `DateTrunc`, `Func_`, and more
49
+ - **Aggregates** — `Min`, `Max`, `Count`, `Sum`, `Avg`
50
+ - **Writes included** — `create`, `bulk_create`, `update`, `delete` (sync or `await` with async engines)
51
+ - **Engine abstraction** — Sync (psycopg2, psycopg3) and async (psycopg3_async, asyncpg) interfaces
52
+ - **Transactions** — `with pgstorm.transaction():` or `async with pgstorm.transaction():`
53
+ - **Schema support** — `using_schema()` and per-join `rhs_schema`
54
+
55
+ ## Requirements
56
+
57
+ - Python **3.10+**
58
+ - A PostgreSQL database
59
+ - A driver (installed automatically via extras): `psycopg3` (default), `psycopg2`, or `asyncpg`
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ pip install pgstorm
65
+ ```
66
+
67
+ This installs pgstorm with **psycopg3** (the default driver). To use a different driver:
68
+
69
+ ```bash
70
+ # psycopg2: choose normal (requires libpq) or binary (pre-built)
71
+ pip install pgstorm[psycopg2] # psycopg2 (sync, normal build)
72
+ pip install pgstorm[psycopg2-binary] # psycopg2-binary (sync, pre-built)
73
+
74
+ # psycopg3: choose normal or binary (default uses binary)
75
+ pip install pgstorm[psycopg3] # psycopg3 (normal build)
76
+ pip install pgstorm[psycopg3-binary] # psycopg3 binary (pre-built)
77
+
78
+ pip install pgstorm[asyncpg] # asyncpg (async)
79
+ pip install pgstorm[all] # all drivers (binary variants)
80
+ ```
81
+
82
+ **From source**: `pip install -e .` or `pip install -e ".[asyncpg]"`
83
+
84
+ ## Quick Start
85
+
86
+ ```python
87
+ from pgstorm import BaseModel, types, create_engine
88
+
89
+ class User(BaseModel):
90
+ __table__ = "users"
91
+ id: types.Integer[types.IS_PRIMARY_KEY_FIELD]
92
+ age: types.Integer
93
+ email: types.String
94
+
95
+ class UserProfile(BaseModel):
96
+ __table__ = "user_profile"
97
+ user: types.ForeignKey[User, types.ON_DELETE_CASCADE]
98
+
99
+ # Create engine (sets global context for querysets)
100
+ engine = create_engine("postgresql://user:pass@localhost/dbname", interface="psycopg3")
101
+
102
+ # Build and compile a query
103
+ qs = UserProfile.objects.filter(
104
+ UserProfile.user.email.like("%@example.com")
105
+ ).join(User, UserProfile.user.id == User.id)
106
+
107
+ compiled = qs.compiled()
108
+ print(compiled.sql.as_string(None))
109
+ print(compiled.params)
110
+
111
+ # Execute and iterate (uses engine from context)
112
+ for profile in qs:
113
+ print(profile.user.email)
114
+ ```
115
+
116
+ ## Async example (asyncpg)
117
+
118
+ `QuerySet.fetch()` and other methods return an awaitable when the configured engine is async.
119
+
120
+ ```python
121
+ import asyncio
122
+ from pgstorm import create_engine, Subquery
123
+ from example.model import User, AuditLog
124
+
125
+ db_credentials = {
126
+ "host": "localhost",
127
+ "port": 5432,
128
+ "user": "postgres",
129
+ "password": "admin",
130
+ "dbname": "testdb",
131
+ }
132
+
133
+ async def main():
134
+ create_engine(db_credentials, interface="asyncpg")
135
+
136
+ log = await AuditLog.objects.create(
137
+ user=Subquery(
138
+ User.objects.using_schema("tenant1")
139
+ .filter(User.email == "mohamed@example.com")
140
+ .columns("id")
141
+ ),
142
+ action="INSERT",
143
+ target_table="user",
144
+ target_id=2,
145
+ )
146
+ print("log id:", log.id)
147
+
148
+ rows = await User.objects.using_schema("tenant1").filter(User.email.like("%@example.com")).fetch()
149
+ for user in rows:
150
+ print(user.email)
151
+
152
+ print("count:", await User.objects.count())
153
+
154
+ asyncio.run(main())
155
+ ```
156
+
157
+ ## Models
158
+
159
+ Define models by subclassing `BaseModel` and annotating attributes with `types`:
160
+
161
+ ```python
162
+ from pgstorm import BaseModel, types
163
+
164
+ class Product(BaseModel):
165
+ __table__ = "products"
166
+ id: types.Integer[types.IS_PRIMARY_KEY_FIELD]
167
+ name: types.String
168
+ price: types.Integer
169
+ ```
170
+
171
+ Use `__table__` or `__tablename__` to set the table name; otherwise the class name (lowercased) is used.
172
+
173
+ ### Types
174
+
175
+ - **Scalars**: `types.Integer`, `types.String`, `types.BigSerial`, `types.Jsonb`, `types.Inet`, `types.Varchar(20)`, `types.TimestampTZ(default=...)`
176
+ - **Relations**: `types.ForeignKey[User]`, `types.OneToOne`, `types.ManyToMany`
177
+ - **Relation metadata**: `types.ON_DELETE_CASCADE`, `types.FK_FIELD("email")`, `types.FK_COLUMN("user_email")`, `types.ReverseName("profiles")`
178
+
179
+ ```python
180
+ user: types.ForeignKey[User, types.ON_DELETE_CASCADE, types.FK_FIELD("email")]
181
+ ```
182
+
183
+ If you want your editor/type checker to understand that `profile.user` is a `User`, use `Annotated`:
184
+
185
+ ```python
186
+ from pgstorm.types import Annotated
187
+
188
+ user: Annotated[User, types.ForeignKey[User, types.ON_DELETE_CASCADE]]
189
+ ```
190
+
191
+ ## Engine & Execution
192
+
193
+ Create an engine with `create_engine()`. By default it sets the engine in a context variable so querysets use it automatically.
194
+
195
+ ```python
196
+ from pgstorm import create_engine
197
+
198
+ # Sync (default)
199
+ engine = create_engine("postgresql://user:pass@localhost/db", interface="psycopg3")
200
+
201
+ # Async
202
+ engine = create_engine("postgresql://...", interface="psycopg3_async")
203
+ # or
204
+ engine = create_engine("postgresql://...", interface="asyncpg")
205
+ ```
206
+
207
+ **Interfaces**: `psycopg2`, `psycopg3`, `psycopg3_sync`, `psycopg3_async`, `asyncpg`
208
+
209
+ ### Fetching results
210
+
211
+ ```python
212
+ # Sync: iterate or index
213
+ users = list(User.objects.filter(User.age > 18))
214
+ user = User.objects.filter(User.id == 1)[0]
215
+
216
+ # Async: use await fetch()
217
+ users = await User.objects.filter(User.age > 18).fetch()
218
+ ```
219
+
220
+ ### Transactions
221
+
222
+ ```python
223
+ import pgstorm
224
+
225
+ # Sync
226
+ with pgstorm.transaction():
227
+ # queries run in transaction
228
+ pass
229
+
230
+ # Async
231
+ async with pgstorm.transaction():
232
+ await User.objects.all().fetch()
233
+ ```
234
+
235
+ ## QuerySet API
236
+
237
+ ### Filtering
238
+
239
+ ```python
240
+ # Simple comparisons (==, !=, <, <=, >, >=)
241
+ User.objects.filter(User.age >= 18)
242
+ User.objects.filter(User.email == "a@b.com")
243
+
244
+ # LIKE / ILIKE
245
+ User.objects.filter(User.email.like("%@example.com"))
246
+ User.objects.filter(User.name.ilike("%john%"))
247
+
248
+ # IN
249
+ User.objects.filter(User.id.in_([1, 2, 3]))
250
+ User.objects.filter(User.id.in_(Subquery(Order.objects.columns("user_id"))))
251
+
252
+ # Exclude
253
+ User.objects.filter(User.age > 18).exclude(User.deleted)
254
+ ```
255
+
256
+ ### Q objects (AND / OR / NOT)
257
+
258
+ ```python
259
+ from pgstorm import Q, and_, or_, not_
260
+
261
+ User.objects.filter(Q(User.age > 18) | Q(User.age < 5))
262
+ User.objects.filter(and_(Q(User.active), Q(User.verified)))
263
+ User.objects.filter(~Q(User.deleted))
264
+ ```
265
+
266
+ ### Joins
267
+
268
+ ```python
269
+ UserProfile.objects.join(
270
+ User,
271
+ UserProfile.user.email == User.email,
272
+ join_type="LEFT"
273
+ )
274
+ ```
275
+
276
+ ### Schemas
277
+
278
+ ```python
279
+ User.objects.using_schema("tenant_1").filter(...)
280
+ UserProfile.objects.join(User, ..., rhs_schema="tenant_2")
281
+ ```
282
+
283
+ ### Aggregates
284
+
285
+ ```python
286
+ from pgstorm import Min, Max, Count, Sum, Avg
287
+
288
+ # Positional: alias = col_name_function_name (e.g. price_min)
289
+ Product.objects.aggregate(Min(Product.price), Max(Product.price))
290
+
291
+ # Keyword: alias = key
292
+ Product.objects.aggregate(total=Sum(Product.price), cnt=Count())
293
+
294
+ # COUNT(*)
295
+ Product.objects.aggregate(row_count=Count())
296
+ ```
297
+
298
+ ### Annotate & Alias
299
+
300
+ ```python
301
+ from pgstorm import Concat, F
302
+
303
+ # annotate: add computed columns to SELECT; results include them
304
+ User.objects.annotate(full_name=Concat(User.first_name, " ", User.last_name))
305
+
306
+ # alias: define expressions for filter/order_by without including in SELECT
307
+ User.objects.alias(full_name=Concat(User.first_name, " ", User.last_name)).filter(
308
+ F("full_name").ilike("%mohamed%")
309
+ )
310
+ ```
311
+
312
+ ### Subqueries & OuterRef
313
+
314
+ ```python
315
+ from pgstorm import Subquery, OuterRef
316
+
317
+ # Users who have at least one order
318
+ User.objects.filter(
319
+ User.id.in_(
320
+ Subquery(
321
+ Order.objects.filter(Order.user_id == OuterRef(User.id)).columns("user_id")
322
+ )
323
+ )
324
+ )
325
+ ```
326
+
327
+ ### Other
328
+
329
+ - `order_by(User.age)` — ORDER BY
330
+ - `limit(10)`, `offset(20)` — LIMIT / OFFSET
331
+ - `distinct()` — SELECT DISTINCT
332
+ - `defer("col")`, `columns("col1", "col2")` — column selection
333
+ - `as_cte(name)` — use queryset as CTE
334
+
335
+ ## Compiling to SQL
336
+
337
+ ```python
338
+ qs = User.objects.filter(User.age > 18).limit(10)
339
+ compiled = qs.compiled()
340
+
341
+ # For psycopg
342
+ sql, params = qs.as_sql()
343
+ cursor.execute(sql, params)
344
+ ```
345
+
346
+ ## SQL Functions
347
+
348
+ Built-in: `Concat`, `Coalesce`, `Upper`, `Lower`, `Length`, `Trim`, `Replace`, `NullIf`, `Abs`, `Round`, `Floor`, `Ceil`, `Now`, `CurrentDate`, `CurrentTimestamp`, `DateTrunc`. Use `Func_("name", arg1, arg2)` for any other PostgreSQL function.
349
+
350
+ ## Documentation
351
+
352
+ See the [docs/](docs/) folder for detailed documentation:
353
+
354
+ - [Installation & Setup](docs/installation.md)
355
+ - [Models & Types](docs/models.md)
356
+ - [QuerySet API](docs/queryset.md)
357
+ - [Engine & Execution](docs/engine.md)
358
+ - [Functions & Aggregates](docs/functions.md)
359
+ - [Subqueries](docs/subqueries.md)
360
+ - [API Reference](docs/api-reference.md)
361
+
362
+ ## License
363
+
364
+ Not specified yet (README previously said MIT, but a `LICENSE` file is not currently present in this repository).
@@ -0,0 +1,328 @@
1
+ # pgstorm
2
+
3
+ A lightweight PostgreSQL query builder and mini-ORM for Python. Compose type-safe queries that compile to parameterized SQL, then execute them using a pluggable engine (sync or async).
4
+
5
+ ## Features
6
+
7
+ - **Type-safe models** — Define models with type annotations; `__table__` or `__tablename__` for table names
8
+ - **Rich QuerySet API** — `filter`, `exclude`, `order_by`, `limit`, `offset`, `join`, `aggregate`, `annotate`, `alias`
9
+ - **Q objects** — Combine conditions with `|` (OR), `&` (AND), `~` (NOT)
10
+ - **Subqueries** — `Subquery` and `OuterRef` for correlated subqueries
11
+ - **F expressions** — Reference annotations/aliases in filters and `order_by`
12
+ - **SQL functions** — `Concat`, `Coalesce`, `Upper`, `Lower`, `Now`, `DateTrunc`, `Func_`, and more
13
+ - **Aggregates** — `Min`, `Max`, `Count`, `Sum`, `Avg`
14
+ - **Writes included** — `create`, `bulk_create`, `update`, `delete` (sync or `await` with async engines)
15
+ - **Engine abstraction** — Sync (psycopg2, psycopg3) and async (psycopg3_async, asyncpg) interfaces
16
+ - **Transactions** — `with pgstorm.transaction():` or `async with pgstorm.transaction():`
17
+ - **Schema support** — `using_schema()` and per-join `rhs_schema`
18
+
19
+ ## Requirements
20
+
21
+ - Python **3.10+**
22
+ - A PostgreSQL database
23
+ - A driver (installed automatically via extras): `psycopg3` (default), `psycopg2`, or `asyncpg`
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install pgstorm
29
+ ```
30
+
31
+ This installs pgstorm with **psycopg3** (the default driver). To use a different driver:
32
+
33
+ ```bash
34
+ # psycopg2: choose normal (requires libpq) or binary (pre-built)
35
+ pip install pgstorm[psycopg2] # psycopg2 (sync, normal build)
36
+ pip install pgstorm[psycopg2-binary] # psycopg2-binary (sync, pre-built)
37
+
38
+ # psycopg3: choose normal or binary (default uses binary)
39
+ pip install pgstorm[psycopg3] # psycopg3 (normal build)
40
+ pip install pgstorm[psycopg3-binary] # psycopg3 binary (pre-built)
41
+
42
+ pip install pgstorm[asyncpg] # asyncpg (async)
43
+ pip install pgstorm[all] # all drivers (binary variants)
44
+ ```
45
+
46
+ **From source**: `pip install -e .` or `pip install -e ".[asyncpg]"`
47
+
48
+ ## Quick Start
49
+
50
+ ```python
51
+ from pgstorm import BaseModel, types, create_engine
52
+
53
+ class User(BaseModel):
54
+ __table__ = "users"
55
+ id: types.Integer[types.IS_PRIMARY_KEY_FIELD]
56
+ age: types.Integer
57
+ email: types.String
58
+
59
+ class UserProfile(BaseModel):
60
+ __table__ = "user_profile"
61
+ user: types.ForeignKey[User, types.ON_DELETE_CASCADE]
62
+
63
+ # Create engine (sets global context for querysets)
64
+ engine = create_engine("postgresql://user:pass@localhost/dbname", interface="psycopg3")
65
+
66
+ # Build and compile a query
67
+ qs = UserProfile.objects.filter(
68
+ UserProfile.user.email.like("%@example.com")
69
+ ).join(User, UserProfile.user.id == User.id)
70
+
71
+ compiled = qs.compiled()
72
+ print(compiled.sql.as_string(None))
73
+ print(compiled.params)
74
+
75
+ # Execute and iterate (uses engine from context)
76
+ for profile in qs:
77
+ print(profile.user.email)
78
+ ```
79
+
80
+ ## Async example (asyncpg)
81
+
82
+ `QuerySet.fetch()` and other methods return an awaitable when the configured engine is async.
83
+
84
+ ```python
85
+ import asyncio
86
+ from pgstorm import create_engine, Subquery
87
+ from example.model import User, AuditLog
88
+
89
+ db_credentials = {
90
+ "host": "localhost",
91
+ "port": 5432,
92
+ "user": "postgres",
93
+ "password": "admin",
94
+ "dbname": "testdb",
95
+ }
96
+
97
+ async def main():
98
+ create_engine(db_credentials, interface="asyncpg")
99
+
100
+ log = await AuditLog.objects.create(
101
+ user=Subquery(
102
+ User.objects.using_schema("tenant1")
103
+ .filter(User.email == "mohamed@example.com")
104
+ .columns("id")
105
+ ),
106
+ action="INSERT",
107
+ target_table="user",
108
+ target_id=2,
109
+ )
110
+ print("log id:", log.id)
111
+
112
+ rows = await User.objects.using_schema("tenant1").filter(User.email.like("%@example.com")).fetch()
113
+ for user in rows:
114
+ print(user.email)
115
+
116
+ print("count:", await User.objects.count())
117
+
118
+ asyncio.run(main())
119
+ ```
120
+
121
+ ## Models
122
+
123
+ Define models by subclassing `BaseModel` and annotating attributes with `types`:
124
+
125
+ ```python
126
+ from pgstorm import BaseModel, types
127
+
128
+ class Product(BaseModel):
129
+ __table__ = "products"
130
+ id: types.Integer[types.IS_PRIMARY_KEY_FIELD]
131
+ name: types.String
132
+ price: types.Integer
133
+ ```
134
+
135
+ Use `__table__` or `__tablename__` to set the table name; otherwise the class name (lowercased) is used.
136
+
137
+ ### Types
138
+
139
+ - **Scalars**: `types.Integer`, `types.String`, `types.BigSerial`, `types.Jsonb`, `types.Inet`, `types.Varchar(20)`, `types.TimestampTZ(default=...)`
140
+ - **Relations**: `types.ForeignKey[User]`, `types.OneToOne`, `types.ManyToMany`
141
+ - **Relation metadata**: `types.ON_DELETE_CASCADE`, `types.FK_FIELD("email")`, `types.FK_COLUMN("user_email")`, `types.ReverseName("profiles")`
142
+
143
+ ```python
144
+ user: types.ForeignKey[User, types.ON_DELETE_CASCADE, types.FK_FIELD("email")]
145
+ ```
146
+
147
+ If you want your editor/type checker to understand that `profile.user` is a `User`, use `Annotated`:
148
+
149
+ ```python
150
+ from pgstorm.types import Annotated
151
+
152
+ user: Annotated[User, types.ForeignKey[User, types.ON_DELETE_CASCADE]]
153
+ ```
154
+
155
+ ## Engine & Execution
156
+
157
+ Create an engine with `create_engine()`. By default it sets the engine in a context variable so querysets use it automatically.
158
+
159
+ ```python
160
+ from pgstorm import create_engine
161
+
162
+ # Sync (default)
163
+ engine = create_engine("postgresql://user:pass@localhost/db", interface="psycopg3")
164
+
165
+ # Async
166
+ engine = create_engine("postgresql://...", interface="psycopg3_async")
167
+ # or
168
+ engine = create_engine("postgresql://...", interface="asyncpg")
169
+ ```
170
+
171
+ **Interfaces**: `psycopg2`, `psycopg3`, `psycopg3_sync`, `psycopg3_async`, `asyncpg`
172
+
173
+ ### Fetching results
174
+
175
+ ```python
176
+ # Sync: iterate or index
177
+ users = list(User.objects.filter(User.age > 18))
178
+ user = User.objects.filter(User.id == 1)[0]
179
+
180
+ # Async: use await fetch()
181
+ users = await User.objects.filter(User.age > 18).fetch()
182
+ ```
183
+
184
+ ### Transactions
185
+
186
+ ```python
187
+ import pgstorm
188
+
189
+ # Sync
190
+ with pgstorm.transaction():
191
+ # queries run in transaction
192
+ pass
193
+
194
+ # Async
195
+ async with pgstorm.transaction():
196
+ await User.objects.all().fetch()
197
+ ```
198
+
199
+ ## QuerySet API
200
+
201
+ ### Filtering
202
+
203
+ ```python
204
+ # Simple comparisons (==, !=, <, <=, >, >=)
205
+ User.objects.filter(User.age >= 18)
206
+ User.objects.filter(User.email == "a@b.com")
207
+
208
+ # LIKE / ILIKE
209
+ User.objects.filter(User.email.like("%@example.com"))
210
+ User.objects.filter(User.name.ilike("%john%"))
211
+
212
+ # IN
213
+ User.objects.filter(User.id.in_([1, 2, 3]))
214
+ User.objects.filter(User.id.in_(Subquery(Order.objects.columns("user_id"))))
215
+
216
+ # Exclude
217
+ User.objects.filter(User.age > 18).exclude(User.deleted)
218
+ ```
219
+
220
+ ### Q objects (AND / OR / NOT)
221
+
222
+ ```python
223
+ from pgstorm import Q, and_, or_, not_
224
+
225
+ User.objects.filter(Q(User.age > 18) | Q(User.age < 5))
226
+ User.objects.filter(and_(Q(User.active), Q(User.verified)))
227
+ User.objects.filter(~Q(User.deleted))
228
+ ```
229
+
230
+ ### Joins
231
+
232
+ ```python
233
+ UserProfile.objects.join(
234
+ User,
235
+ UserProfile.user.email == User.email,
236
+ join_type="LEFT"
237
+ )
238
+ ```
239
+
240
+ ### Schemas
241
+
242
+ ```python
243
+ User.objects.using_schema("tenant_1").filter(...)
244
+ UserProfile.objects.join(User, ..., rhs_schema="tenant_2")
245
+ ```
246
+
247
+ ### Aggregates
248
+
249
+ ```python
250
+ from pgstorm import Min, Max, Count, Sum, Avg
251
+
252
+ # Positional: alias = col_name_function_name (e.g. price_min)
253
+ Product.objects.aggregate(Min(Product.price), Max(Product.price))
254
+
255
+ # Keyword: alias = key
256
+ Product.objects.aggregate(total=Sum(Product.price), cnt=Count())
257
+
258
+ # COUNT(*)
259
+ Product.objects.aggregate(row_count=Count())
260
+ ```
261
+
262
+ ### Annotate & Alias
263
+
264
+ ```python
265
+ from pgstorm import Concat, F
266
+
267
+ # annotate: add computed columns to SELECT; results include them
268
+ User.objects.annotate(full_name=Concat(User.first_name, " ", User.last_name))
269
+
270
+ # alias: define expressions for filter/order_by without including in SELECT
271
+ User.objects.alias(full_name=Concat(User.first_name, " ", User.last_name)).filter(
272
+ F("full_name").ilike("%mohamed%")
273
+ )
274
+ ```
275
+
276
+ ### Subqueries & OuterRef
277
+
278
+ ```python
279
+ from pgstorm import Subquery, OuterRef
280
+
281
+ # Users who have at least one order
282
+ User.objects.filter(
283
+ User.id.in_(
284
+ Subquery(
285
+ Order.objects.filter(Order.user_id == OuterRef(User.id)).columns("user_id")
286
+ )
287
+ )
288
+ )
289
+ ```
290
+
291
+ ### Other
292
+
293
+ - `order_by(User.age)` — ORDER BY
294
+ - `limit(10)`, `offset(20)` — LIMIT / OFFSET
295
+ - `distinct()` — SELECT DISTINCT
296
+ - `defer("col")`, `columns("col1", "col2")` — column selection
297
+ - `as_cte(name)` — use queryset as CTE
298
+
299
+ ## Compiling to SQL
300
+
301
+ ```python
302
+ qs = User.objects.filter(User.age > 18).limit(10)
303
+ compiled = qs.compiled()
304
+
305
+ # For psycopg
306
+ sql, params = qs.as_sql()
307
+ cursor.execute(sql, params)
308
+ ```
309
+
310
+ ## SQL Functions
311
+
312
+ Built-in: `Concat`, `Coalesce`, `Upper`, `Lower`, `Length`, `Trim`, `Replace`, `NullIf`, `Abs`, `Round`, `Floor`, `Ceil`, `Now`, `CurrentDate`, `CurrentTimestamp`, `DateTrunc`. Use `Func_("name", arg1, arg2)` for any other PostgreSQL function.
313
+
314
+ ## Documentation
315
+
316
+ See the [docs/](docs/) folder for detailed documentation:
317
+
318
+ - [Installation & Setup](docs/installation.md)
319
+ - [Models & Types](docs/models.md)
320
+ - [QuerySet API](docs/queryset.md)
321
+ - [Engine & Execution](docs/engine.md)
322
+ - [Functions & Aggregates](docs/functions.md)
323
+ - [Subqueries](docs/subqueries.md)
324
+ - [API Reference](docs/api-reference.md)
325
+
326
+ ## License
327
+
328
+ Not specified yet (README previously said MIT, but a `LICENSE` file is not currently present in this repository).