Skip to content

Queries

Model.where(...) and Model.select() return a Query — an immutable, chainable builder that executes when awaited via all(), first(), count(), exists(), update(), or delete(). Predicates are lambda-only (User.where(lambda user: user.age >= 18)) — a fresh QueryProxy validates column names against the model at build time.

Query

Bases: Generic[T]

Build and execute fluent ORM queries.

Attributes:

Name Type Description
model_cls

Model class used to hydrate results.

where_clause list[QueryNode]

Accumulated filter nodes for the query.

order_by_clause list[dict[str, str]]

Sort definitions sent to the Rust core.

Source code in src/ferro/query/builder.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
class Query(Generic[T]):
    """Build and execute fluent ORM queries.

    Attributes:
        model_cls: Model class used to hydrate results.
        where_clause: Accumulated filter nodes for the query.
        order_by_clause: Sort definitions sent to the Rust core.
    """

    def __init__(
        self, model_cls: Type[T], using: str | None = None, session: Any | None = None
    ):
        """Initialize a query for a model class.

        Args:
            model_cls: Model class that defines the target table.

        Examples:
            >>> query = Query(User)
            >>> query.model_cls is User
            True
        """
        self.model_cls = model_cls
        self._using = using
        self._session = session
        self.where_clause: list["QueryNode"] = []
        self.order_by_clause: list[dict[str, str]] = []
        self._limit: int | None = None
        self._offset: int | None = None
        self._m2m_context: dict[str, Any] | None = None

    def _transaction_or_using(self) -> "RouteHandle":
        from ..state import resolve_operation_scope

        return resolve_operation_scope(using=self._using, session=self._session)

    def _clone(self) -> Self:
        """Return a copy of this query with no shared mutable state.

        ``copy.copy`` preserves the concrete class (``Relation`` stays
        ``Relation``); the mutable containers are then replaced so chained
        queries never alias the originals (FF-F F-1).
        """
        new = copy.copy(self)
        new.where_clause = list(self.where_clause)
        new.order_by_clause = list(self.order_by_clause)
        new._m2m_context = (
            dict(self._m2m_context) if self._m2m_context is not None else None
        )
        return new

    def _m2m(
        self, join_table: str, source_col: str, target_col: str, source_id: Any
    ) -> Self:
        """Store many-to-many linkage context for relationship operations.

        A new ``Query`` with the m2m context set; ``self`` is unchanged.
        """
        new = self._clone()
        new._m2m_context = {
            "join_table": join_table,
            "source_col": source_col,
            "target_col": target_col,
            "source_id": source_id,
        }
        return new

    def where(self, predicate: "Predicate[T]") -> Self:
        """Add a filter condition to the query.

        ``predicate`` is a lambda of shape ``Callable[[QueryProxy[T]], QueryNode]``.
        The lambda receives a fresh :class:`QueryProxy` whose attributes
        return :class:`FieldProxy` instances, so
        ``lambda user: user.archived == False`` builds a comparison. Column
        names are validated at build time against the model's declared
        fields (plus shadow ``{fk}_id`` columns): a misspelled column raises
        ``AttributeError`` naming the closest valid match, before any query
        is sent to the database.

        Args:
            predicate: A callable that takes a :class:`QueryProxy` and
                returns a :class:`QueryNode`.

        Returns:
            A new ``Query`` with the clause added; ``self`` is unchanged.

        Raises:
            TypeError: If ``predicate`` is not callable, or if it does not
                return a ``QueryNode``.

        Examples:
            >>> q1 = User.where(lambda user: user.archived == False)  # noqa: E712
            >>> q2 = User.where(lambda user: user.id == 1)
            >>> isinstance(q1, Query) and isinstance(q2, Query)
            True
        """
        new = self._clone()
        new.where_clause.append(_resolve_where_node(predicate, self.model_cls))
        return new

    def order_by(
        self,
        field: "str | Callable[[QueryProxy[T]], FieldProxy[Any]]",
        direction: str = "asc",
    ) -> Self:
        """Add an ordering clause and return a new query.

        Accepts a lambda naming the column (``order_by(lambda u: u.created_at,
        "desc")`` — the documented style, matching ``where`` predicates) or a
        column-name string (``order_by("created_at", "desc")``). Both forms are
        validated against the model's queryable columns at build time.

        Args:
            field: Column selector — lambda receiving a :class:`QueryProxy`,
                or a column-name string.
            direction: ``"asc"`` (default) or ``"desc"``.

        Returns:
            A new ``Query`` with the ordering added; ``self`` is unchanged.

        Raises:
            AttributeError: If the column is not a queryable column.
            TypeError: If a lambda selector returns something other than a
                single column reference.
            ValueError: If ``direction`` is not ``"asc"`` or ``"desc"``.

        Examples:
            >>> newest = await Post.select().order_by(lambda p: p.created_at, "desc").all()
        """
        if direction.lower() not in ("asc", "desc"):
            raise ValueError("direction must be 'asc' or 'desc'")

        if isinstance(field, str):
            col_name = validate_query_column(self.model_cls, field)
        elif callable(field):
            selected = field(QueryProxy(self.model_cls))
            if not isinstance(selected, FieldProxy):
                raise TypeError(
                    "order_by() selector must return a FieldProxy "
                    f"(e.g. `lambda u: u.created_at`), got {type(selected).__name__}"
                )
            col_name = selected.column
        else:
            raise TypeError(
                "order_by() expected a column-name string or a lambda selector, "
                f"got {type(field).__name__}"
            )

        new = self._clone()
        new.order_by_clause.append({"column": col_name, "direction": direction.lower()})
        return new

    def limit(self, value: int) -> Self:
        """Limit the number of records returned

        Args:
            value: The maximum number of records to return.

        Returns:
            A new ``Query`` with the clause added; ``self`` is unchanged.

        Examples:
            >>> query = User.select().limit(10)
            >>> query._limit
            10
        """
        new = self._clone()
        new._limit = value
        return new

    def offset(self, value: int) -> Self:
        """Skip a specific number of records

        Args:
            value: The number of records to skip.

        Returns:
            A new ``Query`` with the clause added; ``self`` is unchanged.

        Examples:
            >>> query = User.select().offset(20)
            >>> query._offset
            20
        """
        new = self._clone()
        new._offset = value
        return new

    def _mutating_query_def(self, operation: str) -> dict[str, Any]:
        """Build the QueryIR payload for a mutating operation (update/delete).

        Mutating payloads never carry ``limit``/``offset`` keys: portable SQL
        has no ``UPDATE/DELETE ... LIMIT``, so pagination on a mutation is
        rejected loudly instead of being silently ignored.

        Raises:
            ValueError: If ``limit()`` or ``offset()`` was set on this query.
        """
        if self._limit is not None or self._offset is not None:
            raise ValueError(
                f"{operation}() does not support limit/offset: portable SQL has "
                f"no {operation.upper()} ... LIMIT. Remove the .limit()/.offset() "
                f"call, or fetch primary keys first and {operation} by "
                "primary-key set."
            )
        return {
            "model_name": _model_identity(self.model_cls),
            "where": [node.to_ir_dict() for node in self.where_clause],
            "order_by": [],
            "m2m": None,
        }

    async def all(self) -> list[T]:
        """Return all model instances that match the current query

        Returns:
            A list of model instances.

        Examples:
            >>> users = await User.where(lambda user: user.active == True).all()  # noqa: E712
            >>> isinstance(users, list)
            True
        """
        query_def = {
            "model_name": _model_identity(self.model_cls),
            "where": [node.to_ir_dict() for node in self.where_clause],
            "order_by": self.order_by_clause,
            "limit": self._limit,
            "offset": self._offset,
            "m2m": self._m2m_context,
        }
        route = self._transaction_or_using()
        return await fetch_filtered(
            self.model_cls,
            _query_ir_payload_to_json(query_def),
            route,
        )

    async def count(self) -> int:
        """Return the number of records that match the current query

        Returns:
            The count of matching records.

        Examples:
            >>> total = await User.where(lambda user: user.active == True).count()  # noqa: E712
            >>> isinstance(total, int)
            True
        """
        query_def = {
            "model_name": _model_identity(self.model_cls),
            "where": [node.to_ir_dict() for node in self.where_clause],
            "order_by": [],
            "limit": None,
            "offset": None,
            "m2m": self._m2m_context,
        }
        route = self._transaction_or_using()
        return await count_filtered(
            _model_identity(self.model_cls),
            _query_ir_payload_to_json(query_def),
            route,
        )

    async def update(self, **fields) -> int:
        """Update all records matching the current query

        Args:
            **fields: Field names and values to update.

        Returns:
            The number of records updated.

        Raises:
            ValueError: If ``limit()`` or ``offset()`` was set on this query.

        Examples:
            >>> updated = await User.where(lambda user: user.id == 1).update(name="Taylor")
            >>> isinstance(updated, int)
            True
        """
        query_def = self._mutating_query_def("update")
        route = self._transaction_or_using()
        return await update_filtered(
            _model_identity(self.model_cls),
            _query_ir_payload_to_json(query_def),
            update_bind_payload(fields),
            route,
        )

    async def first(self) -> T | None:
        """Return the first matching record, or None

        Returns:
            A model instance or None.

        Examples:
            >>> user = await User.select().order_by("id").first()
            >>> user is None or isinstance(user, User)
            True
        """
        results = await self.limit(1).all()
        return results[0] if results else None

    async def delete(self) -> int:
        """Delete all records matching the current query

        Returns:
            The number of records deleted.

        Raises:
            ValueError: If ``limit()`` or ``offset()`` was set on this query.

        Examples:
            >>> deleted = await User.where(lambda user: user.disabled == True).delete()  # noqa: E712
            >>> isinstance(deleted, int)
            True
        """
        query_def = self._mutating_query_def("delete")
        route = self._transaction_or_using()
        return await delete_filtered(
            _model_identity(self.model_cls),
            _query_ir_payload_to_json(query_def),
            route,
        )

    async def exists(self) -> bool:
        """Return whether at least one record matches the current query

        Returns:
            True if records exist, otherwise False.

        Examples:
            >>> found = await User.where(lambda user: user.email == "[email protected]").exists()
            >>> isinstance(found, bool)
            True
        """
        return await self.count() > 0

    async def add(self, *instances: Any) -> None:
        """Add links to a many-to-many relationship

        Args:
            *instances: Target model instances that provide an ``id`` attribute.

        Raises:
            RuntimeError: If the query is not bound to a many-to-many context.

        Examples:
            >>> user = await User.create(email="[email protected]")
            >>> admin = await Group.create(name="admin")
            >>> staff = await Group.create(name="staff")
            >>> await user.groups.add(admin, staff)
        """
        if not self._m2m_context:
            raise RuntimeError(
                "'.add()' can only be used on Many-to-Many relationships"
            )

        ids = []
        for inst in instances:
            # Assume 'id' for now
            ids.append(getattr(inst, "id"))

        route = self._transaction_or_using()
        await add_m2m_links(
            self._m2m_context["join_table"],
            self._m2m_context["source_col"],
            self._m2m_context["target_col"],
            self._m2m_context["source_id"],
            ids,
            route,
        )

    async def remove(self, *instances: Any) -> None:
        """Remove links from a many-to-many relationship

        Args:
            *instances: Target model instances that provide an ``id`` attribute.

        Raises:
            RuntimeError: If the query is not bound to a many-to-many context.

        Examples:
            >>> user = await User.create(email="[email protected]")
            >>> admin = await Group.create(name="admin")
            >>> await user.groups.remove(admin)
        """
        if not self._m2m_context:
            raise RuntimeError(
                "'.remove()' can only be used on Many-to-Many relationships"
            )

        ids = []
        for inst in instances:
            ids.append(getattr(inst, "id"))

        route = self._transaction_or_using()
        await remove_m2m_links(
            self._m2m_context["join_table"],
            self._m2m_context["source_col"],
            self._m2m_context["target_col"],
            self._m2m_context["source_id"],
            ids,
            route,
        )

    async def clear(self) -> None:
        """Clear all links in a many-to-many relationship

        Raises:
            RuntimeError: If the query is not bound to a many-to-many context.

        Examples:
            >>> user = await User.create(email="[email protected]")
            >>> await user.groups.clear()
        """
        if not self._m2m_context:
            raise RuntimeError(
                "'.clear()' can only be used on Many-to-Many relationships"
            )

        route = self._transaction_or_using()
        await clear_m2m_links(
            self._m2m_context["join_table"],
            self._m2m_context["source_col"],
            self._m2m_context["source_id"],
            route,
        )

    def __repr__(self):
        """Return a developer-friendly representation of the query"""
        return f"<Query model={self.model_cls.__name__} where={self.where_clause}>"

