git.iko.soy/parseable-macos
on master help sign in
Fix 2 bugs: UserInfo decode aborts on non-string id, results truncation never fires on default queries
47d7125iko
ParseableViewer/Models/ServerInfo.swift+14−3
@@ -85,9 +85,20 @@ struct UserInfo: Identifiable, Codable, Sendable {
85 }
85 }
86
86
87 init(from decoder: Decoder) throws {
87 init(from decoder: Decoder) throws {
88− // Handle both {"id": "x", ...} and variations
88+ // Handle both {"id": "x", ...} and a bare "x" string element.
89− if let container = try? decoder.container(keyedBy: CodingKeys.self) {
89+ if let container = try? decoder.container(keyedBy: CodingKeys.self),
90+ container.contains(.id) {
91+ // Decode `id` defensively: the server may quote it as a string or
92+ // send it as a number/other JSON. Fall back to JSONValue.displayString
93+ // so a single odd entry doesn't fail the whole user list (matching the
94+ // file's otherwise-defensive decoding).
90− self.id = try container.decode(String.self, forKey: .id)
95+ if let stringID = try? container.decode(String.self, forKey: .id) {
96+ self.id = stringID
97+ } else if let valueID = try? container.decode(JSONValue.self, forKey: .id) {
98+ self.id = valueID.displayString
99+ } else {
100+ self.id = ""
101+ }
91 self.method = try? container.decode(String.self, forKey: .method)
102 self.method = try? container.decode(String.self, forKey: .method)
92 } else {
103 } else {
93 let container = try decoder.singleValueContainer()
104 let container = try decoder.singleValueContainer()
ParseableViewer/ViewModels/QueryViewModel.swift+20−3
@@ -427,9 +427,13 @@ final class QueryViewModel {
427 queryDuration = CFAbsoluteTimeGetCurrent() - startTime
427 queryDuration = CFAbsoluteTimeGetCurrent() - startTime
428 resultCount = results.count
428 resultCount = results.count
429
429
430− // Detect if results were likely truncated (only meaningful
430+ // Detect if results were likely truncated. We know the cap for
431− // for auto-generated queries where we control the LIMIT)
431+ // auto-generated queries; for user-entered SQL (which includes the
432+ // default query `setDefaultQuery` writes into the editor — the common
433+ // stream-selection path), parse a trailing `LIMIT n` so the indicator
434+ // still fires when the row count lands exactly on the limit.
435+ let effectiveLimit = usedAutoLimit ? limit : Self.trailingLimit(in: sql)
432− resultsTruncated = usedAutoLimit && results.count == limit
436+ resultsTruncated = effectiveLimit.map { results.count == $0 } ?? false
433
437
434 // Extract columns in a single pass, with priority fields first,
438 // Extract columns in a single pass, with priority fields first,
435 // then apply any saved column configuration for this stream
439 // then apply any saved column configuration for this stream
@@ -753,6 +757,19 @@ final class QueryViewModel {
753 "\"\(identifier.replacingOccurrences(of: "\"", with: "\"\""))\""
757 "\"\(identifier.replacingOccurrences(of: "\"", with: "\"\""))\""
754 }
758 }
755
759
760+ /// Extracts the row cap from a trailing `LIMIT n` clause (tolerating a trailing
761+ /// semicolon/whitespace), used to detect when a result set was truncated.
762+ /// Returns nil when there's no recognizable trailing `LIMIT` — e.g. when the
763+ /// query uses `LIMIT n OFFSET m` — so we never report a false truncation.
764+ nonisolated static func trailingLimit(in sql: String) -> Int? {
765+ let pattern = #"(?i)\bLIMIT\s+(\d+)\s*;?\s*$"#
766+ guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
767+ let range = NSRange(sql.startIndex..<sql.endIndex, in: sql)
768+ guard let match = regex.firstMatch(in: sql, range: range),
769+ let numberRange = Range(match.range(at: 1), in: sql) else { return nil }
770+ return Int(sql[numberRange])
771+ }
772+
756 // MARK: - Query History
773 // MARK: - Query History
757
774
758 private func addToHistory(_ entry: QueryHistoryEntry) {
775 private func addToHistory(_ entry: QueryHistoryEntry) {