Projet

Général

Profil

0002-engine-make-Dimension.order_by-a-list-fixes-28175.patch

Benjamin Dauvergne, 09 décembre 2018 01:21

Télécharger (14,2 ko)

Voir les différences:

Subject: [PATCH 2/2] engine: make Dimension.order_by a list (fixes #28175)

 bijoe/engine.py                       | 144 +++++++++++++++-----------
 bijoe/schemas.py                      |   8 +-
 tests/fixtures/schema1/01_schema.json |   8 ++
 tests/test_schema1.py                 |   4 +-
 4 files changed, 96 insertions(+), 68 deletions(-)
bijoe/engine.py
60 60
    @property
61 61
    def members(self):
62 62
        assert self.type != 'date'
63
        value = self.value
64
        value_label = self.value_label or value
65
        order_by = self.order_by
66

  
63 67
        with self.engine.get_cursor() as cursor:
64 68
            sql = self.members_query
65 69
            if not sql:
66
                if self.dimension.join:
67
                    join = self.engine_cube.get_join(self.dimension.join[-1])
68
                    sql = ('SELECT %s AS value, %s::text AS label FROM %s AS "%s" '
69
                           'GROUP BY %s, %s ORDER BY %s' % (
70
                               self.value, self.value_label or self.value, join.table, join.name,
71
                               self.value, self.value_label or self.value, self.order_by or self.value))
70
                table_expression = self.engine_cube.fact_table
71
                if self.join:
72
                    table_expression = self.engine_cube.build_table_expression(
73
                        self.join, self.engine_cube.fact_table)
74
                sql = 'SELECT %s AS value, %s::text AS label ' % (value, value_label)
75
                sql += 'FROM %s ' % table_expression
76
                if order_by:
77
                    if not isinstance(order_by, list):
78
                        order_by = [order_by]
72 79
                else:
73
                    sql = ('SELECT %s AS value, %s::text AS label FROM {fact_table} '
74
                           'GROUP BY %s, %s ORDER BY %s' % (
75
                               self.value, self.value_label or self.value, self.value,
76
                               self.value_label or self.value, self.order_by or self.value))
80
                    order_by = [value]
81
                group_by = [value]
82
                if value_label not in group_by:
83
                    group_by.append(value_label)
84
                for order_value in order_by:
85
                    if order_value not in group_by:
86
                        group_by.append(order_value)
87
                sql += 'GROUP BY %s ' % ', '.join(group_by)
88
                sql += 'ORDER BY (%s) ' % ', '.join(order_by)
77 89
            sql = sql.format(fact_table=self.engine_cube.fact_table)
78 90
            self.engine.log.debug('SQL: %s', sql)
79 91
            cursor.execute(sql)
80 92
            for row in cursor.fetchall():
93
                if row[0] is None:
94
                    continue
81 95
                yield Member(*row)
82 96

  
83 97

  
......
196 210
        return ProxyList(obj.engine, obj, self.attribute, self.cls, chain=self.chain)
197 211

  
198 212

  
199
def build_table_expression(join_tree, table_name, alias=None, top=True, other_conditions=None):
200
    '''Recursively build the table expression from the join tree,
201
       starting from the fact table'''
202

  
203
    sql = table_name
204
    if alias:
205
        sql += ' AS "%s"' % alias
206
    add_paren = False
207
    for kind in ['left', 'inner', 'right', 'full']:
208
        joins = join_tree.get(table_name, {}).get(kind)
209
        if not joins:
210
            continue
211
        add_paren = True
212
        join_kinds = {
213
            'inner': 'INNER JOIN',
214
            'left': 'LEFT OUTER JOIN',
215
            'right': 'RIGHT OUTER JOIN',
216
            'full': 'FULL OUTER JOIN',
217
        }
218
        sql += ' %s ' % join_kinds[kind]
219
        sub_joins = []
220
        conditions = []
221
        if other_conditions:
222
            conditions = other_conditions
223
            other_conditions = None
224
        for join_name, join in joins.iteritems():
225
            sub_joins.append(
226
                build_table_expression(join_tree, join.table, join.name, top=False))
227
            conditions.append('"%s".%s = "%s"."%s"' % (
228
                alias or table_name,
229
                join.master.split('.')[-1],
230
                join.name, join.detail))
231
        sub_join = ' CROSS JOIN '.join(sub_joins)
232
        if len(sub_joins) > 1:
233
            sub_join = '(%s)' % sub_join
234
        sql += sub_join
235
        sql += ' ON %s' % ' AND '.join(conditions)
236
    if not top and add_paren:
237
        sql = '(%s)' % sql
238
    return sql
239

  
240

  
241 213
class EngineCube(object):
242 214
    dimensions = ProxyListDescriptor('all_dimensions', EngineDimension, chain=JSONDimensions)
243 215
    measures = ProxyListDescriptor('measures', EngineMeasure)
......
293 265
                projections.append('%s AS %s' % (dimension.value_label or dimension.value,
294 266
                                                 dimension.name))
295 267
                group_by.append(dimension.group_by or dimension.value)
296
                order_by.append(dimension.order_by or dimension.value)
268
                order_by.extend(dimension.order_by or [dimension.value])
269

  
270
            for order_value in order_by:
271
                if order_value not in group_by:
272
                    group_by.append(order_value)
297 273

  
298 274
            for measure_name in measures:
299 275
                measure = self.get_measure(measure_name)
......
302 278
            sql = 'SELECT ' + ', '.join(projections)
303 279
            table_expression = ' %s' % self.cube.fact_table
304 280
            if joins:
305
                join_tree = {}
306
                # Build join tree
307
                for join_name in joins:
308
                    join = self.get_join(join_name)
309
                    master_table = join.master_table or self.fact_table
310
                    join_tree.setdefault(master_table, {}).setdefault(join.kind, {})[join.name] = join
311
                table_expression = build_table_expression(join_tree,
312
                                                          self.fact_table,
313
                                                          other_conditions=join_conditions)
281
                table_expression = self.build_table_expression(
282
                    joins, self.fact_table, other_conditions=join_conditions)
314 283
            sql += ' FROM %s' % table_expression
315 284
            where_conditions = 'true'
316 285
            if where:
......
349 318
                    'value': value,
350 319
                } for cell, value in zip(cells, row)]
