@@ -2,6 +2,7 @@ import Foundation
2import SwiftUI
2import SwiftUI
5final class QueryViewModel {
6final class QueryViewModel {
6 var sqlQuery = "" {
7 var sqlQuery = "" {
@@ -24,14 +25,20 @@ final class QueryViewModel {
24 // re-applies the filter.
25 // re-applies the filter.
25 updateFilteredResults()
26 updateFilteredResults()
26 let snapshot = results
27 let snapshot = results
28+ // The class is @MainActor, so this Task inherits main-actor isolation —
29+ // its mutations of cachedSearchTexts/filteredResults are safely serialized
30+ // with SwiftUI reads. Only the pure (and potentially large) displayString
31+ // mapping is pushed off-main via Task.detached so it doesn't block the UI.
27− searchTextTask = Task {
32+ searchTextTask = Task { [weak self] in
33+ let texts = await Task.detached(priority: .userInitiated) {
28− let texts = snapshot.map { record in
34+ snapshot.map { record in
29− record.values.map { $0.displayString }.joined(separator: " ")
35+ record.values.map { $0.displayString }.joined(separator: " ")
31− guard !Task.isCancelled else { return }
38+ guard let self, !Task.isCancelled else { return }
32− cachedSearchTexts = texts
39+ self.cachedSearchTexts = texts
33− if !filterText.isEmpty {
40+ if !self.filterText.isEmpty {
34− updateFilteredResults()
41+ self.updateFilteredResults()
@@ -551,7 +558,7 @@ final class QueryViewModel {
551 /// Projects each record down to the given columns so JSON export, like CSV,
558 /// Projects each record down to the given columns so JSON export, like CSV,
552 /// respects hidden columns instead of leaking every field. Safe to call from
559 /// respects hidden columns instead of leaking every field. Safe to call from
553 /// any thread.
560 /// any thread.
554− static func projectRecords(_ records: [LogRecord], to columns: [String]) -> [LogRecord] {
561+ nonisolated static func projectRecords(_ records: [LogRecord], to columns: [String]) -> [LogRecord] {
555 // No column list means no restriction (columns not computed yet) — export
562 // No column list means no restriction (columns not computed yet) — export
556 // the full records rather than stripping every field to an empty object.
563 // the full records rather than stripping every field to an empty object.
557 guard !columns.isEmpty else { return records }
564 guard !columns.isEmpty else { return records }
@@ -560,7 +567,7 @@ final class QueryViewModel {
562 /// Builds a CSV string from records and columns. Safe to call from any thread.
569 /// Builds a CSV string from records and columns. Safe to call from any thread.
563− static func buildCSV(records: [LogRecord], columns: [String]) -> String {
570+ nonisolated static func buildCSV(records: [LogRecord], columns: [String]) -> String {
564 guard !records.isEmpty, !columns.isEmpty else { return "" }
571 guard !records.isEmpty, !columns.isEmpty else { return "" }
566 var csv = String()
573 var csv = String()
@@ -586,7 +593,7 @@ final class QueryViewModel {
586 return csv
593 return csv
589− private static func escapeCSV(_ value: String) -> String {
596+ nonisolated private static func escapeCSV(_ value: String) -> String {
590 // Check at the Unicode scalar level to correctly detect \r and \n
597 // Check at the Unicode scalar level to correctly detect \r and \n
591 // inside \r\n (CRLF) grapheme clusters, which Swift's String.contains
598 // inside \r\n (CRLF) grapheme clusters, which Swift's String.contains
592 // treats as a single Character that doesn't match "\r" or "\n" alone.
599 // treats as a single Character that doesn't match "\r" or "\n" alone.
@@ -731,7 +738,7 @@ final class QueryViewModel {
733 /// Escapes a SQL identifier by doubling internal double-quotes, then wrapping in double-quotes.
740 /// Escapes a SQL identifier by doubling internal double-quotes, then wrapping in double-quotes.
734− static func escapeSQLIdentifier(_ identifier: String) -> String {
741+ nonisolated static func escapeSQLIdentifier(_ identifier: String) -> String {
735 "\"\(identifier.replacingOccurrences(of: "\"", with: "\"\""))\""
742 "\"\(identifier.replacingOccurrences(of: "\"", with: "\"\""))\""