git.iko.soy/parseable-macos
on master help sign in
Fix 5 bugs: zero-column table, stale tail filter, large-int fingerprint, inf/nan display, Unicode identifiers
7ff5b74iko
ParseableViewer/Models/JSONValue.swift+4
@@ -50,6 +50,10 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
50 case .bool(let v): return v ? "true" : "false"
50 case .bool(let v): return v ? "true" : "false"
51 case .int(let v): return String(v)
51 case .int(let v): return String(v)
52 case .double(let v):
52 case .double(let v):
53+ // Non-finite doubles have no display (and no JSON) representation;
54+ // render them as "null" to match jsonNumber/exportString rather than
55+ // letting String(_:) emit "inf"/"-inf"/"nan" into the UI and CSV cells.
56+ if !v.isFinite { return "null" }
53 if v == v.rounded() && abs(v) < 1e15 {
57 if v == v.rounded() && abs(v) < 1e15 {
54 return String(format: "%.0f", v)
58 return String(format: "%.0f", v)
55 }
59 }
ParseableViewer/ViewModels/LiveTailViewModel.swift+20−1
@@ -195,6 +195,10 @@ final class LiveTailViewModel {
195 hiddenColumns = []
195 hiddenColumns = []
196 savedHiddenColumns = []
196 savedHiddenColumns = []
197 columnFilters = []
197 columnFilters = []
198+ // Clear the text filter too. Leaving it set while clearing the column-filter
199+ // chips is inconsistent and would let a stale search from a previous stream
200+ // make the fresh tail come up appearing empty or partially filtered.
201+ filterText = ""
198 currentStream = stream
202 currentStream = stream
199 rebuildFilteredEntries()
203 rebuildFilteredEntries()
200
204
@@ -246,6 +250,9 @@ final class LiveTailViewModel {
246 hiddenColumns = []
250 hiddenColumns = []
247 savedHiddenColumns = []
251 savedHiddenColumns = []
248 columnFilters = []
252 columnFilters = []
253+ // Match start(): clear the text filter alongside the column-filter chips so
254+ // a clear()ed tail isn't immediately re-hidden by a lingering search string.
255+ filterText = ""
249 rebuildFilteredEntries()
256 rebuildFilteredEntries()
250 }
257 }
251
258
@@ -580,7 +587,19 @@ final class LiveTailViewModel {
580 // (with NaN folded to a single bit pattern) so a field that serializes as `5`
587 // (with NaN folded to a single bit pattern) so a field that serializes as `5`
581 // in one poll and `5.0` in another doesn't bypass dedup as a phantom row.
588 // in one poll and `5.0` in another doesn't bypass dedup as a phantom row.
582 case .int(let i):
589 case .int(let i):
590+ // If the integer round-trips losslessly through Double, hash via the
591+ // Double path so `.int(5)` and `.double(5.0)` (which are ==) share a
592+ // fingerprint. Integers beyond 2^53 lose precision in Double, so two
593+ // distinct values (e.g. 2^53 and 2^53+1) would otherwise collide and
594+ // the second would be silently dropped as a phantom duplicate — the
595+ // dedup set has no `==` fallback. No Double can equal such a
596+ // non-representable integer, so hashing its exact 64-bit pattern can't
597+ // wrongly merge it with a `.double`.
598+ if let exact = Int(exactly: Double(i)), exact == i {
583− hashNumberBits(Double(i).bitPattern, h0: &h0, h1: &h1)
599+ hashNumberBits(Double(i).bitPattern, h0: &h0, h1: &h1)
600+ } else {
601+ hashNumberBits(UInt64(bitPattern: Int64(i)), h0: &h0, h1: &h1)
602+ }
584 case .double(let d):
603 case .double(let d):
585 // Canonicalize NaN (so all NaNs fold together) AND signed zero: JSONValue
604 // Canonicalize NaN (so all NaNs fold together) AND signed zero: JSONValue
586 // equality treats `-0.0 == 0.0`, but their raw bit patterns differ, which
605 // equality treats `-0.0 == 0.0`, but their raw bit patterns differ, which
ParseableViewer/ViewModels/QueryViewModel.swift+11
@@ -291,6 +291,17 @@ final class QueryViewModel {
291 }
291 }
292 columnOrder = merged
292 columnOrder = merged
293 hiddenColumns = config.hidden.intersection(extractedSet)
293 hiddenColumns = config.hidden.intersection(extractedSet)
294+
295+ // The `visibleCount > 1` guard in toggleColumnVisibility only covers
296+ // user toggles. A saved config can hide every currently-extracted column
297+ // (e.g. the stream's schema shrank so only previously-hidden columns
298+ // remain), which would render the results table with zero columns and
299+ // leave `updateSQLColumns` early-returning on the empty case. Keep at
300+ // least the first column visible. Mirrors LiveTailViewModel.updateColumns.
301+ if !columnOrder.isEmpty, hiddenColumns.count >= columnOrder.count,
302+ let firstColumn = columnOrder.first {
303+ hiddenColumns.remove(firstColumn)
304+ }
294 }
305 }
295
306
296 private(set) var filteredResults: [LogRecord] = []
307 private(set) var filteredResults: [LogRecord] = []
ParseableViewer/Views/SQLAutoComplete.swift+6−1
@@ -263,7 +263,12 @@ enum SQLCompletionProvider {
263 (ch >= 0x41 && ch <= 0x5A) || // A-Z
263 (ch >= 0x41 && ch <= 0x5A) || // A-Z
264 (ch >= 0x61 && ch <= 0x7A) || // a-z
264 (ch >= 0x61 && ch <= 0x7A) || // a-z
265 (ch >= 0x30 && ch <= 0x39) || // 0-9
265 (ch >= 0x30 && ch <= 0x39) || // 0-9
266− ch == 0x5F // _
266+ ch == 0x5F || // _
267+ ch >= 0x80 // any non-ASCII code unit: Unicode letters
268+ // and digits in identifiers (e.g. accented or CJK schema field names), plus
269+ // both halves of a surrogate pair. SQL delimiters — whitespace, operators,
270+ // quotes, parens, commas — are all ASCII (< 0x80), so this only sweeps in
271+ // genuine identifier characters and won't run a word past a token boundary.
267 }
272 }
268}
273}
269
274