Attributes

model_cls = model_cls instance-attribute

where_clause = [] instance-attribute

order_by_clause = [] instance-attribute

Functions

__init__(model_cls, using=None, session=None)

Initialize a query for a model class.

Parameters:

Name Type Description Default
model_cls Type[T]

Model class that defines the target table.

required

Examples:

>>> query = Query(User)
>>> query.model_cls is User
True
Source code in src/ferro/query/builder.py
def __init__(
    self, model_cls: Type[T], using: str | None = None, session: Any | None = None
):
    """Initialize a query for a model class.

    Args:
        model_cls: Model class that defines the target table.

    Examples:
        >>> query = Query(User)
        >>> query.model_cls is User
        True
    """
    self.model_cls = model_cls
    self._using = using
    self._session = session
    self.where_clause: list["QueryNode"] = []
    self.order_by_clause: list[dict[str, str]] = []
    self._limit: int | None = None
    self._offset: int | None = None
    self._m2m_context: dict[str, Any] | None = None

where(predicate)

Add a filter condition to the query.

predicate is a lambda of shape Callable[[QueryProxy[T]], QueryNode]. The lambda receives a fresh :class:QueryProxy whose attributes return :class:FieldProxy instances, so lambda user: user.archived == False builds a comparison. Column names are validated at build time against the model's declared fields (plus shadow {fk}_id columns): a misspelled column raises AttributeError naming the closest valid match, before any query is sent to the database.

