allow many customization for filters (fixes #15171)

- a dimension can provide a custom filter query (%s will be replaced by the value
  or the list of values).
- a dimension can ask for its filter to be done inside the join.
- a diemnsion can tell that filtering does not need the join table.
This commit is contained in:
Benjamin Dauvergne 2017-02-27 23:57:22 +01:00
parent 573012169d
commit 09fc7cee35
2 changed files with 82 additions and 45 deletions

View File

@ -97,6 +97,50 @@ class ProxyListDescriptor(object):
return ProxyList(obj.engine, obj, self.attribute, self.cls)
def build_table_expression(join_tree, table_name, alias=None, top=True, other_conditions=None):
'''Recursively build the table expression from the join tree,
starting from the fact table'''
sql = table_name
if alias:
sql += ' AS "%s"' % alias
add_paren = False
for kind in ['left', 'inner', 'right']:
joins = join_tree.get(table_name, {}).get(kind)
if not joins:
continue
add_paren = True
join_kinds = {
'inner': 'INNER JOIN',
'left': 'LEFT OUTER JOIN',
'right': 'RIGHT OUTER JOIN',
}
sql += ' %s ' % join_kinds[kind]
sub_joins = []
conditions = []
if other_conditions:
conditions = other_conditions
other_conditions = None
for join_name, join in joins.iteritems():
sub_joins.append(
build_table_expression(join_tree, join.table, join.name, top=False))
conditions.append('"%s".%s = "%s"."%s"' % (
alias or table_name,
join.master.split('.')[-1],
join.name, join.detail))
sub_join = ' CROSS JOIN '.join(sub_joins)
if len(sub_joins) > 1:
sub_join = '(%s)' % sub_join
sql += sub_join
sql += ' ON %s' % ' AND '.join(conditions)
if not top and add_paren:
sql = '(%s)' % sql
return sql
class EngineCube(object):
dimensions = ProxyListDescriptor('all_dimensions', EngineDimension)
measures = ProxyListDescriptor('measures', EngineMeasure)
@ -113,6 +157,9 @@ class EngineCube(object):
cursor.execute('SELECT count(%s) FROM %s' % (self.key, self.fact_table))
return cursor.fetchone()[0]
def get_join(self, name):
return self.cube.get_join(name)
def sql_query(self, filters, drilldown, measures, **kwargs):
cursor = self.engine.get_cursor()
@ -121,14 +168,19 @@ class EngineCube(object):
where = []
group_by = []
order_by = []
join_conditions = []
for dimension_name, values in filters:
dimension = self.cube.get_dimension(dimension_name)
assert dimension.filter
condition, values = dimension.build_filter(values)
condition = cursor.mogrify(condition, values)
where.append(condition)
joins.update(dimension.join)
if dimension.filter_needs_join:
joins.update(dimension.join)
if dimension.filter_in_join:
join_conditions.append(condition)
else:
where.append(condition)
for dimension_name in drilldown:
dimension = self.cube.get_dimension(dimension_name)
@ -146,48 +198,14 @@ class EngineCube(object):
table_expression = ' %s' % self.cube.fact_table
if joins:
join_tree = {}
# Build join tree
for join_name in joins:
join = self.cube.get_join(join_name)
if '.' in join.master:
master_table = join.master.split('.', 1)[0]
else:
master_table = self.fact_table
join = self.get_join(join_name)
master_table = join.master_table or self.fact_table
join_tree.setdefault(master_table, {}).setdefault(join.kind, {})[join.name] = join
def build_table_expression(table_name, alias=None, top=True):
sql = table_name
if alias:
sql += ' AS %s' % alias
add_paren = False
for kind in ['left', 'inner', 'right']:
joins = join_tree.get(table_name, {}).get(kind)
if not joins:
continue
add_paren = True
join_kinds = {
'inner': 'INNER JOIN',
'left': 'LEFT OUTER JOIN',
'right': 'RIGHT OUTER JOIN',
}
sql += ' %s ' % join_kinds[kind]
sub_joins = []
conditions = []
for join_name, join in joins.iteritems():
sub_joins.append(
build_table_expression(join.table, join.name, top=False))
conditions.append('%s.%s = %s.%s' % (alias or table_name,
join.master.split('.')[-1],
join.name, join.detail))
sub_sql = ' CROSS JOIN '.join(sub_joins)
if len(sub_joins) > 1:
sub_sql = '(%s)' % sub_sql
sql += sub_sql
sql += ' ON %s' % ' AND '.join(conditions)
if not top and add_paren:
sql = '(%s)' % sql
return sql
table_expression = build_table_expression(self.fact_table)
table_expression = build_table_expression(join_tree,
self.fact_table,
other_conditions=join_conditions)
sql += ' FROM %s' % table_expression
where_conditions = 'true'
if where:

View File

@ -128,7 +128,8 @@ class Measure(Base):
class Dimension(Base):
__slots__ = ['name', 'label', 'type', 'join', 'value', 'value_label',
'order_by', 'group_by', 'filter_in_join', 'filter']
'order_by', 'group_by', 'filter_in_join', 'filter', 'filter_value',
'filter_needs_join', 'filter_expression']
__types__ = {
'name': str,
'label': unicode,
@ -140,6 +141,10 @@ class Dimension(Base):
'group_by': str,
'filter': bool,
'members_query': str,
# do filtering need joins ?
'filter_in_join': bool,
'filter_value': str,
'filter_needs_join': bool,
}
label = None
@ -148,6 +153,10 @@ class Dimension(Base):
group_by = None
join = None
filter = True
filter_in_join = False
filter_value = None
filter_expression = None
filter_needs_join = True
members_query = None
@property
@ -200,6 +209,8 @@ class Dimension(Base):
return [self]
def build_filter(self, filter_values):
value = self.filter_value or self.value
if self.type == 'date':
assert isinstance(filter_values, dict) and set(filter_values.keys()) == set(['start',
'end'])
@ -209,7 +220,7 @@ class Dimension(Base):
def date_filter(tpl, filter_value):
if not isinstance(filter_value, (datetime.date, datetime.datetime)):
filter_value = RelativeDate(filter_value)
filters.append(tpl % (self.value, '%s'))
filters.append(tpl % (value, '%s'))
values.append(filter_value)
try:
if filter_values['start']:
@ -230,7 +241,9 @@ class Dimension(Base):
else:
values = filter_values
s = ', '.join(['%s'] * len(values))
return '%s IN (%s)' % (self.value, s), values
if self.filter_expression:
return self.filter_expression % s, values
return '%s IN (%s)' % (value, s), values
def join_kind(kind):
@ -251,6 +264,12 @@ class Join(Base):
kind = 'right'
@property
def master_table(self):
if '.' in self.master:
return self.master.split('.', 1)[0]
return None
class Cube(Base):
__slots__ = ['name', 'label', 'fact_table', 'key', 'joins', 'dimensions', 'measures']