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.
- pgstorm-0.1.0/PKG-INFO +364 -0
- pgstorm-0.1.0/README.md +328 -0
- pgstorm-0.1.0/pgstorm/__init__.py +77 -0
- pgstorm-0.1.0/pgstorm/columns/__init__.py +247 -0
- pgstorm-0.1.0/pgstorm/columns/base.py +770 -0
- pgstorm-0.1.0/pgstorm/columns/binary.py +25 -0
- pgstorm-0.1.0/pgstorm/columns/bit.py +70 -0
- pgstorm-0.1.0/pgstorm/columns/boolean.py +40 -0
- pgstorm-0.1.0/pgstorm/columns/character.py +134 -0
- pgstorm-0.1.0/pgstorm/columns/datetime.py +249 -0
- pgstorm-0.1.0/pgstorm/columns/geometric.py +139 -0
- pgstorm-0.1.0/pgstorm/columns/json_types.py +47 -0
- pgstorm-0.1.0/pgstorm/columns/money.py +25 -0
- pgstorm-0.1.0/pgstorm/columns/network.py +86 -0
- pgstorm-0.1.0/pgstorm/columns/numeric.py +257 -0
- pgstorm-0.1.0/pgstorm/columns/snapshot.py +63 -0
- pgstorm-0.1.0/pgstorm/columns/textsearch.py +44 -0
- pgstorm-0.1.0/pgstorm/columns/uuid_type.py +41 -0
- pgstorm-0.1.0/pgstorm/columns/vector.py +130 -0
- pgstorm-0.1.0/pgstorm/columns/xml_type.py +25 -0
- pgstorm-0.1.0/pgstorm/engine/__init__.py +49 -0
- pgstorm-0.1.0/pgstorm/engine/base.py +265 -0
- pgstorm-0.1.0/pgstorm/engine/context.py +15 -0
- pgstorm-0.1.0/pgstorm/engine/create.py +77 -0
- pgstorm-0.1.0/pgstorm/engine/interface.py +46 -0
- pgstorm-0.1.0/pgstorm/engine/interfaces/__init__.py +15 -0
- pgstorm-0.1.0/pgstorm/engine/interfaces/asyncpg.py +71 -0
- pgstorm-0.1.0/pgstorm/engine/interfaces/psycopg2.py +67 -0
- pgstorm-0.1.0/pgstorm/engine/interfaces/psycopg3_async.py +67 -0
- pgstorm-0.1.0/pgstorm/engine/interfaces/psycopg3_sync.py +59 -0
- pgstorm-0.1.0/pgstorm/engine/query_utils.py +56 -0
- pgstorm-0.1.0/pgstorm/functions/__init__.py +0 -0
- pgstorm-0.1.0/pgstorm/functions/aggregate.py +61 -0
- pgstorm-0.1.0/pgstorm/functions/expression.py +459 -0
- pgstorm-0.1.0/pgstorm/functions/func.py +139 -0
- pgstorm-0.1.0/pgstorm/models.py +243 -0
- pgstorm-0.1.0/pgstorm/observers.py +189 -0
- pgstorm-0.1.0/pgstorm/operator.py +22 -0
- pgstorm-0.1.0/pgstorm/prefetch.py +29 -0
- pgstorm-0.1.0/pgstorm/queryset/__init__.py +0 -0
- pgstorm-0.1.0/pgstorm/queryset/base.py +888 -0
- pgstorm-0.1.0/pgstorm/queryset/parser.py +1873 -0
- pgstorm-0.1.0/pgstorm/queryset/prefetch_impl.py +224 -0
- pgstorm-0.1.0/pgstorm/queryset/q.py +0 -0
- pgstorm-0.1.0/pgstorm/types.py +91 -0
- pgstorm-0.1.0/pgstorm/views.py +46 -0
- pgstorm-0.1.0/pgstorm.egg-info/PKG-INFO +364 -0
- pgstorm-0.1.0/pgstorm.egg-info/SOURCES.txt +51 -0
- pgstorm-0.1.0/pgstorm.egg-info/dependency_links.txt +1 -0
- pgstorm-0.1.0/pgstorm.egg-info/requires.txt +21 -0
- pgstorm-0.1.0/pgstorm.egg-info/top_level.txt +1 -0
- pgstorm-0.1.0/pyproject.toml +52 -0
- 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).
|
pgstorm-0.1.0/README.md
ADDED
|
@@ -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).
|