Skip to content

Commit c2a092a

Browse files
committed
feat: preserve MySQL table modifiers and lateral derived queries
Parse PARTITION selections and USE, FORCE, and IGNORE INDEX/KEY hints into structured table-reference children, retaining hint scope and empty USE lists. Preserve the distinct clause ordering required by SELECT/UPDATE and single-table DELETE, and reject malformed lists and misplaced clauses. Support SELECT-based LATERAL derived tables, UNION sources, mandatory MySQL derived-table aliases, and optional alias-column lists. Require complete nested query operands. Share the locking-clause parser so nested queries retain NOWAIT, SKIP LOCKED, and structured OF targets, including qualified and wildcard targets. Require BY after GROUP and LOCKED after SKIP instead of silently repairing malformed input. Validate table identifiers and aliases against the common reserved-word baseline verified on pinned MySQL 8.4.8 and 9.7.2. Preserve delimited names and the special PRIMARY index name. Keep version-specific reserved words outside this unversioned parser policy. Append AST kinds without changing existing values or the node layout, and reject unsupported table modifiers and derived column renaming in local planning. Expand the pinned compatibility corpus from 43 to 74 hand-reduced witnesses, verifying 71 exact grammar anchors per release. Promote the three previous table-reference gaps and require independently derived emission expectations. Give native checks a separate populated partitioned fixture, preserving the existing fulltext fixture for unrelated cases. Document SELECT-only LATERAL scope and the remaining identifier limitations; these selected witnesses do not measure complete dialect coverage. Validation: - Forced full build, corpus-test build, and make test: 1,539 passed, 37 external-database tests skipped. - Nine new focused regressions plus 14 existing MySQL grammar tests pass under ASan/UBSan with leak detection. - All 74 compatibility expectations match: 33 CHECKED, 21 malformed controls REJECTED, and 20 explicitly tracked gaps; 11 Python tests pass. - Native MySQL 8.4.8 and 9.7.2 runs have zero validity mismatches. Server errors and three skipped DDL cases remain separately unresolved. - PostgreSQL harness: 87 tests pass; 5,122 committed fixtures and the full 51,415-statement pinned baseline match unchanged. - Independent review verifies nested locking and reserved-name fixes; seven parser/emitter benchmark smoke cases run successfully. Benchmark sources, reports, and benchmark-only README/.gitignore changes are excluded. Production parsing retains no native MySQL dependency.
1 parent 1a5c49e commit c2a092a

14 files changed

Lines changed: 1292 additions & 63 deletions

File tree

‎Makefile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ TEST_SRCS = $(TEST_DIR)/test_main.cpp \
6666
$(TEST_DIR)/test_pg_maintenance.cpp \
6767
$(TEST_DIR)/test_review_expressions.cpp \
6868
$(TEST_DIR)/test_mysql_grammar.cpp \
69+
$(TEST_DIR)/test_mysql_tables.cpp \
6970
$(TEST_DIR)/test_review_ast.cpp \
7071
$(TEST_DIR)/test_review_transactions.cpp \
7172
$(TEST_DIR)/test_review_runtime.cpp \

‎docs/mysql-compatibility.md‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ This harness tracks a small, deliberately selected set of MySQL syntax witnesses
44

55
ParserSQL now preserves MySQL JSON arrows, named/inherited windows, `GROUP_CONCAT` modifiers and separators, and `GROUP BY ... WITH ROLLUP` as structured ASTs. Database-qualified column names are retained. The local engine rejects these unsupported semantics rather than executing a simplified interpretation; parameterization remains conservative for JSON arrows and `GROUP_CONCAT`. Empty MySQL SELECT target lists are rejected.
66