351 320

  
321
    def build_table_expression(self, joins, table_name, other_conditions=None):
322
        '''Recursively build the table expression from the join tree,
323
           starting from the fact table'''
324

  
325
        join_tree = {}
326
        # Build join tree
327
        for join_name in joins:
328
            join = self.get_join(join_name)
329
            master_table = join.master_table or self.fact_table
330
            join_tree.setdefault(master_table, {}).setdefault(join.kind, {})[join.name] = join
331

  
332
        def build_table_expression_helper(join_tree, table_name, alias=None, top=True, other_conditions=None):
333
            sql = table_name
334
            if alias:
335
                sql += ' AS "%s"' % alias
336
            add_paren = False
337
            for kind in ['left', 'inner', 'right', 'full']:
338
                joins = join_tree.get(alias or table_name, {}).get(kind)
339
                if not joins:
340
                    continue
341
                add_paren = True
342
                join_kinds = {
343
                    'inner': 'INNER JOIN',
344
                    'left': 'LEFT OUTER JOIN',
345
                    'right': 'RIGHT OUTER JOIN',
346
                    'full': 'FULL OUTER JOIN',
347
                }
348
                sql += ' %s ' % join_kinds[kind]
349
                sub_joins = []
350
                conditions = []
351
                if other_conditions:
352
                    conditions = other_conditions
353
                    other_conditions = None
354
                for join_name, join in joins.iteritems():
355
                    sub_joins.append(
356
                        build_table_expression_helper(join_tree, join.table, alias=join.name, top=False))
357
                    conditions.append('"%s".%s = "%s"."%s"' % (
358
                        alias or table_name,
359
                        join.master.split('.')[-1],
360
                        join.name, join.detail))
361
                sub_join = ' CROSS JOIN '.join(sub_joins)
362
                if len(sub_joins) > 1:
363
                    sub_join = '(%s)' % sub_join
364
                sql += sub_join
365
                sql += ' ON %s' % ' AND '.join(conditions)
366
            if not top and add_paren:
367
                sql = '(%s)' % sql
368
            return sql
369
        return build_table_expression_helper(
370
            join_tree, table_name, other_conditions=other_conditions)
371

  
352 372

  
353 373
class Engine(object):
354 374
    def __init__(self, warehouse):
bijoe/schemas.py
143 143
        'join': [str],
144 144
        'value': str,
145 145
        'value_label': str,
