Chapter 11 · Database backends
11.8 What a database can do: BaseDatabaseFeatures
BaseDatabaseFeatures is the class of a connection’s features object, BaseDatabaseWrapper: flags, attributes that say what one database can do, which the compiler, the queryset, the schema editor, migrations and the test framework read before they choose what to write or whether to refuse. A flag is a class attribute, a property, a cached_property worked out once for each connection, or a value the connection sets on the object.
Code above the backend is written once for every database, and where the databases differ in what they can do it mostly does not ask which database it faces: it asks the question it needs answered. The compiler writes a "FOR UPDATE" only where BaseDatabaseFeatures is true, bulk_create asks for the new rows’ keys only where BaseDatabaseFeatures is, and the schema editor runs a migration inside a transaction only where BaseDatabaseFeatures is. Each such question is a flag of the connection’s features object, an instance of the backend’s own subclass of BaseDatabaseFeatures. The base class declares the flags that code common to every backend reads, with a default for each, and a backend’s class overrides what differs for its database. GeoDjango’s spatial backends add the flags of BaseSpatialFeatures, and a backend may declare flags of its own besides, which only its own code and its vendor methods read, as the database function UUID4 reads PostgreSQL’s is_postgresql_18 in UUID4. A backend from outside Django sets them for its own database.
A flag, and where its value comes from
BaseDatabaseWrapper makes the features object as it makes the connection, from BaseDatabaseWrapper, and the object keeps the connection it was made for. There is one for each connection, then, and a connection is one alias in one thread (The connection object: BaseDatabaseWrapper and what a backend provides). Code asks a question by reading an attribute, and the attribute can get its value in four ways.
The plain flag is a class attribute: the base class gives the default, and the backend’s class body sets another value where its database differs. Most are true or false, and most of their comments in the base class put the flag as a question: whether the backend supports JSONField, whether DDL can be rolled back in a transaction. The rest are values that code uses as they stand, such as BaseDatabaseFeatures, a number or None, and BaseDatabaseFeatures, text to put after a "SELECT" that has no table.
SQLite is a library in Django’s own process, and its version is known without a connection. SQLite’s DatabaseFeatures is set in the class body by comparing the library’s version with 3.44.0, when the module is imported. What follows was recorded from a running Django on SQLite, each line a question and its answer after ->; the connection’s Database is the driver’s module:
connection.Database.sqlite_version_info -> (3, 50, 4)
connection.features.minimum_database_version -> (3, 37)
...
connection.features.supports_aggregate_order_by_clause -> True
A property works its value out at each read. Some are another flag read under a second name: SQLite’s DatabaseFeatures and DatabaseFeatures are its supports_json_field. SQLite’s DatabaseFeatures is a property that makes sure the connection is open and asks the driver’s connection for the number of parameters a statement may hold: 32766 unless the library was compiled with another limit, or less where the connection has lowered its own.
A cached_property works its value out at the first read and puts it in the object’s own dictionary, where every later read finds it without calling the method again. On the four backends a flag whose answer takes a connection or a statement, because it depends on the server’s version or settings or asks the database by trying, is a cached_property or a property that reads one, so that the cost is paid once, SQLite’s max_query_params aside, as a connection can lower its limit. The cache belongs to the features object, and so to one connection: a second thread’s connection to the same alias works each such flag out again for itself, and a connection that is closed and opened again keeps the answers.
A flag can also be set on the object by the connection. Oracle’s DatabaseWrapper sets BaseDatabaseFeatures and BaseDatabaseFeatures on its features object to the alias’s "OPTIONS" entry "use_returning_into", true unless the setting gives it, and its DatabaseWrapper leaves the entry out of the driver’s arguments; an attribute set on the object hides the class’s for that one connection.
One flag of the base class is a method, given a model. Where the backend has BaseDatabaseFeatures, the compiler drops from a grouping the other columns of a table whose primary key is grouped, and BaseDatabaseFeatures allows that only for a managed model, since an unmanaged one could stand for a view, on which the database might not allow that (GROUP BY and HAVING: set_group_by, get_group_by and split_having_qualify).
How a flag reaches the server’s version
A flag that depends on the server’s version reads it from the connection. PostgreSQL’s, MySQL’s and Oracle’s backends keep the version on the connection as a cached_property of their own, which gets it through BaseDatabaseWrapper: that uses the open connection where there is one, and otherwise opens one and closes it afterwards (Opening and closing a connection).
PostgreSQL’s DatabaseWrapper is the server’s version as one number, 170000 for 17.0, read from the driver’s connection; its features compare it in DatabaseFeatures, which other flags read in turn. Some of PostgreSQL’s flags read the alias’s options instead: DatabaseFeatures, whether psycopg 3 is the driver and "server_side_binding" is True, decides DatabaseFeatures (PostgreSQL).
MySQL’s DatabaseWrapper runs one statement for everything the backend wants to know of the server: the version string, the SQL mode, the default storage engine, whether "SQL_AUTO_IS_NULL" is on, whether table names are kept in lower case, and whether the server’s time zone tables are loaded; DatabaseWrapper and DatabaseWrapper are worked out from it and cached in turn. That is how one features class answers for two databases: MySQL’s DatabaseFeatures is whether the server is MariaDB and DatabaseFeatures whether it is not. MySQL and MariaDB has the rest.
Oracle’s DatabaseWrapper is the driver’s connection’s version string as a tuple of numbers, and its features compare it for frame exclusion in a window, a "SELECT" without "FROM DUAL", tuple lookups and stored generated columns among others. SQLite’s DatabaseWrapper is the library’s version as the driver reports it.
minimum_database_version and the version check
BaseDatabaseFeatures is the oldest version of the database the backend supports, as a tuple, and None in the base class. BaseDatabaseWrapper compares the backend’s BaseDatabaseWrapper with it and raises NotSupportedError where the version is older; a backend that leaves the flag at None is never checked. BaseDatabaseWrapper calls it for each alias until it passes once in a process, keeping the aliases that have passed in RAN_DB_VERSION_CHECK. The values are SQLite 3.37, PostgreSQL 16 and Oracle 19 as class attributes, and on MySQL a cached_property that answers 10.11 for MariaDB and 8.4 for MySQL. The recorded library, 3.50.4, is newer than SQLite’s 3.37, and the first connection opened without complaint.
A flag that asks the database: supports_transactions
BaseDatabaseFeatures is the base class’s example of a flag that finds its answer by trying. It is a cached_property that, on a cursor of the connection, creates a table, turns autocommit off, inserts a row, rolls back, turns autocommit on, counts the rows and drops the table; the database supports transactions if the count is nought.
None of the backends Django ships runs that probe: SQLite’s, PostgreSQL’s and Oracle’s set the flag true, the dummy backend’s sets it false, and MySQL’s reads the server’s data. The probe is for a backend that does not say.
The probe that runs on SQLite is its DatabaseFeatures, which executes a "SELECT" of SQLite’s JSON function inside an atomic block and answers false if the database raises OperationalError. Here it is read for the first time on the connection, with the object’s own dictionary asked before and after; under the question stand the statements it sent, a line beginning SQL as Django handed it to its cursor and one beginning sqlite as SQLite ran it:
'supports_json_field' in vars(connection.features) -> False
connection.features.supports_json_field -> True
SQL BEGIN
sqlite BEGIN
SQL SELECT JSON('{"a": "b"}')
sqlite SELECT JSON('{"a": "b"}')
sqlite COMMIT
'supports_json_field' in vars(connection.features) -> True
The "BEGIN" is SQLite’s backend starting the atomic block, and the commit, made by the driver’s own method, has no SQL line (Transactions: autocommit, atomic and savepoints). Oracle’s DatabaseFeatures is another such probe.
Who reads the features
Of the features’ readers, the compiler reads the most: the lock and each of its options, the compound statements, chunked reads, the grouping, and whether an insert asks for rows back (The compiler: as_sql, execute_sql and results_iter, INSERT, UPDATE and DELETE: the insert, update and delete compilers). Expressions read them as they compile, to refuse or to write something else: Window.as_sql raises NotSupportedError on a backend without BaseDatabaseFeatures, and where BaseDatabaseFeatures is false an aggregate’s filter raises it too, which Aggregate.as_sql catches to write the filter as a "CASE".
The queryset reads them where it chooses before it compiles: for conflicts and returned rows in bulk_create (bulk_create and bulk_update), and in QuerySet.get, which limits the rows it fetches unless the query locks them on a backend that cannot lock a sliced query. Model reads BaseDatabaseFeatures to choose what a save asks back (Saving an instance), and the manager of a many-to-many relation adds rows to an automatic table with conflicts ignored where BaseDatabaseFeatures holds. The operations object, which writes the database’s SQL, reads its own connection’s flags, BaseDatabaseOperations dividing BaseDatabaseFeatures, and the connection reads BaseDatabaseFeatures before each savepoint on every backend but SQLite, whose connection asks only whether it is inside an atomic block (Transactions: autocommit, atomic and savepoints).
The schema editor reads them as it writes DDL: whether a migration runs in a transaction, whether foreign keys, check constraints, comments and partial, covering and expression indexes are written at all, whether a default has to be written as a literal (The schema editor: BaseDatabaseSchemaEditor). And the test framework reads BaseDatabaseFeatures and uses_savepoints for TestCase, and any flag by name through skipUnlessDBFeature and its relatives, which a test gives as strings (Which databases a test uses: databases). BaseDatabaseFeatures and BaseDatabaseFeatures name tests of Django’s own suite to skip or to expect to fail on the database, and are read only when that suite runs (Test databases: BaseDatabaseCreation).
Chosen flags on the four backends
The flags of BaseDatabaseFeatures in the table differ by backend.
BaseDatabaseFeatures on the four database backends Django ships (django/db/backends/sqlite3/features.py , django/db/backends/postgresql/features.py , django/db/backends/mysql/features.py , django/db/backends/oracle/features.py ). “By version” is the server’s, or on SQLite the library’s; “none” is None, no limit. The insert and conflict flags are told in Inserts: returned keys, conflicts and batch sizes, and can_rollback_ddl in The schema editor: BaseDatabaseSchemaEditor.| flag | SQLite | PostgreSQL | MySQL | Oracle |
|---|---|---|---|---|
supports_ |
SQLite true | PostgreSQL true | MySQL true unless the default storage engine is MyISAM | Oracle true |
can_ |
SQLite true | PostgreSQL true | MySQL false | Oracle false |
can_rollback_ddl |
SQLite true | PostgreSQL true | MySQL false | Oracle false |
can_ |
SQLite true | PostgreSQL true | MySQL true on MariaDB, false on MySQL | Oracle true unless "use_ is false |
can_ |
SQLite true | PostgreSQL true | MySQL true on MariaDB, false on MySQL | Oracle false |
can_ |
SQLite true | PostgreSQL true | MySQL false | Oracle true unless "use_ is false |
supports_ |
SQLite true | PostgreSQL true | MySQL true | Oracle false |
supports_ |
SQLite true | PostgreSQL true | MySQL true | Oracle false |
supports_ |
SQLite true | PostgreSQL true | MySQL false | Oracle false |
has_ |
SQLite false | PostgreSQL true | MySQL true | Oracle true |
has_ |
SQLite false | PostgreSQL true | MySQL true | Oracle true |
has_ |
SQLite false | PostgreSQL true | MySQL true | Oracle true |
has_ |
SQLite false | PostgreSQL true | MySQL true on MySQL, false on MariaDB | Oracle true |
has_ |
SQLite false | PostgreSQL true | MySQL false | Oracle false |
supports_ |
SQLite true | PostgreSQL true | MySQL true | Oracle false |
supports_ |
SQLite true | PostgreSQL true | MySQL false | Oracle by version: 21 and later |
supports_ |
SQLite asks the database, once for each connection | PostgreSQL true | MySQL true | Oracle true |
has_ |
SQLite false | PostgreSQL true | MySQL false | Oracle false |
supports_ |
SQLite false | PostgreSQL true | MySQL true | Oracle false |
interprets_ |
SQLite false | PostgreSQL false | MySQL false | Oracle true |
supports_ |
SQLite false | PostgreSQL true | MySQL false | Oracle false |
has_ |
SQLite true | PostgreSQL true | MySQL whether the server’s time zone tables are loaded | Oracle true |
supports_ |
SQLite false | PostgreSQL true | MySQL true | Oracle true |
supports_ |
SQLite by the library’s version: 3.44.0 and later | PostgreSQL true | MySQL true | Oracle true |
supports_ |
SQLite true | PostgreSQL true | MySQL true | Oracle by version: 23.7 and later |
supports_ |
SQLite true | PostgreSQL by version: 18 and later | MySQL true | Oracle true |
max_query_params |
SQLite the connection’s limit, asked at each read: 32766 unless the library was compiled otherwise or the connection lowered it | PostgreSQL none; 65535 where parameters are bound on the server under psycopg 3 | MySQL none | Oracle 65535 |