Chapter 11 · Database backends
11.2 The connection object: BaseDatabaseWrapper and what a backend provides
A backend is a package whose base module defines a class named DatabaseWrapper, a subclass of BaseDatabaseWrapper that names the driver, where it has one, and the classes of the connection’s helper objects (DatabaseWrapper). BaseDatabaseWrapper gives each connection its state and its own helpers, among them the client that the command dbshell runs and the validation of fields against a database, and the class declares the methods that switch foreign key checks off and on.
Django asks a backend for one thing, its DatabaseWrapper, by name, and reaches everything else from that class (Connections by alias: DATABASES and ConnectionHandler). Each of the four backends Django ships keeps its helpers in modules of their own, operations, features and so on, but "ENGINE" leads Django to the base module alone, and the helpers’ modules are found because base imports their classes and names them in class attributes. So one backend can be built on another by subclassing its wrapper, as the backends of django do: PostGIS’s DatabaseWrapper is PostgreSQL’s with its own schema editor, features, operations and introspection named in those attributes. The smallest backend is the dummy, which stands in when no database is configured (Connections by alias: DATABASES and ConnectionHandler).
What a backend’s class declares
A backend’s DatabaseWrapper is mostly class attributes. In a recording from a running Django, each line under the title but the first, which closes the connection and so leaves the driver’s connection None, is a question put to the connection "default", SQLite’s, and its answer after ->, and full( is the recording’s own helper for writing a value whole:
What a connection carries: its class, the backend's names for itself, the driver, and the helper objects it made
connection.close()
type(connections['default']) -> django.db.backends.sqlite3.base.DatabaseWrapper
connection.vendor -> 'sqlite'
connection.display_name -> 'SQLite'
connection.Database -> the module sqlite3.dbapi2
connection.SchemaEditorClass -> django.db.backends.sqlite3.schema.DatabaseSchemaEditor
type(connection.ops) -> django.db.backends.sqlite3.operations.DatabaseOperations
type(connection.features) -> django.db.backends.sqlite3.features.DatabaseFeatures
type(connection.introspection) -> django.db.backends.sqlite3.introspection.DatabaseIntrospection
type(connection.creation) -> django.db.backends.sqlite3.creation.DatabaseCreation
type(connection.client) -> django.db.backends.sqlite3.client.DatabaseClient
type(connection.validation) -> django.db.backends.base.validation.BaseDatabaseValidation
connection.ops.connection is connections['default'] -> True
connection.connection -> None
full(list(connection.settings_dict)) -> ['ENGINE', 'NAME', 'ATOMIC_REQUESTS', 'AUTOCOMMIT', 'CONN_MAX_AGE', 'CONN_HEALTH_CHECKS', 'OPTIONS', 'TIME_ZONE', 'USER', 'PASSWORD', 'HOST', 'PORT', 'TEST']
connection.settings_dict['NAME'] -> 'harbour.sqlite3'
| attribute | what it holds | what reads it |
|---|---|---|
Base |
what it holds a short name: "sqlite", "postgresql", "mysql" or "oracle", and "unknown" on the base class |
what reads it SQLCompiler, which looks on each node for a method named "as_" and the vendor; Options django/required_ names one; code that branches on the database by name, as the JSON field and its lookups do (json.py django/ |
Base |
what it holds the database’s name in messages; MySQL’s is a cached property that asks the server, and is "MariaDB" or "MySQL" |
what reads it the refusal of a server too old; the system checks of fields, constraints and comments; the operations object’s refusals |
Database (Database django/ |
what it holds the driver’s module | what reads it Database, which looks up on it the driver’s exception of the same name as each of Django’s (Cursors: executing a statement, the query log and execute wrappers); Cursor django/Binary, for the driver’s constructor of binary values |
Base |
what it holds the schema editor’s class | what reads it Base, which refuses with Not while it is None |
Base and its siblings, one for each helper |
what it holds the classes of the helper objects; Base is Base unless a backend names another |
what reads it Base |
Base, Base, Base |
what it holds each field class’s column type, the SQL after it, and its check | what reads it the fields and the schema editor: Column types: from a field to a column |
operators, pattern_esc, pattern_ops (Database django/ |
what it holds the SQL of each comparison | what reads it the lookups: The SQL a backend writes: Base |
Base |
what it holds how many statements the query log keeps | what reads it Cursors: executing a statement, the query log and execute wrappers |
The base class leaves to the backend what only the backend can know, and raises NotImplementedError from each such method: BaseDatabaseWrapper, BaseDatabaseWrapper, BaseDatabaseWrapper and BaseDatabaseWrapper, which Opening and closing a connection tells with each backend’s; BaseDatabaseWrapper, in Cursors: executing a statement, the query log and execute wrappers; and BaseDatabaseWrapper, in Transactions: autocommit, atomic and savepoints. Other methods are hooks that do nothing until a backend fills them. BaseDatabaseWrapper is one: migrate calls it on its connection before it works out what to migrate, and PostGIS’s creates the extension "postgis" there where it is missing. The methods that turn foreign key checks off and on are others.
The helper objects
BaseDatabaseWrapper ends by making the helpers, each by calling its class with the connection. Every connection object has its own: two threads’ connections for one alias share no helper, and a feature that is a cached property, worked out by asking the server, is worked out once for each connection object (What a database can do: BaseDatabaseFeatures). The schema editor is not among them; BaseDatabaseWrapper makes a new one each time it is called (The schema editor: BaseDatabaseSchemaEditor).
Each helper keeps the connection it was given as its attribute connection, as the recording’s line connection shows, and reads there what it needs of it: the operations object the time zone and the features, the validation the display name.
A backend that has to adjust a helper does so after the base class’s __init__ has made it. Oracle’s DatabaseWrapper sets two features, whether an insert can hand back columns and an update rows, from the option "use_returning_into".
The state of one connection
BaseDatabaseWrapper takes the alias’s settings and the alias, opens nothing, and sets the rest of the connection’s state on the instance in groups, most of them under a comment of their own:
BaseDatabaseWrapper.__init__ sets, grouped by what they serve. The connection’s time zone is worked out from the settings when it is first asked for (Values in and out: adapters, converters and time zones).| what it serves | attributes | told in |
|---|---|---|
| the driver’s connection | attributes connection, None until it is first needed (Base django/settings_dict; alias |
told in Opening and closing a connection |
| the query log | attributes queries_log, which keeps at most queries_limit entries; force_ |
told in Cursors: executing a statement, the query log and execute wrappers |
| the transaction | attributes autocommit, False as PEP 249 has a new connection; in_atomic_block, savepoint_state, savepoint_ids, atomic_blocks, commit_on_exit, needs_rollback, rollback_exc |
told in Transactions: autocommit, atomic and savepoints |
| when to close | attributes close_at, closed_, errors_occurred, health_, health_ |
told in Opening and closing a connection |
| threads | attributes the identity of the thread that made the connection, and a count, kept under a lock, of the permissions to share it | told in Opening and closing a connection |
| work held until the commit | attributes run_on_commit, run_ |
told in Work held until the commit: on_commit |
| execute wrappers | attributes execute_wrappers |
told in Cursors: executing a statement, the query log and execute wrappers |
| the helper objects | attributes client, creation, features, introspection, ops, validation |
told in the table in Database backends |
The settings dictionary is the connection handler’s own for the alias, and not a copy, so every thread’s connection for an alias holds the same one (Connections by alias: DATABASES and ConnectionHandler). BaseDatabaseWrapper makes a second connection to the same database: a wrapper of the same class with a deep copy of the settings, under the same alias unless it is given another, registered nowhere: connections never hands it out. It is for tests that need two connections to one database, and nothing in Django calls it but Django’s own test suite.
The client: dbshell
The command dbshell runs the database’s own command-line program, connected as the alias’s settings say. It calls BaseDatabaseClient on the client of the connection its "--database" option names, with the arguments given after it on the command line. runshell asks the class method BaseDatabaseClient for the program’s arguments and an environment, sets that environment over the process’s own where there is one, and runs the program with subprocess.run, raising if it exits with a failure. The base class’s class method raises NotImplementedError with a message that gives a backend its choice: provide the class method, or override runshell. The command turns two failures into a CommandError: FileNotFoundError, which it takes to mean that the program named by BaseDatabaseClient is not installed, and the program’s exit with a failure, which it reports with the command line and passes on as its own status.
runshell also make Django’s process ignore an interrupt while the program runs, which, their comment says, lets the interrupt pass to the program to stop a query.| backend | the program and its arguments | the password |
|---|---|---|
SQLite (DatabaseClient django/ |
the program and its arguments "sqlite3" with the file’s name, then the arguments given |
the password none: a file has none |
PostgreSQL (DatabaseClient django/ |
the program and its arguments "psql" with the user, host and port as flags, the arguments given, and last the database, "postgres" where neither "NAME" nor the option "service" names one |
the password in the environment, as "PGPASSWORD", with the options for a service, a password file and SSL |
MySQL and MariaDB (DatabaseClient django/ |
the program and its arguments "mysql" with a defaults file, the user, the host or a socket, the port, the SSL files and the character set as flags, then the database, then the arguments given; the user, password, host, port and database taken from "OPTIONS" before the settings |
the password in the environment, as "MYSQL_PWD" |
Oracle (DatabaseClient django/ |
the program and its arguments "sqlplus" and "-L" with one connect string of the user, the password in double quotes and the address from dsn, then the arguments given; "rlwrap" in front where it is on the path |
the password among the arguments |
MySQL’s client gives its reason for the environment in a comment: MySQL’s documentation discourages the variable, since on old Unix systems the command ps can show it, but a password among the arguments is exposed on more systems, and the environment keeps it out of the subprocess raised when the program fails, whose text holds every argument. Oracle’s client puts the password among the arguments, and dbshell’s message for a program that failed is built from them.
Validation: check_field and the database checks
A connection’s validation object is how a backend adds its own warnings to the system checks, through two methods of BaseDatabaseValidation. BaseDatabaseValidation returns no messages in the base class, and is called for each alias by check_database_backends, a check registered under the tag for databases. A run of the checks that names neither aliases nor tags leaves such checks out, because, a comment says, they do more than read the code; they run under check given its "--database" option or that tag, under migrate for its alias, and under the test runner (The system checks).
BaseDatabaseValidation is called by the checks of fields, which are not under the tag for databases, so a run that names neither aliases nor tags calls it too, for each field and every alias, where the router allows the field’s model to be migrated there (The model checks). It does nothing unless the backend’s validation defines check_field_type, which it hands the field and its column type on this connection, passing over related fields and a few others. SQLite’s and PostgreSQL’s wrappers name no validation class, so theirs is the base class, as the recording shows for SQLite.
MySQL’s validation has both methods. Its check warns, as "mysql.W002", when neither of MySQL’s strict modes is among the server’s SQL modes, which it learns by asking the server; its check_field_type warns, as "mysql.W003", of a unique field whose column is a "varchar" longer than 255 characters or of no stated length, and, as "fields.W162", of an indexed field whose column is of a type MySQL cannot index in full, a text, a blob or JSON. Oracle’s has the last alone, for its large objects. Both take the types from the wrapper’s _limited_data_types; the warning’s hint says the index will not be created, and each backend’s schema editor reads the same attribute to leave it out.
Foreign key checks switched off: constraint_checks_disabled
Some writes put rows in an order the database’s foreign keys would refuse: a fixture whose rows refer forward to rows later in the file, or a cycle of references. BaseDatabaseWrapper is a context manager for them. It calls BaseDatabaseWrapper, and on leaving calls BaseDatabaseWrapper only if the first returned true, which the base class’s docstring asks a backend to return only when the checks were turned off and need turning on again. Its callers then check what was written meanwhile with BaseDatabaseWrapper, which is to raise IntegrityError for a row that refers to nothing.
disable_constraint_checking, and on, in enable_constraint_checking, and checks what was written meanwhile, in check_constraints.| backend | turning off | turning on | checking |
|---|---|---|---|
| the base class | turning off does nothing; false | turning on does nothing | checking does nothing |
SQLite (DatabaseWrapper django/ |
turning off "PRAGMA foreign_, then reads the pragma back: true only if the checks are off; inside a transaction the pragma cannot turn them off |
turning on "PRAGMA foreign_ |
checking "PRAGMA foreign_, of the tables named or of all; the first row found raises, naming its table, key and value |
PostgreSQL (DatabaseWrapper django/ |
turning off the base class’s | turning on the base class’s | checking "SET CONSTRAINTS ALL IMMEDIATE", then "SET CONSTRAINTS ALL DEFERRED" |
MySQL (DatabaseWrapper django/ |
turning off "SET foreign_; always true |
turning on "SET foreign_, with needs_rollback cleared while it runs |
checking for each foreign key of each table named, or of every table, a join that finds the rows whose reference has no row; the first raises |
Oracle (DatabaseWrapper django/ |
turning off the base class’s | turning on the base class’s | checking as PostgreSQL’s |
The backends differ because their foreign keys do. PostgreSQL’s and Oracle’s operations objects make each foreign key "DEFERRABLE INITIALLY DEFERRED", and SQLite’s schema editor writes its foreign keys so too, so that within a transaction a row may refer to one not yet written and the database checks when the transaction commits; setting the constraints to immediate, as PostgreSQL’s and Oracle’s check_constraints does before setting them back, makes the database check them at once. SQLite’s schema editor, some of whose alterations need the checks off, turns them off as it is entered, refusing where they are on inside a transaction, since the pragma cannot turn them off there, and as it is left checks every table and turns them on again (The schema editor: BaseDatabaseSchemaEditor). MySQL checks its foreign keys as each statement runs, which a comment gives as the reason for turning the checks off where the test databases are filled again. MySQL’s enable_constraint_checking clears needs_rollback while its statement runs, for constraint_checks_disabled nested inside atomic, its comment says; a cursor refuses every statement while needs_rollback is set (Transactions: autocommit, atomic and savepoints).
The callers pair the three, and both enter constraint_checks_disabled inside an atomic block: loaddata loads its fixtures within its own and then checks the tables it wrote to, and the test databases are filled again from their serialized rows the same way (Test databases: BaseDatabaseCreation). Inside a transaction only MySQL’s checks go off; SQLite’s disable_constraint_checking asks for the pragma off and reads it back still on, as this recording shows:
with transaction.atomic(), connection.constraint_checks_disabled(): Ship.objects.count()
Atomic.__enter__
SQL BEGIN
BaseDatabaseWrapper.constraint_checks_disabled
DatabaseWrapper.disable_constraint_checking
SQL PRAGMA foreign_keys = OFF
SQL PRAGMA foreign_keys
-> False
On the other three the deferred foreign keys let the rows in, and check_constraints checks them. TestCase calls check_constraints as each test ends, before it rolls the test’s transaction back, on each database whose features say it can defer checks and whose connection is usable and not marked for rollback (A transaction around each test: TestCase).