Parameters:

Name Type Description Default
predicate Predicate[T]

A callable that takes a :class:QueryProxy and returns a :class:QueryNode.

required

Returns:

Type Description
Self

A new Query with the clause added; self is unchanged.

Raises:

Type Description
TypeError

If predicate is not callable, or if it does not return a QueryNode.

Examples:

>>> q1 = User.where(lambda user: user.archived == False)  # noqa: E712
>>> q2 = User.where(lambda user: user.id == 1)
>>> isinstance(q1, Query) and isinstance(q2, Query)
True
Source code in src/ferro/query/builder.py
def where(self, predicate: "Predicate[T]") -> Self:
    """Add a filter condition to the query.

    ``predicate`` is a lambda of shape ``Callable[[QueryProxy[T]], QueryNode]``.
    The lambda receives a fresh :class:`QueryProxy` whose attributes
    return :class:`FieldProxy` instances, so
    ``lambda user: user.archived == False`` builds a comparison. Column
    names are validated at build time against the model's declared
    fields (plus shadow ``{fk}_id`` columns): a misspelled column raises
    ``AttributeError`` naming the closest valid match, before any query
    is sent to the database.

    Args:
        predicate: A callable that takes a :class:`QueryProxy` and
            returns a :class:`QueryNode`.

    Returns:
        A new ``Query`` with the clause added; ``self`` is unchanged.

    Raises:
        TypeError: If ``predicate`` is not callable, or if it does not
            return a ``QueryNode``.

    Examples:
        >>> q1 = User.where(lambda user: user.archived == False)  # noqa: E712
        >>> q2 = User.where(lambda user: user.id == 1)
        >>> isinstance(q1, Query) and isinstance(q2, Query)
        True
    """
    new = self._clone()
    new.where_clause.append(_resolve_where_node(predicate, self.model_cls))
    return new

