supabase-orm 0.1.0__tar.gz → 0.1.2__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.
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/PKG-INFO +210 -30
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/README.md +209 -29
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/src/supabase_orm/__init__.py +5 -2
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/src/supabase_orm/_base.py +48 -7
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/src/supabase_orm/_client.py +29 -51
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/src/supabase_orm/_filters.py +44 -21
- supabase_orm-0.1.2/src/supabase_orm/_predicates.py +309 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/src/supabase_orm/_query.py +281 -112
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/src/supabase_orm/_serializers.py +15 -5
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/src/supabase_orm/_version.py +2 -2
- supabase_orm-0.1.2/tests/integration/test_iter.py +91 -0
- supabase_orm-0.1.2/tests/integration/test_predicates.py +226 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/test_base.py +78 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/test_client.py +34 -76
- supabase_orm-0.1.2/tests/test_iter.py +231 -0
- supabase_orm-0.1.2/tests/test_predicates.py +379 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/test_query.py +92 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/test_wire.py +3 -3
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/uv.lock +3 -3
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/.env.example +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/.github/workflows/publish.yml +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/.gitignore +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/LICENSE +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/pyproject.toml +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/src/supabase_orm/_embed.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/src/supabase_orm/_exceptions.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/src/supabase_orm/_rpc.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/__init__.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/conftest.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/integration/README.md +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/integration/__init__.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/integration/conftest.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/integration/schema.sql +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/integration/test_embeds.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/integration/test_filters.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/integration/test_rpc.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/integration/test_writes_and_terminals.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/test_embed.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/test_exceptions.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/test_filters.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/test_rpc.py +0 -0
- {supabase_orm-0.1.0 → supabase_orm-0.1.2}/tests/test_serializers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: supabase-orm
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Lightweight async ORM on top of supabase-py with Pydantic validation.
|
|
5
5
|
Project-URL: Homepage, https://github.com/viperadnan-git/supabase-orm
|
|
6
6
|
Project-URL: Repository, https://github.com/viperadnan-git/supabase-orm
|
|
@@ -48,19 +48,23 @@ Description-Content-Type: text/markdown
|
|
|
48
48
|
|
|
49
49
|
- **Type-safe query builder.** Every operator on `Model.query` is a real method with a real signature — autocomplete works, typos surface as `AttributeError` at call time, not as silent server-side errors.
|
|
50
50
|
|
|
51
|
+
- **Typed predicate expressions.** `Pet.f.age >= 5` builds a composable `Predicate`. Combine with `|` / `&` / `~` and pass to `.or_()` / `.not_()`. Reads as boolean logic, type-checks as expressions.
|
|
52
|
+
|
|
53
|
+
- **Keyset iteration that scales.** `async for pet in Pet.query.eq(...).iter():` paginates by PK with constant-time-per-batch and no offset cliff. Works on tables of any size; race-safe under concurrent inserts and deletes.
|
|
54
|
+
|
|
51
55
|
- **Pydantic v2 throughout.** Your model *is* the row schema, the response schema, and the request body schema. No DTO layer.
|
|
52
56
|
|
|
53
57
|
- **Async-first.** Built for `asyncio` and FastAPI — every terminal call is `await`-able, every chain is a fresh builder.
|
|
54
58
|
|
|
55
59
|
- **PostgREST embeds, declared at the type level.** Mark a field as `Annotated[Owner, Relation(...)]` and the ORM builds the right `select=` string for you, including `!inner` / FK hints / per-relation filters.
|
|
56
60
|
|
|
57
|
-
- **Per-request RLS via `ContextVar`.**
|
|
61
|
+
- **Per-request RLS via `ContextVar`.** Pair `use_client()` with a JWT-authenticated client in a FastAPI middleware — Postgres row-level security sees the user, with zero leakage between concurrent requests.
|
|
58
62
|
|
|
59
63
|
- **Typed RPC helpers.** Call `setof` functions with row validation, get a single row, or coerce a scalar — all with one line.
|
|
60
64
|
|
|
61
65
|
- **Opt-in foot-gun guards.** Unfiltered bulk `delete()` / `update()` raise unless you pass `allow_unfiltered=True`.
|
|
62
66
|
|
|
63
|
-
- **Tested both ways.**
|
|
67
|
+
- **Tested both ways.** 230+ mock tests cover the wire contract (every operator, every serializer, every shorthand, predicate composition, keyset iteration). 60+ integration tests run against a real Supabase project to confirm PostgREST actually interprets those calls the way we expect.
|
|
64
68
|
|
|
65
69
|
---
|
|
66
70
|
|
|
@@ -105,10 +109,16 @@ class PetWithOwner(SupabaseModel, table="pets"):
|
|
|
105
109
|
|
|
106
110
|
|
|
107
111
|
async with lifespan(SUPABASE_URL, SUPABASE_KEY):
|
|
108
|
-
# Read
|
|
112
|
+
# Read — chain style for sequential AND
|
|
109
113
|
cats = await Pet.query.eq("species", "cat").order_by("-created_at").limit(10).all()
|
|
110
114
|
one = await PetWithOwner.get(some_id)
|
|
111
115
|
|
|
116
|
+
# Read — typed predicates for OR / NOT / nested boolean logic
|
|
117
|
+
rescues = await Pet.query.or_(
|
|
118
|
+
Pet.f.species == "cat",
|
|
119
|
+
(Pet.f.species == "dog") & (Pet.f.adopted == False),
|
|
120
|
+
).all()
|
|
121
|
+
|
|
112
122
|
# Write
|
|
113
123
|
p = await Pet.create(name="Whiskers", species="cat", adopted=False)
|
|
114
124
|
p.name = "Mr. Whiskers"
|
|
@@ -135,11 +145,12 @@ class Pet(SupabaseModel, table="pets"):
|
|
|
135
145
|
|
|
136
146
|
### Class kwargs
|
|
137
147
|
|
|
138
|
-
| Kwarg
|
|
139
|
-
|
|
140
|
-
| `table`
|
|
141
|
-
| `pk`
|
|
142
|
-
| `select`
|
|
148
|
+
| Kwarg | Default | What it does |
|
|
149
|
+
|---------------|------------------|-----------------------------------------------------------------------------|
|
|
150
|
+
| `table` | — | PostgREST table or view name. **Required.** |
|
|
151
|
+
| `pk` | `"id"` | Primary-key field name. Used by `get()`, `save()`, `delete()`, `iter()`. |
|
|
152
|
+
| `select` | auto | Override the auto-derived `select=` string. Escape hatch only. |
|
|
153
|
+
| `query_class` | `QueryBuilder` | Custom `QueryBuilder` subclass for `.query`. MRO-inherited via base models. |
|
|
143
154
|
|
|
144
155
|
### Relations
|
|
145
156
|
|
|
@@ -164,24 +175,58 @@ class PetWithOwner(SupabaseModel, table="pets"):
|
|
|
164
175
|
|
|
165
176
|
### Lean projections
|
|
166
177
|
|
|
167
|
-
Two models
|
|
178
|
+
Two models on the same table — full-detail vs. trimmed view. Just query the lean one directly when its fields are enough:
|
|
168
179
|
|
|
169
180
|
```python
|
|
170
181
|
class Pet(SupabaseModel, table="pets"):
|
|
171
182
|
id: UUID
|
|
172
183
|
name: str
|
|
173
184
|
species: str
|
|
185
|
+
bio: str # heavy column you don't always need
|
|
174
186
|
created_at: datetime
|
|
175
187
|
|
|
176
188
|
class PetMini(SupabaseModel, table="pets"):
|
|
177
189
|
id: UUID
|
|
178
190
|
name: str
|
|
179
191
|
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
192
|
+
# Half the wire bytes, fewer Pydantic fields to validate:
|
|
193
|
+
await PetMini.query.eq("species", "cat").all()
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
For huge result sets, pair with `.iter()` so the lean projection runs per batch:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
async for mini in PetMini.query.iter(batch_size=5000):
|
|
200
|
+
process(mini)
|
|
183
201
|
```
|
|
184
202
|
|
|
203
|
+
### `.as_(target)` — rebind the response shape
|
|
204
|
+
|
|
205
|
+
Use `.as_()` when you need to **filter on source columns the lean model doesn't expose**, or to switch the response shape conditionally without rebuilding the chain. Two modes:
|
|
206
|
+
|
|
207
|
+
**Same-table SupabaseModel** — narrows the wire `select` AND validates against the target:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
# Wire: ?select=id,name → list[PetMini]
|
|
211
|
+
await Pet.query.eq("adopted", False).as_(PetMini).all()
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Plain Pydantic BaseModel** — validation only, wire `select` unchanged. The chain keeps using the source model's columns and predicates:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from pydantic import BaseModel
|
|
218
|
+
|
|
219
|
+
class PetCard(BaseModel):
|
|
220
|
+
id: UUID
|
|
221
|
+
name: str
|
|
222
|
+
|
|
223
|
+
# Filter on `bio` (only on Pet) but validate as PetCard:
|
|
224
|
+
await Pet.query.fts("bio", "fluffy").as_(PetCard).all()
|
|
225
|
+
# → list[PetCard], wire still selects all of Pet's columns
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
If a same-table SupabaseModel target works, prefer it — you get the wire-narrowing optimization. Reach for the plain BaseModel form only when the lean schema doesn't have all the columns you need to filter on.
|
|
229
|
+
|
|
185
230
|
---
|
|
186
231
|
|
|
187
232
|
## Querying
|
|
@@ -196,12 +241,13 @@ mini_rows = await Pet.query.eq("adopted", False).as_(PetMini).all()
|
|
|
196
241
|
| `.maybe_one()` | `T \| None` | At most one row. Raises if >1. |
|
|
197
242
|
| `.count()` | `int` | Head-only request with `count="exact"`. |
|
|
198
243
|
| `.all_with_count()` | `tuple[list[T], int]` | Rows + filtered total in one round-trip (for paginated endpoints). |
|
|
244
|
+
| `.iter(batch_size=)` | `AsyncIterator[T]` | Stream every matching row via PK keyset pagination. Scales to any size. |
|
|
199
245
|
| `.values(*cols)` | `list[dict]` | Ad-hoc projection, raw dicts, no Pydantic validation. |
|
|
200
246
|
| `.raw()` | postgrest builder | Escape hatch. |
|
|
201
247
|
|
|
202
248
|
### Filter operators
|
|
203
249
|
|
|
204
|
-
Every operator below works as a method on `Model.query`
|
|
250
|
+
Every operator below works as a method on `Model.query` (chain style) and as a method/operator on `Model.f.<column>` (predicate style):
|
|
205
251
|
|
|
206
252
|
```python
|
|
207
253
|
Pet.query.eq("species", "cat")
|
|
@@ -216,34 +262,127 @@ Pet.query.overlaps("tags", ["indoor"])
|
|
|
216
262
|
Pet.query.fts("description", "fluffy") # plus plfts / phfts / wfts
|
|
217
263
|
```
|
|
218
264
|
|
|
219
|
-
###
|
|
265
|
+
### Typed predicates (`Pet.f`)
|
|
266
|
+
|
|
267
|
+
For OR / NOT / nested boolean logic, use the `Model.f` namespace. Every column is a typed `Column[T]` with operator overloads — `==`, `!=`, `<`, `<=`, `>`, `>=` build atomic predicates, and `|` / `&` / `~` compose them.
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
from supabase_orm import SupabaseModel
|
|
271
|
+
|
|
272
|
+
class Pet(SupabaseModel, table="pets"):
|
|
273
|
+
id: UUID
|
|
274
|
+
name: str
|
|
275
|
+
species: str
|
|
276
|
+
age: int
|
|
277
|
+
adopted: bool
|
|
278
|
+
tags: list[str]
|
|
279
|
+
|
|
280
|
+
# Atomic predicates — what `==`, `>=` etc. return:
|
|
281
|
+
Pet.f.species == "cat" # column.eq.value
|
|
282
|
+
Pet.f.age >= 5
|
|
283
|
+
Pet.f.name.like("Mr%")
|
|
284
|
+
Pet.f.tags.contains(["indoor"])
|
|
285
|
+
Pet.f.owner_id.is_null()
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Compose with boolean operators:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
# OR
|
|
292
|
+
await Pet.query.or_(
|
|
293
|
+
Pet.f.species == "cat",
|
|
294
|
+
Pet.f.species == "dog",
|
|
295
|
+
).all()
|
|
296
|
+
|
|
297
|
+
# OR with AND inside a branch
|
|
298
|
+
await Pet.query.or_(
|
|
299
|
+
Pet.f.species == "cat",
|
|
300
|
+
(Pet.f.species == "dog") & (Pet.f.age >= 5),
|
|
301
|
+
).all()
|
|
302
|
+
|
|
303
|
+
# NOT — negates any predicate, atomic or compound
|
|
304
|
+
await Pet.query.not_(Pet.f.adopted == True).all()
|
|
305
|
+
await Pet.query.not_((Pet.f.species == "cat") | (Pet.f.species == "dog")).all()
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The chain and predicates compose freely — chain handles sequential AND, predicates handle boolean logic:
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
await (
|
|
312
|
+
Pet.query.eq("owner_id", uid) # chain: AND
|
|
313
|
+
.or_(Pet.f.species == "cat", Pet.f.species == "dog") # predicate: OR
|
|
314
|
+
.order_by("-created_at")
|
|
315
|
+
.limit(20)
|
|
316
|
+
.all()
|
|
317
|
+
)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
#### Foot-gun guards
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
if Pet.f.age >= 5: # TypeError: Predicate is not a bool
|
|
324
|
+
...
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
A predicate is an expression that builds a query — it has no truth value at the call site. The runtime rejects `bool(predicate)` loudly so `if Pet.f.adopted == True:` mistakes fail at the first hit instead of always being truthy.
|
|
328
|
+
|
|
329
|
+
#### Lambda form
|
|
220
330
|
|
|
221
331
|
```python
|
|
222
|
-
# OR across branches:
|
|
223
332
|
await Pet.query.or_(
|
|
224
333
|
lambda q: q.eq("species", "cat"),
|
|
225
334
|
lambda q: q.eq("species", "dog").gte("age", 5),
|
|
226
335
|
).all()
|
|
227
|
-
|
|
228
|
-
# NOT:
|
|
229
|
-
await Pet.query.not_(lambda q: q.eq("adopted", True)).all()
|
|
230
336
|
```
|
|
231
337
|
|
|
338
|
+
Prefer the `Pet.f` form — it composes, reads as boolean logic, and gives operator-level type safety.
|
|
339
|
+
|
|
232
340
|
### `match()` — multi-column equality
|
|
233
341
|
|
|
234
342
|
```python
|
|
235
343
|
await Pet.query.match({"species": "cat", "adopted": False}).all()
|
|
236
344
|
```
|
|
237
345
|
|
|
238
|
-
|
|
346
|
+
`match` is a `postgrest-py` convenience — it just expands to multiple `eq` filters on the wire, equivalent to chaining `.eq()` per pair or composing `(Pet.f.a == 1) & (Pet.f.b == 2)`. Use whichever reads best.
|
|
239
347
|
|
|
240
348
|
### Ordering & pagination
|
|
241
349
|
|
|
350
|
+
`order_by()` accepts strings (`"-col"` for descending) or typed `Pet.f.<col>.asc()` / `.desc()`. The typed form unlocks `nulls="first"` / `"last"` and gets autocomplete + column-existence checks.
|
|
351
|
+
|
|
242
352
|
```python
|
|
353
|
+
# String shorthand
|
|
243
354
|
await Pet.query.order_by("-created_at", "name").limit(20).offset(40).all()
|
|
244
|
-
await Pet.query.range(0, 9).all()
|
|
355
|
+
await Pet.query.range(0, 9).all() # inclusive range
|
|
356
|
+
|
|
357
|
+
# Typed form — autocomplete on Pet.f, nulls position support
|
|
358
|
+
await Pet.query.order_by(Pet.f.created_at.desc()).all()
|
|
359
|
+
await Pet.query.order_by(Pet.f.last_login.desc(nulls="last")).all()
|
|
360
|
+
|
|
361
|
+
# Mix freely
|
|
362
|
+
await Pet.query.order_by("species", Pet.f.amount.desc()).all()
|
|
245
363
|
```
|
|
246
364
|
|
|
365
|
+
Typos in either form raise `AttributeError` at call time — never silent server errors.
|
|
366
|
+
|
|
367
|
+
### Iteration over large result sets
|
|
368
|
+
|
|
369
|
+
`.iter()` streams every matching row using **PK keyset pagination** — `WHERE pk > :cursor ORDER BY pk LIMIT :batch_size` per batch. Constant time per batch regardless of position; scales to billions of rows.
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
async for pet in Pet.query.eq("species", "cat").iter():
|
|
373
|
+
process(pet)
|
|
374
|
+
|
|
375
|
+
# Tune batch size for the memory / round-trip tradeoff:
|
|
376
|
+
async for row in BigTable.query.iter(batch_size=5000):
|
|
377
|
+
...
|
|
378
|
+
|
|
379
|
+
# Compose with projections to narrow wire bytes per batch:
|
|
380
|
+
async for mini in Pet.query.eq("species", "cat").as_(PetMini).iter():
|
|
381
|
+
...
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`.iter()` owns ordering and pagination — chaining `.order_by()` / `.limit()` / `.offset()` / `.range()` before it raises `SupabaseORMUsageError`. Race-safe under concurrent inserts (new rows with `pk > cursor` get picked up) and deletes (deleted rows simply don't appear).
|
|
385
|
+
|
|
247
386
|
### Pagination with total
|
|
248
387
|
|
|
249
388
|
```python
|
|
@@ -363,19 +502,24 @@ app = FastAPI(lifespan=lifespan)
|
|
|
363
502
|
|
|
364
503
|
### Per-request RLS via JWT
|
|
365
504
|
|
|
366
|
-
The client is stored in a `ContextVar`, so each FastAPI request runs in its own copied context
|
|
505
|
+
The client is stored in a `ContextVar`, so each FastAPI request runs in its own copied context. Pair `use_client()` with a per-request authenticated client to isolate the JWT — and therefore the RLS identity — to that request only:
|
|
367
506
|
|
|
368
507
|
```python
|
|
369
|
-
from
|
|
508
|
+
from supabase import acreate_client
|
|
509
|
+
from supabase_orm import use_client
|
|
370
510
|
|
|
371
511
|
@app.middleware("http")
|
|
372
|
-
async def
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
512
|
+
async def per_request_client(request, call_next):
|
|
513
|
+
auth = request.headers.get("authorization")
|
|
514
|
+
if not auth:
|
|
515
|
+
return await call_next(request)
|
|
516
|
+
|
|
517
|
+
jwt = auth.removeprefix("Bearer ")
|
|
518
|
+
client = await acreate_client(SUPABASE_URL, SUPABASE_ANON_KEY)
|
|
519
|
+
client.postgrest.auth(jwt)
|
|
520
|
+
|
|
521
|
+
async with use_client(client):
|
|
376
522
|
return await call_next(request)
|
|
377
|
-
finally:
|
|
378
|
-
set_auth(None) # back to the anon/service-role key from lifespan
|
|
379
523
|
```
|
|
380
524
|
|
|
381
525
|
Inside a handler, just call the ORM as usual — Postgres RLS sees the user:
|
|
@@ -386,6 +530,9 @@ async def my_pets():
|
|
|
386
530
|
return await Pet.query.order_by("-created_at").all()
|
|
387
531
|
```
|
|
388
532
|
|
|
533
|
+
> [!IMPORTANT]
|
|
534
|
+
> Don't mutate the app-wide client's auth headers across requests (e.g. `get_client().postgrest.auth(jwt)` directly). The underlying postgrest sub-client is shared, so the mutation leaks across overlapping requests. `ContextVar` isolates *references*, not the objects they point at — `use_client()` with a per-request client is the only safe pattern.
|
|
535
|
+
|
|
389
536
|
### Per-request override with a dedicated client
|
|
390
537
|
|
|
391
538
|
For background tasks, scripts, or any place outside the request lifecycle:
|
|
@@ -428,6 +575,40 @@ class Money:
|
|
|
428
575
|
register_serializer(Money, lambda v: v.cents)
|
|
429
576
|
```
|
|
430
577
|
|
|
578
|
+
### Custom `QueryBuilder` methods
|
|
579
|
+
|
|
580
|
+
Subclass `QueryBuilder` to add your own chainable methods, then opt in either per-model or project-wide.
|
|
581
|
+
|
|
582
|
+
```python
|
|
583
|
+
from supabase_orm import QueryBuilder, SupabaseModel
|
|
584
|
+
|
|
585
|
+
class PaginatedQB(QueryBuilder):
|
|
586
|
+
async def paginate(self, *, page: int, per_page: int):
|
|
587
|
+
return await self.range(
|
|
588
|
+
page * per_page, (page + 1) * per_page - 1
|
|
589
|
+
).all_with_count()
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Project-wide** — define a base model once, every subclass inherits via MRO:
|
|
593
|
+
|
|
594
|
+
```python
|
|
595
|
+
class _AppModel(SupabaseModel):
|
|
596
|
+
__query_class__ = PaginatedQB
|
|
597
|
+
|
|
598
|
+
class User(_AppModel, table="users"): # gets PaginatedQB automatically
|
|
599
|
+
id: UUID
|
|
600
|
+
email: str
|
|
601
|
+
|
|
602
|
+
rows, total = await User.query.eq("is_active", True).paginate(page=0, per_page=20)
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
**Per-model** — opt in inline, no base class needed:
|
|
606
|
+
|
|
607
|
+
```python
|
|
608
|
+
class Audit(SupabaseModel, table="audit_log", query_class=PaginatedQB):
|
|
609
|
+
...
|
|
610
|
+
```
|
|
611
|
+
|
|
431
612
|
---
|
|
432
613
|
|
|
433
614
|
## Error handling
|
|
@@ -456,7 +637,6 @@ from supabase_orm import (
|
|
|
456
637
|
- Unfiltered bulk `delete()` / `update()` without `allow_unfiltered=True`.
|
|
457
638
|
- `as_()` to a model on a different table.
|
|
458
639
|
- `instance.update()` trying to change the primary key or a relation field.
|
|
459
|
-
- `set_auth(None)` with no recorded default key.
|
|
460
640
|
|
|
461
641
|
Map them to HTTP responses in one place:
|
|
462
642
|
|