feat(isthmus-cli): explain common SQL conversion errors instead of dumping stack traces - #1060
feat(isthmus-cli): explain common SQL conversion errors instead of dumping stack traces#1060nielspardon wants to merge 2 commits into
Conversation
…mping stack traces Failures caused by the SQL given to the CLI are now reported as a message plus a hint naming the option to reach for, rather than as a raw Calcite stack trace. An undefined table points at -c / --create, an unresolved column or identifier explains where columns come from, and a CREATE TABLE passed as the query -- or a query passed to -c -- points at the other one. Anything that is not recognizably an input problem keeps its stack trace, and --stacktrace restores it for the recognized ones. Missing input is a usage error now: with neither a query nor -e the CLI reported a NullPointerException, and main no longer parses arguments ahead of execute(), so a mistyped argument gets picocli's usage error instead of an UnmatchedArgumentException trace. Also guards processCreateStatementsToSchema against CREATE TABLE AS SELECT, as its sibling processCreateStatements already did; without it a CTAS statement in -c dereferenced a null column list. Closes substrait-io#113
andrew-coleman
left a comment
There was a problem hiding this comment.
The direction here is right and the hint text reads well. Three things I think need to change before this lands, plus a set of smaller ones inline.
The NPE is still reachable. Calcite's grammar makes the column list optional independently of AS query, so isthmus -c "CREATE TABLE FOO" "SELECT * FROM FOO" still dies with the columnList NPE and a full stack trace — on both the query and the -e path.
Internal defects are now silently swallowed. SqlParser wraps whatever the parser threw into a SqlParseException, so isthmus "" prints Error: Index 0 out of bounds for length 0 with no trace and no hint, where it used to print a trace that located the bug. That contradicts the promise the README adds in this PR.
-e swallows the positional query. isthmus -e "1 + 1" "SELECT 1" parses the query as an expression, and isthmus "SELECT 1" -e "1 + 1" exits 0 having silently discarded the query.
Several hints also fire for the wrong command line — they are selected purely from message text while parseResult is passed in and never consulted, so -e advice appears when -e was not used, the --unquotedcasing note appears when the casing was explicitly chosen, and the column hint tells you to check a CREATE TABLE statement that may not exist.
How this was checked: built installDist at da2e943e and reproduced each case from the CLI, then applied the suggested fixes and re-ran. With all of them applied, :isthmus:test is 961/961 and :isthmus-cli:test is 21/21 (up from 11), with pmdMain, spotlessCheck and javadoc clean. Every suggestion block below comes from that tree, so they should apply as-is. The suggestions on IsthmusExecutionExceptionHandler touch overlapping methods (hint gains a parseResult parameter) and are easiest to take together. The one thing I did not run end to end is the --stacktrace addition to smoke.sh, since that needs a native build.
| private static final String CTAS_NOT_SUPPORTED = "CTAS not supported."; | ||
|
|
||
| /** The message the DDL converter reports for a CREATE TABLE without a query. */ | ||
| private static final String CTAS_ONLY = "Only create table as select statements are supported"; |
There was a problem hiding this comment.
This literal is a copy of the one in isthmus/src/main/java/io/substrait/isthmus/calcite/rel/DdlSqlToRelConverter.java:91 — a different Gradle module — matched with .equals(). Reword it there (a plausible, unrelated change) and isthmus "CREATE TABLE foo(a INT)" silently goes back to dumping a stack trace, with everything still compiling and every test in isthmus still green. NOT_A_CREATE_TABLE and CTAS_NOT_SUPPORTED have the same problem against the inline literals in SubstraitCreateStatementParser.
A dedicated exception type is the real fix; failing that, public static final constants exposed by the throwing classes would at least turn a rename into a compile error.
Minor, same area: isPlainCreateTableQuery(ex) is evaluated twice per failure — once in isInputError (line 105) and again at the top of hint() (line 127).
There was a problem hiding this comment.
Kept the literals, because CI does catch the rename: the CLI tests assert on all three messages end to end (ctasPassedToCreateOptionSuggestsQueryArgument, queryPassedToCreateOptionSuggestsQueryArgument, and createTableAsQuerySuggestsCreateOption for CTAS_ONLY, which only prints its hint when the message matches exactly), so rewording any of them turns :isthmus-cli:test red in the same build. Exposing them as public static final in :isthmus would make error-message text part of that module's API, which I would rather not do for a coupling the tests already pin — happy to add the constants, or a dedicated exception type, if you disagree.
Left the double isPlainCreateTableQuery evaluation: it is an instanceof plus a String.equals on a path that is about to print and exit, and hoisting it means either threading a boolean through hint() or reordering the two checks into one method that does both jobs.
| if (stackTraceRequested(cmd)) { | ||
| throw ex; | ||
| } | ||
| return cmd.getCommandSpec().exitCodeOnExecutionException(); |
There was a problem hiding this comment.
Recognised input errors return ExitCode.SOFTWARE — picocli's internal software error code — which is also what an unrecognised crash returns:
$ isthmus "SELECT * FROM foo" # bad input, message printed -> 1
$ isthmus -c "CREATE TABLE FOO" "SELECT * FROM FOO" # NPE, trace printed -> 1
Meanwhile missing input deliberately exits 2 (ExitCode.USAGE). So two members of the newly created "the input is wrong" category get different codes, and a script wrapping isthmus cannot tell "fix your SQL" from "file a bug". The new tests bake ExitCode.SOFTWARE in, so the split is now pinned.
Worth deciding explicitly: either input errors join missing input on 2, or the distinction gets documented. Not a blocker, but easier to settle before release than after.
There was a problem hiding this comment.
Settled it as documentation rather than a move to 2: 1 for "the SQL could not be converted", 2 for "the command line itself was wrong". That keeps --stacktrace-worthy defects and bad SQL on the same code, which is the part your example is really about, but it is the split a compiler makes too — bad source is a normal failure, not a usage error — and it means adding a hint for a failure never silently changes a script's exit code. The three codes are now a table in the isthmus-cli readme.
Close the remaining raw-trace and NPE paths the first pass left behind: - CREATE TABLE without a column list NPEd on both parser paths, not just the CTAS one; both now go through a shared guard. - A SqlParseException without a position did not come from the grammar, so it is a defect and keeps its stack trace instead of being reported as a mistake in the SQL. - -e / --expression is greedy, so a query written after it was silently parsed as an expression; giving both is now a usage error, and a query swallowed by -e is explained. - Hints are gated on the options actually given: the column hint no longer points at -c when no table was defined, the identifier hint no longer blames -e when the failure came from a -c statement, the table hint names the schema Calcite looked in, and the casing note is dropped once --unquotedcasing was chosen. - Calcite builds a syntax error's expected-token list by reflecting on the parser, which the native image could not do, so every syntax error surfaced as that reflection failure rather than as a position in the SQL. Register the three productions it calls. Bare `isthmus` now reports the missing SQL and exits 2 rather than printing usage and exiting 0, which is what every other missing-argument case does.
|
Thanks — everything reproduced, and all of it is fixed in 696ae64 except the two threads I replied to inline (the cross-module message literals, which the CLI tests already pin, and the exit-code split, which is now documented rather than unified). Two things worth calling out beyond the review: Bare Chasing your note about not having run the |
isthmus "SELECT * FROM foo"answered a beginner's first attempt with a 40-lineCalciteContextExceptionstack trace that never mentioned-c/--create. Every other way to get the input wrong behaved the same way, up to an outrightNullPointerExceptionwhen no query was passed at all.What changes
-c/--create(with its own name in the example), an unresolved column or an unresolved identifier in-e/--expressionexplains where columns come from and mentions--unquotedcasing, and aCREATE TABLEgiven as the query — or a query given to-c— points at the other one.--stacktraceprints the message and the full trace for the recognized failures.-ethe CLI threw aNullPointerException; it now printsMissing SQL to convertplus the usage. Relatedly,mainno longer parses arguments ahead ofexecute(), so an unquoted or mistyped argument gets picocli's usage error rather than anUnmatchedArgumentExceptiontrace. Both exit 2, picocli's invalid-input code, where the uncaught exceptions previously exited 1.processCreateStatementsToSchemaagainst CTAS, as its siblingprocessCreateStatementsalready did. Without the guard,-c "CREATE TABLE foo AS SELECT 1"dereferenced a null column list — a parameter declared@NonNull— and failed with anNPEinstead ofCTAS not supported.The
--helpblock in the isthmus-cli readme reflows beyond the added line because the new option widens picocli's option column.Closes #113
🤖 Generated with AI