order_by(field, direction='asc')

Add an ordering clause and return a new query.

Accepts a lambda naming the column (order_by(lambda u: u.created_at, "desc") — the documented style, matching where predicates) or a column-name string (order_by("created_at", "desc")). Both forms are validated against the model's queryable columns at build time.

Parameters:

Name Type Description Default
field str | Callable[[QueryProxy[T]], FieldProxy[Any]]

Column selector — lambda receiving a :class:QueryProxy, or a column-name string.

required
direction str

"asc" (default) or "desc".

'asc'

Returns:

Type Description
Self

A new Query with the ordering added; self is unchanged.

Raises:

Type Description
AttributeError

If the column is not a queryable column.

TypeError

If a lambda selector returns something other than a single column reference.

ValueError

If direction is not "asc" or "desc".

Examples:

>>> newest = await Post.select().order_by(lambda p: p.created_at, "desc").all()
Source code in src/ferro/query/builder.py
def order_by(
    self,
    field: "str | Callable[[QueryProxy[T]], FieldProxy[Any]]",
    direction: str = "asc",
) -> Self:
    """Add an ordering clause and return a new query.

    Accepts a lambda naming the column (``order_by(lambda u: u.created_at,
    "desc")`` — the documented style, matching ``where`` predicates) or a
    column-name string (``order_by("created_at", "desc")``). Both forms are
    validated against the model's queryable columns at build time.

    Args:
        field: Column selector — lambda receiving a :class:`QueryProxy`,
            or a column-name string.
        direction: ``"asc"`` (default) or ``"desc"``.

    Returns:
        A new ``Query`` with the ordering added; ``self`` is unchanged.

    Raises:
        AttributeError: If the column is not a queryable column.
        TypeError: If a lambda selector returns something other than a
            single column reference.
        ValueError: If ``direction`` is not ``"asc"`` or ``"desc"``.

    Examples:
        >>> newest = await Post.select().order_by(lambda p: p.created_at, "desc").all()
    """
    if direction.lower() not in ("asc", "desc"):
        raise ValueError("direction must be 'asc' or 'desc'")

    if isinstance(field, str):
        col_name = validate_query_column(self.model_cls, field)
    elif callable(field):
        selected = field(QueryProxy(self.model_cls))
        if not isinstance(selected, FieldProxy):
            raise TypeError(
                "order_by() selector must return a FieldProxy "
                f"(e.g. `lambda u: u.created_at`), got {type(selected).__name__}"
            )
        col_name = selected.column
    else:
        raise TypeError(
            "order_by() expected a column-name string or a lambda selector, "
            f"got {type(field).__name__}"
        )

    new = self._clone()
    new.order_by_clause.append({"column": col_name, "direction": direction.lower()})
    return new

