{"record":{"id":"49cb8be406861bf3","repo":"alibaba/canal","slug":"parse-sql-error","errorCode":null,"errorMessage":"Parse sql error","messagePattern":"Parse sql error","errorType":"validation","errorClass":"ParserException","httpStatus":null,"severity":"error","filePath":"client-adapter/escore/src/main/java/com/alibaba/otter/canal/client/adapter/es/core/config/SqlParser.java","lineNumber":67,"sourceCode":"        try {\n            SQLStatementParser parser = new MySqlStatementParser(sql);\n            SQLSelectStatement statement = (SQLSelectStatement) parser.parseStatement();\n            MySqlSelectQueryBlock sqlSelectQueryBlock = (MySqlSelectQueryBlock) statement.getSelect().getQuery();\n\n            SchemaItem schemaItem = new SchemaItem();\n            schemaItem.setSql(SQLUtils.toMySqlString(sqlSelectQueryBlock));\n            SQLTableSource sqlTableSource = sqlSelectQueryBlock.getFrom();\n            List<TableItem> tableItems = new ArrayList<>();\n            SqlParser.visitSelectTable(schemaItem, sqlTableSource, tableItems, null);\n            tableItems.forEach(tableItem -> schemaItem.getAliasTableItems().put(tableItem.getAlias(), tableItem));\n\n            List<FieldItem> fieldItems = collectSelectQueryFields(sqlSelectQueryBlock);\n            fieldItems.forEach(fieldItem -> schemaItem.getSelectFields().put(fieldItem.getFieldName(), fieldItem));\n\n            schemaItem.init();\n\n            if (schemaItem.getAliasTableItems().isEmpty() || schemaItem.getSelectFields().isEmpty()) {\n                throw new ParserException(\"Parse sql error\");\n            }\n            return schemaItem;\n        } catch (Exception e) {\n            throw new ParserException();\n        }\n    }\n\n    /**\n     * 归集字段\n     *\n     * @param sqlSelectQueryBlock sqlSelectQueryBlock\n     * @return 字段属性列表\n     */\n    private static List<FieldItem> collectSelectQueryFields(MySqlSelectQueryBlock sqlSelectQueryBlock) {\n        return sqlSelectQueryBlock.getSelectList().stream().map(selectItem -> {\n            FieldItem fieldItem = new FieldItem();\n            fieldItem.setFieldName(selectItem.getAlias());\n            fieldItem.setExpr(selectItem.toString());","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/alibaba/canal/blob/87be50e87686a3e8af08c368d0e1ffd1f59eb04a/client-adapter/escore/src/main/java/com/alibaba/otter/canal/client/adapter/es/core/config/SqlParser.java#L49-L85","documentation":"Thrown by SqlParser.parse() when the ES-adapter SQL fails to yield at least one alias table item and at least one select field after Druid parsing. It is a deliberately generic ParserException; note the surrounding catch(Exception) at line 70 re-wraps EVERY failure (including Druid parse errors, ClassCastExceptions, and the empty-fields check itself) into a message-less ParserException, so the on-screen text 'Parse sql error' usually masks the real cause.","triggerScenarios":"Calling SqlParser.parse(sql) with a query that (a) is not a single MySQL SELECT (UNION/subquery-only, DDL, multi-statement), (b) has a FROM clause the parser cannot turn into an ExprTableSource/Join/Subquery, (c) selects zero columns, or (d) triggers any Druid ClassCastException inside the try block.","commonSituations":"ES yml mapping 'esMapping.sql' is malformed, uses a non-SELECT statement, omits the FROM clause, or references a subquery in a way visitSelectTable cannot walk; upgrading the Druid dependency so AST node types change; a typo in the SQL string.","solutions":["Enable Druid debug logging and reproduce SqlParser.parse locally with the exact SQL to read the real underlying exception before it is re-wrapped.","Validate the SQL is a single flat MySQL SELECT with a FROM table/join that the parser understands; simplify joins to plain 'a JOIN b ON a.id=b.aid' until it parses.","Ensure the SELECT list is non-empty and every selected field resolves to a column, property, method, or supported CASE expression.","If you must diagnose production, temporarily patch the catch block to chain 'e' (throw new ParserException(e.getMessage(), e)) so the root cause surfaces.","Run the same SQL through a MySQL client to confirm syntax validity before feeding it to the adapter."],"exampleFix":"// before\ntry {\n    ...\n} catch (Exception e) {\n    throw new ParserException();  // swallows real cause\n}\n\n// after\n} catch (Exception e) {\n    throw new ParserException(\"Parse sql error: \" + e.getMessage(), e);\n}","handlingStrategy":"validation","validationCode":"// Pre-validate the SQL is a single MySQL SELECT before parsing\nString sql = mapping.getSql();\nif (sql == null || sql.trim().isEmpty() || !sql.trim().toUpperCase().startsWith(\"SELECT\")) {\n    throw new IllegalArgumentException(\"esMapping.sql must be a non-empty SELECT statement\");\n}\n// Optionally test-parse with Druid standalone\nMySqlStatementParser p = new MySqlStatementParser(sql);\nSQLStatement stmt = p.parseStatement(); // throws ParserException with real message\nif (!(stmt instanceof SQLSelectStatement)) {\n    throw new IllegalArgumentException(\"esMapping.sql must be a SELECT\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    SchemaItem item = SqlParser.parse(sql);\n} catch (ParserException e) {\n    // SqlParser.parse swallows the root cause; re-parse with Druid directly to surface it\n    logger.error(\"ES SQL parse failed; re-parsing for root cause:\");\n    try { new MySqlStatementParser(sql).parseStatement(); }\n    catch (Exception real) { logger.error(\"Root cause: \", real); }\n    throw e;\n}","preventionTips":["Always test the mapping SQL in a MySQL client before deploying it.","Keep the SELECT to a single flat query with FROM table [JOIN ... ON col=col AND col=col].","Run a unit test invoking SqlParser.parse on every mapping YAML at build time."],"tags":["es-adapter","sql-parser","druid","configuration"],"backgroundTag":null,"analyzedSha":"87be50e87686a3e8af08c368d0e1ffd1f59eb04a","analyzedAt":"2026-08-14T04:30:11.918Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}