ADR 0002 — Convert only exact values, and keep a stand-in field for migration history#
Status: accepted
Decision#
Moving a column from django-partial-date rests on two rules.
A value converts only if it is exactly what django-partial-date writes. That is a timestamp at
00:00:00 on 1 January (year), 00:00:01 on the first of a month (month) or 00:00:02 on any date (day),
in the years 0001 to 9999, read in the time zone of the database connection. An empty value stays
empty. ConvertPartialDateField checks every value before it changes anything. Any other value
stops the migration with RefusedValuesError, and the column and its rows stay as they were. The
same holds on PostgreSQL and SQLite.
Existing migrations point at OldPartialDateField, never at PartialDateField. It is a field
that describes the old column. It stays in the package for good, because a project’s migration
history refers to it after the project has uninstalled django-partial-date.
Why#
What to do with a value that does not fit#
Goal G5 says a move loses no data. Three treatments of a value that does not fit were weighed.
Option |
Result |
|---|---|
Convert to what django-partial-date displays |
Rejected. |
Skip the value and report it |
Rejected. The column then cannot take the check ADR 0001 requires, and a report is easy to miss in a deployment. |
Stop the migration |
Chosen. Nothing is lost, and the developer is the only one who can say what the value meant. |
What django-partial-date displays is not evidence of what was recorded, so a value it reads without complaint is still refused if it holds more than the ISO form can carry. A refused migration changes nothing, so applying it to a copy of the data is also how a project checks its data.
The time zone follows from the same rule. django-partial-date writes a naive timestamp, so the database reads it in the connection’s zone, and it reads it back the same way. A project whose settings changed after the rows were written has values that no longer fall on the three stored times. They are refused, instead of converted a day out.
What existing migrations point at#
Option |
Result |
|---|---|
Point them at |
Rejected. The migrations would claim a text column while a deployed database holds a timestamp. On a database behind the latest migration, any pending change that makes SQLite rebuild the table creates the text column with its check, and copying the old values fails. A data migration reading the column fails too. |
Keep a field describing the old column |
Chosen. History stays true, so a new database and a deployed one take the same path and differ only in holding rows. Django’s own documentation recommends keeping a field class that historical migrations reference. |
The stand-in reports CharField from get_internal_type(), which does not match its column, on
purpose. Without that, a generated AlterField left in the migration casts the timestamps to
varchar(10) on PostgreSQL, which truncates them silently, and the check accepts the result:
1990 becomes 1990-01-01. With it, PostgreSQL raises an error.
Revisit if#
A project has to move a table it cannot stop writing to. The check runs before the column changes, and a row written between the two is not checked.
django-partial-date is no longer in use anywhere. The stand-in stays for as long as any project’s migration history names it.