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:
parent
573012169d
commit
09fc7cee35
102
bijoe/engine.py
102
bijoe/engine.py
|
@ -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:
|
||||
|
|
|
@ -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']
|
||||
|
|
Loading…
Reference in New Issue