limit(value)

Limit the number of records returned

Parameters:

Name Type Description Default
value int

The maximum number of records to return.

required

Returns:

Type Description
Self

A new Query with the clause added; self is unchanged.

Examples:

>>> query = User.select().limit(10)
>>> query._limit
10
Source code in src/ferro/query/builder.py
def limit(self, value: int) -> Self:
    """Limit the number of records returned

    Args:
        value: The maximum number of records to return.

    Returns:
        A new ``Query`` with the clause added; ``self`` is unchanged.

    Examples:
        >>> query = User.select().limit(10)
        >>> query._limit
        10
    """
    new = self._clone()
    new._limit = value
    return new

offset(value)

Skip a specific number of records

Parameters:

Name Type Description Default
value int

The number of records to skip.

required

Returns:

Type Description
Self

A new Query with the clause added; self is unchanged.

Examples:

>>> query = User.select().offset(20)
>>> query._offset
20
Source code in src/ferro/query/builder.py
def offset(self, value: int) -> Self:
    """Skip a specific number of records

    Args:
        value: The number of records to skip.

    Returns:
        A new ``Query`` with the clause added; ``self`` is unchanged.

    Examples:
        >>> query = User.select().offset(20)
        >>> query._offset
        20
    """
    new = self._clone()
    new._offset = value
    return new

