0

I'm trying to implement a subquery with Django ORM, but I can't find a working solution. The SQL query that I would need to reverse-engineer is:

select t1.location, sum(t1.value_relative::numeric) as total
from (
    select
        administrative_division_id as "location",
        jsonb_array_elements("values"->'loss'->'values')->>'dim_value' as dim_value,
        jsonb_array_elements("values"->'loss'->'values')->>'value_relative' as value_relative
    from assessments_entrybaserisk
    where "values"->'scenario'->>'value' = 'Total'
) t1
where t1.dim_value::numeric = 2
group by t1.location

I tried to use Django Subquery like below:

subq = EntryBaseRisk.objects \
    .filter(id=OuterRef('pk'), values__scenario__value='Total') \
    .annotate(
    _dim_value=Cast(
        KeyTextTransform('dim_value',
                         JsonbArrayElements(KeyTransform('values', KeyTransform('loss', 'values')))),
        IntegerField()),
    _value_relative=Cast(
        KeyTextTransform('value_relative',
                         JsonbArrayElements(KeyTransform('values', KeyTransform('loss', 'values')))),
        FloatField())) \
    .values('assessment', 'administrative_division', '_dim_value', '_value_relative')

data = EntryBaseRisk.objects \
    .annotate(
    dim_value=Subquery(subq.values('_dim_value'), output_field=IntegerField()),
    total_relative_value=Sum(Subquery(subq.values('_value_relative'),
                                      output_field=FloatField()))) \
    .filter(**filters) \
    .values('administrative_division', 'total_relative_value') \
    .order_by('administrative_division')

but I get an error ProgrammingError: more than one row returned by a subquery used as an expression. The problem is that SQL generated by Django is different from my SQL above, because it doesn't select from a select and it works only if the subquery is returning only one row. I can't perform the Sum aggregation in the subquery, because aggregate function calls cannot contain set-returning function calls. Is there another way to make Django generate an SQL as per my needs?

Stefano Luoni
  • 81
  • 1
  • 8

1 Answers1

0

You can try something like this:

relative_subquery = subq.values('_value_relative').annotate(total=Sum("*")).values('total')
dim_subquery = subq.values('_dim_value')

data = EntryBaseRisk.objects.annotate(
    dim_value=Subquery(dim_subquery[:1], output_field=IntegerField()),
    total_relative_value=Subquery(relative_subquery, output_field=FloatField())) \
    .filter(**filters) \
    .values('administrative_division', 'total_relative_value') \
    .order_by('administrative_division')

Here I am summing the value _value_relative from the subq and using the first value of the _dim_value to be annotated with EntryBaseRisk queryset.

ruddra
  • 50,746
  • 7
  • 78
  • 101
  • Unfortunately, this won't work because the aggregation is attempted to be resolved within the subquery, thus getting an error "aggregate function calls cannot contain set-returning function calls" from the database. – Stefano Luoni Aug 06 '20 at 08:59
  • You moved the slice applied to "dim_value" field but the main problem is with the Sum(), because the subquery takes rows from a jsonb array using jsonb_array_elements and it's not possible to perform aggregations within the same query. That's why an inner query is needed. I think there's no way to solve this using Django Subquery because it works differently... – Stefano Luoni Aug 06 '20 at 09:16