Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add feature having clause #334

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions cubes/cells.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
18 changes: 16 additions & 2 deletions cubes/sql/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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))

Expand Down
6 changes: 6 additions & 0 deletions cubes/sql/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"),
Expand Down
101 changes: 85 additions & 16 deletions cubes/sql/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -959,26 +959,19 @@ 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),
path,
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),
Expand All @@ -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

Expand Down Expand Up @@ -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