Queiries¶
QyazyDB supports wide subset of SQL features, including expressions, filters, joins and grouping.
Basics¶
The query is represented by DBQuery class.
It is bound to a specific database DBFactory and optionally to a DBTable.
Queries, which is not bound to a table, works as cross-table queries.
Queiries works as subqueries for other queries.
It is possible to extract data row as dict:
row['col'] = "hello"
or as SimpleNamespace:
row.col = "hello"
or as DBTable instance:
row.col = "hello"
row.save()
It is possible to request one row, list of rows, or interate through result rows sequentially.
It is possible to build queries as chain calls:
db.query().select(...).filter(...).group_by(...).exclude(...).fetch_all()
Expressions¶
Query object allows to use lambda expressions in almost every method, like select, filter, ‘sort_by` and so on. It is possible to write Python-like expressions to make translated to SQL directly, but with several nuances.
Python |
SQL |
|---|---|
expr[1] |
expr[1] |
expr[1:2] |
substr(expr, 1, 2) |
expr[‘a’] |
expr->’a’ |
expr + a |
expr + a |
expr - a |
expr - a |
expr * a |
expr * a |
expr / a |
expr / a |
expr % a |
expr % a |
expr ** a |
expr ^ a |
abs(a) |
@(a) |
-expr |
-expr |
expr & a |
expr AND a |
expr | a |
expr OR a |
expr.bin_and(a) |
expr & a |
expr.bin_or(a) |
expr | a |
expr ^ a |
expr # a |
~expr |
NOT expr |
expr.invert() |
~expr |
expr << a |
expr IN a |
expr.lshift(a) |
expr << a |
expr >> a |
expr >> a |
expr == a |
expr = a |
expr != a |
expr <> a |
expr < a |
expr < a |
expr > a |
expr > a |
expr <= a |
expr <= a |
expr >= a |
expr >= a |
expr.as_integer |
expr::integer |
expr.as_string |
expr::text |
expr.as_float |
expr::double precision |
round(a) |
round(a) |
trunc(a) |
trunc(a) |
expr.contains(a) |
expr LIKE ‘%a%’ |
expr.upper() |
expr.upper() |
expr.lower() |
expr.lower() |
len(expr) |
length(expr, ‘UTF8’) |
expr.is_null |
expr IS NULL |
expr.is_not_null |
expr IS NOT NULL |
expr.left(n) |
left(expr, n) |
expr.right() |
right(expr, n) |
expr.starswith(s) |
left(expr, length(s)) = s |
expr.endswith() |
right(expr, length(s)) = s |
expr.min |
min(expr) |
expr.max |
max(expr) |
expr.count |
count(expr) |
|
count(distinct expr) |
expr.avg |
avg(expr) |
Note
There is no native overload for several kind of operators, like OR, AND, NOT, ‘IN’ etc. Actually, there is overloads supported, but it is limited to specifit returning types. So, __or__ method should return bool and nothing more. As a trade-off we’ve reused bitwise operators for that purpose, like | -> OR, which evaluation priority is lower and it is needed to use parenhesis for this kind.
Examples¶
Single table¶
class SomeTable(DBTable):
col: str
SomeTable(col="hello").save()
db.insert(SomeTable(col="world"))
# request all rows as `SomeTable` instances
q1 = SomeTable.query()
q2 = db.query(SomeTable)
# request all rows as `SimpleNamespace` ala named tuple
q1.select("name")
for x in q1:
print(x.name)
# request all rows as dict
for x in q1.fetch_all(as_dict=True):
print(x['name'])
# simple filter
q4 = SomeTable.query().filter(name="world")
q5 = SomeTable.query().filter(lambda x: x.name == "world")
# iterate via elements
for x in q4:
print(x)
for x in q5.fetch_all():
print(x)
for x in q4.select("name").fetch_values():
print(x)
with q4.execute(as_dict=True) as curr:
for x in curr:
print(x)
# iterate by slice
for x in q4[0:5]:
print(x)
# reuse queries
q7 = SomeTable.query()
q7.filter(name=q7.var("name"))
for n in ('hello', 'world'):
q7["name"] = n
item = q7.fetch_one()
print(item)
# reuse query builders
for n in ('hello', 'world'):
with SomeTable.query() as q8:
q8.reuse()
q8.filter(name=q7.var("name")) # this line run once
print("hello once")
q8["name"] = n
print(q8.fetch_one())
# freeze quieries
q9 = SomeTable.query()
q9.filter(name=q9.var("name")).freeze()
# at this point SQL code is statically generated and query is restricted against changes
for n in ('One', 'Two'):
q9["name"] = n
print(q9.fetch_one())
Multiple tables¶
class One(DBTable):
name: str
numbers: int
class Two(DBTable):
name: str
numbers: int
One(name="Con", numbers=3785).save()
One(name="Sir", numbers=6566).save()
One(name="Mon", numbers=1554).save()
Two(name="Phaz", numbers=9985).save()
Two(name="Jorn", numbers=6566).save()
Two(name="Dil", numbers=3154).save()
q = db.query()
# all tables are accessible from the schema by snake_names
q.select(one="ones.name", two="twos.name")
q.filter(lambda x: x.ones.numbers == x.twos.numbers)
for x in q:
print(x)
Aggregates¶
Deal with aggregates without worries about GROUP BY and HAVING clauses.
class Customer(DBTable):
name: str
class Sale(DBTable):
customer: Customer
amount: float
q = Sale.query()
print("Rows:", q.fetch_count())
print("Total:", q.fetch_sum("amount"))
q = Customer.query()
q.select("name", total=lambda x: x.sales.amount.sum)
q.filter(lambda x: x.sales.sell_date > datetime.now() - timedelta(days=7))
q.filter(lambda x: x.sales.amount.sum > 1000)
SQL code generated:
SELECT
"customer".name AS "name",
sum("customer__sales".amount) AS "total"
FROM "public"."customer" AS "customer"
INNER JOIN "public"."sale" AS "customer__sales"
ON "customer".id = "customer__sales".customer
WHERE
"customer__sales".sell_date>%(_arg_1)s
GROUP BY
1
HAVING
sum("customer__sales".amount)>%(_arg_2)s
Custom items represenantion¶
Let’s imagine, you have a warehouse with books.
class Book(DBTable):
title: str
description: Text | None
author: str | None
year: int | None
pages: int | None
class Storage(DBTable):
book: Book
qty: int
b1 = Book(title="Alice in wonderland", description="A good book for kid").save()
b2 = Book(title="Rust for noobies", description="Not for kids").save()
b3 = Book(title="Backside of the life", description="For zombies").save()
Storage(book=b1, qty=5).save()
Storage(book=b2, qty=8).save()
Storage(book=b3, qty=10).save()
Now we want to request all your rests.
for x in Storage.select("book", "qty"):
print(x)
As a result we get:
Row(book=1, qty=5)
Row(book=2, qty=8)
Row(book=3, qty=10)
Not something we want to, because there are no book names. Let’s extend query.
for x in Storage.select("book.title", "qty"):
print(x)
It gives:
Row(book_title='Alice in wonderland', qty=5)
Row(book_title='Rust for noobies', qty=8)
Row(book_title='Backside of the life', qty=10)
Looks better. But what if we want dive deeper to book properties? It seems, we need to extract id explicitly and get a book by it.
for x in Storage.Select("book.id", "book.title", "qty"):
print(x)
print(Book[x.id].description)
We get:
Row(book_id=1, book_title='Alice in wonderland', qty=5)
A good book for kid
Row(book_id=2, book_title='Rust for noobies', qty=8)
Not for kids
Row(book_id=3, book_title='Backside of the life', qty=10)
For zombies
But it’s even much better to define a presentation path with special method override
class Book(DBTable):
title: str
description: Text | None
author: str | None
year: int | None
pages: int | None
@classmethod
def _view_(cls, item: DBQueryField[typing.Self]):
return item.title
for x in Storage.select("book", "qty"):
print(x)
We are getting __view field now:
Row(book=1, book__view='Alice in wonderland', qty=5)
Row(book=2, book__view='Rust for noobies', qty=8)
Row(book=3, book__view='Backside of the life', qty=10)
Note
Method _view_ should return DBSQL object, so it has to perform QuazyDB-compatible expression. Strings or any other values are unsupported.
Let’s simplify even more and check actual SQL query:
for x in Storage.query():
print(x)
Actual query:
SELECT
"storage".book AS "book",
"storage__books".title AS "book__view",
"storage".qty AS "qty",
"storage".id AS "id"
FROM "public"."storage" AS "storage"
INNER JOIN "public"."book" AS "storage__books"
ON "storage".book = "storage__books".id
Results:
Storage[1]
Storage[2]
Storage[3]
Results doesn’t seem interesing. Let’s make it human-readable
class Storage(DBTable):
book: BookA
qty: int
def __str__(self):
return f'{self.book} -> {self.qty}'
And new results:
Alice in wonderland -> 5
Rust for noobies -> 8
Backside of the life -> 10
Row id is accessible also via pk property:
class Storage(DBTable):
book: BookA
qty: int
def __str__(self):
return f'{self.book.pk}:: {self.book} -> {self.qty}'
Results:
1:: Alice in wonderland -> 5
2:: Rust for noobies -> 8
3:: Backside of the life -> 10
More then that is it even possible to perform implicit get
for x in Storage.query():
print(x)
print(x.book.description)
Voila:
1:: Alice in wonderland -> 5
A good book for kid
2:: Rust for noobies -> 8
Not for kids
3:: Backside of the life -> 10
For zombies
Such operation performs simple query for each execution, with no caching. Use with aware:
SELECT
"book".description AS "description"
FROM "public"."book" AS "book"
WHERE
"book".id=%(_arg_1)s
There is a helper method to fetch all fields or related item and cache for a further usage.
for x in Storage.query():
print(x)
# call `fetch` one time and no further calls necessary
print(x.book.fetch().description)
# just use `x.book.description` after
SQL CASE statement¶
QuazyDB supports SQL CASE … syntax out-of-the box:
class User(DBTable):
name: str
age: int
q = db.query(User)
c = q.case().
condition("baby", lambda x: x.age <= 1).
condition("toddler", lambda x: x.age <= 3).
condition("kid", lambda x: x.age < 18).
default("adult")
for x in q.select("name", age_category=c):
print(x.name, "is a", x.age_category)
Chained selection¶
QuazyDB has strong support of One-to-Self relations, in case you build hierarchy of elements.
class Chained(DBTable):
name: str
next: 'Chained | None'
@classmethod
def _view_(cls, item):
return item.name
# prefill table
for i in range(20):
db.insert(Chained(name=f"Chained #{i+1}"))
# update with random links
for i in range(20):
link = i+2+randint(0, 3)
if link < 20:
Chained[i+1].save(next=link)
q = db.query(Chained2).chained("id", "next", 1)
for x in q:
print(x.name, "==>", x.next)
The output would randomly get to this:
Chained #1 ==> Chained #3
Chained #3 ==> Chained #7
Chained #7 ==> Chained #9
Chained #9 ==> Chained #12
Chained #12 ==> Chained #14
Chained #14 ==> Chained #16
Chained #16 ==> Chained #18
Chained #18 ==> Chained[None]