all() async

Return all model instances that match the current query

Returns:

Type Description
list[T]

A list of model instances.

Examples:

>>> users = await User.where(lambda user: user.active == True).all()  # noqa: E712
>>> isinstance(users, list)
True
Source code in src/ferro/query/builder.py
async def all(self) -> list[T]:
    """Return all model instances that match the current query

    Returns:
        A list of model instances.

    Examples:
        >>> users = await User.where(lambda user: user.active == True).all()  # noqa: E712
        >>> isinstance(users, list)
        True
    """
    query_def = {
        "model_name": _model_identity(self.model_cls),
        "where": [node.to_ir_dict() for node in self.where_clause],
        "order_by": self.order_by_clause,
        "limit": self._limit,
        "offset": self._offset,
        "m2m": self._m2m_context,
    }
    route = self._transaction_or_using()
    return await fetch_filtered(
        self.model_cls,
        _query_ir_payload_to_json(query_def),
        route,
    )

count() async

Return the number of records that match the current query

Returns:

Type Description
int

The count of matching records.

Examples:

>>> total = await User.where(lambda user: user.active == True).count()  # noqa: E712
>>> isinstance(total, int)
True
Source code in src/ferro/query/builder.py
async def count(self) -> int:
    """Return the number of records that match the current query

    Returns:
        The count of matching records.

    Examples:
        >>> total = await User.where(lambda user: user.active == True).count()  # noqa: E712
        >>> isinstance(total, int)
        True
    """
    query_def = {
        "model_name": _model_identity(self.model_cls),
        "where": [node.to_ir_dict() for node in self.where_clause],
        "order_by": [],
        "limit": None,
        "offset": None,
        "m2m": self._m2m_context,
    }
    route = self._transaction_or_using()
    return await count_filtered(
        _model_identity(self.model_cls),
        _query_ir_payload_to_json(query_def),
        route,
    )

update(**fields) async

Update all records matching the current query

Parameters:

Name Type Description Default
**fields

Field names and values to update.

{}

Returns:

Type Description
int

The number of records updated.

Raises:

Type Description
ValueError

If limit() or offset() was set on this query.

Examples:

>>> updated = await User.where(lambda user: user.id == 1).update(name="Taylor")
>>> isinstance(updated, int)
True
Source code in src/ferro/query/builder.py
async def update(self, **fields) -> int:
    """Update all records matching the current query

    Args:
        **fields: Field names and values to update.

    Returns:
        The number of records updated.

    Raises:
        ValueError: If ``limit()`` or ``offset()`` was set on this query.

    Examples:
        >>> updated = await User.where(lambda user: user.id == 1).update(name="Taylor")
        >>> isinstance(updated, int)
        True
    """
    query_def = self._mutating_query_def("update")
    route = self._transaction_or_using()
    return await update_filtered(
        _model_identity(self.model_cls),
        _query_ir_payload_to_json(query_def),
        update_bind_payload(fields),
        route,
    )

first() async

Return the first matching record, or None

Returns:

Type Description
T | None

A model instance or None.

Examples:

>>> user = await User.select().order_by("id").first()
>>> user is None or isinstance(user, User)
True
Source code in src/ferro/query/builder.py
async def first(self) -> T | None:
    """Return the first matching record, or None

    Returns:
        A model instance or None.

    Examples:
        >>> user = await User.select().order_by("id").first()
        >>> user is None or isinstance(user, User)
        True
    """
    results = await self.limit(1).all()
    return results[0] if results else None

delete() async

Delete all records matching the current query

Returns:

Type Description
int

The number of records deleted.

Raises:

Type Description
ValueError

If limit() or offset() was set on this query.

