@@ -126,7 +126,7 @@ struct SQLTokenizer: Sendable {
126 tokens.append(Token(kind: .star, range: start..<i))
126 tokens.append(Token(kind: .star, range: start..<i))
128 // Number (digit or leading dot followed by digit)
128 // Number (digit or leading dot followed by digit)
129− case _ where c.isNumber || (c == "." && peek(sql, at: i, offset: 1)?.isNumber == true):
129+ case _ where isASCIIDigit(c) || (c == "." && peek(sql, at: i, offset: 1).map(isASCIIDigit) == true):
130 consumeNumber(&i, in: sql)
130 consumeNumber(&i, in: sql)
131 tokens.append(Token(kind: .number(String(sql[start..<i])), range: start..<i))
131 tokens.append(Token(kind: .number(String(sql[start..<i])), range: start..<i))
@@ -255,6 +255,15 @@ struct SQLTokenizer: Sendable {
255 return value // unterminated
255 return value // unterminated
258+ /// SQL number literals use only ASCII digits. `Character.isNumber` also
259+ /// matches Unicode numerics (`²`, `½`, Arabic-Indic digits, etc.), which
260+ /// DataFusion's lexer rejects — accepting them here would tokenize those
261+ /// characters as numbers and diverge token offsets from the server, throwing
262+ /// off `errorHighlightRange` mapping.
263+ private static func isASCIIDigit(_ c: Character) -> Bool {
264+ c.isASCII && c.isNumber
258 private static func consumeNumber(_ i: inout String.Index, in sql: String) {
267 private static func consumeNumber(_ i: inout String.Index, in sql: String) {
259 // Digits with at most one decimal point, so malformed input like
268 // Digits with at most one decimal point, so malformed input like
260 // `1.2.3` doesn't get swallowed into a single number token.
269 // `1.2.3` doesn't get swallowed into a single number token.
@@ -265,7 +274,7 @@ struct SQLTokenizer: Sendable {
265 seenDot = true
274 seenDot = true
266 return true
275 return true
268− return c.isNumber
277+ return isASCIIDigit(c)
270 // Exponent — only consume `e`/`E` (and an optional sign) when actual
279 // Exponent — only consume `e`/`E` (and an optional sign) when actual
271 // exponent digits follow, so `1e` or `1e+` aren't swallowed whole and
280 // exponent digits follow, so `1e` or `1e+` aren't swallowed whole and
@@ -275,9 +284,9 @@ struct SQLTokenizer: Sendable {
275 if lookahead < sql.endIndex, sql[lookahead] == "+" || sql[lookahead] == "-" {
284 if lookahead < sql.endIndex, sql[lookahead] == "+" || sql[lookahead] == "-" {
276 sql.formIndex(after: &lookahead)
285 sql.formIndex(after: &lookahead)
278− if lookahead < sql.endIndex, sql[lookahead].isNumber {
287+ if lookahead < sql.endIndex, isASCIIDigit(sql[lookahead]) {
279 i = lookahead
288 i = lookahead
280− advance(&i, in: sql, while: { $0.isNumber })
289+ advance(&i, in: sql, while: { isASCIIDigit($0) })