Support Contract and Compatibility Matrices

This is the single source of truth for what django-ydb-backend supports. Issues, pull requests, release notes, and the other docs in this directory should link here rather than restating support claims. It exists to make the backend’s guarantees explicit before the first non-beta release (issue #30).

Status: ratified. The support levels below were reviewed against the evidence (see How this was determined) and ratified by the maintainer. Remaining work is tracked in the linked issues and split into release-blocking vs non-blocking.

Support levels

Level

Meaning

βœ… Supported

Works as documented and is exercised by tests. Safe to rely on in production within the stated caveats.

🟑 Best-effort

Works, but with documented functional caveats (partial coverage, edge cases, reduced precision). Use only after reading the caveat.

❌ Unsupported

Does not work, or the database does not enforce it. The backend may raise a clear error, silently skip the operation (documented), or accept an ORM declaration that YDB never guarantees.

βšͺ Not yet evaluated

No conformance evidence yet. Treat as unsupported until verified.

Database-enforced guarantees are credited only when YDB itself enforces them. Foreign keys, uniqueness, and check constraints are not enforced by YDB, so they are marked ❌ β€” even though the ORM still accepts the declaration. You can reimplement them in application code (validate_unique(), clean(), validators), but that is the application’s responsibility, not backend support.

Version support

The first non-beta target (issue #30):

Component

Supported range

Recommended

Notes

Python

>=3.10,<4

3.12

Declared in pyproject.toml (requires-python) and ruff target-version = py310.

Django

>=4.2,<7.0

5.2 LTS

Matches the pyproject.toml pin. The explicitly targeted versions are 4.2 (legacy floor), 5.2 LTS (recommended production target), and 6.0 (current major).

YDB

>=20

latest stable / ydbplatform/local-ydb:trunk

DatabaseFeatures.minimum_database_version = (20,).

ydb-dbapi

>=0.1.8,<0.2.0

0.1.8+

Dependency pin in pyproject.toml.

How this was determined

Three sources back every classification:

  1. Conformance harness (issue #72). conformance/run.sh <module> checks out the matching Django source tree and runs Django’s own database test suite (tests/runtests.py) against a local YDB. This is the standard third-party backend approach. Per-module results are summarised in the matrices below; reproduce any of them with, e.g., conformance/run.sh lookup.

  2. DatabaseFeatures flags (ydb_backend/backend/features.py). The backend’s declared capabilities, and django_test_skips, which records each skipped Django test with the reason.

  3. The shipped behavior documented in FIELDS, MIGRATIONS, TRANSACTIONS, OPERATIONS, and CONTRIB.

The conformance harness reports supports_transactions = False for its own process only, to degrade Django’s TestCase to TransactionTestCase (YDB has no savepoints). The shipped backend keeps supports_transactions = True; the real limitation is savepoints, not transactions (see Transactions).


Fields

Full type mapping is in FIELDS.md. Status of each Django field:

Field

Status

Notes

AutoField, SmallAutoField, BigAutoField

βœ…

Serial family; value generated by YDB and read back via INSERT ... RETURNING.

CharField, TextField, SlugField, EmailField

βœ…

Stored as Utf8.

IPAddressField, GenericIPAddressField

βœ…

Stored as Utf8.

BinaryField, FileField, FilePathField

βœ…

String / Utf8.

IntegerField & Small/Big/Positive* variants

βœ…

Signed/unsigned int types per range; see FIELDS.md.

BooleanField

βœ…

Bool.

FloatField

βœ…

Float; cannot be a primary key.

DecimalField

βœ…

Arbitrary max_digits/decimal_places (column and bound parameters derive from the field); values rounded to scale. YDB reliably keeps ~26 significant digits.

UUIDField

βœ…

Native UUID (has_native_uuid_field).

DurationField

βœ…

Interval (Β±136 years); cannot be a primary key. Temporal subtraction supported.

DateField

βœ…

Date32 (signed wide); pre-1970 dates round-trip.

DateTimeField (USE_TZ=True)

βœ…

Timestamp64 (signed wide), microsecond precision, pre-1970 instants.

DateTimeField (USE_TZ=False)

🟑

Naive datetimes shift by the server timezone on round-trip (issue #78). Microsecond precision is fine. Production default USE_TZ=True is unaffected.

TimeField

🟑

No native YDB time type; stored as Int64 microseconds-since-midnight, introspected back as BigIntegerField. __hour/__minute/__second lookups are supported (computed by integer arithmetic on the stored value).

JSONField

🟑

Native Json. Equality filter (filter(data=value)) is unsupported by YDB; cannot be introspected; no JSONObject(). null=True is supported (stored as SQL NULL; __isnull filtering works).

Relations

ORM-level relations work; YDB enforces none of the relational guarantees β€” see FIELDS.md and CONTRIB.md.

Feature

Status

Notes

ForeignKey, OneToOneField (ORM)

βœ…

Stored as a scalar <name>_id column typed from the target PK.

ManyToManyField (auto-through & through=)

βœ…

Through tables are ordinary YDB tables; add/list/remove works.

select_related

βœ…

Conformance: 20/20.

prefetch_related

🟑

Conformance: ~108/113; a few edge cases fail.

DB-level FK constraints / REFERENCES / ON DELETE

❌

supports_foreign_keys = False. Not emitted, not introspected.

DB-level cascade delete

❌

Django ORM on_delete still runs for ORM deletes; raw/external writes can orphan rows.

O2O / M2M-pair uniqueness

❌

Not enforced by YDB. The ORM declaration is accepted but duplicates are not rejected; enforce in app code if needed.

Constraints

See MIGRATIONS.md. β€œSkipped” operations are logged with a warning and the migration proceeds; the guarantee is not enforced.

Constraint

Status

Notes

Primary key

βœ…

Must be set at table creation; immutable afterwards (changing it raises NotSupportedError).

NOT NULL (at create)

βœ…

Enforced.

unique / unique_together

❌

Not enforced by YDB (unique secondary indexes are unreleased). Migration skips the constraint with a warning and the DB will accept duplicates; enforce in app code (validate_unique()) if needed.

Nullable / partially-nullable unique

❌

supports_nullable_unique_constraints = False.

CHECK (column & table)

❌

Not supported by YDB (supports_*_check_constraints = False); migration skips it with a warning. Enforce in app code (clean() / validators) if needed.

Foreign-key constraints

❌

Not created or introspected.

Multiple constraints/indexes on the same fields

❌

allows_multiple_constraints_on_same_fields = False.

Indexes

Index type

Status

Notes

Secondary, non-unique (db_index, Index)

βœ…

ADD INDEX ... GLOBAL or at table creation.

Unique index

❌

Secondary indexes are non-unique; unique indexes are unreleased in YDB.

Partial index (... WHERE)

❌

supports_partial_indexes = False.

Expression index

❌

supports_expression_indexes = False.

Covering index (Index(include=...))

βœ…

Emits ADD INDEX ... GLOBAL ON (...) COVER (...); supports_covering_indexes = True, tested in tests/backends/ydb/test_indexes.py.

Rename index

βœ…

can_rename_index = True.

Introspect column ordering (ASC/DESC)

❌

supports_index_column_ordering = False; introspection reports ASC.

Transactions

Full contract in TRANSACTIONS.md.

Feature

Status

Notes

transaction.atomic() commit / rollback

βœ…

Body runs in a YDB interactive transaction; commit on clean exit, rollback on exception.

Autocommit (outside atomic())

βœ…

Each statement is its own transaction; driver auto-retries transient errors.

SERIALIZABLE isolation

βœ…

Optimistic concurrency; conflicts surface as OperationalError.

Savepoints

❌

uses_savepoints = False. Fundamental β€” see below.

Nested atomic() + rollback of inner block

❌

No savepoints: catching an exception inside a nested block poisons the whole transaction.

Django TestCase

❌

Relies on savepoints. Use TransactionTestCase (flush isolation).

DDL inside atomic()

❌

can_rollback_ddl = False; migrations run non-atomically.

Automatic retry of atomic() blocks

❌

Application responsibility β€” catch OperationalError and re-run the block.

Migrations & schema operations

See MIGRATIONS.md. The schema editor never silently ignores an operation: it either raises NotSupportedError or skips with a warning.

Operation

Status

Notes

CREATE / DROP TABLE

βœ…

Add nullable column

βœ…

Add NOT NULL column with default

βœ…

Default materialised into DDL for backfill.

Add NOT NULL column without default

❌

Raises NotSupportedError (cannot backfill).

Drop column

βœ…

Rename table

βœ…

Relax NOT NULL β†’ nullable

βœ…

Make column NOT NULL after creation

❌

Skipped with a warning (YDB can only drop NOT NULL).

Change column type

❌

Raises NotSupportedError.

Rename column

❌

Raises NotSupportedError.

Change primary key

❌

Raises NotSupportedError.

Add / drop secondary index

βœ…

migrate for contenttypes/auth/admin/sessions

βœ…

Runs to completion (unenforceable constraints skipped).

Default-value change

❌

No-op at the DB level β€” YDB does not store column defaults. Harmless for the ORM: Django applies field defaults in Python, so new rows still get the new default.

Table/column comments, stored procedures

❌

Not supported.

ORM query features

Feature

Status

Notes

CRUD (create/get/filter/update/delete)

βœ…

Most field lookups

βœ…

exact, in, ranges, icontains, date-extract (week_day/week/quarter/…), etc.

Backslash / % / _ escaping in pattern & exact lookups

βœ…

Escaped correctly via ESCAPE '~' (issue #75). Substr() on a text column works via Unicode::Substring (#87); a pattern lookup whose right-hand side is a nullable expression remains unsupported (#91).

Coercing lookups (int-as-str, date-as-str), regex on NULL/non-string

❌

Raise during parameter handling.

Correlated subqueries (Exists/Subquery/OuterRef as LHS)

❌

YDB cannot resolve the outer reference (issue #77). Non-correlated subqueries work.

Aggregation / annotation

🟑

GROUP BY validation fixed (#76); tail remains: ORDER BY aggregated values, GROUP BY constant, PI()/Random()/CURRENT_TIMESTAMP (issue #80).

union()

βœ…

intersection() / difference()

❌

supports_select_intersection/_difference = False.

UNION with order_by / values_list ordering

🟑

Several orderings not yet handled.

UNION as a subquery / wrapped for COUNT

❌

Generates invalid SQL.

bulk_create

βœ…

Reads back generated PKs (can_return_rows_from_bulk_insert).

bulk_update

βœ…

Works; with DB functions / JSONField / MTI it is 🟑 (partial).

F(), Case/When

βœ…

Window functions (OVER)

βœ…

ROWS BETWEEN N PRECEDING/FOLLOWING.

RANGE BETWEEN N PRECEDING ... (bounded offsets)

❌

only_supports_unbounded_with_preceding_and_following = True.

select_for_update(... limit)

❌

supports_select_for_update_with_limit = False.

Insert into a primary-key-only / MTI-parent table

❌

Raises NotSupportedError β€” fundamental, see below (issue #79).

ignore_conflicts=True

❌

supports_ignore_conflicts = False.

Introspection & inspectdb

See MIGRATIONS.md.

Aspect

Status

Notes

Column nullability

βœ…

Optional<T> β‡’ nullable.

Indexes (columns, ASC order)

βœ…

Secondary indexes non-unique.

Sequences

🟑

Reported only for an integer primary key.

Field-type mapping (inspectdb)

🟑

Lossy where several Django fields share a YDB type (e.g. Utf8→TextField, Double→FloatField).

Foreign keys

❌

Never reported (not enforced).

Check constraints

❌

Never reported.

Column default

❌

can_introspect_default = False.

Admin, Auth & contrib apps

Covered by tests/django_contrib/test_smoke.py; see CONTRIB.md.

Workflow

Status

Notes

migrate for admin/auth/contenttypes/sessions

βœ…

Create users / superusers, password checks

βœ…

Groups, permissions, User.groups / user_permissions M2M

βœ…

Session create / load / delete

βœ…

Admin login & model changelists

βœ…

Unique usernames / M2M-pair uniqueness

❌

Not DB-enforced (YDB has no unique constraints). Django’s validate_unique() still runs at the ORM level; rely on app logic.

UPSERT

The supported public UPSERT is the native YDB UPSERT INTO β€” a single atomic, race-free statement keyed on the primary key. The user-facing API is YDBManager: set objects = YDBManager() on the model, then call Model.objects.upsert(obj) / bulk_upsert(objs) (each accepts a model instance or a dict and returns the persisted instances).

Aspect

Status

Notes

YDBManager.upsert() / bulk_upsert()

βœ…

Native UPSERT INTO via SQLUpsertCompiler. One statement (rows passed as a List<Struct> parameter), so no read-modify-write and no race window. Covered by tests/compiler/test_upsert.py.

Conflict target

βœ… (PK only)

UPSERT is keyed on the primary key. conflict_target defaults to the PK; passing anything else raises NotSupportedError.

update_fields (write a subset of columns)

🟑

Restricts the written columns; columns left out are preserved on existing rows. YDB requires every NOT NULL column to be present, so update_fields may only drop nullable columns β€” omitting a NOT NULL column raises NotSupportedError.

The docs/examples in OPERATIONS.md are rewritten separately in #47.


Fundamental limitations

These follow from YDB’s architecture and are not expected to change (issue #79). Design around them:

  1. No savepoints. Nested atomic() blocks cannot roll back independently, and Django’s TestCase does not work β€” use TransactionTestCase.

  2. No database-level referential / unique / check enforcement. Foreign keys, uniqueness, and checks are application responsibilities.

  3. No insert into a primary-key-only table. YDB has no INSERT ... DEFAULT VALUES and rejects NULL for a Serial column, and the database generates the key β€” so a row whose only column is an auto PK cannot be inserted. This rules out multi-table inheritance (concrete parents) and primary-key-only models; both raise NotSupportedError. Give every model at least one non-PK field, and use abstract = True base classes instead of concrete parents.

  4. No correlated subqueries. A subquery that references the enclosing query’s table fails with Member not found: <table> (the outer row is not in scope). This covers OuterRef inside Exists/Subquery, an Exists/subquery used as a lookup’s left-hand side, and Django’s exclude() across a multivalued relationship (which Django compiles to a correlated subquery). Non-correlated subqueries (e.g. field__in=<queryset>) work. This is a YDB platform limitation (#77); it is distinct from the Unknown name: $element_N parameter-wiring error (related-field DELETE), which is fixed.

Release-blocking vs non-blocking gaps

Proposed split for the first non-beta (issue #51 tracks readiness):

Release-blocking (must be resolved or accurately documented before non-beta):

  • This support contract exists and is referenced by README/docs βœ… (this issue, #30).

  • README/docs support claims match tested behavior (#50).

  • Version target named and packaging metadata aligned (Python >=3.10, Django >=4.2,<7.0). βœ…

  • Transaction contract documented (#36, done).

  • Native UPSERT INTO wired up as the supported upsert path (#46). βœ…

  • Pattern/exact lookup escaping returns the correct rows (#75). βœ… Fixed β€” backslash, % and _ are escaped with ESCAPE '~'.

Non-blocking (documented limitations, can ship as known gaps):

  • Naive datetime shift under USE_TZ=False (#78) β€” production default unaffected.

  • Function/aggregate tail: PI()/Random()/CURRENT_TIMESTAMP, ORDER BY aggregated (#80).

  • Correlated-subquery LHS (#77).

  • UNION ordering / subquery edge cases.

  • UPSERT docs/examples rewrite (#47) β€” follows the #46 implementation.

Ratification status

All matrix support levels and the blocking/non-blocking split above have been reviewed and ratified by the maintainer. Decisions recorded:

  • Python floor β†’ >=3.10.

  • Django range β†’ >=4.2,<7.0 (matches the packaging pin).

  • Unenforced guarantees (foreign keys, uniqueness, checks) β†’ ❌, not best-effort; application-level workarounds do not count as backend support.

  • UPSERT contract β†’ native UPSERT INTO is the supported path (implementation tracked in #46).

  • Pattern/exact lookup escaping (#75) β†’ release-blocking.

This contract is the baseline; future changes to a support level should update this document in the same PR.