[SPARK-58738][SQL] Add parse_sql function for SQL statement JSON analysis - #57962
[SPARK-58738][SQL] Add parse_sql function for SQL statement JSON analysis#57962srielau wants to merge 9 commits into
Conversation
…rences UnresolvedWith keeps CTE definitions in innerChildren, so collectWithSubqueries missed tables/functions/params inside WITH. Add foreachPlanDeep and broader CTE and nested-subquery tests.
Classify CompoundBody as BEGIN END (-22) and deep-walk SingleStatement roots, exception handlers, and simple CASE else bodies so nested refs are not dropped.
Cover SELECT/DML/DDL, CTEs, scripts, errors, and batch evaluation via sql-tests inputs with regenerated result and analyzer golden files.
Keep only statement_identifier/code on success, expose error line/position, and broaden complex SQL, scripting, and non-syntax parser error tests.
Declare nullIntolerant since ParseCommand implements nullSafeEval, replace the elided example outputs with exact reproducible ones, and register the function in the expression schema golden file.
srielau
left a comment
There was a problem hiding this comment.
SQL Expert Review (SPARK-58738)
Domain review of the new parse_command surface (parser helpers + scalar function + goldens).
Summary
| Severity | Count |
|---|---|
| Critical | 2 |
| High | 4 |
| Medium | 2 |
| Low | 2 |
| Total | 10 |
Top actions
- Parse with the same surface as production (
SparkSqlParser/ session parser). - Freeze the JSON contract deliberately (versioning +
Expression.sqlstability). - Do not map unknown non-commands to SELECT; sync the PR description.
- Close
CreateView/ deep-walk gaps; clarify CTE vs table refs. - Fix
@sinceto4.4.0(or document master-only).
Inline comments below cover each finding.
Move parsing onto SparkSqlParser in sql/core, rename parse_command to parse_sql, drop select_list expression text, tighten error handling and classification, and keep table_references lineage-focused.
srielau
left a comment
There was a problem hiding this comment.
SQL Expert Re-Review (SPARK-58738 / 617940c)
Prior Criticals and most Highs from the first review look addressed (SparkSqlParser, rename to parse_sql, classification allowlist, CreateView/select_list, CTE lineage filter, private collectors, @since 5.0.0, PR description).
Verdict: Request changes — remaining Highs below should be fixed before merge.
| Severity | Count |
|---|---|
| Critical | 0 |
| High | 4 |
| Medium | 2 |
| Low | 2 |
| Total | 8 |
Top actions
- Narrow the
SparkThrowablecatch so internals fail the task. - Extend
isQueryPlanforTABLE/VALUES/ time travel. - Stop blanket
UnresolvedIdentifierlineage collection. - Add
schema_version; document stock parser vs session extensions.
Narrow errors to ParseException/SqlScriptingException, classify TABLE/VALUES as SELECT, collect only table/view lineage targets, document stock-parser limits, and add CREATE METRIC VIEW code -37.
Add spark.sql.parseSql.enabled (default false) while iterating, omit unused JSON fields, fix select-list/error context, and dump full JSON goldens.
cloud-fan
left a comment
There was a problem hiding this comment.
1 blocking, 1 non-blocking, 1 nit.
The core design is coherent, but positional markers inside SQL scripts can be silently overcounted and should be fixed before merge.
Correctness (1)
- Blocking: sql/core/src/main/scala/org/apache/spark/sql/catalyst/parser/ParseSqlResult.scala:183: Avoid re-traversing
SingleStatementchildren so positional parameter markers are counted exactly once. -- see inline
Suggestions (1)
- Non-blocking: sql/core/src/main/scala/org/apache/spark/sql/catalyst/parser/ParseSqlResult.scala:103: Combine table, function, and parameter collection after the required CTE-name pass to avoid four deep walks per row. -- see inline
Nits: 1 minor item (see inline comments).
Verification
I traced SingleStatement.children, QueryPlan.foreachWithSubqueries, the explicit parsedPlan recursion, and positional-marker accumulation. The first traversal visits the wrapped plan's children; the recursive traversal then visits those children again, while each PosParameter visit increments unnamedCount. I also verified the four independent deep-plan walks and the repository's ASCII-comment rule. I did not run tests.
PR metadata suggestions
- Update the successful JSON example: the implementation omits empty
function_referencesandparameter_markers, but the PR body currently shows them as populated empty structures.
| foreachPlanDeep(table)(f) | ||
| case s: SingleStatement => | ||
| // Root of the wrapped statement is skipped by SingleStatement.children. | ||
| foreachPlanDeep(s.parsedPlan)(f) |
There was a problem hiding this comment.
Blocking:
This re-traverses children that SingleStatement.children already exposed to the outer foreachWithSubqueries walk. Named markers hide the duplicate visit in a set, but every PosParameter increments unnamedCount, so BEGIN SELECT * FROM t WHERE a = ?; END reports too many markers. Please visit the wrapped root without revisiting its children, and add a scripting test with ? below the root.
| if (selectList.nonEmpty) { | ||
| fields += "select_list" -> JArray(selectList.toList) | ||
| } | ||
| parameterMarkersJson(plan).foreach(markers => fields += "parameter_markers" -> markers) |
There was a problem hiding this comment.
Non-blocking:
Each input row currently performs four full deep-plan walks: two for tables, one for functions, and one here for parameters. Since this API is intended for batch evaluation, please keep the CTE-name pass and collect tables, functions, and parameters together in one subsequent walk while preserving first-seen order.
| case u: UnresolvedTable => add(u.multipartIdentifier) | ||
| case u: UnresolvedView => add(u.multipartIdentifier) | ||
| case u: UnresolvedTableOrView => add(u.multipartIdentifier) | ||
| // Table/view DDL targets only — not CreateFunction / CreateVariable names. |
There was a problem hiding this comment.
Nit:
The repository guidance requires ASCII punctuation in code comments.
| // Table/view DDL targets only — not CreateFunction / CreateVariable names. | |
| // Table/view DDL targets only -- not CreateFunction / CreateVariable names. |
What changes were proposed in this pull request?
Add a built-in scalar SQL function
parse_sql(sqlStmt)that parse-only analyzes a SQL statement string (viaSparkSqlParser, matching the session parser surface) and returns a compact JSON description:parse_successand, on failure, a nested STANDARD-format error object (errorClass,messageTemplate,sqlState,messageParameters, plusline/positionwhen available). User-facing parse/scripting errors become JSON; unexpected internal failures propagate.statement_identifier,statement_code). Spark-only statements use product-specific identifiers with append-only negative codes. Unknown plans map to Unrecognized (code0); known query shapes are allowlisted as SELECT.table_referencesandfunction_referencesfor lineage (CTE names and correlation aliases omitted; tables inside CTE bodies included).select_listentries with multipartnameonly (no expression text).parameter_markers(namedandunnamed_count).The expression lives in
sql/core(needsSparkSqlParser) and is registered when building session state.Why are the changes needed?
Dialect-migration and SQL analysis tooling need an efficient, batchable way to verify that translated SQL parses and to extract statement kind / lineage references without requiring catalog objects to exist.
Does this PR introduce any user-facing change?
Yes. New SQL function (
@since 5.0.0, master-only):How was this patch tested?
ParseSqlResultSuite/ParseSqlSuite(sql/core)sql-tests/inputs/parse-sql.sql(+ analyzer results)ExpressionInfoSuite/ExpressionsSchemaSuiteWas this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor Grok 4.5