146
        'order_by': str,
146
        'order_by': [str],
147 147
        'group_by': str,
148 148
        'filter': bool,
149 149
        'members_query': str,
......
202 202
                    filter_value='EXTRACT(dow from %s)' % filter_value,
203 203
                    filter_in_join=self.filter_in_join,
204 204
                    value='EXTRACT(dow from %s)' % self.value,
205
                    order_by='(EXTRACT(dow from %s) + 6)::integer %% 7' % self.value,
205
                    order_by=['(EXTRACT(dow from %s) + 6)::integer %% 7' % self.value],
206 206
                    value_label='to_char(date_trunc(\'week\', current_date)::date '
207 207
                                '+ EXTRACT(dow from %s)::integer - 1, \'TMday\')' % self.value,
208 208
                    filter=False),
......
218 218
                          % (self.value, self.value),
219 219
                    group_by='EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value,
220 220
                                                                                  self.value),
221
                    order_by='EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value,
222
                                                                                  self.value),
221
                    order_by=['EXTRACT(isoyear from %s), EXTRACT(week from %s)' % (self.value,
222
                                                                                   self.value)],
223 223
                    filter=False)
224 224
            ]
225 225
        return [self]
tests/fixtures/schema1/01_schema.json
86 86
                    "type": "integer",
87 87
                    "join": ["innercategory", "innersubcategory"],
88 88
                    "value": "innersubcategory.id",
89
                    "order_by": ["innercategory.ord", "innersubcategory.ord", "innersubcategory.label"],
89 90
                    "value_label": "innersubcategory.label"
90 91
                },
91 92
                {
......
94 95
                    "type": "integer",
95 96
                    "join": ["leftcategory", "leftsubcategory"],
96 97
                    "value": "leftsubcategory.id",
98
                    "order_by": ["leftcategory.ord", "leftsubcategory.ord", "leftsubcategory.label"],
97 99
                    "value_label": "leftsubcategory.label"
98 100
                },
99 101
                {
......
102 104
                    "type": "integer",
103 105
                    "join": ["rightcategory", "rightsubcategory"],
104 106
                    "value": "rightsubcategory.id",
107
                    "order_by": ["rightcategory.ord", "rightsubcategory.ord", "rightsubcategory.label"],
105 108
                    "value_label": "rightsubcategory.label"
106 109
                },
107 110
                {
......
110 113
                    "type": "integer",
111 114
                    "join": ["outercategory", "outersubcategory"],
112 115
                    "value": "outersubcategory.id",
116
                    "order_by": ["outercategory.ord", "outersubcategory.ord", "outersubcategory.label"],
113 117
                    "value_label": "outersubcategory.label"
114 118
                },
115 119
                {
......
118 122
                    "type": "integer",
119 123
                    "join": ["innersubcategory", "innercategory"],
120 124
                    "value": "innercategory.id",
125
                    "order_by": "innercategory.ord",
121 126
                    "value_label": "innercategory.label"
122 127
                },
123 128
                {
......
126 131
                    "type": "integer",
127 132
                    "join": ["leftsubcategory", "leftcategory"],
128 133
                    "value": "leftcategory.id",
134
                    "order_by": "leftcategory.ord",
129 135
                    "value_label": "leftcategory.label"
130 136
                },
131 137
                {
......
134 140
                    "type": "integer",
135 141
                    "join": ["rightsubcategory", "rightcategory"],
136 142
                    "value": "rightcategory.id",
143
                    "order_by": "rightcategory.ord",
137 144
                    "value_label": "rightcategory.label"
138 145
                },
139 146
                {
......
142 149
                    "type": "integer",
143 150
                    "join": ["outersubcategory", "outercategory"],
144 151
                    "value": "outercategory.id",
152
                    "order_by": "outercategory.ord",
145 153
                    "value_label": "outercategory.label"
146 154
                }
147 155
            ],
tests/test_schema1.py
15 15
    response = form.submit('visualize')
16 16
    assert 'big-msg-info' not in response
17 17
    assert get_table(response) == [
18
        [u'Inner SubCategory', u'subé1', u'subé3'],
19
        ['number of rows', '15', '1'],
18
        [u'Inner SubCategory', u'subé3', u'subé1'],
19
        ['number of rows', '1', '15'],
20 20
    ]
21 21
    form = response.form
22 22
    form.set('representation', 'table')
23
-