git.iko.soy/parseable-macos
on master help sign in
Fix 2 bugs: export filter mismatch, Unicode digits in SQL tokenizer
395f7afiko
ParseableViewer/Models/SQLTokenizer.swift+13−4
@@ -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))
127
127
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))
132
132
@@ -255,6 +255,15 @@ struct SQLTokenizer: Sendable {
255 return value // unterminated
255 return value // unterminated
256 }
256 }
257
257
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+
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
267 }
276 }
268− return c.isNumber
277+ return isASCIIDigit(c)
269 })
278 })
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)
277 }
286 }
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) })
281 }
290 }
282 }
291 }
283 }
292 }
ParseableViewer/Views/QueryView.swift+5−2
@@ -89,7 +89,7 @@ struct QueryView: View {
89 } label: {
89 } label: {
90 Image(systemName: "square.and.arrow.up")
90 Image(systemName: "square.and.arrow.up")
91 }
91 }
92− .disabled(viewModel.results.isEmpty)
92+ .disabled(viewModel.filteredResults.isEmpty)
93 .accessibilityLabel("Export results")
93 .accessibilityLabel("Export results")
94 .help("Export results")
94 .help("Export results")
95
95
@@ -404,7 +404,10 @@ struct QueryView: View {
404 panel.begin { result in
404 panel.begin { result in
405 guard result == .OK, let url = panel.url else { return }
405 guard result == .OK, let url = panel.url else { return }
406 let isCSV = url.pathExtension.lowercased() == "csv"
406 let isCSV = url.pathExtension.lowercased() == "csv"
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.
407− let results = viewModel.results
410+ let results = viewModel.filteredResults
408 let columns = viewModel.visibleColumns
411 let columns = viewModel.visibleColumns
409 Task.detached(priority: .userInitiated) {
412 Task.detached(priority: .userInitiated) {
410 let content: String?
413 let content: String?