Examples:

>>> deleted = await User.where(lambda user: user.disabled == True).delete()  # noqa: E712
>>> isinstance(deleted, int)
True
Source code in src/ferro/query/builder.py
async def delete(self) -> int:
    """Delete all records matching the current query

    Returns:
        The number of records deleted.

    Raises:
        ValueError: If ``limit()`` or ``offset()`` was set on this query.

    Examples:
        >>> deleted = await User.where(lambda user: user.disabled == True).delete()  # noqa: E712
        >>> isinstance(deleted, int)
        True
    """
    query_def = self._mutating_query_def("delete")
    route = self._transaction_or_using()
    return await delete_filtered(
        _model_identity(self.model_cls),
        _query_ir_payload_to_json(query_def),
        route,
    )

exists() async

Return whether at least one record matches the current query

Returns:

Type Description
bool

True if records exist, otherwise False.

Examples:

>>> found = await User.where(lambda user: user.email == "[email protected]").exists()
>>> isinstance(found, bool)
True
Source code in src/ferro/query/builder.py
async def exists(self) -> bool:
    """Return whether at least one record matches the current query

    Returns:
        True if records exist, otherwise False.

    Examples:
        >>> found = await User.where(lambda user: user.email == "[email protected]").exists()
        >>> isinstance(found, bool)
        True
    """
    return await self.count() > 0

add(*instances) async

Add links to a many-to-many relationship

Parameters:

Name Type Description Default
*instances Any

Target model instances that provide an id attribute.

()

Raises:

Type Description
RuntimeError

If the query is not bound to a many-to-many context.

Examples:

>>> user = await User.create(email="[email protected]")
>>> admin = await Group.create(name="admin")
>>> staff = await Group.create(name="staff")
>>> await user.groups.add(admin, staff)
Source code in src/ferro/query/builder.py
async def add(self, *instances: Any) -> None:
    """Add links to a many-to-many relationship

    Args:
        *instances: Target model instances that provide an ``id`` attribute.

    Raises:
        RuntimeError: If the query is not bound to a many-to-many context.

    Examples:
        >>> user = await User.create(email="[email protected]")
        >>> admin = await Group.create(name="admin")
        >>> staff = await Group.create(name="staff")
        >>> await user.groups.add(admin, staff)
    """
    if not self._m2m_context:
        raise RuntimeError(
            "'.add()' can only be used on Many-to-Many relationships"
        )

    ids = []
    for inst in instances:
        # Assume 'id' for now
        ids.append(getattr(inst, "id"))

    route = self._transaction_or_using()
    await add_m2m_links(
        self._m2m_context["join_table"],
        self._m2m_context["source_col"],
        self._m2m_context["target_col"],
        self._m2m_context["source_id"],
        ids,
        route,
    )

remove(*instances) async

Remove links from a many-to-many relationship

Parameters:

Name Type Description Default
*instances Any

Target model instances that provide an id attribute.

()

Raises:

Type Description
RuntimeError

If the query is not bound to a many-to-many context.

Examples:

>>> user = await User.create(email="[email protected]")
>>> admin = await Group.create(name="admin")
>>> await user.groups.remove(admin)
Source code in src/ferro/query/builder.py
async def remove(self, *instances: Any) -> None:
    """Remove links from a many-to-many relationship

    Args:
        *instances: Target model instances that provide an ``id`` attribute.

    Raises:
        RuntimeError: If the query is not bound to a many-to-many context.

    Examples:
        >>> user = await User.create(email="[email protected]")
        >>> admin = await Group.create(name="admin")
        >>> await user.groups.remove(admin)
    """
    if not self._m2m_context:
        raise RuntimeError(
            "'.remove()' can only be used on Many-to-Many relationships"
        )

    ids = []
    for inst in instances:
        ids.append(getattr(inst, "id"))

    route = self._transaction_or_using()
    await remove_m2m_links(
        self._m2m_context["join_table"],
        self._m2m_context["source_col"],
        self._m2m_context["target_col"],
        self._m2m_context["source_id"],
        ids,
        route,
    )