7+
ParserSQL also preserves partition lists, `USE`/`FORCE`/`IGNORE INDEX` or `KEY`, hint scopes and empty `USE INDEX ()`, and `LATERAL` derived SELECTs with required aliases and optional alias-column lists. The local engine rejects these unsupported table-reference semantics. `SELECT` and `UPDATE` place `PARTITION` before the table alias; single-table `DELETE` places it after the alias. The current LATERAL implementation scope is SELECT-based queries, including UNION: both native releases also accept `LATERAL (TABLE t) AS q` and `LATERAL (VALUES ROW(1)) AS q`, but those forms remain outside this implementation and are not malformed controls.
8+
9+
Table identifiers and aliases use the common 259-word reserved baseline observed in the pinned 8.4.8 and 9.7.2 releases. ParserSQL has no MySQL version setting, so the five 9.7-only reserved additions (`CUBE`, `EXTERNAL`, `LIBRARY`, `QUALIFY`, and `TABLESAMPLE`) remain accepted as names. Strict version-specific identifier rules remain a gap.
10+
11+
Numeric-looking unquoted qualified table names such as `db.123` are valid natively but remain unsupported: the tokenizer reads `.123` as a numeric literal. ParserSQL rejects these forms; earlier behavior could emit an incorrect alias. Lossless parsing of this identifier edge remains a gap.
12+
713
The corpus pins the official `mysql/mysql-server` sources:
814

915
| Version | Commit |
@@ -36,6 +42,8 @@ Malformed controls are handled separately: a complete AST is `MALFORMED_ACCEPTED
3642

3743
Every fixture freezes one exact expected outcome. Known gaps do not make arbitrary failures acceptable. Both regression and unexpected improvement cause exit status 1, so a fixed gap must be reviewed and explicitly promoted in the corpus. When promoting, add an independently derived `expected_sql`, set `known_gap=false`, and require `CHECKED`. Harness/transport/source errors return 2. Exit status 0 means the expected baseline matched, including its documented known gaps.
3844

45+
The 74-case baseline has 33 `CHECKED` witnesses and 21 `REJECTED` negative controls. Its remaining known gaps are 13 `TRAILING_INPUT`, five `NO_AST`, one `PARTIAL`, and one `EMISSION_MISMATCH`. Source verification checks 71 exact grammar anchors per pinned release. These counts describe this corpus only.
46+
3947
The JSON report includes all original and reparsed records, source verification counts, pinned revisions, SQL-mode metadata, and SHA-256 hashes of the corpus and probe. Counts apply only to these witnesses. The probe uses hex-line input and hex fields in its TSV response, preserving tabs, newlines and embedded NUL bytes. It emits while both the input string and parser arena are alive; Python reparses owned copies in a second probe invocation.
4048

4149
The default corpus oracle state is **NOT VERIFIED**. Its intended `sql_mode` is the empty string; ParserSQL accepts no mode configuration here and the harness does not apply or emulate MySQL session modes. A native server check requires its exact version, actual session mode, schema fixtures, SQL sent, and result/error code. Missing tables, mode-dependent errors, or unsupported runtime features must not be relabeled as syntax rejection. Without native options the harness does not connect to a database. An optional native run uses an explicit absolute socket path to an already-running, isolated server:
@@ -47,15 +55,15 @@ python3 scripts/mysql_compat/run.py --probe /tmp/parsersql-mysql-probe \
4755
--report /tmp/parsersql-mysql-native.json
4856
```
4957

50-
The client uses `--no-defaults`, the supplied socket, and `root` unless `--native-user` is specified. There is no host discovery or automatic connection. Use an isolated test server: this option explicitly creates a uniquely named `parsersql_compat_<uuid>` database, installs the small fixture in [native.py](../scripts/mysql_compat/native.py), and drops only that owned database in cleanup. The supplied user needs permission to create/drop that database and its tables. The helper requires server version 8.4.8 or 9.7.2, records the exact reported version and session SQL mode, and reapplies that mode for each query. It does not assert that the native mode equals the corpus's empty intended mode.
58+
The client uses `--no-defaults`, the supplied socket, and `root` unless `--native-user` is specified. There is no host discovery or automatic connection. Use an isolated test server: this option explicitly creates a uniquely named `parsersql_compat_<uuid>` database, installs the small fixture in [native.py](../scripts/mysql_compat/native.py), and drops only that owned database in cleanup. The partition witnesses use a separate `pt` table with populated `p0` and `p1` range partitions; `t` retains its fulltext index for the existing search witness. The supplied user needs permission to create/drop that database and its tables. The helper requires server version 8.4.8 or 9.7.2, records the exact reported version and session SQL mode, and reapplies that mode for each query. It does not assert that the native mode equals the corpus's empty intended mode.
5159

