|
14 | 14 | @time: 2026/03/27 |
15 | 15 | @file: sql_syntax_handler.py |
16 | 16 | @desc: Validate SQL syntax/semantics against a live OceanBase instance |
17 | | - using EXPLAIN — without executing the SQL. |
| 17 | + using EXPLAIN or PREPARE — without executing the SQL. |
18 | 18 | See https://github.com/oceanbase/obdiag/issues/1181 |
19 | 19 | """ |
20 | 20 |
|
@@ -45,6 +45,32 @@ def normalize_sql_for_syntax_check(sql): |
45 | 45 | return s, None |
46 | 46 |
|
47 | 47 |
|
| 48 | +def strip_leading_block_comments(sql): |
| 49 | + pos = 0 |
| 50 | + length = len(sql) |
| 51 | + while pos < length: |
| 52 | + while pos < length and sql[pos].isspace(): |
| 53 | + pos += 1 |
| 54 | + if not sql.startswith("/*", pos): |
| 55 | + break |
| 56 | + end = sql.find("*/", pos + 2) |
| 57 | + if end == -1: |
| 58 | + return "" |
| 59 | + pos = end + 2 |
| 60 | + return sql[pos:] |
| 61 | + |
| 62 | + |
| 63 | +def is_ddl_statement(sql): |
| 64 | + matched = re.match(r'(\w+)', strip_leading_block_comments(sql)) |
| 65 | + if not matched: |
| 66 | + return False |
| 67 | + return matched.group(1).upper() in ("ALTER", "CREATE", "DROP", "RENAME", "TRUNCATE") |
| 68 | + |
| 69 | + |
| 70 | +def quote_sql_literal(value): |
| 71 | + return str(value).replace("\\", "\\\\").replace("'", "''") |
| 72 | + |
| 73 | + |
48 | 74 | class SqlSyntaxHandler: |
49 | 75 | def __init__(self, context): |
50 | 76 | self.context = context |
@@ -131,33 +157,62 @@ def _resolve_connection(self): |
131 | 157 | return host, int(port), user, password, database |
132 | 158 |
|
133 | 159 | def _check_syntax(self, connector, sql): |
134 | | - """Run EXPLAIN against the SQL and interpret the result.""" |
| 160 | + if is_ddl_statement(sql): |
| 161 | + return self._check_ddl_syntax(connector, sql) |
| 162 | + return self._check_explain_syntax(connector, sql) |
| 163 | + |
| 164 | + def _check_explain_syntax(self, connector, sql): |
| 165 | + """Run EXPLAIN against DML SQL and interpret the result.""" |
135 | 166 | explain_sql = "EXPLAIN {0}".format(sql) |
136 | 167 | self.stdio.verbose("[sql-syntax] exec: {0}".format(explain_sql)) |
137 | 168 |
|
138 | 169 | try: |
139 | 170 | connector.execute_sql(explain_sql) |
140 | 171 | self.stdio.print("Result: VALID") |
141 | 172 | return ObdiagResult(ObdiagResult.SUCCESS_CODE, data={"result": "VALID", "sql": sql}) |
142 | | - |
143 | 173 | except mysql.Error as e: |
144 | | - error_code = e.args[0] if e.args else None |
145 | | - error_msg = e.args[1] if len(e.args) > 1 else str(e) |
146 | | - |
147 | | - if error_code == 1064: |
148 | | - self.stdio.print("Result: SYNTAX ERROR") |
149 | | - self.stdio.print("Detail: {0}".format(error_msg)) |
150 | | - return ObdiagResult( |
151 | | - ObdiagResult.SUCCESS_CODE, |
152 | | - data={"result": "SYNTAX_ERROR", "error_code": error_code, "detail": error_msg}, |
153 | | - ) |
154 | | - else: |
155 | | - self.stdio.print("Result: VALID (syntax OK, but semantic error [{0}]: {1})".format(error_code, error_msg)) |
156 | | - return ObdiagResult( |
157 | | - ObdiagResult.SUCCESS_CODE, |
158 | | - data={"result": "SEMANTIC_ERROR", "error_code": error_code, "detail": error_msg}, |
159 | | - ) |
| 174 | + return self._handle_mysql_syntax_error(e) |
| 175 | + except Exception as e: |
| 176 | + self.stdio.error("Unexpected error during SQL syntax check: {0}".format(e)) |
| 177 | + return ObdiagResult(ObdiagResult.SERVER_ERROR_CODE, error_data=str(e)) |
160 | 178 |
|
| 179 | + def _check_ddl_syntax(self, connector, sql): |
| 180 | + """Use PREPARE to validate DDL syntax without executing the DDL statement.""" |
| 181 | + stmt_name = "obdiag_sql_syntax_stmt" |
| 182 | + prepare_sql = "PREPARE {0} FROM '{1}'".format(stmt_name, quote_sql_literal(sql)) |
| 183 | + prepared = False |
| 184 | + self.stdio.verbose("[sql-syntax] exec: {0}".format(prepare_sql)) |
| 185 | + try: |
| 186 | + connector.execute_sql(prepare_sql) |
| 187 | + prepared = True |
| 188 | + self.stdio.print("Result: VALID") |
| 189 | + return ObdiagResult(ObdiagResult.SUCCESS_CODE, data={"result": "VALID", "sql": sql, "method": "PREPARE"}) |
| 190 | + except mysql.Error as e: |
| 191 | + return self._handle_mysql_syntax_error(e) |
161 | 192 | except Exception as e: |
162 | 193 | self.stdio.error("Unexpected error during SQL syntax check: {0}".format(e)) |
163 | 194 | return ObdiagResult(ObdiagResult.SERVER_ERROR_CODE, error_data=str(e)) |
| 195 | + finally: |
| 196 | + if prepared: |
| 197 | + try: |
| 198 | + connector.execute_sql("DEALLOCATE PREPARE {0}".format(stmt_name)) |
| 199 | + except Exception as e: |
| 200 | + self.stdio.warn("Failed to deallocate prepared statement {0}: {1}".format(stmt_name, e)) |
| 201 | + |
| 202 | + def _handle_mysql_syntax_error(self, error): |
| 203 | + error_code = error.args[0] if error.args else None |
| 204 | + error_msg = error.args[1] if len(error.args) > 1 else str(error) |
| 205 | + |
| 206 | + if error_code == 1064: |
| 207 | + self.stdio.print("Result: SYNTAX ERROR") |
| 208 | + self.stdio.print("Detail: {0}".format(error_msg)) |
| 209 | + return ObdiagResult( |
| 210 | + ObdiagResult.SUCCESS_CODE, |
| 211 | + data={"result": "SYNTAX_ERROR", "error_code": error_code, "detail": error_msg}, |
| 212 | + ) |
| 213 | + |
| 214 | + self.stdio.print("Result: VALID (syntax OK, but semantic error [{0}]: {1})".format(error_code, error_msg)) |
| 215 | + return ObdiagResult( |
| 216 | + ObdiagResult.SUCCESS_CODE, |
| 217 | + data={"result": "SEMANTIC_ERROR", "error_code": error_code, "detail": error_msg}, |
| 218 | + ) |
0 commit comments