Files
superset2/superset/sql_validators/sqlite.py
2026-04-20 18:29:51 -04:00

143 lines
4.6 KiB
Python

# 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.
from __future__ import annotations
import logging
import re
import subprocess
from superset.models.core import Database
from superset.sql_validators.base import BaseSQLValidator, SQLValidationAnnotation
try:
from syntaqlite import get_binary_path
except ModuleNotFoundError:
get_binary_path = None
logger = logging.getLogger(__name__)
DIAGNOSTIC_RE = re.compile(
r"^(?:error|warning): (.+)\n"
r" --> .+?:(\d+):(\d+)\n"
r" +\|\n"
r"\d+ \| .+\n"
r" +\| +\^(~*)",
re.MULTILINE,
)
class SQLiteSQLValidator(BaseSQLValidator): # pylint: disable=too-few-public-methods
"""Validate SQL queries using the syntaqlite binary"""
name = "SQLiteSQLValidator"
@classmethod
def validate(
cls,
sql: str,
catalog: str | None,
schema: str | None,
database: Database,
) -> list[SQLValidationAnnotation]:
annotations: list[SQLValidationAnnotation] = []
if get_binary_path is None:
return [
SQLValidationAnnotation(
message=(
"syntaqlite is not installed. Install it with: "
'pip install "apache-superset[sqlite]"'
),
line_number=None,
start_column=None,
end_column=None,
)
]
try:
result = subprocess.run( # noqa: S603
[
get_binary_path(),
"--no-config",
"validate",
"--allow",
"schema",
],
input=sql,
capture_output=True,
text=True,
timeout=10,
)
except FileNotFoundError:
logger.warning("syntaqlite binary not found")
return [
SQLValidationAnnotation(
message=(
"syntaqlite binary not found. Ensure it is correctly installed "
'via "pip install \\"apache-superset[sqlite]\\"" and available '
"on the system PATH."
),
line_number=None,
start_column=None,
end_column=None,
)
]
except subprocess.TimeoutExpired:
logger.warning("syntaqlite timed out validating SQL")
return [
SQLValidationAnnotation(
message="SQL validation timed out — the query may be too complex.",
line_number=None,
start_column=None,
end_column=None,
)
]
if result.returncode == 0:
return annotations
output = (result.stderr or result.stdout).replace("\r\n", "\n")
for match in DIAGNOSTIC_RE.finditer(output):
message = match.group(1)
line_number = int(match.group(2))
start_column = int(match.group(3))
# The caret (^) plus tildes (~) span the error token
end_column = start_column + 1 + len(match.group(4))
annotations.append(
SQLValidationAnnotation(
message=message,
line_number=line_number,
start_column=start_column,
end_column=end_column,
)
)
# If we couldn't parse the output but got a non-zero exit, add a generic error
if not annotations and result.returncode != 0:
annotations.append(
SQLValidationAnnotation(
message=output.strip() or "SQL syntax validation failed",
line_number=None,
start_column=None,
end_column=None,
)
)
return annotations