superset/panoramix/models.py

1173 lines
38 KiB
Python
Raw Normal View History

2015-12-24 11:39:33 -05:00
from copy import deepcopy, copy
from collections import namedtuple
from datetime import timedelta, datetime
import json
from six import string_types
import sqlparse
import requests
import textwrap
2015-07-15 13:12:32 -04:00
from dateutil.parser import parse
2015-09-09 13:37:59 -04:00
from flask import flash
from flask.ext.appbuilder import Model
from flask.ext.appbuilder.models.mixins import AuditMixin
from pandas import read_sql_query
2015-08-05 20:36:33 -04:00
from pydruid import client
from pydruid.utils.filters import Dimension, Filter
import sqlalchemy as sqla
2015-09-09 13:37:59 -04:00
from sqlalchemy import (
Column, Integer, String, ForeignKey, Text, Boolean, DateTime,
2015-12-04 14:52:32 -05:00
Table, create_engine, MetaData, desc, select, and_, func)
2015-09-09 13:37:59 -04:00
from sqlalchemy.orm import relationship
2015-12-22 11:12:47 -05:00
from sqlalchemy.sql import table, literal_column, text, column
2015-10-06 14:50:04 -04:00
from sqlalchemy.sql.elements import ColumnClause
from sqlalchemy_utils import EncryptedType
2015-07-15 13:12:32 -04:00
2015-10-20 14:52:40 -04:00
2015-09-25 18:43:50 -04:00
from panoramix import app, db, get_session, utils
2015-09-17 21:06:03 -04:00
from panoramix.viz import viz_types
from sqlalchemy.ext.declarative import declared_attr
2015-08-03 11:34:58 -04:00
2015-09-25 18:43:50 -04:00
config = app.config
2015-08-07 19:25:19 -04:00
QueryResult = namedtuple('namedtuple', ['df', 'query', 'duration'])
class AuditMixinNullable(AuditMixin):
@declared_attr
def created_by_fk(cls):
return Column(Integer, ForeignKey('ab_user.id'),
default=cls.get_user_id, nullable=True)
@declared_attr
def changed_by_fk(cls):
return Column(Integer, ForeignKey('ab_user.id'),
default=cls.get_user_id, onupdate=cls.get_user_id, nullable=True)
@property
def changed_on_(cls):
return utils.datetime_f(cls.changed_on)
2016-01-24 13:09:26 -05:00
@property
def created_by_(self):
return self.created_by or ''
@property
def changed_by_(self):
return self.changed_by or ''
2016-01-14 00:44:05 -05:00
class Url(Model, AuditMixinNullable):
"""Used for the short url feature"""
__tablename__ = 'url'
id = Column(Integer, primary_key=True)
url = Column(Text)
2016-02-03 21:04:28 -05:00
class CssTemplate(Model, AuditMixinNullable):
"""CSS templates for dashboards"""
__tablename__ = 'css_templates'
id = Column(Integer, primary_key=True)
template_name = Column(String(250))
css = Column(Text)
class Slice(Model, AuditMixinNullable):
2015-09-11 18:32:42 -04:00
"""A slice is essentially a report or a view on data"""
__tablename__ = 'slices'
id = Column(Integer, primary_key=True)
2015-09-12 00:49:04 -04:00
slice_name = Column(String(250))
2015-09-15 15:33:26 -04:00
druid_datasource_id = Column(Integer, ForeignKey('datasources.id'))
2015-09-13 22:07:54 -04:00
table_id = Column(Integer, ForeignKey('tables.id'))
2015-09-12 00:49:04 -04:00
datasource_type = Column(String(200))
datasource_name = Column(String(2000))
2015-09-11 18:32:42 -04:00
viz_type = Column(String(250))
2015-09-12 00:49:04 -04:00
params = Column(Text)
description = Column(Text)
2015-09-12 00:49:04 -04:00
2015-09-15 15:33:26 -04:00
table = relationship(
2015-10-04 11:27:09 -04:00
'SqlaTable', foreign_keys=[table_id], backref='slices')
2015-09-15 15:33:26 -04:00
druid_datasource = relationship(
2016-02-10 09:56:35 -05:00
'DruidDatasource', foreign_keys=[druid_datasource_id], backref='slices')
2015-09-13 22:07:54 -04:00
def __repr__(self):
return self.slice_name
@property
def datasource(self):
return self.table or self.druid_datasource
2016-01-24 13:09:26 -05:00
@property
def datasource_link(self):
if self.table:
return self.table.link
elif self.druid_datasource:
return self.druid_datasource.link
2015-09-17 21:06:03 -04:00
@property
@utils.memoized
def viz(self):
d = json.loads(self.params)
viz = viz_types[self.viz_type](
self.datasource,
form_data=d)
return viz
@property
def description_markeddown(self):
return utils.markdown(self.description)
2015-09-15 15:33:26 -04:00
@property
def datasource_id(self):
2015-12-04 12:30:43 -05:00
return self.table_id or self.druid_datasource_id
2015-09-15 15:33:26 -04:00
2015-12-20 11:33:08 -05:00
@property
def data(self):
d = self.viz.data
d['slice_id'] = self.id
return d
@property
def json_data(self):
return json.dumps(self.data)
2015-09-12 00:49:04 -04:00
@property
2015-09-17 21:06:03 -04:00
def slice_url(self):
2015-09-21 16:48:02 -04:00
try:
2015-12-04 12:30:43 -05:00
slice_params = json.loads(self.params)
2015-09-21 16:48:02 -04:00
except Exception as e:
2015-12-04 12:30:43 -05:00
slice_params = {}
slice_params['slice_id'] = self.id
slice_params['slice_name'] = self.slice_name
2015-09-17 21:06:03 -04:00
from werkzeug.urls import Href
href = Href(
2015-12-04 12:30:43 -05:00
"/panoramix/explore/{self.datasource_type}/"
2015-09-17 21:06:03 -04:00
"{self.datasource_id}/".format(self=self))
2015-12-04 12:30:43 -05:00
return href(slice_params)
2015-09-17 21:06:03 -04:00
@property
def edit_url(self):
return "/slicemodelview/edit/{}".format(self.id)
2015-09-17 21:06:03 -04:00
@property
def slice_link(self):
url = self.slice_url
2015-09-12 00:49:04 -04:00
return '<a href="{url}">{self.slice_name}</a>'.format(**locals())
2015-09-11 18:32:42 -04:00
2015-09-13 22:07:54 -04:00
@property
def js_files(self):
from panoramix.viz import viz_types
return viz_types[self.viz_type].js_files
@property
def css_files(self):
from panoramix.viz import viz_types
return viz_types[self.viz_type].css_files
2015-09-15 15:33:26 -04:00
def get_viz(self):
pass
2015-09-13 22:07:54 -04:00
dashboard_slices = Table('dashboard_slices', Model.metadata,
Column('id', Integer, primary_key=True),
Column('dashboard_id', Integer, ForeignKey('dashboards.id')),
Column('slice_id', Integer, ForeignKey('slices.id')),
)
class Dashboard(Model, AuditMixinNullable):
2015-09-13 22:07:54 -04:00
"""A dash to slash"""
__tablename__ = 'dashboards'
id = Column(Integer, primary_key=True)
dashboard_title = Column(String(500))
2015-09-17 21:06:03 -04:00
position_json = Column(Text)
description = Column(Text)
css = Column(Text)
json_metadata = Column(Text)
slug = Column(String(255), unique=True)
2015-09-13 22:07:54 -04:00
slices = relationship(
'Slice', secondary=dashboard_slices, backref='dashboards')
def __repr__(self):
return self.dashboard_title
2015-09-25 19:42:56 -04:00
@property
def url(self):
return "/panoramix/dashboard/{}/".format(self.slug or self.id)
2015-09-25 19:42:56 -04:00
@property
def metadata_dejson(self):
2016-01-19 15:58:44 -05:00
if self.json_metadata:
return json.loads(self.json_metadata)
else:
return {}
2015-09-13 22:07:54 -04:00
def dashboard_link(self):
2015-09-25 19:42:56 -04:00
return '<a href="{self.url}">{self.dashboard_title}</a>'.format(self=self)
2015-09-13 22:07:54 -04:00
@property
def js_files(self):
l = []
for o in self.slices:
2015-09-14 11:04:32 -04:00
l += [f for f in o.js_files if f not in l]
return l
2015-09-13 22:07:54 -04:00
@property
def css_files(self):
l = []
for o in self.slices:
l += o.css_files
return list(set(l))
2015-09-11 18:32:42 -04:00
2015-08-03 11:34:58 -04:00
class Queryable(object):
@property
def column_names(self):
return sorted([c.column_name for c in self.columns])
@property
def main_dttm_col(self):
return "timestamp"
2015-08-03 11:34:58 -04:00
@property
def groupby_column_names(self):
return sorted([c.column_name for c in self.columns if c.groupby])
@property
def filterable_column_names(self):
return sorted([c.column_name for c in self.columns if c.filterable])
@property
def dttm_cols(self):
return []
2015-09-09 13:37:59 -04:00
class Database(Model, AuditMixinNullable):
2015-08-06 17:44:25 -04:00
__tablename__ = 'dbs'
2015-08-03 11:34:58 -04:00
id = Column(Integer, primary_key=True)
2015-09-11 18:32:42 -04:00
database_name = Column(String(250), unique=True)
2015-08-03 11:34:58 -04:00
sqlalchemy_uri = Column(String(1024))
password = Column(EncryptedType(String(1024), config.get('SECRET_KEY')))
2015-08-03 11:34:58 -04:00
def __repr__(self):
return self.database_name
2015-08-05 02:41:00 -04:00
def get_sqla_engine(self):
return create_engine(self.sqlalchemy_uri_decrypted)
def safe_sqlalchemy_uri(self):
return self.sqlalchemy_uri
2015-08-05 02:41:00 -04:00
2015-08-06 01:42:42 -04:00
def get_table(self, table_name):
2015-08-05 02:41:00 -04:00
meta = MetaData()
2015-10-04 11:27:09 -04:00
return Table(
2015-08-06 01:42:42 -04:00
table_name, meta,
2015-08-05 02:41:00 -04:00
autoload=True,
autoload_with=self.get_sqla_engine())
2016-02-04 02:09:52 -05:00
def get_columns(self, table_name):
from sqlalchemy import create_engine
from sqlalchemy.engine import reflection
engine = self.get_sqla_engine()
insp = reflection.Inspector.from_engine(engine)
return insp.get_columns(table_name)
@property
def sqlalchemy_uri_decrypted(self):
conn = sqla.engine.url.make_url(self.sqlalchemy_uri)
conn.password = self.password
return str(conn)
2016-02-02 12:40:51 -05:00
@property
def sql_url(self):
return '/panoramix/sql/{}/'.format(self.id)
2016-01-31 11:42:22 -05:00
@property
def sql_link(self):
2016-02-02 12:40:51 -05:00
return '<a href="{}">SQL</a>'.format(self.sql_url)
2016-01-31 11:42:22 -05:00
2015-08-03 11:34:58 -04:00
2015-10-20 00:46:10 -04:00
class SqlaTable(Model, Queryable, AuditMixinNullable):
2015-09-12 00:49:04 -04:00
type = "table"
2015-08-03 11:34:58 -04:00
__tablename__ = 'tables'
id = Column(Integer, primary_key=True)
table_name = Column(String(250), unique=True)
main_dttm_col = Column(String(250))
description = Column(Text)
2015-08-03 11:34:58 -04:00
default_endpoint = Column(Text)
database_id = Column(Integer, ForeignKey('dbs.id'), nullable=False)
2015-12-15 19:03:39 -05:00
is_featured = Column(Boolean, default=False)
user_id = Column(Integer, ForeignKey('ab_user.id'))
owner = relationship('User', backref='tables', foreign_keys=[user_id])
2015-08-03 11:34:58 -04:00
database = relationship(
'Database', backref='tables', foreign_keys=[database_id])
offset = Column(Integer, default=0)
2015-08-03 11:34:58 -04:00
baselink = "tablemodelview"
2015-09-13 22:07:54 -04:00
def __repr__(self):
return self.table_name
@property
def description_markeddown(self):
return utils.markdown(self.description)
2016-01-24 13:09:26 -05:00
@property
def url(self):
return '/tablemodelview/edit/{}'.format(self.id)
2016-01-24 13:09:26 -05:00
@property
def link(self):
return '<a href="{self.url}">{self.table_name}</a>'.format(**locals())
2015-10-04 12:33:08 -04:00
@property
def perm(self):
return (
"[{self.database}].[{self.table_name}]"
"(id:{self.id})").format(self=self)
2016-01-31 11:42:22 -05:00
@property
def full_name(self):
return "[{self.database}].[{self.table_name}]".format(self=self)
2015-10-04 12:33:08 -04:00
@property
def dttm_cols(self):
l = [c.column_name for c in self.columns if c.is_dttm]
if self.main_dttm_col not in l:
l.append(self.main_dttm_col)
return l
2016-01-31 11:42:22 -05:00
@property
def html(self):
import pandas as pd
t = ((c.column_name, c.type) for c in self.columns)
df = pd.DataFrame(t)
df.columns = ['field', 'type']
return df.to_html(
index=False,
classes=(
"dataframe table table-striped table-bordered "
"table-condensed"))
2015-08-05 02:41:00 -04:00
@property
def name(self):
return self.table_name
2015-08-03 11:34:58 -04:00
@property
def table_link(self):
2015-12-04 12:30:43 -05:00
url = "/panoramix/explore/{self.type}/{self.id}/".format(self=self)
2015-08-03 11:34:58 -04:00
return '<a href="{url}">{self.table_name}</a>'.format(**locals())
@property
def metrics_combo(self):
return sorted(
[
2016-01-11 19:01:54 -05:00
(m.metric_name, m.verbose_name or m.metric_name)
2015-08-05 02:41:00 -04:00
for m in self.metrics],
2015-08-03 11:34:58 -04:00
key=lambda x: x[1])
def query_bkp(
2015-08-05 02:41:00 -04:00
self, groupby, metrics,
granularity,
from_dttm, to_dttm,
limit_spec=None,
filter=None,
is_timeseries=True,
timeseries_limit=15,
row_limit=None,
extras=None): # pragma: no cover
"""
Unused, legacy way of querying by building a SQL string without
using the sqlalchemy expression API (new approach which supports
all dialects)
"""
2015-08-05 02:41:00 -04:00
from pandas import read_sql_query
2015-08-07 19:25:19 -04:00
qry_start_dttm = datetime.now()
2015-08-05 02:41:00 -04:00
metrics_exprs = [
"{} AS {}".format(m.expression, m.metric_name)
for m in self.metrics if m.metric_name in metrics]
from_dttm_iso = from_dttm.isoformat()
to_dttm_iso = to_dttm.isoformat()
2015-08-06 03:00:17 -04:00
if metrics:
2015-09-09 13:37:59 -04:00
main_metric_expr = [
m.expression for m in self.metrics
if m.metric_name == metrics[0]][0]
2015-08-06 03:00:17 -04:00
else:
main_metric_expr = "COUNT(*)"
2015-08-05 02:41:00 -04:00
select_exprs = []
groupby_exprs = []
if groupby:
2015-08-06 03:00:17 -04:00
select_exprs = copy(groupby)
2015-08-05 02:41:00 -04:00
groupby_exprs = [s for s in groupby]
2015-08-06 03:00:17 -04:00
inner_groupby_exprs = [s for s in groupby]
2015-08-05 02:41:00 -04:00
select_exprs += metrics_exprs
if granularity != "all":
select_exprs += ['ds as timestamp']
groupby_exprs += ['ds']
select_exprs = ",\n".join(select_exprs)
groupby_exprs = ",\n".join(groupby_exprs)
where_clause = [
"ds >= '{from_dttm_iso}'",
"ds < '{to_dttm_iso}'"
]
2015-08-06 01:42:42 -04:00
for col, op, eq in filter:
if op in ('in', 'not in'):
l = ["'{}'".format(s) for s in eq.split(",")]
l = ", ".join(l)
op = op.upper()
where_clause.append(
"{col} {op} ({l})".format(**locals())
)
2015-08-05 02:41:00 -04:00
where_clause = " AND\n".join(where_clause).format(**locals())
2015-08-06 03:00:17 -04:00
on_clause = " AND ".join(["{g} = __{g}".format(g=g) for g in groupby])
limiting_join = ""
if timeseries_limit and groupby:
2015-09-09 13:37:59 -04:00
inner_select = ", ".join([
"{g} as __{g}".format(g=g) for g in inner_groupby_exprs])
2015-08-06 03:00:17 -04:00
inner_groupby_exprs = ", ".join(inner_groupby_exprs)
2015-08-07 19:25:19 -04:00
limiting_join = (
2015-09-09 13:37:59 -04:00
"JOIN ( \n"
" SELECT {inner_select} \n"
" FROM {self.table_name} \n"
" WHERE \n"
" {where_clause}\n"
" GROUP BY {inner_groupby_exprs}\n"
" ORDER BY {main_metric_expr} DESC\n"
" LIMIT {timeseries_limit}\n"
") z ON {on_clause}\n"
2015-08-07 19:25:19 -04:00
).format(**locals())
sql = (
2015-09-09 13:37:59 -04:00
"SELECT\n"
" {select_exprs}\n"
"FROM {self.table_name}\n"
"{limiting_join}"
"WHERE\n"
" {where_clause}\n"
"GROUP BY\n"
" {groupby_exprs}\n"
2015-08-07 19:25:19 -04:00
).format(**locals())
2015-08-05 02:41:00 -04:00
df = read_sql_query(
sql=sql,
con=self.database.get_sqla_engine()
)
2015-08-07 19:25:19 -04:00
textwrap.dedent(sql)
return QueryResult(
df=df, duration=datetime.now() - qry_start_dttm, query=sql)
2015-08-05 02:41:00 -04:00
2016-02-02 12:40:51 -05:00
@property
def sql_url(self):
2016-02-04 03:26:20 -05:00
return self.database.sql_url + "?table_name=" + str(self.table_name)
2016-02-02 12:40:51 -05:00
@property
def sql_link(self):
return '<a href="{}">SQL</a>'.format(self.sql_url)
def query(
self, groupby, metrics,
granularity,
from_dttm, to_dttm,
limit_spec=None,
filter=None,
is_timeseries=True,
timeseries_limit=15, row_limit=None,
2015-10-03 01:54:12 -04:00
inner_from_dttm=None, inner_to_dttm=None,
2015-12-18 15:06:01 -05:00
extras=None,
columns=None):
# For backward compatibility
if granularity not in self.dttm_cols:
granularity = self.main_dttm_col
2015-10-06 14:50:04 -04:00
cols = {col.column_name: col for col in self.columns}
qry_start_dttm = datetime.now()
if not self.main_dttm_col:
raise Exception(
"Datetime column not provided as part table configuration")
dttm_expr = cols[granularity].expression
if dttm_expr:
2016-01-16 03:05:55 -05:00
timestamp = literal_column(dttm_expr).label('timestamp')
else:
timestamp = literal_column(granularity).label('timestamp')
metrics_exprs = [
literal_column(m.expression).label(m.metric_name)
for m in self.metrics if m.metric_name in metrics]
if metrics:
2015-09-09 13:37:59 -04:00
main_metric_expr = literal_column([
m.expression for m in self.metrics
if m.metric_name == metrics[0]][0])
else:
main_metric_expr = literal_column("COUNT(*)")
select_exprs = []
groupby_exprs = []
if groupby:
2015-10-06 14:50:04 -04:00
select_exprs = []
inner_select_exprs = []
inner_groupby_exprs = []
for s in groupby:
col = cols[s]
expr = col.expression
if expr:
2016-01-16 03:05:55 -05:00
outer = literal_column(expr).label(s)
inner = literal_column(expr).label('__' + s)
2015-10-06 14:50:04 -04:00
else:
2015-12-22 11:12:47 -05:00
outer = column(s).label(s)
inner = column(s).label('__' + s)
2015-10-06 14:50:04 -04:00
groupby_exprs.append(outer)
select_exprs.append(outer)
inner_groupby_exprs.append(inner)
inner_select_exprs.append(inner)
2015-12-18 15:06:01 -05:00
elif columns:
for s in columns:
select_exprs.append(s)
metrics_exprs = []
2015-10-06 14:50:04 -04:00
2015-12-18 18:21:10 -05:00
if is_timeseries:
select_exprs += [timestamp]
groupby_exprs += [timestamp]
2015-08-13 00:22:02 -04:00
select_exprs += metrics_exprs
qry = select(select_exprs)
from_clause = table(self.table_name)
2015-12-18 18:21:10 -05:00
if not columns:
2015-12-18 15:06:01 -05:00
qry = qry.group_by(*groupby_exprs)
2016-01-16 03:05:55 -05:00
tf = '%Y-%m-%d %H:%M:%S.%f'
2015-10-03 01:54:12 -04:00
time_filter = [
2016-01-16 03:05:55 -05:00
timestamp >= from_dttm.strftime(tf),
timestamp <= to_dttm.strftime(tf),
]
2015-10-03 01:54:12 -04:00
inner_time_filter = copy(time_filter)
if inner_from_dttm:
2016-01-16 03:05:55 -05:00
inner_time_filter[0] = timestamp >= inner_from_dttm.strftime(tf)
2015-10-03 01:54:12 -04:00
if inner_to_dttm:
2016-01-16 03:05:55 -05:00
inner_time_filter[1] = timestamp <= inner_to_dttm.strftime(tf)
2015-10-03 01:54:12 -04:00
where_clause_and = []
2015-11-24 14:37:20 -05:00
having_clause_and = []
for col, op, eq in filter:
2015-10-06 14:50:04 -04:00
col_obj = cols[col]
if op in ('in', 'not in'):
values = eq.split(",")
2015-10-06 14:50:04 -04:00
if col_obj.expression:
cond = ColumnClause(
col_obj.expression, is_literal=True).in_(values)
else:
2015-12-22 11:12:47 -05:00
cond = column(col).in_(values)
if op == 'not in':
cond = ~cond
where_clause_and.append(cond)
if extras and 'where' in extras:
where_clause_and += [text(extras['where'])]
2015-11-24 14:37:20 -05:00
if extras and 'having' in extras:
having_clause_and += [text(extras['having'])]
2015-10-03 01:54:12 -04:00
qry = qry.where(and_(*(time_filter + where_clause_and)))
2015-11-24 14:37:20 -05:00
qry = qry.having(and_(*having_clause_and))
2015-12-18 15:06:01 -05:00
if groupby:
qry = qry.order_by(desc(main_metric_expr))
qry = qry.limit(row_limit)
if timeseries_limit and groupby:
2015-10-06 14:50:04 -04:00
subq = select(inner_select_exprs)
subq = subq.select_from(table(self.table_name))
2015-10-03 01:54:12 -04:00
subq = subq.where(and_(*(where_clause_and + inner_time_filter)))
subq = subq.group_by(*inner_groupby_exprs)
2015-08-13 00:22:02 -04:00
subq = subq.order_by(desc(main_metric_expr))
subq = subq.limit(timeseries_limit)
on_clause = []
2015-10-06 14:50:04 -04:00
for i, gb in enumerate(groupby):
2015-09-09 13:37:59 -04:00
on_clause.append(
2015-12-22 11:12:47 -05:00
groupby_exprs[i] == column("__" + gb))
from_clause = from_clause.join(subq.alias(), and_(*on_clause))
qry = qry.select_from(from_clause)
engine = self.database.get_sqla_engine()
sql = str(qry.compile(engine, compile_kwargs={"literal_binds": True}))
df = read_sql_query(
sql=sql,
con=engine
)
sql = sqlparse.format(sql, reindent=True)
return QueryResult(
df=df, duration=datetime.now() - qry_start_dttm, query=sql)
2015-08-03 11:34:58 -04:00
def fetch_metadata(self):
2015-09-25 18:43:50 -04:00
table = self.database.get_table(self.table_name)
try:
table = self.database.get_table(self.table_name)
except Exception as e:
2015-09-03 18:23:44 -04:00
flash(str(e))
flash(
2015-09-03 18:23:44 -04:00
"Table doesn't seem to exist in the specified database, "
"couldn't fetch column information", "danger")
return
2015-08-03 11:34:58 -04:00
TC = TableColumn
M = SqlMetric
metrics = []
any_date_col = None
2015-08-03 11:34:58 -04:00
for col in table.columns:
2015-09-03 18:23:44 -04:00
try:
datatype = str(col.type)
except Exception as e:
datatype = "UNKNOWN"
2015-08-03 11:34:58 -04:00
dbcol = (
db.session
.query(TC)
2015-09-09 13:37:59 -04:00
.filter(TC.table == self)
.filter(TC.column_name == col.name)
2015-08-03 11:34:58 -04:00
.first()
)
db.session.flush()
if not dbcol:
dbcol = TableColumn(column_name=col.name)
2015-09-03 18:23:44 -04:00
if (
2015-09-03 18:23:44 -04:00
str(datatype).startswith('VARCHAR') or
str(datatype).startswith('STRING')):
2015-08-03 11:34:58 -04:00
dbcol.groupby = True
dbcol.filterable = True
2015-09-25 19:18:31 -04:00
elif str(datatype).upper() in ('DOUBLE', 'FLOAT', 'INT', 'BIGINT'):
dbcol.sum = True
db.session.merge(self)
self.columns.append(dbcol)
2015-09-03 18:23:44 -04:00
if not any_date_col and 'date' in datatype.lower():
any_date_col = col.name
2015-12-22 23:46:19 -05:00
quoted = "{}".format(
2015-12-22 11:12:47 -05:00
column(dbcol.column_name).compile(dialect=db.engine.dialect))
if dbcol.sum:
metrics.append(M(
metric_name='sum__' + dbcol.column_name,
verbose_name='sum__' + dbcol.column_name,
metric_type='sum',
2015-12-22 11:12:47 -05:00
expression="SUM({})".format(quoted)
))
if dbcol.max:
metrics.append(M(
metric_name='max__' + dbcol.column_name,
verbose_name='max__' + dbcol.column_name,
metric_type='max',
2015-12-22 11:12:47 -05:00
expression="MAX({})".format(quoted)
))
if dbcol.min:
metrics.append(M(
metric_name='min__' + dbcol.column_name,
verbose_name='min__' + dbcol.column_name,
metric_type='min',
2015-12-22 11:12:47 -05:00
expression="MIN({})".format(quoted)
))
if dbcol.count_distinct:
metrics.append(M(
metric_name='count_distinct__' + dbcol.column_name,
verbose_name='count_distinct__' + dbcol.column_name,
metric_type='count_distinct',
2015-12-22 11:12:47 -05:00
expression="COUNT(DISTINCT {})".format(quoted)
))
2015-09-03 18:23:44 -04:00
dbcol.type = datatype
db.session.merge(self)
2015-08-03 11:34:58 -04:00
db.session.commit()
metrics.append(M(
metric_name='count',
verbose_name='COUNT(*)',
metric_type='count',
expression="COUNT(*)"
))
for metric in metrics:
m = (
db.session.query(M)
2015-09-09 13:37:59 -04:00
.filter(M.metric_name == metric.metric_name)
.filter(M.table_id == self.id)
.first()
)
metric.table_id = self.id
if not m:
db.session.add(metric)
db.session.commit()
if not self.main_dttm_col:
self.main_dttm_col = any_date_col
class SqlMetric(Model, AuditMixinNullable):
2015-08-05 02:41:00 -04:00
__tablename__ = 'sql_metrics'
id = Column(Integer, primary_key=True)
metric_name = Column(String(512))
verbose_name = Column(String(1024))
metric_type = Column(String(32))
2015-09-09 13:37:59 -04:00
table_id = Column(Integer, ForeignKey('tables.id'))
2015-08-05 02:41:00 -04:00
table = relationship(
2015-10-04 11:27:09 -04:00
'SqlaTable', backref='metrics', foreign_keys=[table_id])
2015-08-05 02:41:00 -04:00
expression = Column(Text)
description = Column(Text)
class TableColumn(Model, AuditMixinNullable):
2015-08-03 11:34:58 -04:00
__tablename__ = 'table_columns'
id = Column(Integer, primary_key=True)
2015-08-06 17:44:25 -04:00
table_id = Column(Integer, ForeignKey('tables.id'))
2015-10-04 11:27:09 -04:00
table = relationship(
'SqlaTable', backref='columns', foreign_keys=[table_id])
2015-08-03 11:34:58 -04:00
column_name = Column(String(256))
is_dttm = Column(Boolean, default=False)
2015-08-03 11:34:58 -04:00
is_active = Column(Boolean, default=True)
type = Column(String(32), default='')
groupby = Column(Boolean, default=False)
count_distinct = Column(Boolean, default=False)
sum = Column(Boolean, default=False)
max = Column(Boolean, default=False)
min = Column(Boolean, default=False)
filterable = Column(Boolean, default=False)
2015-10-06 14:50:04 -04:00
expression = Column(Text, default='')
2015-08-03 11:34:58 -04:00
description = Column(Text, default='')
def __repr__(self):
return self.column_name
2015-07-29 20:33:37 -04:00
@property
def isnum(self):
return self.type in ('LONG', 'DOUBLE', 'FLOAT')
2015-09-09 13:37:59 -04:00
2016-02-10 09:56:35 -05:00
class DruidCluster(Model, AuditMixinNullable):
2015-07-29 20:33:37 -04:00
__tablename__ = 'clusters'
id = Column(Integer, primary_key=True)
2015-09-11 18:32:42 -04:00
cluster_name = Column(String(250), unique=True)
2015-07-29 20:33:37 -04:00
coordinator_host = Column(String(256))
coordinator_port = Column(Integer)
coordinator_endpoint = Column(
String(256), default='druid/coordinator/v1/metadata')
2015-07-29 20:33:37 -04:00
broker_host = Column(String(256))
broker_port = Column(Integer)
broker_endpoint = Column(String(256), default='druid/v2')
2015-07-29 20:33:37 -04:00
metadata_last_refreshed = Column(DateTime)
def __repr__(self):
return self.cluster_name
def get_pydruid_client(self):
cli = client.PyDruid(
"http://{0}:{1}/".format(self.broker_host, self.broker_port),
self.broker_endpoint)
return cli
def refresh_datasources(self):
endpoint = (
"http://{self.coordinator_host}:{self.coordinator_port}/"
"{self.coordinator_endpoint}/datasources"
).format(self=self)
2015-09-02 01:15:02 -04:00
datasources = json.loads(requests.get(endpoint).text)
for datasource in datasources:
2016-02-10 09:56:35 -05:00
DruidDatasource.sync_to_db(datasource, self)
2015-08-05 02:41:00 -04:00
2015-07-29 20:33:37 -04:00
2016-02-10 09:56:35 -05:00
class DruidDatasource(Model, AuditMixinNullable, Queryable):
2015-09-21 18:49:30 -04:00
type = "druid"
2015-08-11 00:12:21 -04:00
baselink = "datasourcemodelview"
2015-07-15 13:12:32 -04:00
__tablename__ = 'datasources'
id = Column(Integer, primary_key=True)
2015-09-11 18:32:42 -04:00
datasource_name = Column(String(250), unique=True)
2015-07-15 13:12:32 -04:00
is_featured = Column(Boolean, default=False)
is_hidden = Column(Boolean, default=False)
description = Column(Text)
2015-07-15 20:38:03 -04:00
default_endpoint = Column(Text)
2015-08-06 17:44:25 -04:00
user_id = Column(Integer, ForeignKey('ab_user.id'))
2015-07-21 14:56:05 -04:00
owner = relationship('User', backref='datasources', foreign_keys=[user_id])
2015-09-09 13:37:59 -04:00
cluster_name = Column(
String(250), ForeignKey('clusters.cluster_name'))
2015-09-09 13:37:59 -04:00
cluster = relationship(
2016-02-10 09:56:35 -05:00
'DruidCluster', backref='datasources', foreign_keys=[cluster_name])
offset = Column(Integer, default=0)
2015-07-15 13:12:32 -04:00
@property
2015-07-15 20:38:03 -04:00
def metrics_combo(self):
return sorted(
[(m.metric_name, m.verbose_name) for m in self.metrics],
key=lambda x: x[1])
2015-07-15 13:12:32 -04:00
2015-08-05 02:41:00 -04:00
@property
def name(self):
return self.datasource_name
@property
def perm(self):
return (
"[{self.cluster_name}].[{self.datasource_name}]"
"(id:{self.id})").format(self=self)
2016-01-24 13:09:26 -05:00
@property
def url(self):
return '/datasourcemodelview/edit/{}'.format(self.id)
@property
def link(self):
return (
'<a href="{self.url}">'
'{self.datasource_name}</a>').format(**locals())
@property
def full_name(self):
2016-01-24 13:09:26 -05:00
return (
"[{self.cluster_name}]."
"[{self.datasource_name}]").format(self=self)
2015-07-15 13:12:32 -04:00
def __repr__(self):
return self.datasource_name
@property
def datasource_link(self):
2015-12-04 12:30:43 -05:00
url = "/panoramix/explore/{self.type}/{self.id}/".format(self=self)
2015-07-15 13:12:32 -04:00
return '<a href="{url}">{self.datasource_name}</a>'.format(**locals())
2015-07-15 20:38:03 -04:00
def get_metric_obj(self, metric_name):
return [
m.json_obj for m in self.metrics
if m.metric_name == metric_name
][0]
def latest_metadata(self):
client = self.cluster.get_pydruid_client()
results = client.time_boundary(datasource=self.datasource_name)
2015-09-02 01:15:02 -04:00
if not results:
return
2015-07-16 20:55:36 -04:00
max_time = results[0]['result']['minTime']
2015-07-15 13:12:32 -04:00
max_time = parse(max_time)
intervals = (max_time - timedelta(seconds=1)).isoformat() + '/'
intervals += (max_time + timedelta(seconds=1)).isoformat()
segment_metadata = client.segment_metadata(
datasource=self.datasource_name,
2015-07-15 13:12:32 -04:00
intervals=intervals)
2015-07-15 20:38:03 -04:00
if segment_metadata:
return segment_metadata[-1]['columns']
def generate_metrics(self):
for col in self.columns:
col.generate_metrics()
2015-07-15 13:12:32 -04:00
@classmethod
def sync_to_db(cls, name, cluster):
session = get_session()
datasource = session.query(cls).filter_by(datasource_name=name).first()
2015-07-15 13:12:32 -04:00
if not datasource:
datasource = cls(datasource_name=name)
session.add(datasource)
datasource.cluster = cluster
cols = datasource.latest_metadata()
2015-07-15 20:38:03 -04:00
if not cols:
return
2015-07-15 13:12:32 -04:00
for col in cols:
col_obj = (
session
2015-07-15 13:12:32 -04:00
.query(Column)
.filter_by(datasource_name=name, column_name=col)
.first()
)
datatype = cols[col]['type']
if not col_obj:
col_obj = Column(datasource_name=name, column_name=col)
session.add(col_obj)
2015-07-15 13:12:32 -04:00
if datatype == "STRING":
col_obj.groupby = True
col_obj.filterable = True
if col_obj:
col_obj.type = cols[col]['type']
2015-08-01 20:08:00 -04:00
col_obj.datasource = datasource
2015-07-15 20:38:03 -04:00
col_obj.generate_metrics()
2015-08-05 20:36:33 -04:00
2015-08-05 02:41:00 -04:00
def query(
2015-09-03 18:23:44 -04:00
self, groupby, metrics,
granularity,
from_dttm, to_dttm,
limit_spec=None,
filter=None,
is_timeseries=True,
timeseries_limit=None,
row_limit=None,
2015-10-03 01:54:12 -04:00
inner_from_dttm=None, inner_to_dttm=None,
2015-12-18 15:06:01 -05:00
extras=None,
select=None):
2015-08-07 19:25:19 -04:00
qry_start_dttm = datetime.now()
2015-09-09 13:37:59 -04:00
2015-10-03 01:54:12 -04:00
inner_from_dttm = inner_from_dttm or from_dttm
inner_to_dttm = inner_to_dttm or to_dttm
2015-09-09 13:37:59 -04:00
# add tzinfo to native datetime with config
2015-09-25 18:43:50 -04:00
from_dttm = from_dttm.replace(tzinfo=config.get("DRUID_TZ"))
to_dttm = to_dttm.replace(tzinfo=config.get("DRUID_TZ"))
2015-08-05 02:41:00 -04:00
query_str = ""
2015-08-05 02:41:00 -04:00
aggregations = {
m.metric_name: m.json_obj
for m in self.metrics if m.metric_name in metrics
}
2015-10-20 19:39:52 -04:00
granularity = granularity or "all"
if granularity != "all":
granularity = utils.parse_human_timedelta(
granularity).total_seconds() * 1000
2015-10-20 14:52:40 -04:00
if not isinstance(granularity, string_types):
2015-08-05 02:41:00 -04:00
granularity = {"type": "duration", "duration": granularity}
qry = dict(
datasource=self.datasource_name,
dimensions=groupby,
aggregations=aggregations,
granularity=granularity,
2015-09-09 13:37:59 -04:00
intervals=from_dttm.isoformat() + '/' + to_dttm.isoformat(),
2015-08-05 02:41:00 -04:00
)
2015-08-06 01:42:42 -04:00
filters = None
for col, op, eq in filter:
cond = None
if op == '==':
2015-09-09 13:37:59 -04:00
cond = Dimension(col) == eq
2015-08-06 01:42:42 -04:00
elif op == '!=':
2015-09-09 13:37:59 -04:00
cond = ~(Dimension(col) == eq)
2015-08-06 01:42:42 -04:00
elif op in ('in', 'not in'):
fields = []
splitted = eq.split(',')
if len(splitted) > 1:
for s in eq.split(','):
s = s.strip()
2015-09-09 13:37:59 -04:00
fields.append(Filter.build_filter(Dimension(col) == s))
2015-08-06 01:42:42 -04:00
cond = Filter(type="or", fields=fields)
else:
2015-09-09 13:37:59 -04:00
cond = Dimension(col) == eq
2015-08-06 01:42:42 -04:00
if op == 'not in':
cond = ~cond
if filters:
filters = Filter(type="and", fields=[
Filter.build_filter(cond),
Filter.build_filter(filters)
])
else:
filters = cond
if filters:
qry['filter'] = filters
2015-08-05 20:36:33 -04:00
2015-08-05 02:41:00 -04:00
client = self.cluster.get_pydruid_client()
2015-08-06 01:42:42 -04:00
orig_filters = filters
2015-09-03 18:23:44 -04:00
if timeseries_limit and is_timeseries:
2015-08-05 20:36:33 -04:00
# Limit on the number of timeseries, doing a two-phases query
pre_qry = deepcopy(qry)
pre_qry['granularity'] = "all"
2015-08-06 01:42:42 -04:00
pre_qry['limit_spec'] = {
"type": "default",
"limit": timeseries_limit,
2015-10-03 01:54:12 -04:00
'intervals': inner_from_dttm.isoformat() + '/' + inner_to_dttm.isoformat(),
2015-08-06 01:42:42 -04:00
"columns": [{
"dimension": metrics[0] if metrics else self.metrics[0],
"direction": "descending",
}],
}
client.groupby(**pre_qry)
query_str += "// Two phase query\n// Phase 1\n"
query_str += json.dumps(client.query_dict, indent=2) + "\n"
query_str += "//\nPhase 2 (built based on phase one's results)\n"
2015-08-05 20:36:33 -04:00
df = client.export_pandas()
2015-09-09 13:37:59 -04:00
if df is not None and not df.empty:
2015-08-05 20:36:33 -04:00
dims = qry['dimensions']
filters = []
for index, row in df.iterrows():
fields = []
for dim in dims:
f = Filter.build_filter(Dimension(dim) == row[dim])
fields.append(f)
if len(fields) > 1:
filt = Filter(type="and", fields=fields)
filters.append(Filter.build_filter(filt))
elif fields:
filters.append(fields[0])
if filters:
ff = Filter(type="or", fields=filters)
2015-08-06 01:42:42 -04:00
if not orig_filters:
2015-08-05 20:36:33 -04:00
qry['filter'] = ff
else:
qry['filter'] = Filter(type="and", fields=[
Filter.build_filter(ff),
2015-08-06 01:42:42 -04:00
Filter.build_filter(orig_filters)])
2015-08-05 20:36:33 -04:00
qry['limit_spec'] = None
2015-09-03 18:23:44 -04:00
if row_limit:
qry['limit_spec'] = {
"type": "default",
"limit": row_limit,
"columns": [{
"dimension": metrics[0] if metrics else self.metrics[0],
"direction": "descending",
}],
}
2015-08-05 02:41:00 -04:00
client.groupby(**qry)
query_str += json.dumps(client.query_dict, indent=2)
2015-08-05 02:41:00 -04:00
df = client.export_pandas()
2016-01-12 15:06:34 -05:00
if df is None or df.size == 0:
raise Exception("No data was returned.")
2016-01-12 17:57:34 -05:00
if (
not is_timeseries and
granularity == "all"
and 'timestamp' in df.columns):
del df['timestamp']
# Reordering columns
cols = []
if 'timestamp' in df.columns:
cols += ['timestamp']
cols += [col for col in groupby if col in df.columns]
cols += [col for col in metrics if col in df.columns]
cols += [col for col in df.columns if col not in cols]
df = df[cols]
2015-08-07 19:25:19 -04:00
return QueryResult(
df=df,
query=query_str,
duration=datetime.now() - qry_start_dttm)
2015-07-15 13:12:32 -04:00
2015-12-04 14:52:32 -05:00
class Log(Model):
__tablename__ = 'logs'
id = Column(Integer, primary_key=True)
action = Column(String(512))
user_id = Column(Integer, ForeignKey('ab_user.id'))
2016-02-10 11:41:37 -05:00
dashboard_id = Column(Integer)
slice_id = Column(Integer)
user_id = Column(Integer, ForeignKey('ab_user.id'))
2015-12-04 14:52:32 -05:00
json = Column(Text)
user = relationship('User', backref='logs', foreign_keys=[user_id])
dttm = Column(DateTime, default=func.now())
2016-02-10 10:18:59 -05:00
class DruidMetric(Model):
2015-07-15 20:38:03 -04:00
__tablename__ = 'metrics'
2015-07-15 13:12:32 -04:00
id = Column(Integer, primary_key=True)
2015-07-15 20:38:03 -04:00
metric_name = Column(String(512))
verbose_name = Column(String(1024))
metric_type = Column(String(32))
2015-07-15 13:12:32 -04:00
datasource_name = Column(
2015-09-11 18:32:42 -04:00
String(250),
2015-07-15 13:12:32 -04:00
ForeignKey('datasources.datasource_name'))
2016-02-10 09:56:35 -05:00
datasource = relationship('DruidDatasource', backref='metrics')
2015-07-15 20:38:03 -04:00
json = Column(Text)
2015-07-21 14:56:05 -04:00
description = Column(Text)
2015-07-15 13:12:32 -04:00
2015-07-15 20:38:03 -04:00
@property
def json_obj(self):
try:
obj = json.loads(self.json)
2015-09-09 13:37:59 -04:00
except:
obj = {}
return obj
2015-07-15 13:12:32 -04:00
2016-02-10 09:56:35 -05:00
class DruidColumn(Model, AuditMixinNullable):
2015-07-15 13:12:32 -04:00
__tablename__ = 'columns'
id = Column(Integer, primary_key=True)
datasource_name = Column(
2015-09-11 18:32:42 -04:00
String(250),
2015-07-15 13:12:32 -04:00
ForeignKey('datasources.datasource_name'))
2016-02-10 09:56:35 -05:00
datasource = relationship('DruidDatasource', backref='columns')
2015-07-15 13:12:32 -04:00
column_name = Column(String(256))
is_active = Column(Boolean, default=True)
type = Column(String(32))
groupby = Column(Boolean, default=False)
count_distinct = Column(Boolean, default=False)
sum = Column(Boolean, default=False)
max = Column(Boolean, default=False)
min = Column(Boolean, default=False)
filterable = Column(Boolean, default=False)
2015-07-21 14:56:05 -04:00
description = Column(Text)
2015-07-15 13:12:32 -04:00
def __repr__(self):
return self.column_name
2015-07-15 20:38:03 -04:00
@property
def isnum(self):
return self.type in ('LONG', 'DOUBLE', 'FLOAT')
2015-07-15 20:38:03 -04:00
def generate_metrics(self):
2016-02-10 10:18:59 -05:00
M = DruidMetric
2015-07-15 20:38:03 -04:00
metrics = []
2016-02-10 10:18:59 -05:00
metrics.append(DruidMetric(
2015-07-15 20:38:03 -04:00
metric_name='count',
verbose_name='COUNT(*)',
metric_type='count',
json=json.dumps({'type': 'count', 'name': 'count'})
2015-07-15 20:38:03 -04:00
))
# Somehow we need to reassign this for UDAFs
2015-09-09 13:37:59 -04:00
if self.type in ('DOUBLE', 'FLOAT'):
corrected_type = 'DOUBLE'
else:
corrected_type = self.type
2015-07-15 20:38:03 -04:00
if self.sum and self.isnum:
mt = corrected_type.lower() + 'Sum'
2015-09-09 13:37:59 -04:00
name = 'sum__' + self.column_name
2015-07-15 20:38:03 -04:00
metrics.append(Metric(
metric_name=name,
metric_type='sum',
verbose_name='SUM({})'.format(self.column_name),
json=json.dumps({
'type': mt, 'name': name, 'fieldName': self.column_name})
))
if self.min and self.isnum:
mt = corrected_type.lower() + 'Min'
2015-09-09 13:37:59 -04:00
name = 'min__' + self.column_name
2016-02-10 10:18:59 -05:00
metrics.append(DruidMetric(
2015-07-15 20:38:03 -04:00
metric_name=name,
metric_type='min',
verbose_name='MIN({})'.format(self.column_name),
json=json.dumps({
'type': mt, 'name': name, 'fieldName': self.column_name})
))
if self.max and self.isnum:
mt = corrected_type.lower() + 'Max'
2015-09-09 13:37:59 -04:00
name = 'max__' + self.column_name
2016-02-10 10:18:59 -05:00
metrics.append(DruidMetric(
2015-07-15 20:38:03 -04:00
metric_name=name,
metric_type='max',
verbose_name='MAX({})'.format(self.column_name),
json=json.dumps({
'type': mt, 'name': name, 'fieldName': self.column_name})
))
if self.count_distinct:
mt = 'count_distinct'
2015-09-09 13:37:59 -04:00
name = 'count_distinct__' + self.column_name
2016-02-10 10:18:59 -05:00
metrics.append(DruidMetric(
2015-07-15 20:38:03 -04:00
metric_name=name,
verbose_name='COUNT(DISTINCT {})'.format(self.column_name),
metric_type='count_distinct',
json=json.dumps({
'type': 'cardinality',
'name': name,
'fieldNames': [self.column_name]})
))
session = get_session()
2015-07-15 20:38:03 -04:00
for metric in metrics:
m = (
session.query(M)
2015-09-09 13:37:59 -04:00
.filter(M.metric_name == metric.metric_name)
.filter(M.datasource_name == self.datasource_name)
.filter(Cluster.cluster_name == self.datasource.cluster_name)
2015-07-15 20:38:03 -04:00
.first()
)
metric.datasource_name = self.datasource_name
if not m:
session.add(metric)
session.commit()