Skip to content

SQLAlchemy Query Builder

Uvicore provides 3 Layers of database access. Here we discuss forgoing the Uvicore ORM and the Uvicore Query Builder and jumping straight into SQLAlchemy's own Query Builder!

Generally the ORM is the best way to utilize and query your tables. And if you prefer to skip the ORM, give the Uvicore Query Builder a shot, it's simple and elegant.

However, if you need the ultimate SQLAlchemy power, keep reading!

Note

A prerequisite to using SQLAlchemy's Query Builder is to define your DB Tables. If you prefer zero table definitions, then SQLAlchemy RAW SQL may be for you!


Getting the Table

To build a SQLAlchemy Core query you first need the sa.Table object. Grab it from Uvicore with uvicore.db.table(), which returns the real, prefix-aware SQLAlchemy table for the given connection. This is better than importing your Table class directly because it honors any table prefix configured for the connection.

import uvicore
import sqlalchemy as sa

# Get the SQLAlchemy table for the 'wiki' connection
posts = uvicore.db.table('posts', 'wiki')

# Or using dotnotation (connection.table)
posts = uvicore.db.table('wiki.posts')

Executing a Query

You build a query with SQLAlchemy Core, then run it through Uvicore's database manager. The handy execution helpers are:

Method Returns
await uvicore.db.fetchall(query, connection=...) a list of rows
await uvicore.db.first(query, connection=...) the first row (or None)
await uvicore.db.scalar(query, connection=...) the first column of the first row
await uvicore.db.execute(query, connection=...) runs INSERT / UPDATE / DELETE

All of these accept a connection= keyword to target a specific connection (it defaults to your configured default connection).


Select

import uvicore
import sqlalchemy as sa

posts = uvicore.db.table('wiki.posts')

# SELECT * FROM posts
query = sa.select(posts)
rows = await uvicore.db.fetchall(query, connection='wiki')

# Select specific columns
query = sa.select(posts.c.id, posts.c.title)
rows = await uvicore.db.fetchall(query, connection='wiki')

Where

posts = uvicore.db.table('wiki.posts')

query = (sa.select(posts)
    .where(posts.c.creator_id == 1)
    .where(posts.c.title.like('%uvicore%'))
    .order_by(posts.c.id)
)
rows = await uvicore.db.fetchall(query, connection='wiki')

# OR conditions
query = sa.select(posts).where(
    sa.or_(posts.c.id == 1, posts.c.id == 7)
)
rows = await uvicore.db.fetchall(query, connection='wiki')

Joins

A simple single-column join:

posts = uvicore.db.table('wiki.posts')
users = uvicore.db.table('auth.users')

query = (sa.select(posts, users.c.email)
    .select_from(posts.join(users, posts.c.creator_id == users.c.id))
    .where(users.c.email.like('%example.com'))
)
rows = await uvicore.db.fetchall(query, connection='wiki')

Multiple ON conditions (composite joins)

A join's onclause is just a SQLAlchemy boolean expression, so you can AND several conditions together with sa.and_(). This is essential on sharded backends such as Vitess / PlanetScale, where every join must include the shard key (e.g. tenant_id, workspace_id) alongside the natural key, otherwise the query scatters across all shards and is rejected or times out.

posts    = uvicore.db.table('wiki.posts')
comments = uvicore.db.table('wiki.comments')

# JOIN comments c ON p.tenant_id    = c.tenant_id
#                AND p.workspace_id = c.workspace_id
#                AND p.id           = c.post_id
query = (sa.select(posts, comments.c.id.label('comment_id'))
    .select_from(posts.join(comments, sa.and_(
        posts.c.tenant_id    == comments.c.tenant_id,
        posts.c.workspace_id == comments.c.workspace_id,
        posts.c.id           == comments.c.post_id,
    )))
)
rows = await uvicore.db.fetchall(query, connection='wiki')

The conditions are ANDed in the exact order you list them. Use posts.outerjoin(...) instead of posts.join(...) for a LEFT OUTER JOIN.


Aggregates, Group By and Order By

posts = uvicore.db.table('wiki.posts')

# Count posts per creator
query = (sa.select(posts.c.creator_id, sa.func.count().label('total'))
    .group_by(posts.c.creator_id)
    .order_by(sa.desc('total'))
)
rows = await uvicore.db.fetchall(query, connection='wiki')

# A single scalar value
query = sa.select(sa.func.count()).select_from(posts)
total = await uvicore.db.scalar(query, connection='wiki')

Insert, Update and Delete

Use uvicore.db.execute() for write operations.

posts = uvicore.db.table('wiki.posts')

# Insert
await uvicore.db.execute(
    posts.insert().values(slug='hello', title='Hello', creator_id=1, owner_id=1),
    connection='wiki',
)

# Update
await uvicore.db.execute(
    posts.update().where(posts.c.id == 1).values(title='Updated'),
    connection='wiki',
)

# Delete
await uvicore.db.execute(
    posts.delete().where(posts.c.id == 1),
    connection='wiki',
)

Tip

This is the full SQLAlchemy Core query builder, so anything in the SQLAlchemy 2.0 docs works here. When even this isn't enough, drop all the way down to RAW SQL.