2018-02-25 18:06:11 -05:00
|
|
|
# -*- coding: utf-8 -*-
|
2018-02-23 19:42:09 -05:00
|
|
|
from __future__ import absolute_import
|
|
|
|
from __future__ import division
|
|
|
|
from __future__ import print_function
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
2017-11-30 23:32:53 -05:00
|
|
|
import json
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
from mock import Mock
|
2017-12-07 00:55:43 -05:00
|
|
|
import pydruid.utils.postaggregator as postaggs
|
2017-11-30 23:32:53 -05:00
|
|
|
|
2017-12-07 00:55:43 -05:00
|
|
|
import superset.connectors.druid.models as models
|
2017-11-30 23:32:53 -05:00
|
|
|
from superset.connectors.druid.models import (
|
|
|
|
DruidColumn, DruidDatasource, DruidMetric,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2017-12-07 00:55:43 -05:00
|
|
|
def mock_metric(metric_name, is_postagg=False):
|
|
|
|
metric = Mock()
|
|
|
|
metric.metric_name = metric_name
|
|
|
|
metric.metric_type = 'postagg' if is_postagg else 'metric'
|
|
|
|
return metric
|
|
|
|
|
|
|
|
|
|
|
|
def emplace(metrics_dict, metric_name, is_postagg=False):
|
|
|
|
metrics_dict[metric_name] = mock_metric(metric_name, is_postagg)
|
|
|
|
|
|
|
|
|
|
|
|
# Unit tests that can be run without initializing base tests
|
2017-11-30 23:32:53 -05:00
|
|
|
class DruidFuncTestCase(unittest.TestCase):
|
|
|
|
|
|
|
|
def test_get_filters_ignores_invalid_filter_objects(self):
|
|
|
|
filtr = {'col': 'col1', 'op': '=='}
|
|
|
|
filters = [filtr]
|
|
|
|
self.assertEqual(None, DruidDatasource.get_filters(filters, []))
|
|
|
|
|
|
|
|
def test_get_filters_constructs_filter_in(self):
|
|
|
|
filtr = {'col': 'A', 'op': 'in', 'val': ['a', 'b', 'c']}
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertIn('filter', res.filter)
|
|
|
|
self.assertIn('fields', res.filter['filter'])
|
|
|
|
self.assertEqual('or', res.filter['filter']['type'])
|
|
|
|
self.assertEqual(3, len(res.filter['filter']['fields']))
|
|
|
|
|
|
|
|
def test_get_filters_constructs_filter_not_in(self):
|
|
|
|
filtr = {'col': 'A', 'op': 'not in', 'val': ['a', 'b', 'c']}
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertIn('filter', res.filter)
|
|
|
|
self.assertIn('type', res.filter['filter'])
|
|
|
|
self.assertEqual('not', res.filter['filter']['type'])
|
|
|
|
self.assertIn('field', res.filter['filter'])
|
|
|
|
self.assertEqual(
|
|
|
|
3,
|
|
|
|
len(res.filter['filter']['field'].filter['filter']['fields']),
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_get_filters_constructs_filter_equals(self):
|
|
|
|
filtr = {'col': 'A', 'op': '==', 'val': 'h'}
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertEqual('selector', res.filter['filter']['type'])
|
|
|
|
self.assertEqual('A', res.filter['filter']['dimension'])
|
|
|
|
self.assertEqual('h', res.filter['filter']['value'])
|
|
|
|
|
|
|
|
def test_get_filters_constructs_filter_not_equals(self):
|
|
|
|
filtr = {'col': 'A', 'op': '!=', 'val': 'h'}
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertEqual('not', res.filter['filter']['type'])
|
|
|
|
self.assertEqual(
|
|
|
|
'h',
|
|
|
|
res.filter['filter']['field'].filter['filter']['value'],
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_get_filters_constructs_bounds_filter(self):
|
|
|
|
filtr = {'col': 'A', 'op': '>=', 'val': 'h'}
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertFalse(res.filter['filter']['lowerStrict'])
|
|
|
|
self.assertEqual('A', res.filter['filter']['dimension'])
|
|
|
|
self.assertEqual('h', res.filter['filter']['lower'])
|
|
|
|
self.assertFalse(res.filter['filter']['alphaNumeric'])
|
|
|
|
filtr['op'] = '>'
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertTrue(res.filter['filter']['lowerStrict'])
|
|
|
|
filtr['op'] = '<='
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertFalse(res.filter['filter']['upperStrict'])
|
|
|
|
self.assertEqual('h', res.filter['filter']['upper'])
|
|
|
|
filtr['op'] = '<'
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertTrue(res.filter['filter']['upperStrict'])
|
|
|
|
|
|
|
|
def test_get_filters_constructs_regex_filter(self):
|
|
|
|
filtr = {'col': 'A', 'op': 'regex', 'val': '[abc]'}
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertEqual('regex', res.filter['filter']['type'])
|
|
|
|
self.assertEqual('[abc]', res.filter['filter']['pattern'])
|
|
|
|
self.assertEqual('A', res.filter['filter']['dimension'])
|
|
|
|
|
|
|
|
def test_get_filters_composes_multiple_filters(self):
|
|
|
|
filtr1 = {'col': 'A', 'op': '!=', 'val': 'y'}
|
|
|
|
filtr2 = {'col': 'B', 'op': 'in', 'val': ['a', 'b', 'c']}
|
|
|
|
res = DruidDatasource.get_filters([filtr1, filtr2], [])
|
|
|
|
self.assertEqual('and', res.filter['filter']['type'])
|
|
|
|
self.assertEqual(2, len(res.filter['filter']['fields']))
|
|
|
|
|
|
|
|
def test_get_filters_ignores_in_not_in_with_empty_value(self):
|
|
|
|
filtr1 = {'col': 'A', 'op': 'in', 'val': []}
|
|
|
|
filtr2 = {'col': 'A', 'op': 'not in', 'val': []}
|
|
|
|
res = DruidDatasource.get_filters([filtr1, filtr2], [])
|
|
|
|
self.assertEqual(None, res)
|
|
|
|
|
|
|
|
def test_get_filters_constructs_equals_for_in_not_in_single_value(self):
|
|
|
|
filtr = {'col': 'A', 'op': 'in', 'val': ['a']}
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertEqual('selector', res.filter['filter']['type'])
|
|
|
|
|
|
|
|
def test_get_filters_handles_arrays_for_string_types(self):
|
|
|
|
filtr = {'col': 'A', 'op': '==', 'val': ['a', 'b']}
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertEqual('a', res.filter['filter']['value'])
|
|
|
|
filtr = {'col': 'A', 'op': '==', 'val': []}
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertEqual('', res.filter['filter']['value'])
|
|
|
|
|
|
|
|
def test_get_filters_handles_none_for_string_types(self):
|
|
|
|
filtr = {'col': 'A', 'op': '==', 'val': None}
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertEqual('', res.filter['filter']['value'])
|
|
|
|
|
|
|
|
def test_get_filters_extracts_values_in_quotes(self):
|
|
|
|
filtr = {'col': 'A', 'op': 'in', 'val': [' "a" ']}
|
|
|
|
res = DruidDatasource.get_filters([filtr], [])
|
|
|
|
self.assertEqual('a', res.filter['filter']['value'])
|
|
|
|
|
|
|
|
def test_get_filters_converts_strings_to_num(self):
|
|
|
|
filtr = {'col': 'A', 'op': 'in', 'val': ['6']}
|
|
|
|
res = DruidDatasource.get_filters([filtr], ['A'])
|
|
|
|
self.assertEqual(6, res.filter['filter']['value'])
|
|
|
|
filtr = {'col': 'A', 'op': '==', 'val': '6'}
|
|
|
|
res = DruidDatasource.get_filters([filtr], ['A'])
|
|
|
|
self.assertEqual(6, res.filter['filter']['value'])
|
|
|
|
|
|
|
|
def test_run_query_no_groupby(self):
|
|
|
|
client = Mock()
|
|
|
|
from_dttm = Mock()
|
|
|
|
to_dttm = Mock()
|
|
|
|
from_dttm.replace = Mock(return_value=from_dttm)
|
|
|
|
to_dttm.replace = Mock(return_value=to_dttm)
|
|
|
|
from_dttm.isoformat = Mock(return_value='from')
|
|
|
|
to_dttm.isoformat = Mock(return_value='to')
|
|
|
|
timezone = 'timezone'
|
|
|
|
from_dttm.tzname = Mock(return_value=timezone)
|
|
|
|
ds = DruidDatasource(datasource_name='datasource')
|
|
|
|
metric1 = DruidMetric(metric_name='metric1')
|
|
|
|
metric2 = DruidMetric(metric_name='metric2')
|
|
|
|
ds.metrics = [metric1, metric2]
|
|
|
|
col1 = DruidColumn(column_name='col1')
|
|
|
|
col2 = DruidColumn(column_name='col2')
|
|
|
|
ds.columns = [col1, col2]
|
|
|
|
all_metrics = []
|
|
|
|
post_aggs = ['some_agg']
|
|
|
|
ds._metrics_and_post_aggs = Mock(return_value=(all_metrics, post_aggs))
|
|
|
|
groupby = []
|
|
|
|
metrics = ['metric1']
|
|
|
|
ds.get_having_filters = Mock(return_value=[])
|
|
|
|
client.query_builder = Mock()
|
|
|
|
client.query_builder.last_query = Mock()
|
|
|
|
client.query_builder.last_query.query_dict = {'mock': 0}
|
|
|
|
# no groupby calls client.timeseries
|
|
|
|
ds.run_query(
|
|
|
|
groupby, metrics, None, from_dttm,
|
|
|
|
to_dttm, client=client, filter=[], row_limit=100,
|
|
|
|
)
|
|
|
|
self.assertEqual(0, len(client.topn.call_args_list))
|
|
|
|
self.assertEqual(0, len(client.groupby.call_args_list))
|
|
|
|
self.assertEqual(1, len(client.timeseries.call_args_list))
|
|
|
|
# check that there is no dimensions entry
|
|
|
|
called_args = client.timeseries.call_args_list[0][1]
|
|
|
|
self.assertNotIn('dimensions', called_args)
|
|
|
|
self.assertIn('post_aggregations', called_args)
|
|
|
|
# restore functions
|
|
|
|
|
|
|
|
def test_run_query_single_groupby(self):
|
|
|
|
client = Mock()
|
|
|
|
from_dttm = Mock()
|
|
|
|
to_dttm = Mock()
|
|
|
|
from_dttm.replace = Mock(return_value=from_dttm)
|
|
|
|
to_dttm.replace = Mock(return_value=to_dttm)
|
|
|
|
from_dttm.isoformat = Mock(return_value='from')
|
|
|
|
to_dttm.isoformat = Mock(return_value='to')
|
|
|
|
timezone = 'timezone'
|
|
|
|
from_dttm.tzname = Mock(return_value=timezone)
|
|
|
|
ds = DruidDatasource(datasource_name='datasource')
|
|
|
|
metric1 = DruidMetric(metric_name='metric1')
|
|
|
|
metric2 = DruidMetric(metric_name='metric2')
|
|
|
|
ds.metrics = [metric1, metric2]
|
|
|
|
col1 = DruidColumn(column_name='col1')
|
|
|
|
col2 = DruidColumn(column_name='col2')
|
|
|
|
ds.columns = [col1, col2]
|
|
|
|
all_metrics = ['metric1']
|
|
|
|
post_aggs = ['some_agg']
|
|
|
|
ds._metrics_and_post_aggs = Mock(return_value=(all_metrics, post_aggs))
|
|
|
|
groupby = ['col1']
|
|
|
|
metrics = ['metric1']
|
|
|
|
ds.get_having_filters = Mock(return_value=[])
|
|
|
|
client.query_builder.last_query.query_dict = {'mock': 0}
|
|
|
|
# client.topn is called twice
|
|
|
|
ds.run_query(
|
2017-12-15 14:47:27 -05:00
|
|
|
groupby, metrics, None, from_dttm, to_dttm, timeseries_limit=100,
|
2017-11-30 23:32:53 -05:00
|
|
|
client=client, order_desc=True, filter=[],
|
|
|
|
)
|
|
|
|
self.assertEqual(2, len(client.topn.call_args_list))
|
|
|
|
self.assertEqual(0, len(client.groupby.call_args_list))
|
|
|
|
self.assertEqual(0, len(client.timeseries.call_args_list))
|
|
|
|
# check that there is no dimensions entry
|
|
|
|
called_args_pre = client.topn.call_args_list[0][1]
|
|
|
|
self.assertNotIn('dimensions', called_args_pre)
|
|
|
|
self.assertIn('dimension', called_args_pre)
|
|
|
|
called_args = client.topn.call_args_list[1][1]
|
|
|
|
self.assertIn('dimension', called_args)
|
|
|
|
self.assertEqual('col1', called_args['dimension'])
|
|
|
|
# not order_desc
|
|
|
|
client = Mock()
|
|
|
|
client.query_builder.last_query.query_dict = {'mock': 0}
|
|
|
|
ds.run_query(
|
|
|
|
groupby, metrics, None, from_dttm, to_dttm, client=client,
|
|
|
|
order_desc=False, filter=[], row_limit=100,
|
|
|
|
)
|
|
|
|
self.assertEqual(0, len(client.topn.call_args_list))
|
|
|
|
self.assertEqual(1, len(client.groupby.call_args_list))
|
|
|
|
self.assertEqual(0, len(client.timeseries.call_args_list))
|
|
|
|
self.assertIn('dimensions', client.groupby.call_args_list[0][1])
|
|
|
|
self.assertEqual(['col1'], client.groupby.call_args_list[0][1]['dimensions'])
|
|
|
|
# order_desc but timeseries and dimension spec
|
2017-12-11 15:35:25 -05:00
|
|
|
# calls topn with single dimension spec 'dimension'
|
|
|
|
spec = {'outputName': 'hello', 'dimension': 'matcho'}
|
2017-11-30 23:32:53 -05:00
|
|
|
spec_json = json.dumps(spec)
|
|
|
|
col3 = DruidColumn(column_name='col3', dimension_spec_json=spec_json)
|
|
|
|
ds.columns.append(col3)
|
|
|
|
groupby = ['col3']
|
|
|
|
client = Mock()
|
|
|
|
client.query_builder.last_query.query_dict = {'mock': 0}
|
|
|
|
ds.run_query(
|
|
|
|
groupby, metrics, None, from_dttm, to_dttm,
|
|
|
|
client=client, order_desc=True, timeseries_limit=5,
|
|
|
|
filter=[], row_limit=100,
|
|
|
|
)
|
2017-12-11 15:35:25 -05:00
|
|
|
self.assertEqual(2, len(client.topn.call_args_list))
|
|
|
|
self.assertEqual(0, len(client.groupby.call_args_list))
|
2017-11-30 23:32:53 -05:00
|
|
|
self.assertEqual(0, len(client.timeseries.call_args_list))
|
2017-12-11 15:35:25 -05:00
|
|
|
self.assertIn('dimension', client.topn.call_args_list[0][1])
|
|
|
|
self.assertIn('dimension', client.topn.call_args_list[1][1])
|
|
|
|
# uses dimension for pre query and full spec for final query
|
|
|
|
self.assertEqual('matcho', client.topn.call_args_list[0][1]['dimension'])
|
|
|
|
self.assertEqual(spec, client.topn.call_args_list[1][1]['dimension'])
|
2017-11-30 23:32:53 -05:00
|
|
|
|
|
|
|
def test_run_query_multiple_groupby(self):
|
|
|
|
client = Mock()
|
|
|
|
from_dttm = Mock()
|
|
|
|
to_dttm = Mock()
|
|
|
|
from_dttm.replace = Mock(return_value=from_dttm)
|
|
|
|
to_dttm.replace = Mock(return_value=to_dttm)
|
|
|
|
from_dttm.isoformat = Mock(return_value='from')
|
|
|
|
to_dttm.isoformat = Mock(return_value='to')
|
|
|
|
timezone = 'timezone'
|
|
|
|
from_dttm.tzname = Mock(return_value=timezone)
|
|
|
|
ds = DruidDatasource(datasource_name='datasource')
|
|
|
|
metric1 = DruidMetric(metric_name='metric1')
|
|
|
|
metric2 = DruidMetric(metric_name='metric2')
|
|
|
|
ds.metrics = [metric1, metric2]
|
|
|
|
col1 = DruidColumn(column_name='col1')
|
|
|
|
col2 = DruidColumn(column_name='col2')
|
|
|
|
ds.columns = [col1, col2]
|
|
|
|
all_metrics = []
|
|
|
|
post_aggs = ['some_agg']
|
|
|
|
ds._metrics_and_post_aggs = Mock(return_value=(all_metrics, post_aggs))
|
|
|
|
groupby = ['col1', 'col2']
|
|
|
|
metrics = ['metric1']
|
|
|
|
ds.get_having_filters = Mock(return_value=[])
|
|
|
|
client.query_builder = Mock()
|
|
|
|
client.query_builder.last_query = Mock()
|
|
|
|
client.query_builder.last_query.query_dict = {'mock': 0}
|
|
|
|
# no groupby calls client.timeseries
|
|
|
|
ds.run_query(
|
|
|
|
groupby, metrics, None, from_dttm,
|
|
|
|
to_dttm, client=client, row_limit=100,
|
|
|
|
filter=[],
|
|
|
|
)
|
|
|
|
self.assertEqual(0, len(client.topn.call_args_list))
|
|
|
|
self.assertEqual(1, len(client.groupby.call_args_list))
|
|
|
|
self.assertEqual(0, len(client.timeseries.call_args_list))
|
|
|
|
# check that there is no dimensions entry
|
|
|
|
called_args = client.groupby.call_args_list[0][1]
|
|
|
|
self.assertIn('dimensions', called_args)
|
|
|
|
self.assertEqual(['col1', 'col2'], called_args['dimensions'])
|
2017-12-07 00:55:43 -05:00
|
|
|
|
|
|
|
def test_get_post_agg_returns_correct_agg_type(self):
|
|
|
|
get_post_agg = DruidDatasource.get_post_agg
|
|
|
|
# javascript PostAggregators
|
|
|
|
function = 'function(field1, field2) { return field1 + field2; }'
|
|
|
|
conf = {
|
|
|
|
'type': 'javascript',
|
|
|
|
'name': 'postagg_name',
|
|
|
|
'fieldNames': ['field1', 'field2'],
|
|
|
|
'function': function,
|
|
|
|
}
|
|
|
|
postagg = get_post_agg(conf)
|
|
|
|
self.assertTrue(isinstance(postagg, models.JavascriptPostAggregator))
|
|
|
|
self.assertEqual(postagg.name, 'postagg_name')
|
|
|
|
self.assertEqual(postagg.post_aggregator['type'], 'javascript')
|
|
|
|
self.assertEqual(postagg.post_aggregator['fieldNames'], ['field1', 'field2'])
|
|
|
|
self.assertEqual(postagg.post_aggregator['name'], 'postagg_name')
|
|
|
|
self.assertEqual(postagg.post_aggregator['function'], function)
|
|
|
|
# Quantile
|
|
|
|
conf = {
|
|
|
|
'type': 'quantile',
|
|
|
|
'name': 'postagg_name',
|
|
|
|
'probability': '0.5',
|
|
|
|
}
|
|
|
|
postagg = get_post_agg(conf)
|
|
|
|
self.assertTrue(isinstance(postagg, postaggs.Quantile))
|
|
|
|
self.assertEqual(postagg.name, 'postagg_name')
|
|
|
|
self.assertEqual(postagg.post_aggregator['probability'], '0.5')
|
|
|
|
# Quantiles
|
|
|
|
conf = {
|
|
|
|
'type': 'quantiles',
|
|
|
|
'name': 'postagg_name',
|
|
|
|
'probabilities': '0.4,0.5,0.6',
|
|
|
|
}
|
|
|
|
postagg = get_post_agg(conf)
|
|
|
|
self.assertTrue(isinstance(postagg, postaggs.Quantiles))
|
|
|
|
self.assertEqual(postagg.name, 'postagg_name')
|
|
|
|
self.assertEqual(postagg.post_aggregator['probabilities'], '0.4,0.5,0.6')
|
|
|
|
# FieldAccess
|
|
|
|
conf = {
|
|
|
|
'type': 'fieldAccess',
|
|
|
|
'name': 'field_name',
|
|
|
|
}
|
|
|
|
postagg = get_post_agg(conf)
|
|
|
|
self.assertTrue(isinstance(postagg, postaggs.Field))
|
|
|
|
self.assertEqual(postagg.name, 'field_name')
|
|
|
|
# constant
|
|
|
|
conf = {
|
|
|
|
'type': 'constant',
|
|
|
|
'value': 1234,
|
|
|
|
'name': 'postagg_name',
|
|
|
|
}
|
|
|
|
postagg = get_post_agg(conf)
|
|
|
|
self.assertTrue(isinstance(postagg, postaggs.Const))
|
|
|
|
self.assertEqual(postagg.name, 'postagg_name')
|
|
|
|
self.assertEqual(postagg.post_aggregator['value'], 1234)
|
|
|
|
# hyperUniqueCardinality
|
|
|
|
conf = {
|
|
|
|
'type': 'hyperUniqueCardinality',
|
|
|
|
'name': 'unique_name',
|
|
|
|
}
|
|
|
|
postagg = get_post_agg(conf)
|
|
|
|
self.assertTrue(isinstance(postagg, postaggs.HyperUniqueCardinality))
|
|
|
|
self.assertEqual(postagg.name, 'unique_name')
|
|
|
|
# arithmetic
|
|
|
|
conf = {
|
|
|
|
'type': 'arithmetic',
|
|
|
|
'fn': '+',
|
|
|
|
'fields': ['field1', 'field2'],
|
|
|
|
'name': 'postagg_name',
|
|
|
|
}
|
|
|
|
postagg = get_post_agg(conf)
|
|
|
|
self.assertTrue(isinstance(postagg, postaggs.Postaggregator))
|
|
|
|
self.assertEqual(postagg.name, 'postagg_name')
|
|
|
|
self.assertEqual(postagg.post_aggregator['fn'], '+')
|
|
|
|
self.assertEqual(postagg.post_aggregator['fields'], ['field1', 'field2'])
|
|
|
|
# custom post aggregator
|
|
|
|
conf = {
|
|
|
|
'type': 'custom',
|
|
|
|
'name': 'custom_name',
|
|
|
|
'stuff': 'more_stuff',
|
|
|
|
}
|
|
|
|
postagg = get_post_agg(conf)
|
|
|
|
self.assertTrue(isinstance(postagg, models.CustomPostAggregator))
|
|
|
|
self.assertEqual(postagg.name, 'custom_name')
|
|
|
|
self.assertEqual(postagg.post_aggregator['stuff'], 'more_stuff')
|
|
|
|
|
|
|
|
def test_find_postaggs_for_returns_postaggs_and_removes(self):
|
|
|
|
find_postaggs_for = DruidDatasource.find_postaggs_for
|
|
|
|
postagg_names = set(['pa2', 'pa3', 'pa4', 'm1', 'm2', 'm3', 'm4'])
|
|
|
|
|
|
|
|
metrics = {}
|
|
|
|
for i in range(1, 6):
|
|
|
|
emplace(metrics, 'pa' + str(i), True)
|
|
|
|
emplace(metrics, 'm' + str(i), False)
|
|
|
|
postagg_list = find_postaggs_for(postagg_names, metrics)
|
|
|
|
self.assertEqual(3, len(postagg_list))
|
|
|
|
self.assertEqual(4, len(postagg_names))
|
|
|
|
expected_metrics = ['m1', 'm2', 'm3', 'm4']
|
|
|
|
expected_postaggs = set(['pa2', 'pa3', 'pa4'])
|
|
|
|
for postagg in postagg_list:
|
|
|
|
expected_postaggs.remove(postagg.metric_name)
|
|
|
|
for metric in expected_metrics:
|
|
|
|
postagg_names.remove(metric)
|
|
|
|
self.assertEqual(0, len(expected_postaggs))
|
|
|
|
self.assertEqual(0, len(postagg_names))
|
|
|
|
|
|
|
|
def test_recursive_get_fields(self):
|
|
|
|
conf = {
|
|
|
|
'type': 'quantile',
|
|
|
|
'fieldName': 'f1',
|
|
|
|
'field': {
|
|
|
|
'type': 'custom',
|
|
|
|
'fields': [{
|
|
|
|
'type': 'fieldAccess',
|
|
|
|
'fieldName': 'f2',
|
|
|
|
}, {
|
|
|
|
'type': 'fieldAccess',
|
|
|
|
'fieldName': 'f3',
|
|
|
|
}, {
|
|
|
|
'type': 'quantiles',
|
|
|
|
'fieldName': 'f4',
|
|
|
|
'field': {
|
|
|
|
'type': 'custom',
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
'type': 'custom',
|
|
|
|
'fields': [{
|
|
|
|
'type': 'fieldAccess',
|
|
|
|
'fieldName': 'f5',
|
|
|
|
}, {
|
|
|
|
'type': 'fieldAccess',
|
|
|
|
'fieldName': 'f2',
|
|
|
|
'fields': [{
|
|
|
|
'type': 'fieldAccess',
|
|
|
|
'fieldName': 'f3',
|
|
|
|
}, {
|
|
|
|
'type': 'fieldIgnoreMe',
|
|
|
|
'fieldName': 'f6',
|
|
|
|
}],
|
|
|
|
}],
|
|
|
|
}],
|
|
|
|
},
|
|
|
|
}
|
|
|
|
fields = DruidDatasource.recursive_get_fields(conf)
|
|
|
|
expected = set(['f1', 'f2', 'f3', 'f4', 'f5'])
|
|
|
|
self.assertEqual(5, len(fields))
|
|
|
|
for field in fields:
|
|
|
|
expected.remove(field)
|
|
|
|
self.assertEqual(0, len(expected))
|
|
|
|
|
|
|
|
def test_metrics_and_post_aggs_tree(self):
|
|
|
|
metrics = ['A', 'B', 'm1', 'm2']
|
|
|
|
metrics_dict = {}
|
|
|
|
for i in range(ord('A'), ord('K') + 1):
|
|
|
|
emplace(metrics_dict, chr(i), True)
|
|
|
|
for i in range(1, 10):
|
|
|
|
emplace(metrics_dict, 'm' + str(i), False)
|
|
|
|
|
|
|
|
def depends_on(index, fields):
|
|
|
|
dependents = fields if isinstance(fields, list) else [fields]
|
|
|
|
metrics_dict[index].json_obj = {'fieldNames': dependents}
|
|
|
|
|
|
|
|
depends_on('A', ['m1', 'D', 'C'])
|
|
|
|
depends_on('B', ['B', 'C', 'E', 'F', 'm3'])
|
|
|
|
depends_on('C', ['H', 'I'])
|
|
|
|
depends_on('D', ['m2', 'm5', 'G', 'C'])
|
|
|
|
depends_on('E', ['H', 'I', 'J'])
|
|
|
|
depends_on('F', ['J', 'm5'])
|
|
|
|
depends_on('G', ['m4', 'm7', 'm6', 'A'])
|
|
|
|
depends_on('H', ['A', 'm4', 'I'])
|
|
|
|
depends_on('I', ['H', 'K'])
|
|
|
|
depends_on('J', 'K')
|
|
|
|
depends_on('K', ['m8', 'm9'])
|
|
|
|
all_metrics, postaggs = DruidDatasource.metrics_and_post_aggs(
|
|
|
|
metrics, metrics_dict)
|
|
|
|
expected_metrics = set(all_metrics)
|
|
|
|
self.assertEqual(9, len(all_metrics))
|
|
|
|
for i in range(1, 10):
|
|
|
|
expected_metrics.remove('m' + str(i))
|
|
|
|
self.assertEqual(0, len(expected_metrics))
|
|
|
|
self.assertEqual(11, len(postaggs))
|
|
|
|
for i in range(ord('A'), ord('K') + 1):
|
|
|
|
del postaggs[chr(i)]
|
|
|
|
self.assertEqual(0, len(postaggs))
|
|
|
|
|
|
|
|
def test_metrics_and_post_aggs(self):
|
|
|
|
"""
|
|
|
|
Test generation of metrics and post-aggregations from an initial list
|
|
|
|
of superset metrics (which may include the results of either). This
|
|
|
|
primarily tests that specifying a post-aggregator metric will also
|
|
|
|
require the raw aggregation of the associated druid metric column.
|
|
|
|
"""
|
|
|
|
metrics_dict = {
|
|
|
|
'unused_count': DruidMetric(
|
|
|
|
metric_name='unused_count',
|
|
|
|
verbose_name='COUNT(*)',
|
|
|
|
metric_type='count',
|
|
|
|
json=json.dumps({'type': 'count', 'name': 'unused_count'}),
|
|
|
|
),
|
|
|
|
'some_sum': DruidMetric(
|
|
|
|
metric_name='some_sum',
|
|
|
|
verbose_name='SUM(*)',
|
|
|
|
metric_type='sum',
|
|
|
|
json=json.dumps({'type': 'sum', 'name': 'sum'}),
|
|
|
|
),
|
|
|
|
'a_histogram': DruidMetric(
|
|
|
|
metric_name='a_histogram',
|
|
|
|
verbose_name='APPROXIMATE_HISTOGRAM(*)',
|
|
|
|
metric_type='approxHistogramFold',
|
|
|
|
json=json.dumps(
|
|
|
|
{'type': 'approxHistogramFold', 'name': 'a_histogram'},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
'aCustomMetric': DruidMetric(
|
|
|
|
metric_name='aCustomMetric',
|
|
|
|
verbose_name='MY_AWESOME_METRIC(*)',
|
|
|
|
metric_type='aCustomType',
|
|
|
|
json=json.dumps(
|
|
|
|
{'type': 'customMetric', 'name': 'aCustomMetric'},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
'quantile_p95': DruidMetric(
|
|
|
|
metric_name='quantile_p95',
|
|
|
|
verbose_name='P95(*)',
|
|
|
|
metric_type='postagg',
|
|
|
|
json=json.dumps({
|
|
|
|
'type': 'quantile',
|
|
|
|
'probability': 0.95,
|
|
|
|
'name': 'p95',
|
|
|
|
'fieldName': 'a_histogram',
|
|
|
|
}),
|
|
|
|
),
|
|
|
|
'aCustomPostAgg': DruidMetric(
|
|
|
|
metric_name='aCustomPostAgg',
|
|
|
|
verbose_name='CUSTOM_POST_AGG(*)',
|
|
|
|
metric_type='postagg',
|
|
|
|
json=json.dumps({
|
|
|
|
'type': 'customPostAgg',
|
|
|
|
'name': 'aCustomPostAgg',
|
|
|
|
'field': {
|
|
|
|
'type': 'fieldAccess',
|
|
|
|
'fieldName': 'aCustomMetric',
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
),
|
|
|
|
}
|
|
|
|
|
|
|
|
metrics = ['some_sum']
|
|
|
|
all_metrics, post_aggs = DruidDatasource.metrics_and_post_aggs(
|
|
|
|
metrics, metrics_dict)
|
|
|
|
|
|
|
|
assert all_metrics == ['some_sum']
|
|
|
|
assert post_aggs == {}
|
|
|
|
|
|
|
|
metrics = ['quantile_p95']
|
|
|
|
all_metrics, post_aggs = DruidDatasource.metrics_and_post_aggs(
|
|
|
|
metrics, metrics_dict)
|
|
|
|
|
|
|
|
result_postaggs = set(['quantile_p95'])
|
|
|
|
assert all_metrics == ['a_histogram']
|
|
|
|
assert set(post_aggs.keys()) == result_postaggs
|
|
|
|
|
|
|
|
metrics = ['aCustomPostAgg']
|
|
|
|
all_metrics, post_aggs = DruidDatasource.metrics_and_post_aggs(
|
|
|
|
metrics, metrics_dict)
|
|
|
|
|
|
|
|
result_postaggs = set(['aCustomPostAgg'])
|
|
|
|
assert all_metrics == ['aCustomMetric']
|
|
|
|
assert set(post_aggs.keys()) == result_postaggs
|