Query API¶
QuerySet¶
- class nextorm.query.QuerySet[source]¶
Bases:
GenericA lazy, immutable query builder for a single entity type.
Each method returns a new
QuerySet— the original is unchanged — so chaining is safe:base = db.select(User) young = base.filter(User.age < 30) old = base.filter(User.age >= 30) # independent of `young`
- filter(*conditions)[source]¶
Narrow results with one or more WHERE conditions.
Multiple conditions are combined with
AND:qs.filter(User.age >= 18, User.active == True) # → WHERE age >= 18 AND active = 1
- Parameters:
conditions (SqlNode)
- Return type:
QuerySet[ET]
- where(predicate)[source]¶
Narrow results using a lambda predicate over a column proxy.
The lambda receives an
EntityProxywhose attribute accesses returnColumnExprobjects, so comparison operators build SQL conditions transparently:qs.where(lambda p: p.price > 100) qs.where(lambda u: u.name == "alice")
Chain multiple
.where()calls to combine conditions with AND:qs.where(lambda u: u.age >= 18).where(lambda u: u.active == True)
- Parameters:
predicate (Callable[[Any], BinOp])
- Return type:
QuerySet[ET]
- order_by(*items)[source]¶
Set the
ORDER BYclause, replacing any previous ordering.Use
asc()/desc()on a column expression to build order items:qs.order_by(User.name.asc(), User.age.desc())
- Parameters:
items (OrderItem)
- Return type:
QuerySet[ET]
- join(table_or_entity, on, *, join_type='INNER', alias=None)[source]¶
Add a JOIN clause to the query.
Parameters¶
- table_or_entity:
The table (name string) or entity class to join.
- on:
The
ONcondition.- join_type:
SQL join type:
"INNER"(default),"LEFT","RIGHT", or"FULL".- alias:
Optional alias for the joined table.
Example:
db.select(Post).join(Comment, Comment.post_id == Post.id, join_type="LEFT").fetch_all()
- prefetch(*relation_attrs)[source]¶
Declare relations to eager-load alongside the main query.
After
fetch_all()executes the main SELECT, one additional query per prefetched relation is issued and the results are attached to the returned entity instances. This avoids the N+1 query problem.Parameters¶
- *relation_attrs:
Attribute descriptors from the entity class, e.g.
Post.tags. String attribute names are also accepted.
Example:
posts = db.select(Post).prefetch(Post.comments).fetch_all() for post in posts: # post.comments is pre-populated — no extra query fired print(post.comments.count())
- Parameters:
relation_attrs (Any)
- Return type:
QuerySet[ET]
- fetch_all()[source]¶
Execute the query and return all matching entity instances.
- Return type:
list[ET]
- fetch_one()[source]¶
Execute with
LIMIT 1and return the first entity, orNone.- Return type:
ET | None
- first()[source]¶
Alias for
fetch_one().- Return type:
ET | None
- get()[source]¶
Return the single matching entity, or
Noneif no rows match.Raises
MultipleObjectsFoundErrorif more than one row matches the current filter — usefetch_one()when you only want the first row regardless of how many exist.Example:
user = db.select(User).filter(User.name == "alice").get()
- Return type:
ET | None
- get_or_raise()[source]¶
Return the single matching entity; raise if zero or more-than-one match.
Raises
ObjectNotFoundwhen no row matches. RaisesMultipleObjectsFoundErrorwhen more than one row matches.Example:
user = db.select(User).filter(User.id == pk).get_or_raise()
- Return type:
ET
- delete()[source]¶
Delete all rows matched by the current filter.
Returns the number of deleted rows.
- Return type:
- update(**field_values)[source]¶
Bulk-update matched rows with the given field-value pairs.
Returns the number of updated rows. Only columns that correspond to direct entity fields (not FK columns) are accepted:
db.select(User).filter(User.active == False).update(active=True)
- without_distinct()[source]¶
Disable
SELECT DISTINCT(reverses a previousdistinct()call).- Return type:
QuerySet[ET]
- for_update(*, skip_locked=False, nowait=False)[source]¶
Append a
FOR UPDATE(orFOR UPDATE SKIP LOCKED/FOR UPDATE NOWAIT) clause.Useful for pessimistic locking. The behaviour is provider-specific: SQLite does not support
FOR UPDATEnatively (it locks at the connection level instead).Parameters¶
- skip_locked:
When
True, rows already locked by another transaction are silently skipped rather than causing a wait or error.- nowait:
When
True, an attempt to lock already-locked rows raises an error immediately instead of waiting. Mutually exclusive with skip_locked.
- page(pagenum, pagesize=10)[source]¶
Return a page of results.
Page numbers are 1-based. Equivalent to
.offset((pagenum-1)*pagesize).limit(pagesize):db.select(Product).order_by(Product.name).page(2, pagesize=20)
- random(n)[source]¶
Return n randomly ordered rows.
Uses
ORDER BY RANDOM()(SQLite / PostgreSQL) orORDER BY RAND()(MariaDB).
- group_concat(attr, sep=',')[source]¶
Return the concatenation of all non-NULL values of attr.
Uses the database-native aggregate:
SQLite / MariaDB:
GROUP_CONCAT(col, sep)PostgreSQL:
STRING_AGG(col, sep)
Returns
Nonewhen no rows match or all values are NULL.Example:
names = db.select(User).filter(User.active == True).group_concat("name", ", ")
- get_sql()[source]¶
Return the SQL string that would be executed by
fetch_all().Useful for debugging or logging. Parameters are shown as
?(SQLite) or%s(PostgreSQL / MariaDB) placeholders:print(db.select(User).filter(User.age > 18).get_sql())
- Return type:
- show(width=120, *, file=None)[source]¶
Pretty-print query results as a plain-text table.
Fetches all rows and renders them to file (default:
sys.stdout) with aligned columns bounded by width characters total. Useful for interactive debugging.Parameters¶
- width:
Maximum total table width in characters. Column content is truncated proportionally when the natural width exceeds this.
- file:
Output stream; defaults to
sys.stdout.
Example:
db.select(User).show() # +----+---------+-----+ # | id | name | age | # +----+---------+-----+ # | 1 | alice | 30 | # | 2 | bob | 25 | # +----+---------+-----+
- raw(sql, params=None)[source]¶
Execute sql and map each result row to an entity instance.
Column names in the cursor description are matched to entity fields by name, so the column order in sql does not have to follow the schema order. Columns that don’t match any field are silently ignored.
Parameters¶
- sql:
Raw SQL SELECT statement.
- params:
Positional bind parameters (
?placeholders for SQLite,%sfor PostgreSQL / MariaDB).
Example:
users = db.select(User).raw("SELECT * FROM user WHERE age > ?", [18])
EntityProxy¶
- class nextorm.query.EntityProxy[source]¶
Bases:
objectA lightweight proxy passed to a
wherelambda.Attribute access returns a
ColumnExprso the lambda can build SQL predicates without referencing the entity class directly:qs.where(lambda p: p.price > 100) qs.where(lambda u: u.name == "alice")
The proxy is created automatically by
QuerySet.where(); direct construction is only needed in tests or advanced usage.
Generator expressions¶
Generator-expression query syntax.
Usage:
from nextorm import Database, Entity, Req
from nextorm.generators import select
class Product(Entity):
name: Req[str]
price: Req[float]
db = Database(entities=[Product])
db.bind("sqlite", ":memory:")
db.generate_mapping(create_tables=True)
# Equivalent to db.select(Product).filter(Product.price > 100)
results = select(p for p in Product if p.price > 100)
Supported filter expressions¶
Simple comparisons:
p.attr == val,p.attr > val, etc.Logical conjunctions:
p.a > 1 and p.b < 5Logical disjunctions:
p.a == 1 or p.b == 2Negation:
not p.active
Limitations¶
The decompiler translates Python bytecode back to SQL AST nodes. It
handles the common single-attribute comparison patterns. Complex
Python expressions (function calls, multi-level attribute access, etc.)
are not supported and will raise DecompileError.
The query is executed against the database that the entity was last
registered with (either through a db_session() or by
calling db.save() / db.select(...) at least once). To use with a
specific database, prefer db.select(Entity).filter(...) instead.
- nextorm.generators.select(gen)[source]¶
Execute a generator-expression query and return a
QuerySet.The generator expression must iterate over a single entity class:
select(p for p in Product if p.price > 100)
This is syntactic sugar for:
db.select(Product).filter(Product.price > 100)
The database to use is determined by inspecting the
_db_attribute on the entity class’s iterator (set fromEntityMeta.__iter__()). The entity class must have been registered with a bound, mapped database.Raises¶
- DecompileError
If the filter condition cannot be decompiled.
- RuntimeError
If the entity class has no associated database context.
- Parameters:
gen (Generator[T, None, None])
- Return type:
QuerySet[T]
- nextorm.generators.count(gen)[source]¶
Return the number of entities matching the generator-expression filter.
Example:
n = count(p for p in Product if p.price > 100)
Equivalent to:
db.select(Product).filter(Product.price > 100).count()
- Parameters:
gen (Generator[T, None, None])
- Return type:
- nextorm.generators.avg(gen)[source]¶
Compute
AVGof the attribute yielded by the generator expression.The generator must yield a field attribute, not the entity itself:
mean_price = avg(p.price for p in Product if p.active)
Equivalent to:
db.select(Product).filter(Product.active == True).avg("price")
Raises¶
- DecompileError
If the generator yields the entity rather than a field attribute.
- Parameters:
gen (Generator[Any, None, None])
- Return type:
Any
- nextorm.generators.sum(gen)[source]¶
Compute
SUMof the attribute yielded by the generator expression.The generator must yield a field attribute, not the entity itself:
total = sum(p.price for p in Product if p.in_stock)
Equivalent to:
db.select(Product).filter(Product.in_stock == True).sum("price")
Raises¶
- DecompileError
If the generator yields the entity rather than a field attribute.
- Parameters:
gen (Generator[Any, None, None])
- Return type:
Any
- nextorm.generators.min(gen)[source]¶
Compute
MINof the attribute yielded by the generator expression.The generator must yield a field attribute, not the entity itself:
cheapest = min(p.price for p in Product)
Equivalent to:
db.select(Product).min("price")
Raises¶
- DecompileError
If the generator yields the entity rather than a field attribute.
- Parameters:
gen (Generator[Any, None, None])
- Return type:
Any
- nextorm.generators.max(gen)[source]¶
Compute
MAXof the attribute yielded by the generator expression.The generator must yield a field attribute, not the entity itself:
priciest = max(p.price for p in Product)
Equivalent to:
db.select(Product).max("price")
Raises¶
- DecompileError
If the generator yields the entity rather than a field attribute.
- Parameters:
gen (Generator[Any, None, None])
- Return type:
Any