2

Example

I'm trying to annotate objects using data from a related model's DateTimeField.

    class Author(models.Model):
            name = models.CharField(max_length=256)

    class Book(models.Model):
        name = models.CharField(max_length=256)
        is_published = models.BooleanField()
        publication_date = models.DateTimeField()
        author = models.ForeignKey(Author, on_delete=models.PROTECT)

    queryset = Author.objects.annotate(
        has_unpublished=Exists(
            Book.objects.filter(
                author=OuterRef('pk'),
                is_published=False))
    ).annotate(
        last_published_date=Max(
            'book__publication_date',
            filter=Q(book__is_published=True)
        )
    )

I alternate between the default local sqlite3 database and MySQL (with mysqlclient). SQLite works and MySQL crashes.

Here's the reason: compilers for both databases

results = compiler.execute_sql(**kwargs)

return a list of lists of tuples of int and string, like so:

SQLite

<class 'list'>: [[(1, 'Alice', 1, '2017-01-01 01:00:00'), (2, 'Bob', 0, '2019-01-05 13:00:00'), (3, 'Carol', 1, None), (4, 'Dan', 0, None)]]

MySQL

<class 'list'>: [((1, 'Alice', 1, '2017-01-01 01:00:00.000000'), (2, 'Bob', 0, '2019-01-05 13:00:00.000000'), (3, 'Carol', 1, None), (4, 'Dan', 0, None))]

Now, when the SQLite backend sees a supposed datetime field, it generates a converter at runtime using this method:

django.db.backends.sqlite3.operations.DatabaseOperations

    def convert_datetimefield_value(self, value, expression, connection):
        if value is not None:
            if not isinstance(value, datetime.datetime):
                value = parse_datetime(value)
            if settings.USE_TZ and not timezone.is_aware(value):
                value = timezone.make_aware(value, self.connection.timezone)
        return value

which works ok.

The MySQL backend, however, does this:

django.db.backends.mysql.operations.DatabaseOperations

    def convert_datetimefield_value(self, value, expression, connection):
        if value is not None:
            value = timezone.make_aware(value, self.connection.timezone)
        return value

Django then proceeds to crash trying to check an <str> for timezone awareness.

Context

The above is a toy example I used to reproduce the crash.

In our actual production environment, the MySQL compiler's execute_sql, bizarrely, returns a list of lists of tuples of "raw" data with datetime.datetime objects if I request a full list of annotated authors (action=='list'), and the same list with a string if I request a particular one (action=='retrieve'). The querysets' respective sql queries only differ by the presence of a single WHERE clause.

This leads me to think I'm doing something wrong and not getting the raw datetime.datetime I'm supposed to get from the driver (rather than the far lower probability of a bug in Django).

What should I do? N+1 is out of the question, this will be the app's main operator view and the datetime field is actually two one-to-many relationships deep.

0 Answers0