2019-05-06 13:21:02 -04:00
|
|
|
# Licensed to the Apache Software Foundation (ASF) under one
|
|
|
|
# or more contributor license agreements. See the NOTICE file
|
|
|
|
# distributed with this work for additional information
|
|
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
|
|
# to you under the Apache License, Version 2.0 (the
|
|
|
|
# "License"); you may not use this file except in compliance
|
|
|
|
# with the License. You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing,
|
|
|
|
# software distributed under the License is distributed on an
|
|
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
|
|
# KIND, either express or implied. See the License for the
|
|
|
|
# specific language governing permissions and limitations
|
|
|
|
# under the License.
|
2020-02-06 19:45:37 -05:00
|
|
|
# isort:skip_file
|
2019-05-06 13:21:02 -04:00
|
|
|
"""Unit tests for Sql Lab"""
|
|
|
|
import unittest
|
2019-06-25 16:34:48 -04:00
|
|
|
from unittest.mock import MagicMock, patch
|
2019-05-06 13:21:02 -04:00
|
|
|
|
2020-08-27 12:49:18 -04:00
|
|
|
import pytest
|
2019-05-06 13:21:02 -04:00
|
|
|
from pyhive.exc import DatabaseError
|
|
|
|
|
|
|
|
from superset import app
|
|
|
|
from superset.sql_validators import SQLValidationAnnotation
|
|
|
|
from superset.sql_validators.base import BaseSQLValidator
|
2020-12-04 22:17:23 -05:00
|
|
|
from superset.sql_validators.postgres import PostgreSQLValidator
|
2019-05-06 13:21:02 -04:00
|
|
|
from superset.sql_validators.presto_db import (
|
|
|
|
PrestoDBSQLValidator,
|
|
|
|
PrestoSQLValidationError,
|
|
|
|
)
|
2020-08-27 12:49:18 -04:00
|
|
|
from superset.utils.core import get_example_database
|
2019-10-18 17:44:27 -04:00
|
|
|
|
2019-05-06 13:21:02 -04:00
|
|
|
from .base_tests import SupersetTestCase
|
|
|
|
|
2021-11-19 10:56:16 -05:00
|
|
|
PRESTO_SQL_VALIDATORS_BY_ENGINE = {
|
|
|
|
"presto": "PrestoDBSQLValidator",
|
|
|
|
"sqlite": "PrestoDBSQLValidator",
|
|
|
|
"postgresql": "PrestoDBSQLValidator",
|
|
|
|
"mysql": "PrestoDBSQLValidator",
|
2019-05-06 13:21:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-06-29 18:36:06 -04:00
|
|
|
class TestSqlValidatorEndpoint(SupersetTestCase):
|
2019-05-06 13:21:02 -04:00
|
|
|
"""Testing for Sql Lab querytext validation endpoint"""
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
self.logout()
|
|
|
|
|
|
|
|
def test_validate_sql_endpoint_noconfig(self):
|
|
|
|
"""Assert that validate_sql_json errors out when no validators are
|
|
|
|
configured for any db"""
|
2019-06-25 16:34:48 -04:00
|
|
|
self.login("admin")
|
2019-05-06 13:21:02 -04:00
|
|
|
|
2019-06-25 16:34:48 -04:00
|
|
|
app.config["SQL_VALIDATORS_BY_ENGINE"] = {}
|
2019-05-06 13:21:02 -04:00
|
|
|
|
|
|
|
resp = self.validate_sql(
|
2019-09-08 13:18:09 -04:00
|
|
|
"SELECT * FROM birth_names", client_id="1", raise_on_error=False
|
2019-05-06 13:21:02 -04:00
|
|
|
)
|
2019-06-25 16:34:48 -04:00
|
|
|
self.assertIn("error", resp)
|
|
|
|
self.assertIn("no SQL validator is configured", resp["error"])
|
2019-05-06 13:21:02 -04:00
|
|
|
|
2019-06-25 16:34:48 -04:00
|
|
|
@patch("superset.views.core.get_validator_by_name")
|
2019-11-20 10:47:06 -05:00
|
|
|
@patch.dict(
|
2021-11-19 10:56:16 -05:00
|
|
|
"superset.config.SQL_VALIDATORS_BY_ENGINE",
|
|
|
|
PRESTO_SQL_VALIDATORS_BY_ENGINE,
|
2019-11-20 10:47:06 -05:00
|
|
|
clear=True,
|
|
|
|
)
|
2019-05-06 13:21:02 -04:00
|
|
|
def test_validate_sql_endpoint_mocked(self, get_validator_by_name):
|
|
|
|
"""Assert that, with a mocked validator, annotations make it back out
|
|
|
|
from the validate_sql_json endpoint as a list of json dictionaries"""
|
2020-08-27 12:49:18 -04:00
|
|
|
if get_example_database().backend == "hive":
|
|
|
|
pytest.skip("Hive validator is not implemented")
|
2019-06-25 16:34:48 -04:00
|
|
|
self.login("admin")
|
2019-05-06 13:21:02 -04:00
|
|
|
|
|
|
|
validator = MagicMock()
|
|
|
|
get_validator_by_name.return_value = validator
|
|
|
|
validator.validate.return_value = [
|
|
|
|
SQLValidationAnnotation(
|
|
|
|
message="I don't know what I expected, but it wasn't this",
|
|
|
|
line_number=4,
|
|
|
|
start_column=12,
|
|
|
|
end_column=42,
|
2019-06-25 16:34:48 -04:00
|
|
|
)
|
2019-05-06 13:21:02 -04:00
|
|
|
]
|
|
|
|
|
|
|
|
resp = self.validate_sql(
|
2019-06-25 16:34:48 -04:00
|
|
|
"SELECT * FROM somewhere_over_the_rainbow",
|
|
|
|
client_id="1",
|
2019-05-06 13:21:02 -04:00
|
|
|
raise_on_error=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assertEqual(1, len(resp))
|
2019-06-25 16:34:48 -04:00
|
|
|
self.assertIn("expected,", resp[0]["message"])
|
2019-05-06 13:21:02 -04:00
|
|
|
|
2019-06-25 16:34:48 -04:00
|
|
|
@patch("superset.views.core.get_validator_by_name")
|
2019-11-20 10:47:06 -05:00
|
|
|
@patch.dict(
|
2021-11-19 10:56:16 -05:00
|
|
|
"superset.config.SQL_VALIDATORS_BY_ENGINE",
|
|
|
|
PRESTO_SQL_VALIDATORS_BY_ENGINE,
|
2019-11-20 10:47:06 -05:00
|
|
|
clear=True,
|
|
|
|
)
|
2019-05-06 13:21:02 -04:00
|
|
|
def test_validate_sql_endpoint_failure(self, get_validator_by_name):
|
|
|
|
"""Assert that validate_sql_json errors out when the selected validator
|
|
|
|
raises an unexpected exception"""
|
2019-06-25 16:34:48 -04:00
|
|
|
self.login("admin")
|
2019-05-06 13:21:02 -04:00
|
|
|
|
|
|
|
validator = MagicMock()
|
|
|
|
get_validator_by_name.return_value = validator
|
2019-06-25 16:34:48 -04:00
|
|
|
validator.validate.side_effect = Exception("Kaboom!")
|
2019-05-06 13:21:02 -04:00
|
|
|
|
|
|
|
resp = self.validate_sql(
|
2019-09-08 13:18:09 -04:00
|
|
|
"SELECT * FROM birth_names", client_id="1", raise_on_error=False
|
2019-05-06 13:21:02 -04:00
|
|
|
)
|
2020-08-27 12:49:18 -04:00
|
|
|
# TODO(bkyryliuk): properly handle hive error
|
|
|
|
if get_example_database().backend == "hive":
|
|
|
|
assert resp["error"] == "no SQL validator is configured for hive"
|
|
|
|
else:
|
|
|
|
self.assertIn("error", resp)
|
|
|
|
self.assertIn("Kaboom!", resp["error"])
|
2019-05-06 13:21:02 -04:00
|
|
|
|
|
|
|
|
2020-06-29 18:36:06 -04:00
|
|
|
class TestBaseValidator(SupersetTestCase):
|
2019-05-06 13:21:02 -04:00
|
|
|
"""Testing for the base sql validator"""
|
2019-06-25 16:34:48 -04:00
|
|
|
|
2019-05-06 13:21:02 -04:00
|
|
|
def setUp(self):
|
|
|
|
self.validator = BaseSQLValidator
|
|
|
|
|
|
|
|
def test_validator_excepts(self):
|
|
|
|
with self.assertRaises(NotImplementedError):
|
|
|
|
self.validator.validate(None, None, None)
|
|
|
|
|
|
|
|
|
2020-06-29 18:36:06 -04:00
|
|
|
class TestPrestoValidator(SupersetTestCase):
|
2019-05-06 13:21:02 -04:00
|
|
|
"""Testing for the prestodb sql validator"""
|
2019-06-25 16:34:48 -04:00
|
|
|
|
2019-05-06 13:21:02 -04:00
|
|
|
def setUp(self):
|
|
|
|
self.validator = PrestoDBSQLValidator
|
2019-10-18 17:44:27 -04:00
|
|
|
self.database = MagicMock()
|
2019-05-06 13:21:02 -04:00
|
|
|
self.database_engine = self.database.get_sqla_engine.return_value
|
|
|
|
self.database_conn = self.database_engine.raw_connection.return_value
|
|
|
|
self.database_cursor = self.database_conn.cursor.return_value
|
|
|
|
self.database_cursor.poll.return_value = None
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
self.logout()
|
|
|
|
|
|
|
|
PRESTO_ERROR_TEMPLATE = {
|
2019-06-25 16:34:48 -04:00
|
|
|
"errorLocation": {"lineNumber": 10, "columnNumber": 20},
|
|
|
|
"message": "your query isn't how I like it",
|
2019-05-06 13:21:02 -04:00
|
|
|
}
|
|
|
|
|
2019-06-25 16:34:48 -04:00
|
|
|
@patch("superset.sql_validators.presto_db.g")
|
2019-05-06 13:21:02 -04:00
|
|
|
def test_validator_success(self, flask_g):
|
2019-06-25 16:34:48 -04:00
|
|
|
flask_g.user.username = "nobody"
|
|
|
|
sql = "SELECT 1 FROM default.notarealtable"
|
|
|
|
schema = "default"
|
2019-05-06 13:21:02 -04:00
|
|
|
|
|
|
|
errors = self.validator.validate(sql, schema, self.database)
|
|
|
|
|
|
|
|
self.assertEqual([], errors)
|
|
|
|
|
2019-06-25 16:34:48 -04:00
|
|
|
@patch("superset.sql_validators.presto_db.g")
|
2019-05-06 13:21:02 -04:00
|
|
|
def test_validator_db_error(self, flask_g):
|
2019-06-25 16:34:48 -04:00
|
|
|
flask_g.user.username = "nobody"
|
|
|
|
sql = "SELECT 1 FROM default.notarealtable"
|
|
|
|
schema = "default"
|
2019-05-06 13:21:02 -04:00
|
|
|
|
|
|
|
fetch_fn = self.database.db_engine_spec.fetch_data
|
2019-06-25 16:34:48 -04:00
|
|
|
fetch_fn.side_effect = DatabaseError("dummy db error")
|
2019-05-06 13:21:02 -04:00
|
|
|
|
|
|
|
with self.assertRaises(PrestoSQLValidationError):
|
|
|
|
self.validator.validate(sql, schema, self.database)
|
|
|
|
|
2019-06-25 16:34:48 -04:00
|
|
|
@patch("superset.sql_validators.presto_db.g")
|
2019-05-06 13:21:02 -04:00
|
|
|
def test_validator_unexpected_error(self, flask_g):
|
2019-06-25 16:34:48 -04:00
|
|
|
flask_g.user.username = "nobody"
|
|
|
|
sql = "SELECT 1 FROM default.notarealtable"
|
|
|
|
schema = "default"
|
2019-05-06 13:21:02 -04:00
|
|
|
|
|
|
|
fetch_fn = self.database.db_engine_spec.fetch_data
|
2019-06-25 16:34:48 -04:00
|
|
|
fetch_fn.side_effect = Exception("a mysterious failure")
|
2019-05-06 13:21:02 -04:00
|
|
|
|
|
|
|
with self.assertRaises(Exception):
|
|
|
|
self.validator.validate(sql, schema, self.database)
|
|
|
|
|
2019-06-25 16:34:48 -04:00
|
|
|
@patch("superset.sql_validators.presto_db.g")
|
2019-05-06 13:21:02 -04:00
|
|
|
def test_validator_query_error(self, flask_g):
|
2019-06-25 16:34:48 -04:00
|
|
|
flask_g.user.username = "nobody"
|
|
|
|
sql = "SELECT 1 FROM default.notarealtable"
|
|
|
|
schema = "default"
|
2019-05-06 13:21:02 -04:00
|
|
|
|
|
|
|
fetch_fn = self.database.db_engine_spec.fetch_data
|
|
|
|
fetch_fn.side_effect = DatabaseError(self.PRESTO_ERROR_TEMPLATE)
|
|
|
|
|
|
|
|
errors = self.validator.validate(sql, schema, self.database)
|
|
|
|
|
|
|
|
self.assertEqual(1, len(errors))
|
|
|
|
|
|
|
|
def test_validate_sql_endpoint(self):
|
2019-06-25 16:34:48 -04:00
|
|
|
self.login("admin")
|
2019-05-06 13:21:02 -04:00
|
|
|
# NB this is effectively an integration test -- when there's a default
|
|
|
|
# validator for sqlite, this test will fail because the validator
|
|
|
|
# will no longer error out.
|
|
|
|
resp = self.validate_sql(
|
2019-09-08 13:18:09 -04:00
|
|
|
"SELECT * FROM birth_names", client_id="1", raise_on_error=False
|
2019-05-06 13:21:02 -04:00
|
|
|
)
|
2019-06-25 16:34:48 -04:00
|
|
|
self.assertIn("error", resp)
|
|
|
|
self.assertIn("no SQL validator is configured", resp["error"])
|
2019-05-06 13:21:02 -04:00
|
|
|
|
|
|
|
|
2020-12-04 22:17:23 -05:00
|
|
|
class TestPostgreSQLValidator(SupersetTestCase):
|
|
|
|
def test_valid_syntax(self):
|
|
|
|
if get_example_database().backend != "postgresql":
|
|
|
|
return
|
|
|
|
|
|
|
|
mock_database = MagicMock()
|
|
|
|
annotations = PostgreSQLValidator.validate(
|
|
|
|
sql='SELECT 1, "col" FROM "table"', schema="", database=mock_database
|
|
|
|
)
|
|
|
|
assert annotations == []
|
|
|
|
|
|
|
|
def test_invalid_syntax(self):
|
|
|
|
if get_example_database().backend != "postgresql":
|
|
|
|
return
|
|
|
|
|
|
|
|
mock_database = MagicMock()
|
|
|
|
annotations = PostgreSQLValidator.validate(
|
|
|
|
sql='SELECT 1, "col"\nFROOM "table"', schema="", database=mock_database
|
|
|
|
)
|
|
|
|
|
|
|
|
assert len(annotations) == 1
|
|
|
|
annotation = annotations[0]
|
|
|
|
assert annotation.line_number == 2
|
|
|
|
assert annotation.start_column is None
|
|
|
|
assert annotation.end_column is None
|
|
|
|
assert annotation.message == 'ERROR: syntax error at or near """'
|
|
|
|
|
|
|
|
|
2019-06-25 16:34:48 -04:00
|
|
|
if __name__ == "__main__":
|
2019-05-06 13:21:02 -04:00
|
|
|
unittest.main()
|