Skip to content

Commit

Permalink
[#2562] careful with crosstabs
Browse files Browse the repository at this point in the history
  • Loading branch information
wardi committed Nov 21, 2017
1 parent ccec46e commit 6a359af
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 20 deletions.
2 changes: 1 addition & 1 deletion ckanext/datastore/backend/postgres.py
Expand Up @@ -1497,7 +1497,7 @@ def search_sql(context, data_dict):
})

def _remove_explain(msg):
return (msg.replace('EXPLAIN (FORMAT JSON) ', '')
return (msg.replace('EXPLAIN (VERBOSE, FORMAT JSON) ', '')
.replace('EXPLAIN ', ''))

raise ValidationError({
Expand Down
70 changes: 51 additions & 19 deletions ckanext/datastore/helpers.py
Expand Up @@ -84,36 +84,68 @@ def get_table_names_from_sql(context, sql):
:rtype: list of strings
'''

def _get_table_names_from_plan(plan):
queries = [sql]
table_names = []

while queries:
sql = queries.pop()
result = context['connection'].execute(
'EXPLAIN (VERBOSE, FORMAT JSON) {0}'.format(
sql.encode('utf-8'))).fetchone()

try:
query_plan = json.loads(result['QUERY PLAN'])
plan = query_plan[0]['Plan']

table_names = []
t, q = _get_table_names_queries_from_plan(plan)
table_names.extend(t)
queries.extend(q)

if plan.get('Relation Name'):
table_names.append(plan['Relation Name'])
except ValueError:
log.error('Could not parse query plan')
raise

if 'Plans' in plan:
for child_plan in plan['Plans']:
table_name = _get_table_names_from_plan(child_plan)
if table_name:
table_names.extend(table_name)
return table_names

return table_names

result = context['connection'].execute(
'EXPLAIN (FORMAT JSON) {0}'.format(sql.encode('utf-8'))).fetchone()
def _get_table_names_queries_from_plan(plan):

table_names = []
queries = []

try:
query_plan = json.loads(result['QUERY PLAN'])
plan = query_plan[0]['Plan']
if plan.get('Relation Name'):
table_names.append(plan['Relation Name'])

table_names.extend(_get_table_names_from_plan(plan))
if 'Function Name' in plan and plan['Function Name'].startswith(
'crosstab'):
try:
queries.append(_get_subquery_from_crosstab_call(
plan['Function Call']))
except ValueError:
table_names.append('_unknown_crosstab_sql')

except ValueError:
log.error('Could not parse query plan')
if 'Plans' in plan:
for child_plan in plan['Plans']:
t, q = _get_table_names_queries_from_plan(child_plan)
table_names.extend(t)
queries.extend(q)

return table_names
return table_names, queries


def _get_subquery_from_crosstab_call(ct):
"""
Crosstabs are a useful feature some sites choose to enable on
their datastore databases. To support the sql parameter passed
safely we accept only the simple crosstab(text) form where text
is a literal SQL string, otherwise raise ValueError
"""
if not ct.startswith("crosstab('") or not ct.endswith("'::text)"):
raise ValueError('only simple crosstab calls supported')
ct = ct[10:-8]
if "'" in ct.replace("''", ""):
raise ValueError('only escaped single quotes allowed in query')
return ct.replace("''", "'")


def datastore_dictionary(resource_id):
Expand Down

0 comments on commit 6a359af

Please sign in to comment.