Skip to content

Commit 121a44e

Browse files
Omer Lachisharikfr
Omer Lachish
authored andcommitted
Remove tree validations and introduce ParameterizedQuery (getredash#3230)
1 parent 823e4cc commit 121a44e

10 files changed

+120
-325
lines changed

redash/handlers/embed.py

+1-53
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,14 @@
11
from __future__ import absolute_import
2-
import logging
3-
import time
42

53
from flask import request
64

75
from .authentication import current_org
86
from flask_login import current_user, login_required
9-
from flask_restful import abort
10-
from redash import models, utils
7+
from redash import models
118
from redash.handlers import routes
129
from redash.handlers.base import (get_object_or_404, org_scoped_rule,
1310
record_event)
14-
from redash.utils import find_missing_params
1511
from redash.handlers.static import render_index
16-
from redash.utils import gen_query_hash, mustache_render
17-
18-
19-
#
20-
# Run a parameterized query synchronously and return the result
21-
# DISCLAIMER: Temporary solution to support parameters in queries. Should be
22-
# removed once we refactor the query results API endpoints and handling
23-
# on the client side. Please don't reuse in other API handlers.
24-
#
25-
def run_query_sync(data_source, parameter_values, query_text, max_age=0):
26-
missing_params = find_missing_params(query_text, parameter_values)
27-
if missing_params:
28-
raise Exception('Missing parameter value for: {}'.format(", ".join(missing_params)))
29-
30-
query_text = mustache_render(query_text, parameter_values)
31-
32-
if max_age <= 0:
33-
query_result = None
34-
else:
35-
query_result = models.QueryResult.get_latest(data_source, query_text, max_age)
36-
37-
query_hash = gen_query_hash(query_text)
38-
39-
if query_result:
40-
logging.info("Returning cached result for query %s" % query_hash)
41-
return query_result.data
42-
43-
try:
44-
started_at = time.time()
45-
data, error = data_source.query_runner.run_query(query_text, current_user)
46-
47-
if error:
48-
return None
49-
# update cache
50-
if max_age > 0:
51-
run_time = time.time() - started_at
52-
query_result, updated_query_ids = models.QueryResult.store_result(data_source.org_id, data_source.id,
53-
query_hash, query_text, data,
54-
run_time, utils.utcnow())
55-
56-
models.db.session.commit()
57-
return data
58-
except Exception:
59-
if max_age > 0:
60-
abort(404, message="Unable to get result from the database, and no cached query result found.")
61-
else:
62-
abort(503, message="Unable to get result from the database.")
63-
return None
6412

6513

6614
@routes.route(org_scoped_rule('/embed/query/<query_id>/visualization/<visualization_id>'), methods=['GET'])

redash/handlers/query_results.py

+16-42
Original file line numberDiff line numberDiff line change
@@ -8,66 +8,42 @@
88
from redash.handlers.base import BaseResource, get_object_or_404
99
from redash.permissions import (has_access, not_view_only, require_access,
1010
require_permission, view_only)
11-
from redash.tasks import QueryTask, record_event
11+
from redash.tasks import QueryTask
1212
from redash.tasks.queries import enqueue_query
13-
from redash.utils import (collect_parameters_from_request, find_missing_params, gen_query_hash, json_dumps, utcnow)
14-
from redash.utils.sql_query import SQLInjectionError, SQLQuery
13+
from redash.utils import (collect_parameters_from_request, gen_query_hash, json_dumps, utcnow)
14+
from redash.utils.parameterized_query import ParameterizedQuery
1515

1616

1717
def error_response(message):
1818
return {'job': {'status': 4, 'error': message}}, 400
1919

2020

21-
def apply_parameters(template, parameters, data_source):
22-
query = SQLQuery(template).apply(parameters)
23-
24-
# for now we only log `SQLInjectionError` to detect false positives
25-
try:
26-
text = query.text
27-
except SQLInjectionError:
28-
record_event({
29-
'action': 'sql_injection',
30-
'object_type': 'query',
31-
'query': template,
32-
'parameters': parameters,
33-
'timestamp': time.time(),
34-
'org_id': data_source.org_id
35-
})
36-
except Exception as e:
37-
logging.info(u"Failed applying parameters for query %s: %s", gen_query_hash(query.query), e.message)
38-
finally:
39-
text = query.query
40-
41-
return text
42-
43-
4421
#
4522
# Run a parameterized query synchronously and return the result
4623
# DISCLAIMER: Temporary solution to support parameters in queries. Should be
4724
# removed once we refactor the query results API endpoints and handling
4825
# on the client side. Please don't reuse in other API handlers.
4926
#
5027
def run_query_sync(data_source, parameter_values, query_text, max_age=0):
51-
missing_params = find_missing_params(query_text, parameter_values)
52-
if missing_params:
53-
raise Exception('Missing parameter value for: {}'.format(", ".join(missing_params)))
28+
query = ParameterizedQuery(query_text).apply(parameter_values)
5429

55-
query_text = apply_parameters(query_text, parameter_values, data_source)
30+
if query.missing_params:
31+
raise Exception('Missing parameter value for: {}'.format(", ".join(query.missing_params)))
5632

5733
if max_age <= 0:
5834
query_result = None
5935
else:
60-
query_result = models.QueryResult.get_latest(data_source, query_text, max_age)
36+
query_result = models.QueryResult.get_latest(data_source, query.text, max_age)
6137

62-
query_hash = gen_query_hash(query_text)
38+
query_hash = gen_query_hash(query.text)
6339

6440
if query_result:
6541
logging.info("Returning cached result for query %s" % query_hash)
6642
return query_result
6743

6844
try:
6945
started_at = time.time()
70-
data, error = data_source.query_runner.run_query(query_text, current_user)
46+
data, error = data_source.query_runner.run_query(query.text, current_user)
7147

7248
if error:
7349
logging.info('got bak error')
@@ -76,9 +52,8 @@ def run_query_sync(data_source, parameter_values, query_text, max_age=0):
7652

7753
run_time = time.time() - started_at
7854
query_result, updated_query_ids = models.QueryResult.store_result(data_source.org_id, data_source,
79-
query_hash, query_text, data,
55+
query_hash, query.text, data,
8056
run_time, utcnow())
81-
8257
models.db.session.commit()
8358
return query_result
8459
except Exception as e:
@@ -90,10 +65,6 @@ def run_query_sync(data_source, parameter_values, query_text, max_age=0):
9065

9166

9267
def run_query(data_source, parameter_values, query_text, query_id, max_age=0):
93-
missing_params = find_missing_params(query_text, parameter_values)
94-
if missing_params:
95-
return error_response(u'Missing parameter value for: {}'.format(u", ".join(missing_params)))
96-
9768
if data_source.paused:
9869
if data_source.pause_reason:
9970
message = '{} is paused ({}). Please try later.'.format(data_source.name, data_source.pause_reason)
@@ -102,17 +73,20 @@ def run_query(data_source, parameter_values, query_text, query_id, max_age=0):
10273

10374
return error_response(message)
10475

105-
query_text = apply_parameters(query_text, parameter_values, data_source)
76+
query = ParameterizedQuery(query_text).apply(parameter_values)
77+
78+
if query.missing_params:
79+
return error_response(u'Missing parameter value for: {}'.format(u", ".join(query.missing_params)))
10680

10781
if max_age == 0:
10882
query_result = None
10983
else:
110-
query_result = models.QueryResult.get_latest(data_source, query_text, max_age)
84+
query_result = models.QueryResult.get_latest(data_source, query.text, max_age)
11185

11286
if query_result:
11387
return {'query_result': query_result.to_dict()}
11488
else:
115-
job = enqueue_query(query_text, data_source, current_user.id, metadata={"Username": current_user.email, "Query ID": query_id})
89+
job = enqueue_query(query.text, data_source, current_user.id, metadata={"Username": current_user.email, "Query ID": query_id})
11690
return {'job': job.to_dict()}
11791

11892

redash/query_runner/google_spreadsheets.py

+3
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ def request(self, *args, **kwargs):
124124

125125

126126
class GoogleSpreadsheet(BaseQueryRunner):
127+
def __init__(self, configuration):
128+
super(GoogleSpreadsheet, self).__init__(configuration)
129+
self.syntax = 'custom'
127130

128131
@classmethod
129132
def annotate_query(cls):

redash/query_runner/graphite.py

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def annotate_query(cls):
5555

5656
def __init__(self, configuration):
5757
super(Graphite, self).__init__(configuration)
58+
self.syntax = 'custom'
5859

5960
if "username" in self.configuration and self.configuration["username"]:
6061
self.auth = (self.configuration["username"], self.configuration["password"])

redash/utils/__init__.py

+1-36
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import pystache
1616
import pytz
1717
import simplejson
18-
from funcy import distinct, select_values
18+
from funcy import select_values
1919
from redash import settings
2020
from sqlalchemy.orm.query import Query
2121

@@ -167,41 +167,6 @@ def writerows(self, rows):
167167
self.writerow(row)
168168

169169

170-
def _collect_key_names(nodes):
171-
keys = []
172-
for node in nodes._parse_tree:
173-
if isinstance(node, pystache.parser._EscapeNode):
174-
keys.append(node.key)
175-
elif isinstance(node, pystache.parser._SectionNode):
176-
keys.append(node.key)
177-
keys.extend(_collect_key_names(node.parsed))
178-
179-
return distinct(keys)
180-
181-
182-
def collect_query_parameters(query):
183-
nodes = pystache.parse(query)
184-
keys = _collect_key_names(nodes)
185-
return keys
186-
187-
188-
def parameter_names(parameter_values):
189-
names = []
190-
for key, value in parameter_values.iteritems():
191-
if isinstance(value, dict):
192-
for inner_key in value.keys():
193-
names.append(u'{}.{}'.format(key, inner_key))
194-
else:
195-
names.append(key)
196-
197-
return names
198-
199-
200-
def find_missing_params(query_text, parameter_values):
201-
query_parameters = set(collect_query_parameters(query_text))
202-
return set(query_parameters) - set(parameter_names(parameter_values))
203-
204-
205170
def collect_parameters_from_request(args):
206171
parameters = {}
207172

redash/utils/parameterized_query.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import pystache
2+
from redash.utils import mustache_render
3+
from funcy import distinct
4+
5+
6+
def _collect_key_names(nodes):
7+
keys = []
8+
for node in nodes._parse_tree:
9+
if isinstance(node, pystache.parser._EscapeNode):
10+
keys.append(node.key)
11+
elif isinstance(node, pystache.parser._SectionNode):
12+
keys.append(node.key)
13+
keys.extend(_collect_key_names(node.parsed))
14+
15+
return distinct(keys)
16+
17+
18+
def _collect_query_parameters(query):
19+
nodes = pystache.parse(query)
20+
keys = _collect_key_names(nodes)
21+
return keys
22+
23+
24+
def _parameter_names(parameter_values):
25+
names = []
26+
for key, value in parameter_values.iteritems():
27+
if isinstance(value, dict):
28+
for inner_key in value.keys():
29+
names.append(u'{}.{}'.format(key, inner_key))
30+
else:
31+
names.append(key)
32+
33+
return names
34+
35+
36+
class ParameterizedQuery(object):
37+
def __init__(self, template):
38+
self.template = template
39+
self.query = template
40+
self.parameters = {}
41+
42+
def apply(self, parameters):
43+
self.parameters.update(parameters)
44+
self.query = mustache_render(self.template, self.parameters)
45+
return self
46+
47+
@property
48+
def missing_params(self):
49+
query_parameters = set(_collect_query_parameters(self.template))
50+
return set(query_parameters) - set(_parameter_names(self.parameters))
51+
52+
@property
53+
def text(self):
54+
return self.query

redash/utils/sql_query.py

-71
This file was deleted.

0 commit comments

Comments
 (0)