feat: add validate-sql command to compile query SQL against the data lake - #808
feat: add validate-sql command to compile query SQL against the data lake#808arielkr256 wants to merge 3 commits into
Conversation
`make fmt` runs black/isort on the whole repo and was reformatting an in-project venv's site-packages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lake Adds a `validate-sql` command that validates the SQL of scheduled queries, saved queries, and SQL-based lookup tables by executing them with an EXPLAIN prefix via the executeDataLakeQuery / dataLakeQuery public API operations. EXPLAIN compiles the query in Snowflake without scanning any data, surfacing syntax errors and invalid table/column references that static analysis misses. Queries referencing tables that don't exist in the instance (e.g. log sources that aren't onboarded) fail validation when the query is enabled, are skipped when the query is disabled, and can be skipped unconditionally with --skip-missing-tables. Jinja macro library files and investigation templates with <PLACEHOLDER> markers are skipped. Closes #639 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR SummaryMedium Risk Overview The backend gains Also excludes Reviewed by Cursor Bugbot for commit 9c43e3f. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c84be6a. Configure here.
| try: | ||
| status_res = backend.get_data_lake_query(GetDataLakeQueryParams(id=execute_res.data.id)) | ||
| except BaseException as err: # pylint: disable=broad-except | ||
| return str(err) |
There was a problem hiding this comment.
Interrupt signals treated as SQL errors
Medium Severity
_validate_target catches BaseException around API calls, so KeyboardInterrupt and similar signals are converted into a per-query error string instead of aborting the command. A long validate-sql run may keep going after Ctrl+C and report a bogus FAIL for the current query.
Reviewed by Cursor Bugbot for commit c84be6a. Configure here.
| status_code=200, | ||
| data=GetDataLakeQueryResponse( | ||
| status=data.get("status", ""), | ||
| message=data.get("message", ""), |
There was a problem hiding this comment.
Null GraphQL fields crash parsing
Medium Severity
New data-lake helpers use res.data.get("executeDataLakeQuery", {}) and res.data.get("dataLakeQuery", {}). When GraphQL returns those fields as JSON null, .get yields None and the following .get("id") / .get("status") calls raise AttributeError instead of a controlled validation failure.
Reviewed by Cursor Bugbot for commit c84be6a. Configure here.
The backend routes queries with a `-- pragma: pantherflow` comment to the PantherFlow engine, which ignores the EXPLAIN prefix and executes the query for real. Skip them until the API exposes a compile-only validation path for PantherFlow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dekatzenel
left a comment
There was a problem hiding this comment.
Cursor comments look worth fixing, plus a couple of other notes. Can you add screenshots of the success and failure output to the top-level comment?
|
|
||
| if error is None: | ||
| print(f" {cli_output.success('PASS')} {target.analysis_id}") | ||
| elif _MISSING_TABLE_ERROR in error and (args.skip_missing_tables or not target.enabled): |
There was a problem hiding this comment.
The missing-table heuristic over-matches and silently hides real bugs.
_MISSING_TABLE_ERROR = "does not exist or not authorized" is a substring match, but Snowflake uses that exact phrasing for functions, views, stages, schemas — not just tables (e.g. Function 'FOO' does not exist or not authorized, Object 'DB.SCHEMA' does not exist or not authorized). So a query with a typo'd UDF name, a dropped view, or a wrong schema gets classified as a "missing table" and SKIPPED whenever the query is disabled (default) or --skip-missing-tables is set — instead of FAILing.
| continue | ||
|
|
||
| placeholder = _PLACEHOLDER_PATTERN.search(sql) | ||
| if placeholder: |
There was a problem hiding this comment.
Should these be counted in the summary?


Summary
Adds a
validate-sqlcommand that validates the SQL of scheduled queries, saved queries, and SQL-based lookup tables by compiling them against the Panther data lake. Each query is executed with anEXPLAINprefix via theexecuteDataLakeQuery/dataLakeQuerypublic API operations — Snowflake compiles the query without scanning any data (0 bytes scanned), surfacing syntax errors and invalid table/column references that static analysis (sqlfluff) misses.Closes #639
Behavior
scheduled_query/saved_queryspecs (QueryorSnowflakeQuery) andlookup_tablespecs with aQueryfield. Supports the standard--filter/--ignore-filesoptions.--skip-missing-tablesextends the skip to enabled queries, for repos that ship content for log sources the validating instance doesn't have.{% macro %}-only SQL, which expands to nothing standalone; queries that use macros validate fine since the backend expands templates server-side) and investigation templates with<PLACEHOLDER>markers.Live testing
Ran against panther-analysis (106 queries) on the threat-research instance (~3.5 min):
kubernetes_ioc_activity_query.ymlhas itsINNER JOINafter theWHEREclause — invalid SQL never caught by static analysis (filed as kubernetes_ioc_activity_query: invalid SQL — INNER JOIN placed after WHERE clause panther-analysis#2101).p_occurs_sinceand Jinja macros expand server-side underEXPLAIN.Implementation notes
.graphqlfiles +PublicAPIRequestsloaders + typed params/response dataclasses +Clientabstract methods.LambdaClientraises (unsupported), consistent with other public-API-only features.dataLakeQueryuntil a terminal status with a 60s per-query timeout.venvfrom black/isort somake fmtdoesn't reformat an in-project venv (separate commit).🤖 Generated with Claude Code