5260
Each curated case explicitly chooses direct query execution, `EXPLAIN` for DML (including CTE-prefixed DML), or no native action. DDL and procedures remain **NOT VERIFIED**. These are query-execution or EXPLAIN observations, not access to MySQL's raw parser. Returned row values are evidence only; they are not compared with ParserSQL execution. The helper neither executes emitted SQL nor claims semantic round-trip validation.
5361

54-
Native results are stored separately in `native_oracle`: `ACCEPTED`, `SYNTAX_REJECTED` (1064/1149), `SERVER_ERROR`, `CLIENT_ERROR`, or **NOT VERIFIED**. For example, the PARTITION witness hits error 1747 because the fixture table is not partitioned; that is an unresolved server error, never an accepted syntax result. Accepted invalid controls and syntax-rejected valid controls count as expectation mismatches, using per-version validity when specified. Server errors remain explicitly unresolved; client errors return status 2. Thus exit status 0 still does not mean all native cases were verified. The default corpus metadata remains **NOT VERIFIED**, while the optional report describes the actual native observations.
62+
Native results are stored separately in `native_oracle`: `ACCEPTED`, `SYNTAX_REJECTED` (1064/1149), `SERVER_ERROR`, `CLIENT_ERROR`, or **NOT VERIFIED**. For example, a derived table missing its required alias returns error 1248; this remains a server error, even though the corpus requires ParserSQL to reject that input. Accepted invalid controls and syntax-rejected valid controls count as expectation mismatches, using per-version validity when specified. Server errors remain explicitly unresolved; client errors return status 2. Thus exit status 0 still does not mean all native cases were verified. The default corpus metadata remains **NOT VERIFIED**, while the optional report describes the actual native observations.
5563

56-
A native check of the shared witnesses against both pinned releases found acceptance of the positive query/DML witnesses and syntax rejection of the negative controls under `ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION`. The three DDL/procedure witnesses were skipped and PARTITION remained unresolved. Re-run the command on each isolated socket to reproduce current observations; reports are intentionally not committed.
64+
A native check of all 74 witnesses under `ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION` found no native expectation mismatches. MySQL 8.4.8 accepted 46, syntax-rejected 24, and returned error 1248 for the missing-alias control; MySQL 9.7.2 accepted 48, syntax-rejected 20, and returned three server errors (1248, 1235, and 3889). Both runs skipped the three DDL/procedure witnesses and cleaned up their owned schemas. All shared positive query/DML witnesses, including partition selection and index hints, were accepted. Re-run the command on each isolated socket to reproduce current observations; reports are intentionally not committed.
5765

58-
The corpus is deliberately selected and is not a representative workload. It contains 39 shared witnesses and four version-specific query witnesses; this does not imply identical dialects in 8.4.8 and 9.7.2. The version-specific entries record `applicability`, source productions for both versions, `native_validity` per version, and observed native outcomes. Their generic `validity=valid` denotes the 9.7.2 grammar target. The native helper selects the intended validity using the actual server version, so an expected 8.4.8 syntax rejection is not treated as a corpus error. Stored native observations are context, not substitutes for a current run. These additions remain ParserSQL gaps. The following differences include those four query witnesses and two separate DDL spot checks:
66+
The corpus is deliberately selected and is not a representative workload. It contains 70 shared witnesses and four version-specific query witnesses; this does not imply identical dialects in 8.4.8 and 9.7.2. The version-specific entries record `applicability`, source productions for both versions, `native_validity` per version, and observed native outcomes. Their generic `validity=valid` denotes the 9.7.2 grammar target. The native helper selects the intended validity using the actual server version, so an expected 8.4.8 syntax rejection is not treated as a corpus error. Stored native observations are context, not substitutes for a current run. These additions remain ParserSQL gaps. The following differences include those four query witnesses and two separate DDL spot checks:
5967

6068
| Reduced feature | MySQL 8.4.8 observation | MySQL 9.7.2 observation |
6169
| --- | --- | --- |

