Opened 5 years ago

Closed 5 years ago

#25091 closed Bug (duplicate)

Array field equality lookup fails with ProgrammingError

Reported by: Villiers Strauss Owned by:
Component: contrib.postgres Version: master
Severity: Normal Keywords:
Cc: Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no


Note: This bug report is mostly based on a reddit post by Glueon:

With the model:

class MyModel(models.Model):
    emails = ArrayField(models.EmailField())

When trying to fetch a row with a list of emails:


the following error occurs:

ProgrammingError: operator does not exist: character varying[] = text[]
HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.

Because the resulting query is:

SELECT * FROM some_table WHERE emails = ARRAY[''];

By default type of ARRAY[''] is text [] while Django stores it as a varchar [].

Glueon suggests that casting the array to varchar [] using raw sql solves the problem:

SELECT * FROM some_table WHERE emails = ARRAY['']::varchar[];

I did post some other workarounds in the comments of the reddit post, but there are some situations in which the workarounds are not possible, such as when using get_or_create or update_or_create.

Change History (6)

comment:1 Changed 5 years ago by Simon Charette

Triage Stage: UnreviewedAccepted
Version: 1.8master

This seems related to #24726.

Last edited 5 years ago by Tim Graham (previous) (diff)

comment:2 Changed 5 years ago by Bertrand Bordage

Here’s a ready-to-use workaround for get_or_create, as described in this reddit post.

def bugfree_get_or_create(manager, **kwargs):
    This is workaround for
    model = manager.model
    defaults = kwargs.pop('defaults', {})
    for k, array in tuple(kwargs.items()):
        if isinstance(model._meta.get_field(k), ArrayField):
            for i, item in enumerate(array):
                kwargs[k + '__%d' % i] = item
            kwargs[k + '__len'] = len(array)
            del kwargs[k]
        return manager.get(**kwargs)
    except model.DoesNotExist:
        return manager.create(**kwargs)

Instead of writing YourModel.objects.get_or_create(your_array_field=['abc', 'def'], other_kwargs=…), write bugfree_get_or_create(YourModel.objects, your_array_field=['abc', 'def'], other_kwargs=…).

Last edited 5 years ago by Bertrand Bordage (previous) (diff)

comment:3 Changed 5 years ago by Simon Charette

I would like to add a notice that the code above is not even close to be as bullet proof as get_or_create and you should expect race conditions if you were to use it.

comment:4 in reply to:  description Changed 5 years ago by Calvin Giles

I have found two simple workarounds to this, but they may have some issues that I am not aware of. The first is to pass the array to the get or get_or_create method not as a list, but as a string formatted using

  '{1.0, 2.0}'

instead of

  ARRAY[1.0, 2.0]

which causes postgres to handle an implicit cast from numeric to real (which I needed in this case).

The other is to perform this conversion in get_db_prep_value() by overriding the ArrayField class. For my specific case, this appears to work, but it may not be robust to arrays of different types (especially strings):

class StrFormatArrayField(ArrayField):
    Override the array format to use "'{1.0, 2.0, 3.0}'" instead of "ARRAY([1.0, 2.0, 3.0])"
      to get around an issue with implicit type conversion.
    def get_db_prep_value(self, value, connection, prepared=False):
        if isinstance(value, list) or isinstance(value, tuple):
            prepped = [self.base_field.get_db_prep_value(i, connection, prepared) for i in value]
            joined = ', '.join([str(v) for v in prepped])
            return '{' + joined + '}'
            return value

Any advice on improving upon this or issues I may face would be greatly appreciated.

comment:5 Changed 5 years ago by Dmitry Dygalo

Has patch: set

comment:6 Changed 5 years ago by Simon Charette

Resolution: duplicate
Status: newclosed

#25666 is a duplicate with a patch.

Note: See TracTickets for help on using tickets.
Back to Top