Operations
The backend compiles the Django ORM into YDB’s YQL dialect, adapting to the features of a distributed database. This page covers query support, query parameters, and UPSERT.
ORM query features
Legend: ✅ supported · 🟡 works with caveats (see the note) · ❌ unsupported or not enforced by YDB.
Feature |
Status |
Notes |
|---|---|---|
CRUD ( |
✅ |
|
Most field lookups |
✅ |
|
Backslash / |
✅ |
|
Built-in scalar functions |
✅ |
|
Coercing lookups ( |
❌ |
Raise during parameter handling. |
Correlated subqueries ( |
❌ |
YDB cannot resolve the outer reference. Non-correlated subqueries work. |
Aggregation / annotation |
🟡 |
Common aggregates and |
|
✅ |
|
|
❌ |
Not supported by YDB. |
UNION with |
🟡 |
Several orderings are not yet handled. |
UNION as a subquery / wrapped for |
❌ |
Generates invalid SQL. |
|
✅ |
Reads back generated primary keys. |
|
🟡 |
Works; partial with database functions, |
|
✅ |
|
Window functions ( |
✅ |
Supports |
|
❌ |
Only unbounded |
|
❌ |
A no-op — YDB has no row locking (optimistic concurrency). See Transactions. |
Insert into a primary-key-only / multi-table-inheritance table |
❌ |
Raises |
|
❌ |
Not supported. Use UPSERT (below) for race-free writes keyed on the primary key. |
Query parameters
YDB requires typed query parameters. The backend infers each parameter’s type
from the query that produced it (the field being filtered, the expression, the
related key), so joins, foreign-key filters, __in, F(), Case / When,
annotations, aggregate (HAVING) filters, and non-correlated subqueries all
work.
UPSERT
UPSERT (UPDATE or INSERT) writes rows keyed on the primary key: a missing
row is inserted, and an existing row has the written columns overwritten while
its other columns are preserved. The backend uses YDB’s native UPSERT INTO,
which runs as a single atomic statement — there is no read-modify-write
step, so concurrent upserts of the same key cannot create duplicates.
Manager setup
UPSERT is provided by YDBManager. Set it as the model’s manager:
from django.db import models
from ydb_backend.models.manager import YDBManager
class NFTToken(models.Model):
contract_address = models.CharField(max_length=42)
token_id = models.CharField(max_length=78, primary_key=True)
owner = models.CharField(max_length=42)
metadata_url = models.CharField(max_length=256)
last_price = models.FloatField()
objects = YDBManager()
upsert() and bulk_upsert()
Both accept a model instance or a dict (bulk_upsert accepts a list, and may
mix the two) and return the persisted instances:
# Insert: the row does not exist yet.
NFTToken.objects.upsert({
"contract_address": "0x1a2b3c4d5e",
"token_id": "12345",
"owner": "0xAlice123",
"metadata_url": "ipfs://QmXyZ123",
"last_price": 1.5,
})
# Update: same primary key — the listed columns are overwritten.
NFTToken.objects.upsert({
"contract_address": "0x1a2b3c4d5e",
"token_id": "12345",
"owner": "0xBob456",
"metadata_url": "ipfs://QmXyZ456",
"last_price": 2.5,
})
# Bulk: one statement upserts every row.
tokens = NFTToken.objects.bulk_upsert([
{"contract_address": "0x11", "token_id": "100", "owner": "0xA",
"metadata_url": "ipfs://a", "last_price": 10.0},
NFTToken(contract_address="0x22", token_id="200", owner="0xB",
metadata_url="ipfs://b", last_price=20.0),
])
Conflict target
UPSERT is always keyed on the primary key. conflict_target may be omitted (it
defaults to the primary key) or set to the primary key explicitly; any other
target raises NotSupportedError, because YDB has no unique constraints to
match on:
NFTToken.objects.upsert(data, conflict_target="token_id") # ok — the PK
NFTToken.objects.upsert(data, conflict_target="owner") # NotSupportedError
Writing a subset of columns
update_fields restricts which columns are written; columns left out are
preserved on existing rows. YDB’s UPSERT INTO requires every NOT NULL
column to be present, so update_fields may only drop nullable columns —
omitting a NOT NULL column raises NotSupportedError.
class InventoryItem(models.Model):
sku = models.CharField(max_length=20, primary_key=True)
name = models.CharField(max_length=100) # NOT NULL
reorder_level = models.IntegerField(null=True) # nullable
quantity = models.IntegerField() # NOT NULL
objects = YDBManager()
# Writes name + quantity; the nullable reorder_level is left untouched.
InventoryItem.objects.upsert(
{"sku": "A1", "name": "Widget", "quantity": 9},
update_fields=["name", "quantity"],
)
# Raises NotSupportedError: omits the NOT NULL column `quantity`.
InventoryItem.objects.upsert(
{"sku": "A1", "name": "Widget"},
update_fields=["name"],
)