git.iko.soy/parseable-macos
on master help sign in
Fix 2 bugs: export filter mismatch, Unicode digits in SQL tokenizer
commit395f7aff13e8782b7686ce2b32f6dfa86bcc3e2c authoriko <mail@iko.soy> dateSun 21 Jun 2026, 14:30 (3 months ago) parent7ff5b74
2 files changed+18−6
ParseableViewer/Models/SQLTokenizer.swift+13−4 ParseableViewer/Views/QueryView.swift+5−2
ParseableViewer/Models/SQLTokenizer.swift+13−4
@@ -126,7 +126,7 @@ struct SQLTokenizer: Sendable {
126126 tokens.append(Token(kind: .star, range: start..<i))
127127
128128 // 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):
130130 consumeNumber(&i, in: sql)
131131 tokens.append(Token(kind: .number(String(sql[start..<i])), range: start..<i))
132132
@@ -255,6 +255,15 @@ struct SQLTokenizer: Sendable {
255255 return value // unterminated
256256 }
257257
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
265+ }
266+
258267 private static func consumeNumber(_ i: inout String.Index, in sql: String) {
259268 // Digits with at most one decimal point, so malformed input like
260269 // `1.2.3` doesn't get swallowed into a single number token.
@@ -265,7 +274,7 @@ struct SQLTokenizer: Sendable {
265274 seenDot = true
266275 return true
267276 }
268− return c.isNumber
277+ return isASCIIDigit(c)
269278 })
270279 // Exponent — only consume `e`/`E` (and an optional sign) when actual
271280 // exponent digits follow, so `1e` or `1e+` aren't swallowed whole and
@@ -275,9 +284,9 @@ struct SQLTokenizer: Sendable {
275284 if lookahead < sql.endIndex, sql[lookahead] == "+" || sql[lookahead] == "-" {
276285 sql.formIndex(after: &lookahead)
277286 }
278− if lookahead < sql.endIndex, sql[lookahead].isNumber {
287+ if lookahead < sql.endIndex, isASCIIDigit(sql[lookahead]) {
279288 i = lookahead
280− advance(&i, in: sql, while: { $0.isNumber })
289+ advance(&i, in: sql, while: { isASCIIDigit($0) })
281290 }
282291 }
283292 }
ParseableViewer/Views/QueryView.swift+5−2
@@ -89,7 +89,7 @@ struct QueryView: View {
8989 } label: {
9090 Image(systemName: "square.and.arrow.up")
9191 }
92− .disabled(viewModel.results.isEmpty)
92+ .disabled(viewModel.filteredResults.isEmpty)
9393 .accessibilityLabel("Export results")
9494 .help("Export results")
9595
@@ -404,7 +404,10 @@ struct QueryView: View {
404404 panel.begin { result in
405405 guard result == .OK, let url = panel.url else { return }
406406 let isCSV = url.pathExtension.lowercased() == "csv"
407− let results = viewModel.results
407+ // Export what's on screen: the table renders `filteredResults`, so a
408+ // user who has typed a result filter expects the export to contain
409+ // those rows, not the full unfiltered set.
410+ let results = viewModel.filteredResults
408411 let columns = viewModel.visibleColumns
409412 Task.detached(priority: .userInitiated) {
410413 let content: String?