diff --git a/cubes/cells.py b/cubes/cells.py index b85287cb..98acc298 100644 --- a/cubes/cells.py +++ b/cubes/cells.py @@ -36,12 +36,13 @@ class Cell(object): """Part of a cube determined by slicing dimensions. Immutable object.""" - def __init__(self, cube=None, cuts=None): + def __init__(self, cube=None, cuts=None, having_cuts=None): if not isinstance(cube, Cube): raise ArgumentError("Cell cube should be sublcass of Cube, " "provided: %s" % type(cube).__name__) self.cube = cube self.cuts = cuts if cuts is not None else [] + self.having_cuts = having_cuts if having_cuts is not None else [] def __and__(self, other): """Returns a new cell that is a conjunction of the two provided @@ -51,13 +52,15 @@ def __and__(self, other): "cubes '%s' and '%s'." % (self.name, other.name)) cuts = self.cuts + other.cuts - return Cell(self.cube, cuts=cuts) + having_cuts = self.having_cuts + other.having_cuts + return Cell(self.cube, cuts=cuts, having_cuts=having_cuts) def to_dict(self): """Returns a dictionary representation of the cell""" result = { "cube": str(self.cube.name), - "cuts": [cut.to_dict() for cut in self.cuts] + "cuts": [cut.to_dict() for cut in self.cuts], + "having_cuts": [clause.to_dict() for clause in self.having_cuts] } return result diff --git a/cubes/sql/browser.py b/cubes/sql/browser.py index bea8676c..869156d5 100644 --- a/cubes/sql/browser.py +++ b/cubes/sql/browser.py @@ -564,7 +564,7 @@ def aggregation_statement(self, cell, aggregates, drilldown=None, # WHERE # ----- - condition = context.condition_for_cell(cell) + condition = context.condition_for_cell(cell, for_summary) group_by = selection[:] if not for_summary else None @@ -579,11 +579,25 @@ def aggregation_statement(self, cell, aggregates, drilldown=None, else: selection += aggregate_cols + # madman: HAVING + # ------ + having_clauses = None + colums_and_havings = context.colums_and_having_cut_for_cell(cell) + if colums_and_havings is not None: + having_clauses = colums_and_havings[1] + group_clauses = colums_and_havings[0] + if group_by is None: + group_by = [] + for group in group_clauses: + if group not in group_by: + group_by.append(group) + statement = sql.expression.select(selection, from_obj=context.star, use_labels=True, whereclause=condition, - group_by=group_by) + group_by=group_by, + having=having_clauses) return (statement, context.get_labels(statement.columns)) diff --git a/cubes/sql/functions.py b/cubes/sql/functions.py index 6c280c84..04fbcfb8 100644 --- a/cubes/sql/functions.py +++ b/cubes/sql/functions.py @@ -153,6 +153,11 @@ def __init__(self, name): function = lambda x: sql.functions.count(sql.expression.distinct(x)) super(FactCountDistinctFunction, self).__init__(name, function) +class FactSumDistinctFunction(AggregateFunction): + def __init__(self, name): + """Creates a function that provides distinct fact (record) counts.""" + function = lambda x: sql.functions.sum(sql.expression.distinct(x)) + super(FactSumDistinctFunction, self).__init__(name, function) class avg(ReturnTypeFromArgs): pass @@ -169,6 +174,7 @@ class variance(ReturnTypeFromArgs): _functions = ( SummaryCoalescingFunction("sum", sql.functions.sum), + FactSumDistinctFunction("sum_distinct"), SummaryCoalescingFunction("count_nonempty", sql.functions.count), FactCountFunction("count"), FactCountDistinctFunction("count_distinct"), diff --git a/cubes/sql/query.py b/cubes/sql/query.py index 27c6cda6..fb058a43 100644 --- a/cubes/sql/query.py +++ b/cubes/sql/query.py @@ -938,18 +938,18 @@ def get_columns(self, refs): return [self._columns[ref] for ref in refs] - def condition_for_cell(self, cell): + def condition_for_cell(self, cell, for_summary=None): """Returns a condition for cell `cell`. If cell is empty or cell is `None` then returns `None`.""" if not cell: return None - condition = and_(*self.conditions_for_cuts(cell.cuts)) + condition = and_(*self.conditions_for_cuts(cell.cuts, for_summary)) return condition - def conditions_for_cuts(self, cuts): + def conditions_for_cuts(self, cuts, for_summary=None): """Constructs conditions for all cuts in the `cell`. Returns a list of SQL conditional expressions. """ @@ -959,6 +959,7 @@ def conditions_for_cuts(self, cuts): for cut in cuts: hierarchy = str(cut.hierarchy) if cut.hierarchy else None + condition = None if isinstance(cut, PointCut): path = cut.path condition = self.condition_for_point(str(cut.dimension), @@ -966,19 +967,11 @@ def conditions_for_cuts(self, cuts): hierarchy, cut.invert) elif isinstance(cut, SetCut): - set_conds = [] - - for path in cut.paths: - condition = self.condition_for_point(str(cut.dimension), - path, + if not for_summary or cut.hidden is not True: + condition = self.condition_for_set(str(cut.dimension), + cut.paths, str(cut.hierarchy), - invert=False) - set_conds.append(condition) - - condition = sql.expression.or_(*set_conds) - - if cut.invert: - condition = sql.expression.not_(condition) + invert=cut.invert) elif isinstance(cut, RangeCut): condition = self.range_condition(str(cut.dimension), @@ -989,7 +982,8 @@ def conditions_for_cuts(self, cuts): else: raise ArgumentError("Unknown cut type %s" % type(cut)) - conditions.append(condition) + if condition is not None: + conditions.append(condition) return conditions @@ -1111,3 +1105,78 @@ def column_for_split(self, split_cell, label=None): return split_column.label(label) + # madman: get having clause and attributes + def colums_and_having_cut_for_cell(self, cell): + """Returns attributes and having clause. If cell is empty, not contain having or cell is + `None` then returns `None`.""" + + if not cell: + return None + + having_cuts = cell.having_cuts + hav_condition = and_(*self.conditions_for_having_cuts(having_cuts)) + + if hav_condition is None: + return None + + colums = self.colums_in_having_cuts(having_cuts) + + return (colums, hav_condition) + + # madman: get attributes in having cuts + def colums_in_having_cuts(self, having_cus): + + columns = [] + + for cut in having_cus: + hierarchy = str(cut.hierarchy) if cut.hierarchy else None + levels = self.hierarchies[(str(cut.dimension), hierarchy)] + for level_key in levels: + column = self.column(level_key) + columns.append(column) + + return columns + + # madman: get condition in having cuts + def conditions_for_having_cuts(self, having_cuts): + """ + Having cuts has only support type PointCut + """ + + conditions = [] + + for cut in having_cuts: + hierarchy = str(cut.hierarchy) if cut.hierarchy else None + + if isinstance(cut, PointCut): + path = cut.path + condition = self.condition_for_point(str(cut.dimension), + path, + hierarchy, cut.invert) + else: + raise ArgumentError("Having cut has not support type %s" % type(cut)) + + conditions.append(condition) + + return conditions + + # madman: fix setcut + def condition_for_set(self, dim, path, hierarchy=None, invert=False): + """Returns a `Condition` tuple (`attributes`, `conditions`, + `group_by`) dimension `dim` point at `path`. It is a compound + condition - one equality condition for each path element in form: + ``level[i].key IN (path[i])``""" + conditions = [] + levels = self.level_keys(dim, hierarchy, path) + for level_key, value in zip(levels, path): + column = self.column(level_key) + values = [] + for v in value: + values.append([v]) + if invert: + condition = sql.expression.tuple_(column).notin_(values) + else: + condition = sql.expression.tuple_(column).in_(values) + conditions.append(condition) + condition = sql.expression.and_(*conditions) + return condition \ No newline at end of file