Chapter 20 · Content types, static files and the other contrib apps
20.14 The PostgreSQL application: PostgresConfig, operations and aggregates
What installing django changes, through PostgresConfig, uninstall_if_needed and register_type_handlers; and its migration operations, such as CreateExtension and AddIndexConcurrently, and aggregates, such as ArrayAgg, some deprecated for those of django.db.models.
Most of the application is classes to import. Installing it also changes registries the whole process shares, and prepares each PostgreSQL connection for hstore and citext, types from extensions, whose numbers (OIDs), a comment says, may vary with the installation; Django asks each database for them.
PostgresConfig.ready
PostgresConfig runs as the app registry is populated (Populating the app registry), in this order:
- It connects
uninstall_if_neededtosetting_changed. - For each PostgreSQL connection object already made, it maps the five range types, by number, and hstore, by name (an extension’s numbers vary, a comment says), to fields for introspection; if the connection is open, it registers the type handlers on it.
- It connects
register_type_handlerstoconnection_created, for each connection opened later. - It registers the transform
"unaccent"and the lookups"search","trigram_similar","trigram_word_similar"and"trigram_strict_word_similar"ondjangoand.db .models .CharField django, and so on their subclasses..db .models .TextField - It registers
RangeSerializerwithMigrationWriter, so that a migration can hold the driver’s range values. - It lets
OpClasswrap an index’s expression, betweenOrderByandCollate(IndexExpression; PostgreSQL indexes and.register_wrappers ExclusionConstraint).
Step 2 reaches only connection objects already made, opened or not. The default alias’s is made as soon as a model without its own db_table is defined, which asks it for the longest name its database allows, and all models are imported before any ready runs. The dictionary updated, DatabaseIntrospection, is the PostgreSQL backend’s class attribute, so one connection found serves all. The harbour office’s run, SQLite by default, ended start-up without the range types (Introspection: reading the schema back).
uninstall_if_needed
override_settings can install the application for a while, running ready again. uninstall_if_needed undoes steps 1, 3, 4 and 5 when a change of INSTALLED_APPS ends and the list then in force lacks the application: at the end of an override that added it, and at neither end of one that removed it. In the run:
after django.setup() "search": True
an override without the application "search": True
a nested override with it "search": True
the nested override ended "search": False
both ended "search": False
Type handlers for hstore and citext
get_type_oids asks pg_type for the numbers of a type and of its array type, returning every row found as two tuples; get_hstore_oids and get_citext_oids call it under functools, so each alias asks once, until CreateExtension clears the caches.
register_type_handlers has a version for psycopg 3 and one for psycopg 2, chosen at import. Both return at once for another vendor and for NO_DB_ALIAS, the connection with no database that makes test databases. Each registers only the types pg_type returned, and of citext only the array type: under psycopg 3 on the driver’s connection object; under psycopg 2 hstore globally, and citext’s array type with no connection named, a comment saying a project may use the application without the extensions, and must connect before it can install one.
CheckPostgresInstalledMixin
CheckPostgresInstalledMixin, new in 6.0, adds the error "postgres.E005" to the parent’s checks when Apps says the application is missing. With the application taken out, the sailings’ crew field reported “‘django.contrib.postgres’ must be in INSTALLED_APPS in order to use ArrayField.”, and their GIN index and exclusion constraint, checked for the “postgres” alias, the same error id, against the model; a model checks those only for the databases the checks are given and the router allows.
The migration operations
Of the operations of django (Operations: the base class and its two halves), those on extensions and collations leave the project state alone, and ask the router with the application’s label and no model (Routing between databases: ConnectionRouter).
CreateExtension
CreateExtension returns unless the connection is PostgreSQL’s and the router, given the operation’s hints (6.0), allows it. Only if CreateExtension finds no row in pg_extension does it write "CREATE EXTENSION IF NOT EXISTS". Either way it then clears both caches of numbers and calls register_type_handlers on the schema editor’s connection, which a comment says must wait for the extension, for a later data migration on the same connection. Backwards, it drops the extension if it exists and clears the caches. Its subclasses, HStoreExtension among them, fix the name.
AddIndexConcurrently and RemoveIndexConcurrently
These subclasses of AddIndex and RemoveIndex pass concurrently=True to the schema editor, whose PostgreSQL version then writes "CREATE INDEX CONCURRENTLY" or "DROP INDEX CONCURRENTLY IF EXISTS". The documentation says the option is not supported inside a transaction, and each database half first calls NotInTransactionMixin, which raises NotSupportedError in an atomic block. What keeps the transaction away is the migration’s atomic = False, handed by the executor to the schema editor; the operation’s own AddIndexConcurrently, False, keeps none away. Compiled for PostgreSQL, then applied on SQLite:
AddIndexConcurrently("sailing", Index(fields=["vessel"], name="sailing_vessel")), atomic = False
CREATE INDEX CONCURRENTLY "sailing_vessel" ON "charts_sailing" ("vessel");
atomic = True, on SQLite:
NotSupportedError: The AddIndexConcurrently operation cannot be executed inside a transaction (set atomic = False on the migration).
Collations and constraints not yet valid
CreateCollation and RemoveCollation hold a name, a locale, a provider ("libc" by default) and a deterministic flag. Forwards, each checks the vendor and the router; backwards, the router alone. CreateCollation cancels a CreateCollation against a following RemoveCollation of the same name, and the optimizer drops both (The optimizer: MigrationOptimizer and reduce). A non-deterministic ICU collation, as handed to PostgreSQL’s schema editor:
forwards: CREATE COLLATION "harbour_ci" (locale="und-u-ks-level2", provider="icu", deterministic=false)
backwards: DROP COLLATION "harbour_ci"
AddConstraintNotValid, an AddConstraint for a CheckConstraint only (The field operations, and those on indexes and constraints), adds "NOT VALID", leaving existing rows unchecked; ValidateConstraint checks them, and backwards does nothing, as a comment says PostgreSQL cannot make a constraint invalid.
Functions and aggregates
RandomUUID writes "GEN_RANDOM_UUID()" and TransactionNow "CURRENT_TIMESTAMP". ArraySubquery is a Subquery inside "ARRAY(...)", whose output is an ArrayField of the inner query’s field.
The aggregates name a function, and some turn on what it accepts (Aggregates: Aggregate, Count and get_aggregation). ArrayAgg and JSONBAgg accept distinct=True and order_by=..., and give an ArrayField of the argument’s field and a JSONField; BoolAnd and BoolOr a boolean. The statistical aggregates take two expressions, y and x, refuse with ValueError unless both are given, and by default give a float, but RegrCount an integer.
RemovedInDjango2028Warning: the mixin when a class is made from it.| deprecated | since | use instead |
|---|---|---|
the PostgreSQL StringAgg |
since 6.0 | use instead django, the delimiter an expression |
BitAnd, BitOr, BitXor |
since 6.1 | use instead those of django.db.models |
Orderable |
since 6.0 | use instead Aggregate with Aggregate |
The general StringAgg reads a string as a field’s name, so the old one wraps a string delimiter in Value, with a second warning. On the harbour’s ports, compiled for PostgreSQL:
StringAgg("ships__name", ", ", order_by="ships__name") # django.contrib.postgres.aggregates
RemovedInDjango2028Warning: delimiter: str will be resolved as a field reference instead of a string literal on Django 2028. Pass `delimiter=Value(', ')` to preserve the previous behavior.
RemovedInDjango2028Warning: The PostgreSQL specific StringAgg function is deprecated. Use django.db.models.aggregates.StringAgg instead.
STRING_AGG("fleet_ship"."name", %s ORDER BY "fleet_ship"."name") params: (', ',)
django given the same arguments raised FieldError, “Cannot resolve keyword ', ’ into field”, and given Value(", ") wrote the same SQL.