clear() async

Clear all links in a many-to-many relationship

Raises:

Type Description
RuntimeError

If the query is not bound to a many-to-many context.

Examples:

>>> user = await User.create(email="[email protected]")
>>> await user.groups.clear()
Source code in src/ferro/query/builder.py
async def clear(self) -> None:
    """Clear all links in a many-to-many relationship

    Raises:
        RuntimeError: If the query is not bound to a many-to-many context.

    Examples:
        >>> user = await User.create(email="[email protected]")
        >>> await user.groups.clear()
    """
    if not self._m2m_context:
        raise RuntimeError(
            "'.clear()' can only be used on Many-to-Many relationships"
        )

    route = self._transaction_or_using()
    await clear_m2m_links(
        self._m2m_context["join_table"],
        self._m2m_context["source_col"],
        self._m2m_context["source_id"],
        route,
    )

__repr__()

Return a developer-friendly representation of the query

Source code in src/ferro/query/builder.py
def __repr__(self):
    """Return a developer-friendly representation of the query"""
    return f"<Query model={self.model_cls.__name__} where={self.where_clause}>"

QueryProxy

Bases: Generic[TModel]

Validating attribute proxy passed to lambda predicates (FF-F F-2).

A fresh QueryProxy is constructed for the queried model each time a lambda predicate is evaluated. Attribute access validates the name against the model's queryable columns (declared fields plus shadow {fk}_id columns) and returns a :class:FieldProxy — so lambda user: user.archived == False builds a :class:QueryNode, while lambda user: user.archievd == False raises AttributeError at build time naming the valid columns.

Attribute types are FieldProxy[Any]: per-field static types for bare lambda parameters require TypeScript-style mapped types, proposed for Python in PEP 827 (draft, targeting 3.16) — adopted here when type checkers support it.

Examples:

>>> rows = await User.where(lambda user: user.archived == False).all()  # noqa: E712
Source code in src/ferro/query/nodes.py
class QueryProxy(Generic[TModel]):
    """Validating attribute proxy passed to lambda predicates (FF-F F-2).

    A fresh ``QueryProxy`` is constructed for the queried model each time a
    lambda predicate is evaluated. Attribute access validates the name
    against the model's queryable columns (declared fields plus shadow
    ``{fk}_id`` columns) and returns a :class:`FieldProxy` — so
    ``lambda user: user.archived == False`` builds a :class:`QueryNode`,
    while ``lambda user: user.archievd == False`` raises ``AttributeError``
    at build time naming the valid columns.

    Attribute types are ``FieldProxy[Any]``: per-field static types for bare
    lambda parameters require TypeScript-style mapped types, proposed for
    Python in PEP 827 (draft, targeting 3.16) — adopted here when type
    checkers support it.

    Examples:
        >>> rows = await User.where(lambda user: user.archived == False).all()  # noqa: E712
    """

    __slots__ = ("_model_cls",)

    def __init__(self, model_cls: type) -> None:
        self._model_cls = model_cls

    def __getattr__(self, name: str) -> "FieldProxy[Any]":
        """Validate ``name`` and return a ``FieldProxy`` for it."""
        validate_query_column(self._model_cls, name)
        return FieldProxy(name)

Attributes

__slots__ = ('_model_cls',) class-attribute instance-attribute

Functions

__init__(model_cls)

Source code in src/ferro/query/nodes.py
def __init__(self, model_cls: type) -> None:
    self._model_cls = model_cls

__getattr__(name)

Validate name and return a FieldProxy for it.

Source code in src/ferro/query/nodes.py
def __getattr__(self, name: str) -> "FieldProxy[Any]":
    """Validate ``name`` and return a ``FieldProxy`` for it."""
    validate_query_column(self._model_cls, name)
    return FieldProxy(name)

Predicate = Callable[[QueryProxy[TModel]], QueryNode] module-attribute

Type alias for lambda predicates accepted by :meth:Query.where.