From 6db1b97a0e7df38a905a6da88c6ea3743e1cf90d Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Tue, 6 Oct 2015 11:50:04 -0700 Subject: [PATCH] Supporting arbitrary expressions --- panoramix/migrations/versions/1e2841a4128_.py | 26 ++++++++++++ panoramix/models.py | 41 +++++++++++++++---- panoramix/static/widgets/viz_nvd3.js | 2 + panoramix/views.py | 3 +- 4 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 panoramix/migrations/versions/1e2841a4128_.py diff --git a/panoramix/migrations/versions/1e2841a4128_.py b/panoramix/migrations/versions/1e2841a4128_.py new file mode 100644 index 0000000000000..887ee38647d1a --- /dev/null +++ b/panoramix/migrations/versions/1e2841a4128_.py @@ -0,0 +1,26 @@ +"""empty message + +Revision ID: 1e2841a4128 +Revises: 5a7bad26f2a7 +Create Date: 2015-10-05 22:11:00.537054 + +""" + +# revision identifiers, used by Alembic. +revision = '1e2841a4128' +down_revision = '5a7bad26f2a7' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('table_columns', sa.Column('expression', sa.Text(), nullable=True)) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('table_columns', 'expression') + ### end Alembic commands ### diff --git a/panoramix/models.py b/panoramix/models.py index 6ee24d6936110..af62011a2367a 100644 --- a/panoramix/models.py +++ b/panoramix/models.py @@ -11,7 +11,8 @@ from sqlalchemy import Table from sqlalchemy import create_engine, MetaData, desc, select, and_ from sqlalchemy.orm import relationship -from sqlalchemy.sql import table, literal_column, text +from sqlalchemy.sql import table, literal_column, text, column +from sqlalchemy.sql.elements import ColumnClause from flask import request from copy import deepcopy, copy @@ -338,6 +339,7 @@ def query( inner_from_dttm=None, inner_to_dttm=None, extras=None): + cols = {col.column_name: col for col in self.columns} qry_start_dttm = datetime.now() if not self.main_dttm_col: raise Exception( @@ -360,9 +362,25 @@ def query( if groupby: select_exprs = [literal_column(s) for s in groupby] - groupby_exprs = [literal_column(s) for s in groupby] - inner_groupby_exprs = [ - literal_column(s).label('__' + s) for s in groupby] + select_exprs = [] + groupby_exprs = [] + inner_select_exprs = [] + inner_groupby_exprs = [] + for s in groupby: + col = cols[s] + expr = col.expression + if expr: + outer = ColumnClause(expr, is_literal=True).label(s) + inner = ColumnClause(expr, is_literal=True).label('__' + s) + else: + outer = literal_column(s).label(s) + inner = literal_column(s).label('__' + s) + + groupby_exprs.append(outer) + select_exprs.append(outer) + inner_groupby_exprs.append(inner) + inner_select_exprs.append(inner) + if granularity != "all": select_exprs += [timestamp] groupby_exprs += [timestamp] @@ -384,9 +402,14 @@ def query( where_clause_and = [] for col, op, eq in filter: + col_obj = cols[col] if op in ('in', 'not in'): values = eq.split(",") - cond = literal_column(col).in_(values) + if col_obj.expression: + cond = ColumnClause( + col_obj.expression, is_literal=True).in_(values) + else: + cond = literal_column(col).in_(values) if op == 'not in': cond = ~cond where_clause_and.append(cond) @@ -397,16 +420,16 @@ def query( qry = qry.limit(row_limit) if timeseries_limit and groupby: - subq = select(inner_groupby_exprs) + subq = select(inner_select_exprs) subq = subq.select_from(table(self.table_name)) subq = subq.where(and_(*(where_clause_and + inner_time_filter))) subq = subq.group_by(*inner_groupby_exprs) subq = subq.order_by(desc(main_metric_expr)) subq = subq.limit(timeseries_limit) on_clause = [] - for gb in groupby: + for i, gb in enumerate(groupby): on_clause.append( - literal_column(gb) == literal_column("__" + gb)) + groupby_exprs[i] == literal_column("__" + gb)) from_clause = from_clause.join(subq.alias(), and_(*on_clause)) @@ -414,6 +437,7 @@ def query( engine = self.database.get_sqla_engine() sql = str(qry.compile(engine, compile_kwargs={"literal_binds": True})) + print sql df = read_sql_query( sql=sql, con=engine @@ -548,6 +572,7 @@ class TableColumn(Model, AuditMixinNullable): max = Column(Boolean, default=False) min = Column(Boolean, default=False) filterable = Column(Boolean, default=False) + expression = Column(Text, default='') description = Column(Text, default='') def __repr__(self): diff --git a/panoramix/static/widgets/viz_nvd3.js b/panoramix/static/widgets/viz_nvd3.js index 2d7e4547f5436..b5b2556c71b30 100644 --- a/panoramix/static/widgets/viz_nvd3.js +++ b/panoramix/static/widgets/viz_nvd3.js @@ -58,6 +58,8 @@ function viz_nvd3(token_name, json_callback) { } else if (viz_type === 'dist_bar') { var chart = nv.models.multiBarChart() .showControls(true) //Allow user to switch between 'Grouped' and 'Stacked' mode. + .reduceXTicks(false) + .rotateLabels(45) .groupSpacing(0.1); //Distance between each group of bars. chart.xAxis .showMaxMin(false); diff --git a/panoramix/views.py b/panoramix/views.py index 84d207752b150..090d388d24e2b 100644 --- a/panoramix/views.py +++ b/panoramix/views.py @@ -42,7 +42,8 @@ class TableColumnInlineView(CompactCRUDMixin, PanoramixModelView): can_delete = False edit_columns = [ 'column_name', 'description', 'groupby', 'filterable', 'table', - 'count_distinct', 'sum', 'min', 'max'] + 'count_distinct', 'sum', 'min', 'max', 'expression'] + add_columns = edit_columns list_columns = [ 'column_name', 'type', 'groupby', 'filterable', 'count_distinct', 'sum', 'min', 'max']