Skip to content

Commit e93385e

Browse files
Allen Shortjezdez
authored andcommitted
Aggregate query results (re getredash#35) (getredash#339)
1 parent 8c176cb commit e93385e

File tree

16 files changed

+294
-15
lines changed

16 files changed

+294
-15
lines changed

client/app/components/queries/schedule-dialog.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@ <h4 class="modal-title">Refresh Schedule</h4>
1919
Stop scheduling at date/time (format yyyy-MM-ddTHH:mm:ss, like 2016-12-28T14:57:00):
2020
<schedule-until query="$ctrl.query" save-query="$ctrl.saveQuery"></schedule-until>
2121
</label>
22+
<label>
23+
Number of query results to keep <schedule-keep-results query="$ctrl.query" save-query="$ctrl.saveQuery"></schedule-keep-results>
24+
</label>
2225
</div>

client/app/components/queries/schedule-dialog.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,21 @@ function scheduleUntil() {
114114
};
115115
}
116116

117+
function scheduleKeepResults() {
118+
return {
119+
restrict: 'E',
120+
scope: {
121+
query: '=',
122+
saveQuery: '=',
123+
},
124+
template: '<input type="number" class="form-control" ng-model="query.schedule_resultset_size" ng-change="saveQuery()" ng-disabled="!query.schedule">',
125+
};
126+
}
127+
117128
const ScheduleForm = {
118129
controller() {
119130
this.query = this.resolve.query;
120131
this.saveQuery = this.resolve.saveQuery;
121-
122132
if (this.query.hasDailySchedule()) {
123133
this.refreshType = 'daily';
124134
} else {
@@ -137,5 +147,6 @@ export default function init(ngModule) {
137147
ngModule.directive('queryTimePicker', queryTimePicker);
138148
ngModule.directive('queryRefreshSelect', queryRefreshSelect);
139149
ngModule.directive('scheduleUntil', scheduleUntil);
150+
ngModule.directive('scheduleKeepResults', scheduleKeepResults);
140151
ngModule.component('scheduleDialog', ScheduleForm);
141152
}

client/app/pages/alerts-list/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const stateClass = {
99

1010
class AlertsListCtrl {
1111
constructor(Alert) {
12-
1312
this.showEmptyState = false;
1413
this.showList = false;
1514

client/app/pages/queries/view.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ function QueryViewCtrl(
205205
} else {
206206
request = pick($scope.query, [
207207
'schedule',
208+
'schedule_resultset_size',
208209
'query',
209210
'id',
210211
'description',

client/app/services/query-result.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ function addPointToSeries(point, seriesCollection, seriesName) {
5454

5555
function QueryResultService($resource, $timeout, $q, QueryResultError) {
5656
const QueryResultResource = $resource('api/query_results/:id', { id: '@id' }, { post: { method: 'POST' } });
57+
const QueryResultSetResource = $resource('api/queries/:id/resultset', { id: '@id' });
5758
const Job = $resource('api/jobs/:id', { id: '@id' });
5859
const statuses = {
5960
1: 'waiting',
@@ -452,6 +453,15 @@ function QueryResultService($resource, $timeout, $q, QueryResultError) {
452453
return queryResult;
453454
}
454455

456+
static getResultSet(queryId) {
457+
const queryResult = new QueryResult();
458+
459+
QueryResultSetResource.get({ id: queryId }, (response) => {
460+
queryResult.update(response);
461+
});
462+
463+
return queryResult;
464+
}
455465
loadResult(tryCount) {
456466
this.isLoadingResult = true;
457467
QueryResultResource.get(

client/app/services/query.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,11 @@ function QueryResource(
449449
this.latest_query_data_id = null;
450450
}
451451

452-
if (this.latest_query_data && maxAge !== 0) {
452+
if (this.schedule_resultset_size) {
453+
if (!this.queryResult) {
454+
this.queryResult = QueryResult.getResultSet(this.id);
455+
}
456+
} else if (this.latest_query_data && maxAge !== 0) {
453457
if (!this.queryResult) {
454458
this.queryResult = new QueryResult({
455459
query_result: this.latest_query_data,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Incremental query results aggregation
2+
3+
Revision ID: 9d7678c47452
4+
Revises: 15041b7085fe
5+
Create Date: 2018-03-08 04:36:12.802199
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '9d7678c47452'
14+
down_revision = '15041b7085fe'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
op.create_table('query_resultsets',
21+
sa.Column('query_id', sa.Integer(), nullable=False),
22+
sa.Column('result_id', sa.Integer(), nullable=False),
23+
sa.ForeignKeyConstraint(['query_id'], ['queries.id'], ),
24+
sa.ForeignKeyConstraint(['result_id'], ['query_results.id'], ),
25+
sa.PrimaryKeyConstraint('query_id', 'result_id')
26+
)
27+
op.add_column(u'queries', sa.Column('schedule_resultset_size', sa.Integer(), nullable=True))
28+
1
29+
30+
def downgrade():
31+
# ### commands auto generated by Alembic - please adjust! ###
32+
op.drop_column(u'queries', 'schedule_resultset_size')
33+
op.drop_table('query_resultsets')
34+
# ### end Alembic commands ###

redash/handlers/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource, DataSourceVersionResource
1111
from redash.handlers.events import EventsResource
1212
from redash.handlers.queries import QueryForkResource, QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource, QueryVersionListResource, ChangeResource
13-
from redash.handlers.query_results import QueryResultListResource, QueryResultResource, JobResource
13+
from redash.handlers.query_results import QueryResultListResource, QueryResultResource, JobResource, QueryResultSetResource
1414
from redash.handlers.users import UserResource, UserListResource, UserInviteResource, UserResetPasswordResource, UserDisableResource
1515
from redash.handlers.visualizations import VisualizationListResource
1616
from redash.handlers.visualizations import VisualizationResource
@@ -85,6 +85,7 @@ def json_representation(data, code, headers=None):
8585
api.add_org_resource(QueryRefreshResource, '/api/queries/<query_id>/refresh', endpoint='query_refresh')
8686
api.add_org_resource(QueryResource, '/api/queries/<query_id>', endpoint='query')
8787
api.add_org_resource(QueryForkResource, '/api/queries/<query_id>/fork', endpoint='query_fork')
88+
api.add_org_resource(QueryResultSetResource, '/api/queries/<query_id>/resultset', endpoint='query_aggregate_results')
8889
api.add_org_resource(QueryVersionListResource, '/api/queries/<query_id>/version', endpoint='query_versions')
8990
api.add_org_resource(ChangeResource, '/api/changes/<change_id>', endpoint='changes')
9091

redash/handlers/queries.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def post(self):
109109
:<json string description:
110110
:<json string schedule: Schedule interval, in seconds, for repeated execution of this query
111111
:<json string schedule_until: Time in ISO format to stop scheduling this query (may be null to run indefinitely)
112+
:<json number schedule_resultset_size: Number of result sets to keep (null to keep only one)
112113
:<json object options: Query options
113114
114115
.. _query-response-label:
@@ -146,6 +147,8 @@ def post(self):
146147
query_def['data_source'] = data_source
147148
query_def['org'] = self.current_org
148149
query_def['is_draft'] = True
150+
if query_def.get('schedule_resultset_size') == 1:
151+
query_def['schedule_resultset_size'] = None
149152
query = models.Query.create(**query_def)
150153
query.record_changes(changed_by=self.current_user)
151154
models.db.session.add(query)

redash/handlers/query_results.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,33 @@ def post(self):
132132
ONE_YEAR = 60 * 60 * 24 * 365.25
133133

134134

135+
class QueryResultSetResource(BaseResource):
136+
@require_permission('view_query')
137+
def get(self, query_id=None, filetype='json'):
138+
query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org)
139+
if not query.schedule_resultset_size:
140+
abort(404, message="query does not keep multiple results")
141+
142+
# Synthesize a result set from the last N results.
143+
total = len(query.query_results)
144+
offset = max(total - query.schedule_resultset_size, 0)
145+
results = [qr.to_dict() for qr in query.query_results[offset:]]
146+
if not results:
147+
aggregate_result = {}
148+
else:
149+
# Start a synthetic data set with the data from the first result...
150+
aggregate_result = results[0].copy()
151+
aggregate_result['data'] = {'columns': results[0]['data']['columns'],
152+
'rows': []}
153+
# .. then add each subsequent result set into it.
154+
for r in results:
155+
aggregate_result['data']['rows'].extend(r['data']['rows'])
156+
157+
data = json.dumps({'query_result': aggregate_result}, cls=utils.JSONEncoder)
158+
headers = {'Content-Type': "application/json"}
159+
return make_response(data, 200, headers)
160+
161+
135162
class QueryResultResource(BaseResource):
136163
@staticmethod
137164
def add_cors_headers(headers):
@@ -194,7 +221,7 @@ def get(self, query_id=None, query_result_id=None, filetype='json'):
194221
query_result = run_query_sync(query.data_source, parameter_values, query.query_text, max_age=max_age)
195222
elif query.latest_query_data_id is not None:
196223
query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query.latest_query_data_id, self.current_org)
197-
224+
198225
if query is not None and query_result is not None and self.current_user.is_api_user():
199226
if query.query_hash != query_result.query_hash:
200227
abort(404, message='No cached result found for this query.')

0 commit comments

Comments
 (0)