‎include/sql_engine/plan_builder.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ class PlanBuilder {
7070
bool dml_values = false) {
7171
using sql_parser::NodeType;
7272
switch (node->type) {
73+
case NodeType::NODE_ALIAS:
74+
if constexpr (D == sql_parser::Dialect::MySQL) {
75+
if (node->first_child) return true; // Derived column renaming needs engine support.
76+
}
77+
break;
78+
case NodeType::NODE_MYSQL_LOCK_TARGETS:
79+
case NodeType::NODE_MYSQL_PARTITION_SELECTION:
80+
case NodeType::NODE_MYSQL_INDEX_HINT:
81+
return true;
7382
case NodeType::NODE_QUALIFIED_NAME:
7483
// The local resolver combines only table.column; it would
7584
// otherwise silently drop the final part of db.table.column.

‎include/sql_parser/common.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ enum class NodeType : uint16_t {
347347
NODE_MYSQL_JSON_EXTRACT, // value=-> or ->>; column reference, literal path
348348
NODE_MYSQL_GROUP_CONCAT, // arguments, optional aggregate ORDER BY and separator
349349
NODE_MYSQL_SEPARATOR, // literal string child
350+
NODE_MYSQL_PARTITION_SELECTION, // nonempty identifier list on a table reference
351+
NODE_MYSQL_LOCK_TARGETS, // OF table identifiers on a locking clause
352+
NODE_MYSQL_INDEX_HINT, // value=USE/FORCE/IGNORE; flags KEY=1, JOIN=2, ORDER=4, GROUP=8; index children
350353

351354
};
352355

‎include/sql_parser/compound_query_parser.h‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,9 @@ class CompoundQueryParser {
5353
}
5454
if (D == Dialect::MySQL && result->type == NodeType::NODE_SELECT_STMT &&
5555
tok_.peek().type == TokenType::TK_FOR) {
56-
tok_.skip();
57-
AstNode* lock = make_node(arena_, NodeType::NODE_LOCKING_CLAUSE);
56+
SelectParser<D> select(tok_, arena_, false, require_operands);
57+
AstNode* lock = select.parse_locking();
5858
if (!lock) return nullptr;
59-
Token strength = tok_.next_token();
60-
lock->add_child(make_node(arena_, NodeType::NODE_IDENTIFIER, strength.text));
6159
result->add_child(lock);
6260
}
6361
return result;

‎include/sql_parser/delete_parser.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,18 @@ class DeleteParser {
7575
AstNode* first_table = parse_simple_table_ref();
7676
if (!first_table) return root;
7777

78+
table_ref_parser_.parse_optional_alias(first_table);
79+
const bool has_partition = tok_.peek().type == TokenType::TK_PARTITION;
80+
if (has_partition) {
81+
auto* partition = table_ref_parser_.parse_mysql_partition_selection();
82+
if (!partition) return expr_parser_.syntax_error();
83+
first_table->add_child(partition);
84+
}
85+
7886
// Check if comma follows (target list) or if USING follows
7987
if (tok_.peek().type == TokenType::TK_COMMA || tok_.peek().type == TokenType::TK_USING) {
88+
if (has_partition || (first_table->first_child &&
89+
first_table->first_child->next_sibling)) return expr_parser_.syntax_error();
8090
// Could be multi-table form 2: DELETE FROM t1[, t2] USING ...
8191
// Or single-table with comma would be unusual. Check for USING after table list.
8292
// Collect all target tables

‎include/sql_parser/emitter.h‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,17 @@ class Emitter {
143143

144144
// ---- Table references ----
145145
case NodeType::NODE_TABLE_REF: emit_table_ref(node); break;
146+
case NodeType::NODE_MYSQL_LOCK_TARGETS:
147+
sb_.append("OF "); emit_list(node, ", "); break;
148+
case NodeType::NODE_MYSQL_PARTITION_SELECTION:
149+
sb_.append(" PARTITION ("); emit_list(node, ", "); sb_.append_char(')'); break;
150+
case NodeType::NODE_MYSQL_INDEX_HINT:
151+
sb_.append_char(' '); emit_value(node);
152+
sb_.append((node->flags & 1) ? " KEY" : " INDEX");
153+
if (node->flags & 2) sb_.append(" FOR JOIN");
154+
else if (node->flags & 4) sb_.append(" FOR ORDER BY");
155+
else if (node->flags & 8) sb_.append(" FOR GROUP BY");
156+
sb_.append(" ("); emit_list(node, ", "); sb_.append_char(')'); break;
146157
case NodeType::NODE_ALIAS: emit_alias(node); break;
147158
case NodeType::NODE_QUALIFIED_NAME: emit_qualified_name(node); break;
148159

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#ifndef SQL_PARSER_MYSQL_IDENTIFIER_H
2+
#define SQL_PARSER_MYSQL_IDENTIFIER_H
3+
4+
#include "sql_parser/token.h"
5+
#include <cstddef>
6+
7+
namespace sql_parser {
8+
9+
// Keywords may be identifiers after a qualification dot, including reserved ones.
10+
inline bool mysql_identifier_word(const Token& token) {
11+
return token.type == TokenType::TK_IDENTIFIER ||
12+
(token.type >= TokenType::TK_SELECT && token.type <= TokenType::TK_RECURSIVE);
13+
}
14+
15+
inline bool mysql_identifier_token(const Token& token) {
16+
if (!mysql_identifier_word(token)) return false;
17+
if (token.source.ptr != token.text.ptr) return true; // delimited identifier
18+
// Sorted reserved-word baseline from INFORMATION_SCHEMA.KEYWORDS, MySQL
19+
// 8.4.8 (0896fcd61dec11a0904166911a0126f59daaa1bf), also reserved in
20+
// 9.7.2 (008e09c2834b98143a8c067d4d225c90953050cf). Version-specific
21+
// additions are not rejected without a server-version parser setting.
22+
static constexpr const char* reserved[] = {
23+
"ACCESSIBLE", "ADD", "ALL", "ALTER", "ANALYZE",
24+
"AND", "AS", "ASC", "ASENSITIVE", "BEFORE",
25+
"BETWEEN", "BIGINT", "BINARY", "BLOB", "BOTH",
26+
"BY", "CALL", "CASCADE", "CASE", "CHANGE",
27+
"CHAR", "CHARACTER", "CHECK", "COLLATE", "COLUMN",
28+
"CONDITION", "CONSTRAINT", "CONTINUE", "CONVERT", "CREATE",
29+
"CROSS", "CUME_DIST", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP",
30+
"CURRENT_USER", "CURSOR", "DATABASE", "DATABASES", "DAY_HOUR",
31+
"DAY_MICROSECOND", "DAY_MINUTE", "DAY_SECOND", "DEC", "DECIMAL",
32+
"DECLARE", "DEFAULT", "DELAYED", "DELETE", "DENSE_RANK",
33+
"DESC", "DESCRIBE", "DETERMINISTIC", "DISTINCT", "DISTINCTROW",
34+
"DIV", "DOUBLE", "DROP", "DUAL", "EACH",
35+
"ELSE", "ELSEIF", "EMPTY", "ENCLOSED", "ESCAPED",
36+
"EXCEPT", "EXISTS", "EXIT", "EXPLAIN", "FALSE",
37+
"FETCH", "FIRST_VALUE", "FLOAT", "FLOAT4", "FLOAT8",
38+
"FOR", "FORCE", "FOREIGN", "FROM", "FULLTEXT",
39+
"FUNCTION", "GENERATED", "GET", "GRANT", "GROUP",
40+
"GROUPING", "GROUPS", "HAVING", "HIGH_PRIORITY", "HOUR_MICROSECOND",
41+
"HOUR_MINUTE", "HOUR_SECOND", "IF", "IGNORE", "IN",
42+
"INDEX", "INFILE", "INNER", "INOUT", "INSENSITIVE",
43+
"INSERT", "INT", "INT1", "INT2", "INT3",
44+
"INT4", "INT8", "INTEGER", "INTERSECT", "INTERVAL",
45+
"INTO", "IO_AFTER_GTIDS", "IO_BEFORE_GTIDS", "IS", "ITERATE",
46+
"JOIN", "JSON_TABLE", "KEY", "KEYS", "KILL",
47+
"LAG", "LAST_VALUE", "LATERAL", "LEAD", "LEADING",
48+
"LEAVE", "LEFT", "LIKE", "LIMIT", "LINEAR",
49+
"LINES", "LOAD", "LOCALTIME", "LOCALTIMESTAMP", "LOCK",
50+
"LONG", "LONGBLOB", "LONGTEXT", "LOOP", "LOW_PRIORITY",
51+
"MATCH", "MAXVALUE", "MEDIUMBLOB", "MEDIUMINT", "MEDIUMTEXT",
52+
"MIDDLEINT", "MINUTE_MICROSECOND", "MINUTE_SECOND", "MOD", "MODIFIES",
53+
"NATURAL", "NOT", "NO_WRITE_TO_BINLOG", "NTH_VALUE", "NTILE",
54+
"NULL", "NUMERIC", "OF", "ON", "OPTIMIZE",
55+
"OPTIMIZER_COSTS", "OPTION", "OPTIONALLY", "OR", "ORDER",
56+
"OUT", "OUTER", "OUTFILE", "OVER", "PARTITION",
57+
"PERCENT_RANK", "PRECISION", "PRIMARY", "PROCEDURE", "PURGE",
58+
"RANGE", "RANK", "READ", "READS", "READ_WRITE",
59+
"REAL", "RECURSIVE", "REFERENCES", "REGEXP", "RELEASE",
60+
"RENAME", "REPEAT", "REPLACE", "REQUIRE", "RESIGNAL",
61+
"RESTRICT", "RETURN", "REVOKE", "RIGHT", "RLIKE",
62+
"ROW", "ROWS", "ROW_NUMBER", "SCHEMA", "SCHEMAS",
63+
"SECOND_MICROSECOND", "SELECT", "SENSITIVE", "SEPARATOR", "SET",
64+
"SHOW", "SIGNAL", "SMALLINT", "SPATIAL", "SPECIFIC",
65+
"SQL", "SQLEXCEPTION", "SQLSTATE", "SQLWARNING", "SQL_BIG_RESULT",
66+
"SQL_CALC_FOUND_ROWS", "SQL_SMALL_RESULT", "SSL", "STARTING", "STORED",
67+
"STRAIGHT_JOIN", "SYSTEM", "TABLE", "TERMINATED", "THEN",
68+
"TINYBLOB", "TINYINT", "TINYTEXT", "TO", "TRAILING",
69+
"TRIGGER", "TRUE", "UNDO", "UNION", "UNIQUE",
70+
"UNLOCK", "UNSIGNED", "UPDATE", "USAGE", "USE",
71+
"USING", "UTC_DATE", "UTC_TIME", "UTC_TIMESTAMP", "VALUES",
72+
"VARBINARY", "VARCHAR", "VARCHARACTER", "VARYING", "VIRTUAL",
73+
"WHEN", "WHERE", "WHILE", "WINDOW", "WITH",
74+
"WRITE", "XOR", "YEAR_MONTH", "ZEROFILL",
75+
};
76+
size_t lo = 0, hi = sizeof(reserved) / sizeof(reserved[0]);
77+
while (lo < hi) {
78+
size_t mid = lo + (hi - lo) / 2;
79+
const char* word = reserved[mid];
80+
uint32_t i = 0;
81+
int difference = 0;
82+
for (; i < token.text.len && word[i]; ++i) {
83+
unsigned char ch = static_cast<unsigned char>(token.text.ptr[i]);
84+
if (ch >= 'a' && ch <= 'z') ch -= 'a' - 'A';
85+
difference = static_cast<int>(ch) - static_cast<unsigned char>(word[i]);
86+
if (difference) break;
87+
}
88+
if (!difference) difference = i < token.text.len ? 1 : word[i] ? -1 : 0;
89+
if (!difference) return false;
90+
if (difference < 0) hi = mid;
91+
else lo = mid + 1;
92+
}
93+
return true;
94+
}
95+
96+
} // namespace sql_parser
97+
#endif // SQL_PARSER_MYSQL_IDENTIFIER_H

0 commit comments

Comments
 (0)