@@ -16,7 +16,10 @@ class Tokenizer {
1616 end_ = input + len;
1717 has_peeked_ = false ;
1818 has_error_ = false ;
19+ has_fatal_error_ = false ;
1920 has_user_variables_ = false ;
21+ paren_depth_ = 0 ;
22+ first_open_paren_ = nullptr ;
2023 error_source_ = {};
2124 }
2225
@@ -25,6 +28,7 @@ class Tokenizer {
2528 // from "the input was syntactically invalid" and surface the latter
2629 // as ParseResult::ERROR rather than PARTIAL.
2730 bool has_error () const { return has_error_; }
31+ bool has_fatal_error () const { return has_fatal_error_; }
2832 bool has_user_variables () const { return has_user_variables_; }
2933 StringRef error_source () const { return error_source_; }
3034
@@ -37,6 +41,10 @@ class Tokenizer {
3741 has_error_ = true ;
3842 if (error_source_.empty ()) error_source_ = source;
3943 }
44+ void flag_fatal_error_at (StringRef source) {
45+ has_fatal_error_ = true ;
46+ flag_error_at (source);
47+ }
4048
4149 Token next_token () {
4250 if (has_peeked_) {
@@ -75,7 +83,10 @@ class Tokenizer {
7583 Token peeked_;
7684 bool has_peeked_ = false ;
7785 bool has_error_ = false ;
86+ bool has_fatal_error_ = false ;
7887 bool has_user_variables_ = false ;
88+ uint32_t paren_depth_ = 0 ;
89+ const char * first_open_paren_ = nullptr ;
7990 StringRef error_source_;
8091
8192 uint32_t offset () const {
@@ -103,8 +114,14 @@ class Tokenizer {
103114 continue ;
104115 }
105116
106- // -- line comment (MySQL requires space after --, PgSQL doesn't but we handle both)
107- if (c == ' -' && peek_char (1 ) == ' -' ) {
117+ // PostgreSQL accepts any `--` line comment. MySQL requires the
118+ // second dash to be followed by whitespace or a control byte.
119+ const bool dash_comment = c == ' -' && peek_char (1 ) == ' -' &&
120+ (D == Dialect::PostgreSQL ||
121+ (cursor_ + 2 < end_ &&
122+ (static_cast <unsigned char >(peek_char (2 )) <= 0x20 ||
123+ static_cast <unsigned char >(peek_char (2 )) == 0x7f )));
124+ if (dash_comment) {
108125 cursor_ += 2 ;
109126 while (cursor_ < end_ && *cursor_ != ' \n ' ) ++cursor_;
110127 continue ;
@@ -121,6 +138,7 @@ class Tokenizer {
121138
122139 // /* block comment */
123140 if (c == ' /' && peek_char (1 ) == ' *' ) {
141+ const char * comment_start = cursor_;
124142 cursor_ += 2 ;
125143 if constexpr (D == Dialect::PostgreSQL) {
126144 // PostgreSQL supports nested block comments
@@ -136,15 +154,36 @@ class Tokenizer {
136154 ++cursor_;
137155 }
138156 }
157+ if (depth != 0 ) {
158+ flag_fatal_error_at (StringRef{comment_start,
159+ static_cast <uint32_t >(end_ - comment_start)});
160+ }
139161 } else {
140162 // MySQL: no nesting
163+ const bool executable = cursor_ < end_ && *cursor_ == ' !' ;
164+ bool has_user_variable_marker = false ;
165+ bool closed = false ;
141166 while (cursor_ < end_) {
142167 if (*cursor_ == ' *' && peek_char (1 ) == ' /' ) {
143168 cursor_ += 2 ;
169+ closed = true ;
144170 break ;
145171 }
172+ if (*cursor_ == ' @' ) has_user_variable_marker = true ;
146173 ++cursor_;
147174 }
175+ if (!closed) {
176+ flag_fatal_error_at (StringRef{comment_start,
177+ static_cast <uint32_t >(end_ - comment_start)});
178+ }
179+ // Versioned comments execute as SQL on MySQL. Until their
180+ // contents are parsed exactly, preserve any possible user
181+ // variable use and force conservative classification.
182+ if (executable && has_user_variable_marker) {
183+ has_user_variables_ = true ;
184+ flag_fatal_error_at (StringRef{comment_start,
185+ static_cast <uint32_t >(cursor_ - comment_start)});
186+ }
148187 }
149188 continue ;
150189 }
@@ -163,6 +202,15 @@ class Tokenizer {
163202 has_error_ = true ;
164203 if (error_source_.empty ()) error_source_ = StringRef{source_start, source_len};
165204 }
205+ if (type == TokenType::TK_LPAREN ) {
206+ if (paren_depth_ == 0 ) first_open_paren_ = source_start;
207+ ++paren_depth_;
208+ } else if (type == TokenType::TK_RPAREN && paren_depth_ > 0 ) {
209+ if (--paren_depth_ == 0 ) first_open_paren_ = nullptr ;
210+ } else if (type == TokenType::TK_EOF && paren_depth_ > 0 ) {
211+ flag_fatal_error_at (StringRef{first_open_paren_,
212+ static_cast <uint32_t >(end_ - first_open_paren_)});
213+ }
166214 if (type == TokenType::TK_USER_VARIABLE ) has_user_variables_ = true ;
167215 return Token{type, StringRef{text_start, text_len},
168216 StringRef{source_start, source_len},
0 commit comments