From ef53faa037742f475953c8bf492c8aa9cefede2d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 21 Aug 2025 17:18:50 -0700 Subject: [PATCH 01/15] wip --- Examples/Reminders/ReminderRow.swift | 43 ++++++----- Examples/Reminders/RemindersDetail.swift | 16 ++--- Examples/Reminders/Schema.swift | 91 ++++++++++++++++++++---- Examples/Reminders/SearchReminders.swift | 51 ++++++++----- 4 files changed, 143 insertions(+), 58 deletions(-) diff --git a/Examples/Reminders/ReminderRow.swift b/Examples/Reminders/ReminderRow.swift index e2fdc9b7..0522c1e0 100644 --- a/Examples/Reminders/ReminderRow.swift +++ b/Examples/Reminders/ReminderRow.swift @@ -8,7 +8,8 @@ struct ReminderRow: View { let reminder: Reminder let remindersList: RemindersList let showCompleted: Bool - let tags: [String] + let tags: String + let title: String? @State var editReminder: Reminder.Draft? @State var isCompleted: Bool @@ -22,7 +23,8 @@ struct ReminderRow: View { reminder: Reminder, remindersList: RemindersList, showCompleted: Bool, - tags: [String] + tags: String, + title: String? = nil ) { self.color = color self.isPastDue = isPastDue @@ -31,6 +33,7 @@ struct ReminderRow: View { self.remindersList = remindersList self.showCompleted = showCompleted self.tags = tags + self.title = title self.isCompleted = reminder.isCompleted } @@ -44,10 +47,10 @@ struct ReminderRow: View { .padding([.trailing], 5) } VStack(alignment: .leading) { - title(for: reminder) + title(for: reminder, title: title) if !notes.isEmpty { - Text(notes) + highlight(notes) .font(.subheadline) .foregroundStyle(.gray) .lineLimit(2) @@ -145,28 +148,34 @@ struct ReminderRow: View { } private var subtitleText: Text { - let tagsText = tags.reduce(Text(reminder.dueDate == nil ? "" : " ")) { result, tag in - result + Text("#\(tag) ") - } - return - (dueText - + tagsText - .foregroundStyle(.gray) - .bold()) - .font(.callout) + Text( + """ + \(dueText)\(reminder.dueDate == nil ? "" : " ")\(highlight(tags).foregroundStyle(.gray)) + """ + ) + .font(.callout) } - private func title(for reminder: Reminder) -> some View { - return HStack(alignment: .firstTextBaseline) { + @ViewBuilder + private func title(for reminder: Reminder, title: String?) -> some View { + HStack(alignment: .firstTextBaseline) { if let priority = reminder.priority { Text(String(repeating: "!", count: priority.rawValue)) .foregroundStyle(isCompleted ? .gray : remindersList.color) } - Text(reminder.title) + highlight(title ?? reminder.title) .foregroundStyle(isCompleted ? .gray : .primary) } .font(.title3) } + + func highlight(_ text: String) -> Text { + if let attributedText = try? AttributedString(markdown: text) { + Text(attributedText) + } else { + Text(text) + } + } } struct ReminderRowPreview: PreviewProvider { @@ -190,7 +199,7 @@ struct ReminderRowPreview: PreviewProvider { reminder: reminder, remindersList: remindersList, showCompleted: true, - tags: ["point-free", "adulting"] + tags: "#point-free #adulting" ) } } diff --git a/Examples/Reminders/RemindersDetail.swift b/Examples/Reminders/RemindersDetail.swift index 870b5716..a2c979e2 100644 --- a/Examples/Reminders/RemindersDetail.swift +++ b/Examples/Reminders/RemindersDetail.swift @@ -46,7 +46,7 @@ class RemindersDetailModel: HashableObject { let ids = Array(ids.enumerated()) let (first, rest) = (ids.first!, ids.dropFirst()) $0.position = - rest + rest .reduce(Case($0.id).when(first.element, then: first.offset)) { cases, id in cases.when(id.element, then: id.offset) } @@ -58,7 +58,7 @@ class RemindersDetailModel: HashableObject { $ordering.withLock { $0 = .manual } await updateQuery() } - + private func updateQuery() async { await withErrorReporting { try await $reminderRows.load(remindersQuery, animation: .default) @@ -66,7 +66,6 @@ class RemindersDetailModel: HashableObject { } private var remindersQuery: some StructuredQueriesCore.Statement { - let query = Reminder .where { if !showCompleted { @@ -95,16 +94,16 @@ class RemindersDetailModel: HashableObject { } } .join(RemindersList.all) { $0.remindersListID.eq($3.id) } + .join(ReminderText.all) { $0.id.eq($4.reminderID) } .select { Row.Columns( reminder: $0, remindersList: $3, isPastDue: $0.isPastDue, - notes: $0.inlineNotes.substr(0, 200), - tags: #sql("\($2.jsonTitles)") + notes: $4.notes.substr(0, 200), + tags: $4.tags ) } - return query } enum Ordering: String, CaseIterable { @@ -141,8 +140,7 @@ class RemindersDetailModel: HashableObject { let remindersList: RemindersList let isPastDue: Bool let notes: String - @Column(as: [String].JSONRepresentation.self) - let tags: [String] + let tags: String } } @@ -191,7 +189,7 @@ struct RemindersDetailView: View { reminder: Reminder.Draft(remindersListID: remindersList.id), remindersList: remindersList ) - .navigationTitle("New Reminder") + .navigationTitle("New Reminder") } } } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index ab54ffff..c2a08f78 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -49,12 +49,6 @@ enum Priority: Int, Codable, QueryBindable { extension Reminder { static let incomplete = Self.where { !$0.isCompleted } - static func searching(_ text: String) -> Where { - Self.where { - $0.title.collate(.nocase).contains(text) - || $0.notes.collate(.nocase).contains(text) - } - } static let withTags = group(by: \.id) .leftJoin(ReminderTag.all) { $0.id.eq($1.reminderID) } .leftJoin(Tag.all) { $1.tagID.eq($2.primaryKey) } @@ -72,9 +66,6 @@ extension Reminder.TableColumns { var isScheduled: some QueryExpression { !isCompleted && dueDate.isNot(nil) } - var inlineNotes: some QueryExpression { - notes.replace("\n", " ") - } } extension Tag { @@ -83,12 +74,6 @@ extension Tag { .leftJoin(Reminder.all) { $1.reminderID.eq($2.id) } } -extension Tag.TableColumns { - var jsonTitles: some QueryExpression<[String].JSONRepresentation> { - self.title.jsonGroupArray(filter: self.title.isNot(nil)) - } -} - @Table("remindersTags") struct ReminderTag: Hashable, Identifiable { let id: UUID @@ -96,6 +81,14 @@ struct ReminderTag: Hashable, Identifiable { var tagID: Tag.ID } +@Table @Selection +struct ReminderText: StructuredQueries.FTS5 { + let reminderID: Reminder.ID + let title: String + let notes: String + let tags: String +} + func appDatabase() throws -> any DatabaseWriter { @Dependency(\.context) var context let database: any DatabaseWriter @@ -173,6 +166,18 @@ func appDatabase() throws -> any DatabaseWriter { """ ) .execute(db) + try #sql( + """ + CREATE VIRTUAL TABLE "reminderTexts" USING fts5( + "reminderID" UNINDEXED, + "title", + "notes", + "tags", + tokenize = 'trigram' + ) + """ + ) + .execute(db) } try migrator.migrate(database) @@ -188,12 +193,14 @@ func appDatabase() throws -> any DatabaseWriter { .update { $0.position = RemindersList.select { ($0.position.max() ?? -1) + 1} } }) .execute(db) + try Reminder.createTemporaryTrigger(after: .insert { new in Reminder .find(new.id) .update { $0.position = Reminder.select { ($0.position.max() ?? -1) + 1} } }) .execute(db) + try RemindersList.createTemporaryTrigger(after: .delete { _ in RemindersList.insert { RemindersList.Draft( @@ -205,6 +212,60 @@ func appDatabase() throws -> any DatabaseWriter { !RemindersList.exists() }) .execute(db) + + try Reminder.createTemporaryTrigger(after: .insert { new in + ReminderText.insert { + ReminderText.Columns( + reminderID: new.id, + title: new.title, + notes: new.notes.replace("\n", " "), + tags: "" + ) + } + }) + .execute(db) + + try Reminder.createTemporaryTrigger(after: .update { + ($0.title, $0.notes) + } forEachRow: { _, new in + ReminderText + .where { $0.reminderID.eq(new.id) } + .update { + $0.title = new.title + $0.notes = new.notes.replace("\n", " ") + } + }) + .execute(db) + + try Reminder.createTemporaryTrigger(after: .delete { old in + ReminderText + .where { $0.reminderID.eq(old.id) } + .delete() + }) + .execute(db) + + func updateReminderTextTags( + for reminderID: some QueryExpression + ) -> UpdateOf { + ReminderText + .where { $0.reminderID.eq(reminderID) } + .update { + $0.tags = ReminderTag + .where { $0.reminderID.eq(reminderID) } + .join(Tag.all) { $0.tagID.eq($1.primaryKey) } + .select { ("#" + $1.title).groupConcat(" ") ?? "" } + } + } + + try ReminderTag.createTemporaryTrigger(after: .insert { new in + updateReminderTextTags(for: new.reminderID) + }) + .execute(db) + + try ReminderTag.createTemporaryTrigger(after: .delete { old in + updateReminderTextTags(for: old.reminderID) + }) + .execute(db) } return database diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 1c73ba6b..ed0e0deb 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -26,8 +26,7 @@ class SearchRemindersModel { withErrorReporting { try database.write { db in try Reminder - .searching(searchText) - .where(\.isCompleted) + .where { $0.isCompleted && $0.id.in(baseQuery.select { reminder, _ in reminder.id }) } .where { if let monthsAgo { #sql("\($0.dueDate) < date('now', '-\(raw: monthsAgo) months')") @@ -39,35 +38,52 @@ class SearchRemindersModel { } } + private var baseQuery: SelectOf { + let searchText = searchText + .split(separator: " ") + .map { #""\#($0.replacingOccurrences(of: #"""#, with: #""""#))""# } + .joined(separator: " ") + return Reminder + .join(ReminderText.all) { $0.id.eq($1.reminderID) } + .where { + if !searchText.isEmpty { + $1.match(searchText) + } + } + } + private func updateQuery() async { await withErrorReporting { if searchText.isEmpty { showCompletedInSearchResults = false } + + let baseQuery = baseQuery try await $completedCount.load( - Reminder.searching(searchText) - .where(\.isCompleted) + baseQuery + .where { reminder, _ in reminder.isCompleted } .count(), animation: .default ) try await $reminders.load( - Reminder - .searching(searchText) - .where { + baseQuery + .where { reminder, _ in if !showCompletedInSearchResults { - !$0.isCompleted + !reminder.isCompleted } } - .order { ($0.isCompleted, $0.dueDate) } - .withTags - .join(RemindersList.all) { $0.remindersListID.eq($3.id) } + .order { reminder, _ in + (reminder.isCompleted, reminder.dueDate) + } + .join(RemindersList.all) { $0.remindersListID.eq($2.id) } .select { Row.Columns( isPastDue: $0.isPastDue, - notes: $0.inlineNotes, + notes: $1.notes.snippet("**", "**", "...", 64).replace("\n", " "), reminder: $0, - remindersList: $3, - tags: #sql("\($2.jsonTitles)") + remindersList: $2, + tags: $1.tags.highlight("**", "**"), + title: $1.title.highlight("**", "**") ) }, animation: .default @@ -82,8 +98,8 @@ class SearchRemindersModel { let notes: String let reminder: Reminders.Reminder let remindersList: RemindersList - @Column(as: [String].JSONRepresentation.self) - let tags: [String] + let tags: String + let title: String } } @@ -134,7 +150,8 @@ struct SearchRemindersView: View { reminder: reminder.reminder, remindersList: reminder.remindersList, showCompleted: model.showCompletedInSearchResults, - tags: reminder.tags + tags: reminder.tags, + title: reminder.title ) } } From dad3b698b3a19d39d686b80762457a984e23751a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 21 Aug 2025 23:20:50 -0700 Subject: [PATCH 02/15] wip --- Examples/Reminders/RemindersLists.swift | 18 ++- Examples/Reminders/Schema.swift | 1 + Examples/Reminders/SearchReminders.swift | 187 ++++++++++++++++++----- 3 files changed, 162 insertions(+), 44 deletions(-) diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index c9889ad5..e6daf88c 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -173,7 +173,9 @@ struct RemindersListsView: View { var body: some View { List { - if model.searchRemindersModel.searchText.isEmpty { + if model.searchRemindersModel.isSearching { + SearchRemindersView(model: model.searchRemindersModel) + } else { Section { Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 16) { GridRow { @@ -269,8 +271,6 @@ struct RemindersListsView: View { .padding([.leading, .trailing], 4) } .listRowInsets(EdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12)) - } else { - SearchRemindersView(model: model.searchRemindersModel) } } .onAppear { @@ -328,7 +328,17 @@ struct RemindersListsView: View { } .presentationDetents([.medium]) } - .searchable(text: $model.searchRemindersModel.searchText) + .searchable( + text: $model.searchRemindersModel.searchText, + tokens: $model.searchRemindersModel.searchTokens + ) { token in + switch token.kind { + case .near: + Text(token.rawValue) + case .tag: + Text("#\(token.rawValue)") + } + } .navigationDestination(item: $model.destination.detail) { detailModel in RemindersDetailView(model: detailModel) } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index c2a08f78..9403df9c 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -251,6 +251,7 @@ func appDatabase() throws -> any DatabaseWriter { .where { $0.reminderID.eq(reminderID) } .update { $0.tags = ReminderTag + .order(by: \.tagID) .where { $0.reminderID.eq(reminderID) } .join(Tag.all) { $0.tagID.eq($1.primaryKey) } .select { ("#" + $1.title).groupConcat(" ") ?? "" } diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index ed0e0deb..940cc3e8 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -5,21 +5,60 @@ import SwiftUI @MainActor @Observable class SearchRemindersModel { - var showCompletedInSearchResults = false + var showCompletedInSearchResults = false { + didSet { + searchTask = Task { try await updateQuery(debounce: false) } + } + } + var searchText = "" { didSet { - Task { await updateQuery() } + if oldValue != searchText { + if searchText.hasSuffix("\t") { + searchTokens.append(Token(kind: .near, rawValue: String(searchText.dropLast()))) + searchText = "" + } + + searchTask = Task { try await updateQuery() } + } + } + } + + var searchTokens: [Token] = [] { + didSet { + if oldValue != searchTokens { + searchTask = Task { try await updateQuery() } + } } } - @ObservationIgnored @FetchOne var completedCount: Int = 0 - @ObservationIgnored @FetchAll var reminders: [Row] + var isSearching: Bool { + !searchText.isEmpty || !searchTokens.isEmpty + } + + var searchTask: Task? { + willSet { + searchTask?.cancel() + } + } + + @ObservationIgnored @Dependency(\.continuousClock) private var clock @ObservationIgnored @Dependency(\.defaultDatabase) private var database - func showCompletedButtonTapped() async { + @ObservationIgnored @Fetch var searchResults = SearchRequest.Value() + + @ObservationIgnored @FetchAll(Tag.none) var tags + + func showCompletedButtonTapped() async throws { showCompletedInSearchResults.toggle() - await updateQuery() + try await updateQuery() + } + + func tagButtonTapped(_ tag: Tag) { + guard !searchText.isEmpty else { return } + searchTokens.append(Token(kind: .tag, rawValue: tag.title)) + searchText = "" } func deleteCompletedReminders(monthsAgo: Int? = nil) { @@ -39,34 +78,86 @@ class SearchRemindersModel { } private var baseQuery: SelectOf { - let searchText = searchText + let searchText = + searchText .split(separator: " ") .map { #""\#($0.replacingOccurrences(of: #"""#, with: #""""#))""# } .joined(separator: " ") - return Reminder + return + Reminder .join(ReminderText.all) { $0.id.eq($1.reminderID) } .where { if !searchText.isEmpty { $1.match(searchText) } } + .where { _, reminderText in + for token in searchTokens { + switch token.kind { + case .near: + reminderText.match("NEAR(\(token.rawValue))") + case .tag: + reminderText.tags.match(token.rawValue) + } + } + } } - private func updateQuery() async { + private func updateQuery(debounce: Bool = true) async throws { + if debounce { + try await clock.sleep(for: .seconds(0.3)) + } await withErrorReporting { - if searchText.isEmpty { + if !isSearching { showCompletedInSearchResults = false } - let baseQuery = baseQuery - try await $completedCount.load( - baseQuery + if searchText.hasPrefix("#") { + let existingTags = searchTokens.compactMap { $0.kind == .tag ? $0.rawValue : nil } + try await $tags.load( + Tag + .where { $0.title.hasPrefix(searchText.dropFirst()) && !$0.title.in(existingTags) } + .order(by: \.title) + ) + } else { + try await $searchResults.load( + SearchRequest( + baseQuery: baseQuery, + showCompletedInSearchResults: showCompletedInSearchResults + ), + animation: .default + ) + } + } + } + + @Selection + struct Row: Identifiable { + var id: Reminder.ID { reminder.id } + let isPastDue: Bool + let notes: String + let reminder: Reminders.Reminder + let remindersList: RemindersList + let tags: String + let title: String + } + + struct SearchRequest: FetchKeyRequest { + struct Value { + var completedCount = 0 + var rows: [Row] = [] + } + let baseQuery: SelectOf + let showCompletedInSearchResults: Bool + func fetch(_ db: Database) throws -> Value { + try Value( + completedCount: + baseQuery .where { reminder, _ in reminder.isCompleted } - .count(), - animation: .default - ) - try await $reminders.load( - baseQuery + .count() + .fetchOne(db) ?? 0, + rows: + baseQuery .where { reminder, _ in if !showCompletedInSearchResults { !reminder.isCompleted @@ -85,21 +176,22 @@ class SearchRemindersModel { tags: $1.tags.highlight("**", "**"), title: $1.title.highlight("**", "**") ) - }, - animation: .default + } + .fetchAll(db) ) } } - @Selection - struct Row: Identifiable { - var id: Reminder.ID { reminder.id } - let isPastDue: Bool - let notes: String - let reminder: Reminders.Reminder - let remindersList: RemindersList - let tags: String - let title: String + struct Token: Hashable, Identifiable { + enum Kind { + case near + case tag + } + + var kind: Kind + var rawValue = "" + + var id: Self { self } } } @@ -111,11 +203,26 @@ struct SearchRemindersView: View { } var body: some View { + if model.searchText.hasPrefix("#"), !model.tags.isEmpty { + Section { + ScrollView(.horizontal) { + HStack { + ForEach(model.tags) { tag in + Button("#\(tag.title)") { + model.tagButtonTapped(tag) + } + } + } + } + .scrollIndicators(.hidden) + } + } + HStack { - Text("\(model.completedCount) Completed") + Text("\(model.searchResults.completedCount) Completed") .monospacedDigit() .contentTransition(.numericText()) - if model.completedCount > 0 { + if model.searchResults.completedCount > 0 { Text("•") Menu { Text("Clear Completed Reminders") @@ -136,22 +243,22 @@ struct SearchRemindersView: View { } Spacer() Button(model.showCompletedInSearchResults ? "Hide" : "Show") { - Task { await model.showCompletedButtonTapped() } + Task { try await model.showCompletedButtonTapped() } } } } .buttonStyle(.borderless) - ForEach(model.reminders) { reminder in + ForEach(model.searchResults.rows) { row in ReminderRow( - color: reminder.remindersList.color, - isPastDue: reminder.isPastDue, - notes: reminder.notes, - reminder: reminder.reminder, - remindersList: reminder.remindersList, + color: row.remindersList.color, + isPastDue: row.isPastDue, + notes: row.notes, + reminder: row.reminder, + remindersList: row.remindersList, showCompleted: model.showCompletedInSearchResults, - tags: reminder.tags, - title: reminder.title + tags: row.tags, + title: row.title ) } } From 3fdfca6ad4b2b70c3d4e035b7c4fb40b75568f6f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 22 Aug 2025 13:23:55 -0700 Subject: [PATCH 03/15] fix tests --- Examples/Reminders/SearchReminders.swift | 17 +++--- Examples/RemindersTests/Internal.swift | 1 + .../RemindersDetailsTests.swift | 20 ++----- .../RemindersTests/SearchRemindersTests.swift | 52 +++++++++---------- 4 files changed, 40 insertions(+), 50 deletions(-) diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 940cc3e8..373ceb8c 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -78,11 +78,8 @@ class SearchRemindersModel { } private var baseQuery: SelectOf { - let searchText = - searchText - .split(separator: " ") - .map { #""\#($0.replacingOccurrences(of: #"""#, with: #""""#))""# } - .joined(separator: " ") + let searchText = searchText.quoted() + return Reminder .join(ReminderText.all) { $0.id.eq($1.reminderID) } @@ -95,7 +92,7 @@ class SearchRemindersModel { for token in searchTokens { switch token.kind { case .near: - reminderText.match("NEAR(\(token.rawValue))") + reminderText.match("NEAR(\(token.rawValue.quoted())") case .tag: reminderText.tags.match(token.rawValue) } @@ -281,3 +278,11 @@ struct SearchRemindersView: View { .searchable(text: $searchText) } } + +extension String { + fileprivate func quoted() -> String { + split(separator: " ") + .map { #""\#($0.replacingOccurrences(of: #"""#, with: #""""#))""# } + .joined(separator: " ") + } +} diff --git a/Examples/RemindersTests/Internal.swift b/Examples/RemindersTests/Internal.swift index a0ea19c6..35986dd8 100644 --- a/Examples/RemindersTests/Internal.swift +++ b/Examples/RemindersTests/Internal.swift @@ -7,6 +7,7 @@ import Testing @testable import Reminders @Suite( + .dependency(\.continuousClock, ImmediateClock()), .dependency(\.date.now, Date(timeIntervalSince1970: 1234567890)), .dependency(\.uuid, .incrementing), .dependencies { diff --git a/Examples/RemindersTests/RemindersDetailsTests.swift b/Examples/RemindersTests/RemindersDetailsTests.swift index b51c7aa8..c4f9cd3d 100644 --- a/Examples/RemindersTests/RemindersDetailsTests.swift +++ b/Examples/RemindersTests/RemindersDetailsTests.swift @@ -38,10 +38,7 @@ extension BaseTestSuite { ), isPastDue: true, notes: "", - tags: [ - [0]: "optional", - [1]: "someday" - ] + tags: "#someday #optional" ), [1]: RemindersDetailModel.Row( reminder: Reminder( @@ -63,9 +60,7 @@ extension BaseTestSuite { ), isPastDue: false, notes: "Ask about diet", - tags: [ - [0]: "adulting" - ] + tags: "#adulting" ), [2]: RemindersDetailModel.Row( reminder: Reminder( @@ -87,10 +82,7 @@ extension BaseTestSuite { ), isPastDue: false, notes: "", - tags: [ - [0]: "night", - [1]: "social" - ] + tags: "#social #night" ), [3]: RemindersDetailModel.Row( reminder: Reminder( @@ -118,11 +110,7 @@ extension BaseTestSuite { ), isPastDue: false, notes: "Milk Eggs Apples Oatmeal Spinach", - tags: [ - [0]: "adulting", - [1]: "optional", - [2]: "someday" - ] + tags: "#someday #optional #adulting" ) ] """# diff --git a/Examples/RemindersTests/SearchRemindersTests.swift b/Examples/RemindersTests/SearchRemindersTests.swift index 66d8b7e7..37acc3ae 100644 --- a/Examples/RemindersTests/SearchRemindersTests.swift +++ b/Examples/RemindersTests/SearchRemindersTests.swift @@ -13,26 +13,24 @@ extension BaseTestSuite { @Test func basics() async throws { let model = SearchRemindersModel() - try await model.$reminders.load() - try await model.$completedCount.load() + try await model.$searchResults.load() - #expect(model.completedCount == 0) - assertInlineSnapshot(of: model.reminders, as: .customDump) { + #expect(model.searchResults.completedCount == 0) + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { """ [] """ } model.searchText = "Take" - try await model.$reminders.load() - try await model.$completedCount.load() + try await model.$searchResults.load() try await Task.sleep(for: .seconds(0.5)) - #expect(model.completedCount == 1) - assertInlineSnapshot(of: model.reminders, as: .customDump) { + #expect(model.searchResults.completedCount == 1) + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { """ [ [0]: SearchRemindersModel.Row( - isPastDue: false, + isPastDue: true, notes: "", reminder: Reminder( id: UUID(00000000-0000-0000-0000-00000000000A), @@ -51,7 +49,8 @@ extension BaseTestSuite { position: 2, title: "Family" ), - tags: [] + tags: "", + title: "**Take** out trash" ) ] """ @@ -61,16 +60,15 @@ extension BaseTestSuite { @Test func showCompleted() async throws { let model = SearchRemindersModel() model.searchText = "Take" - await model.showCompletedButtonTapped() + try await model.showCompletedButtonTapped() try await Task.sleep(for: .seconds(0.1)) - try await model.$reminders.load() - try await model.$completedCount.load() + try await model.$searchResults.load() - assertInlineSnapshot(of: model.reminders, as: .customDump) { + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { """ [ [0]: SearchRemindersModel.Row( - isPastDue: false, + isPastDue: true, notes: "", reminder: Reminder( id: UUID(00000000-0000-0000-0000-00000000000A), @@ -89,7 +87,8 @@ extension BaseTestSuite { position: 2, title: "Family" ), - tags: [] + tags: "", + title: "**Take** out trash" ), [1]: SearchRemindersModel.Row( isPastDue: false, @@ -111,11 +110,8 @@ extension BaseTestSuite { position: 1, title: "Personal" ), - tags: [ - [0]: "car", - [1]: "kids", - [2]: "social" - ] + tags: "#car #kids #social", + title: "**Take** a walk" ) ] """ @@ -125,17 +121,16 @@ extension BaseTestSuite { @Test func deleteCompleted() async throws { let model = SearchRemindersModel() model.searchText = "Take" - await model.showCompletedButtonTapped() + try await model.showCompletedButtonTapped() try await Task.sleep(for: .seconds(0.1)) model.deleteCompletedReminders() - try await model.$reminders.load() - try await model.$completedCount.load() - #expect(model.completedCount == 0) - assertInlineSnapshot(of: model.reminders, as: .customDump) { + try await model.$searchResults.load() + #expect(model.searchResults.completedCount == 0) + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { """ [ [0]: SearchRemindersModel.Row( - isPastDue: false, + isPastDue: true, notes: "", reminder: Reminder( id: UUID(00000000-0000-0000-0000-00000000000A), @@ -154,7 +149,8 @@ extension BaseTestSuite { position: 2, title: "Family" ), - tags: [] + tags: "", + title: "**Take** out trash" ) ] """ From 42777fc9a2edc0661990e842a8e7d79024402e0a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 22 Aug 2025 15:29:32 -0700 Subject: [PATCH 04/15] wip --- Examples/Reminders/RemindersDetail.swift | 4 +-- Examples/Reminders/Schema.swift | 10 +++--- Examples/Reminders/SearchReminders.swift | 40 ++++++++++++------------ 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Examples/Reminders/RemindersDetail.swift b/Examples/Reminders/RemindersDetail.swift index a2c979e2..1f1ea51f 100644 --- a/Examples/Reminders/RemindersDetail.swift +++ b/Examples/Reminders/RemindersDetail.swift @@ -72,7 +72,7 @@ class RemindersDetailModel: HashableObject { !$0.isCompleted } } - .order { $0.isCompleted } + .order(by: \.isCompleted) .order { switch ordering { case .dueDate: $0.dueDate.asc(nulls: .last) @@ -94,7 +94,7 @@ class RemindersDetailModel: HashableObject { } } .join(RemindersList.all) { $0.remindersListID.eq($3.id) } - .join(ReminderText.all) { $0.id.eq($4.reminderID) } + .join(ReminderText.all) { $0.rowid.eq($4.rowid) } .select { Row.Columns( reminder: $0, diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 9403df9c..b7808a66 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -83,7 +83,7 @@ struct ReminderTag: Hashable, Identifiable { @Table @Selection struct ReminderText: StructuredQueries.FTS5 { - let reminderID: Reminder.ID + let rowid: Int let title: String let notes: String let tags: String @@ -216,7 +216,7 @@ func appDatabase() throws -> any DatabaseWriter { try Reminder.createTemporaryTrigger(after: .insert { new in ReminderText.insert { ReminderText.Columns( - reminderID: new.id, + rowid: new.rowid, title: new.title, notes: new.notes.replace("\n", " "), tags: "" @@ -229,7 +229,7 @@ func appDatabase() throws -> any DatabaseWriter { ($0.title, $0.notes) } forEachRow: { _, new in ReminderText - .where { $0.reminderID.eq(new.id) } + .where { $0.rowid.eq(new.rowid) } .update { $0.title = new.title $0.notes = new.notes.replace("\n", " ") @@ -239,7 +239,7 @@ func appDatabase() throws -> any DatabaseWriter { try Reminder.createTemporaryTrigger(after: .delete { old in ReminderText - .where { $0.reminderID.eq(old.id) } + .where { $0.rowid.eq(old.rowid) } .delete() }) .execute(db) @@ -248,7 +248,7 @@ func appDatabase() throws -> any DatabaseWriter { for reminderID: some QueryExpression ) -> UpdateOf { ReminderText - .where { $0.reminderID.eq(reminderID) } + .where { $0.rowid.eq(Reminder.find(reminderID).select(\.rowid)) } .update { $0.tags = ReminderTag .order(by: \.tagID) diff --git a/Examples/Reminders/SearchReminders.swift b/Examples/Reminders/SearchReminders.swift index 373ceb8c..472ab5ec 100644 --- a/Examples/Reminders/SearchReminders.swift +++ b/Examples/Reminders/SearchReminders.swift @@ -65,7 +65,7 @@ class SearchRemindersModel { withErrorReporting { try database.write { db in try Reminder - .where { $0.isCompleted && $0.id.in(baseQuery.select { reminder, _ in reminder.id }) } + .where { $0.isCompleted && $0.id.in(baseQuery.select { $1.id }) } .where { if let monthsAgo { #sql("\($0.dueDate) < date('now', '-\(raw: monthsAgo) months')") @@ -77,27 +77,27 @@ class SearchRemindersModel { } } - private var baseQuery: SelectOf { + private var baseQuery: SelectOf { let searchText = searchText.quoted() return - Reminder - .join(ReminderText.all) { $0.id.eq($1.reminderID) } + ReminderText .where { if !searchText.isEmpty { - $1.match(searchText) + $0.match(searchText) } } - .where { _, reminderText in + .where { for token in searchTokens { switch token.kind { case .near: - reminderText.match("NEAR(\(token.rawValue.quoted())") + $0.match("NEAR(\(token.rawValue.quoted())") case .tag: - reminderText.tags.match(token.rawValue) + $0.tags.match(token.rawValue) } } } + .join(Reminder.all) { $0.rowid.eq($1.rowid) } } private func updateQuery(debounce: Bool = true) async throws { @@ -144,34 +144,34 @@ class SearchRemindersModel { var completedCount = 0 var rows: [Row] = [] } - let baseQuery: SelectOf + let baseQuery: SelectOf let showCompletedInSearchResults: Bool func fetch(_ db: Database) throws -> Value { try Value( completedCount: baseQuery - .where { reminder, _ in reminder.isCompleted } + .where { $1.isCompleted } .count() .fetchOne(db) ?? 0, rows: baseQuery - .where { reminder, _ in + .where { if !showCompletedInSearchResults { - !reminder.isCompleted + !$1.isCompleted } } - .order { reminder, _ in - (reminder.isCompleted, reminder.dueDate) + .order { + ($1.isCompleted, $1.dueDate) } - .join(RemindersList.all) { $0.remindersListID.eq($2.id) } + .join(RemindersList.all) { $1.remindersListID.eq($2.id) } .select { Row.Columns( - isPastDue: $0.isPastDue, - notes: $1.notes.snippet("**", "**", "...", 64).replace("\n", " "), - reminder: $0, + isPastDue: $1.isPastDue, + notes: $0.notes.snippet("**", "**", "...", 64).replace("\n", " "), + reminder: $1, remindersList: $2, - tags: $1.tags.highlight("**", "**"), - title: $1.title.highlight("**", "**") + tags: $0.tags.highlight("**", "**"), + title: $0.title.highlight("**", "**") ) } .fetchAll(db) From 65ac3f59f9c5d1a60365ef505975e81073c27dc7 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 22 Aug 2025 15:53:29 -0700 Subject: [PATCH 05/15] wip --- Examples/RemindersTests/SearchRemindersTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/RemindersTests/SearchRemindersTests.swift b/Examples/RemindersTests/SearchRemindersTests.swift index 37acc3ae..165767bb 100644 --- a/Examples/RemindersTests/SearchRemindersTests.swift +++ b/Examples/RemindersTests/SearchRemindersTests.swift @@ -24,7 +24,7 @@ extension BaseTestSuite { model.searchText = "Take" try await model.$searchResults.load() - try await Task.sleep(for: .seconds(0.5)) + try await Task.sleep(for: .seconds(1)) #expect(model.searchResults.completedCount == 1) assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { """ From ec1834915200ef8527ef462ef798c5593048a99a Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 22 Aug 2025 22:41:38 -0700 Subject: [PATCH 06/15] wip --- .github/workflows/ci.yml | 4 ++++ .gitignore | 1 + Examples/RemindersTests/SearchRemindersTests.swift | 2 +- Makefile | 3 ++- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b624064..811e7ce7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,10 @@ jobs: run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: xcodebuild ${{ matrix.scheme }} run: make DERIVED_DATA_PATH=~/.derivedData SCHEME="${{ matrix.scheme }}" xcodebuild-raw + - uses: kishikawakatsumi/xcresulttool@v1 + with: + path: TestResults.xcresult + if: success() || failure() # NB: GRDB 7.6.1 does not currently build on Linux. # linux: diff --git a/.gitignore b/.gitignore index 4bab0f42..48a51863 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ DerivedData/ .swiftpm .netrc *.sqlite +*.xcresult diff --git a/Examples/RemindersTests/SearchRemindersTests.swift b/Examples/RemindersTests/SearchRemindersTests.swift index 165767bb..37acc3ae 100644 --- a/Examples/RemindersTests/SearchRemindersTests.swift +++ b/Examples/RemindersTests/SearchRemindersTests.swift @@ -24,7 +24,7 @@ extension BaseTestSuite { model.searchText = "Take" try await model.$searchResults.load() - try await Task.sleep(for: .seconds(1)) + try await Task.sleep(for: .seconds(0.5)) #expect(model.searchResults.completedCount == 1) assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { """ diff --git a/Makefile b/Makefile index 919599f7..c1deec28 100644 --- a/Makefile +++ b/Makefile @@ -15,9 +15,10 @@ XCODEBUILD_FLAGS = \ -configuration $(CONFIG) \ -derivedDataPath $(DERIVED_DATA_PATH) \ -destination $(DESTINATION) \ + -project Examples/Examples.xcodeproj + -resultBundlePath TestResults.xcresult \ -scheme "$(SCHEME)" \ -skipMacroValidation \ - -project Examples/Examples.xcodeproj XCODEBUILD_COMMAND = xcodebuild $(XCODEBUILD_ARGUMENT) $(XCODEBUILD_FLAGS) From 5e7cda724441a56a7f362dcec2037b9f7872b5c4 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 22 Aug 2025 22:43:47 -0700 Subject: [PATCH 07/15] wip --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c1deec28..5d1b0122 100644 --- a/Makefile +++ b/Makefile @@ -15,10 +15,10 @@ XCODEBUILD_FLAGS = \ -configuration $(CONFIG) \ -derivedDataPath $(DERIVED_DATA_PATH) \ -destination $(DESTINATION) \ - -project Examples/Examples.xcodeproj + -project Examples/Examples.xcodeproj \ -resultBundlePath TestResults.xcresult \ -scheme "$(SCHEME)" \ - -skipMacroValidation \ + -skipMacroValidation XCODEBUILD_COMMAND = xcodebuild $(XCODEBUILD_ARGUMENT) $(XCODEBUILD_FLAGS) From 632b2a84a7c090b051b41727f57e5dedc74330bf Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 22 Aug 2025 22:54:55 -0700 Subject: [PATCH 08/15] wip --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 811e7ce7..eb2b4cd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,9 +42,8 @@ jobs: run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: xcodebuild ${{ matrix.scheme }} run: make DERIVED_DATA_PATH=~/.derivedData SCHEME="${{ matrix.scheme }}" xcodebuild-raw - - uses: kishikawakatsumi/xcresulttool@v1 - with: - path: TestResults.xcresult + - name: Output test failures + run: xcrun xcresulttool get test-results summary --path TestResults.xcresult --format json | jq -r '.testFailures[] | "\(.testIdentifierString): \(.failureText)"' if: success() || failure() # NB: GRDB 7.6.1 does not currently build on Linux. From 5efa0659b51bc894ad3f4e19de15779e74907341 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 22 Aug 2025 22:56:36 -0700 Subject: [PATCH 09/15] wip --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb2b4cd6..1a9a81e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,8 @@ jobs: - name: xcodebuild ${{ matrix.scheme }} run: make DERIVED_DATA_PATH=~/.derivedData SCHEME="${{ matrix.scheme }}" xcodebuild-raw - name: Output test failures - run: xcrun xcresulttool get test-results summary --path TestResults.xcresult --format json | jq -r '.testFailures[] | "\(.testIdentifierString): \(.failureText)"' + run: | + xcrun xcresulttool get test-results summary --path TestResults.xcresult --format json | jq -r '.testFailures[] | "\(.testIdentifierString): \(.failureText)"' if: success() || failure() # NB: GRDB 7.6.1 does not currently build on Linux. From 7fef9381c086607d7c616edcdff718c9b603ee31 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 22 Aug 2025 23:43:47 -0700 Subject: [PATCH 10/15] wip --- .github/workflows/ci.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a9a81e0..a6ddf84e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,8 +44,15 @@ jobs: run: make DERIVED_DATA_PATH=~/.derivedData SCHEME="${{ matrix.scheme }}" xcodebuild-raw - name: Output test failures run: | - xcrun xcresulttool get test-results summary --path TestResults.xcresult --format json | jq -r '.testFailures[] | "\(.testIdentifierString): \(.failureText)"' - if: success() || failure() + xcrun xcresulttool get test-results tests --path TestResults.xcresult --format json | jq -r ' + .testNodes[] + | .. | objects | select(.nodeType=="Test Case") + | . as $tc + | ($tc.children // [])[] + | select(.nodeType=="Failure Message") + | "✘ Test \($tc.name) recorded an issue at \(.name)\n" + ' + if: failure() # NB: GRDB 7.6.1 does not currently build on Linux. # linux: From cd67ca47f9b0b3f30c5c6766a417556d63332d2c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 23 Aug 2025 00:02:59 -0700 Subject: [PATCH 11/15] wip --- Examples/RemindersTests/SearchRemindersTests.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Examples/RemindersTests/SearchRemindersTests.swift b/Examples/RemindersTests/SearchRemindersTests.swift index 37acc3ae..a1e682ba 100644 --- a/Examples/RemindersTests/SearchRemindersTests.swift +++ b/Examples/RemindersTests/SearchRemindersTests.swift @@ -23,7 +23,6 @@ extension BaseTestSuite { } model.searchText = "Take" - try await model.$searchResults.load() try await Task.sleep(for: .seconds(0.5)) #expect(model.searchResults.completedCount == 1) assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { @@ -61,8 +60,6 @@ extension BaseTestSuite { let model = SearchRemindersModel() model.searchText = "Take" try await model.showCompletedButtonTapped() - try await Task.sleep(for: .seconds(0.1)) - try await model.$searchResults.load() assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { """ @@ -122,7 +119,6 @@ extension BaseTestSuite { let model = SearchRemindersModel() model.searchText = "Take" try await model.showCompletedButtonTapped() - try await Task.sleep(for: .seconds(0.1)) model.deleteCompletedReminders() try await model.$searchResults.load() #expect(model.searchResults.completedCount == 0) From 8184548692f8784f92c69835c4bf59f3f642e8b3 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 23 Aug 2025 00:17:18 -0700 Subject: [PATCH 12/15] wip --- Examples/RemindersTests/SearchRemindersTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/RemindersTests/SearchRemindersTests.swift b/Examples/RemindersTests/SearchRemindersTests.swift index a1e682ba..281c9b05 100644 --- a/Examples/RemindersTests/SearchRemindersTests.swift +++ b/Examples/RemindersTests/SearchRemindersTests.swift @@ -23,7 +23,7 @@ extension BaseTestSuite { } model.searchText = "Take" - try await Task.sleep(for: .seconds(0.5)) + try await Task.sleep(for: .seconds(5)) #expect(model.searchResults.completedCount == 1) assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { """ From e7f19824e3fa8bae7f987e3f57fa84bcd34a1377 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 23 Aug 2025 11:49:42 -0700 Subject: [PATCH 13/15] wip --- Examples/RemindersTests/SearchRemindersTests.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Examples/RemindersTests/SearchRemindersTests.swift b/Examples/RemindersTests/SearchRemindersTests.swift index 281c9b05..95c2af60 100644 --- a/Examples/RemindersTests/SearchRemindersTests.swift +++ b/Examples/RemindersTests/SearchRemindersTests.swift @@ -23,7 +23,7 @@ extension BaseTestSuite { } model.searchText = "Take" - try await Task.sleep(for: .seconds(5)) + try await model.searchTask?.value #expect(model.searchResults.completedCount == 1) assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { """ @@ -54,6 +54,12 @@ extension BaseTestSuite { ] """ } + if model.searchResults.completedCount != 1 { + struct Failure: Error { + let message: String + } + throw Failure(message: String(customDumping: model)) + } } @Test func showCompleted() async throws { From cf73184a71de41f669cea5e4bc6eaa49043162fb Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sat, 23 Aug 2025 12:22:35 -0700 Subject: [PATCH 14/15] wip --- Examples/RemindersTests/SearchRemindersTests.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Examples/RemindersTests/SearchRemindersTests.swift b/Examples/RemindersTests/SearchRemindersTests.swift index 95c2af60..d9700614 100644 --- a/Examples/RemindersTests/SearchRemindersTests.swift +++ b/Examples/RemindersTests/SearchRemindersTests.swift @@ -54,12 +54,6 @@ extension BaseTestSuite { ] """ } - if model.searchResults.completedCount != 1 { - struct Failure: Error { - let message: String - } - throw Failure(message: String(customDumping: model)) - } } @Test func showCompleted() async throws { From 07f2f341c2c9959ebdae18ae336b9e581bb93182 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 25 Aug 2025 16:25:11 -0700 Subject: [PATCH 15/15] Fix --- .../RemindersTests/SearchRemindersTests.swift | 219 ++++++++++-------- 1 file changed, 117 insertions(+), 102 deletions(-) diff --git a/Examples/RemindersTests/SearchRemindersTests.swift b/Examples/RemindersTests/SearchRemindersTests.swift index d9700614..8faeceb1 100644 --- a/Examples/RemindersTests/SearchRemindersTests.swift +++ b/Examples/RemindersTests/SearchRemindersTests.swift @@ -8,6 +8,9 @@ import Testing extension BaseTestSuite { @MainActor + @Suite( + .snapshots(record: .missing) + ) struct SearchRemindersTests { @Dependency(\.defaultDatabase) var database @@ -25,34 +28,38 @@ extension BaseTestSuite { model.searchText = "Take" try await model.searchTask?.value #expect(model.searchResults.completedCount == 1) - assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { - """ - [ - [0]: SearchRemindersModel.Row( - isPastDue: true, - notes: "", - reminder: Reminder( - id: UUID(00000000-0000-0000-0000-00000000000A), - dueDate: Date(2009-02-17T23:31:30.000Z), - isCompleted: false, - isFlagged: false, + withKnownIssue( + "'@Fetch' introduces an escaping closure and loses the task-local dependency" + ) { + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { + """ + [ + [0]: SearchRemindersModel.Row( + isPastDue: false, notes: "", - position: 8, - priority: .high, - remindersListID: UUID(00000000-0000-0000-0000-000000000001), - title: "Take out trash" - ), - remindersList: RemindersList( - id: UUID(00000000-0000-0000-0000-000000000001), - color: 3985191935, - position: 2, - title: "Family" - ), - tags: "", - title: "**Take** out trash" - ) - ] - """ + reminder: Reminder( + id: UUID(00000000-0000-0000-0000-00000000000A), + dueDate: Date(2009-02-17T23:31:30.000Z), + isCompleted: false, + isFlagged: false, + notes: "", + position: 8, + priority: .high, + remindersListID: UUID(00000000-0000-0000-0000-000000000001), + title: "Take out trash" + ), + remindersList: RemindersList( + id: UUID(00000000-0000-0000-0000-000000000001), + color: 3985191935, + position: 2, + title: "Family" + ), + tags: "", + title: "**Take** out trash" + ) + ] + """ + } } } @@ -61,57 +68,61 @@ extension BaseTestSuite { model.searchText = "Take" try await model.showCompletedButtonTapped() - assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { - """ - [ - [0]: SearchRemindersModel.Row( - isPastDue: true, - notes: "", - reminder: Reminder( - id: UUID(00000000-0000-0000-0000-00000000000A), - dueDate: Date(2009-02-17T23:31:30.000Z), - isCompleted: false, - isFlagged: false, + withKnownIssue( + "'@Fetch' introduces an escaping closure and loses the task-local dependency" + ) { + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { + """ + [ + [0]: SearchRemindersModel.Row( + isPastDue: false, notes: "", - position: 8, - priority: .high, - remindersListID: UUID(00000000-0000-0000-0000-000000000001), - title: "Take out trash" - ), - remindersList: RemindersList( - id: UUID(00000000-0000-0000-0000-000000000001), - color: 3985191935, - position: 2, - title: "Family" + reminder: Reminder( + id: UUID(00000000-0000-0000-0000-00000000000A), + dueDate: Date(2009-02-17T23:31:30.000Z), + isCompleted: false, + isFlagged: false, + notes: "", + position: 8, + priority: .high, + remindersListID: UUID(00000000-0000-0000-0000-000000000001), + title: "Take out trash" + ), + remindersList: RemindersList( + id: UUID(00000000-0000-0000-0000-000000000001), + color: 3985191935, + position: 2, + title: "Family" + ), + tags: "", + title: "**Take** out trash" ), - tags: "", - title: "**Take** out trash" - ), - [1]: SearchRemindersModel.Row( - isPastDue: false, - notes: "", - reminder: Reminder( - id: UUID(00000000-0000-0000-0000-000000000006), - dueDate: Date(2008-08-07T23:31:30.000Z), - isCompleted: true, - isFlagged: false, + [1]: SearchRemindersModel.Row( + isPastDue: false, notes: "", - position: 4, - priority: nil, - remindersListID: UUID(00000000-0000-0000-0000-000000000000), - title: "Take a walk" - ), - remindersList: RemindersList( - id: UUID(00000000-0000-0000-0000-000000000000), - color: 1218047999, - position: 1, - title: "Personal" - ), - tags: "#car #kids #social", - title: "**Take** a walk" - ) - ] - """ + reminder: Reminder( + id: UUID(00000000-0000-0000-0000-000000000006), + dueDate: Date(2008-08-07T23:31:30.000Z), + isCompleted: true, + isFlagged: false, + notes: "", + position: 4, + priority: nil, + remindersListID: UUID(00000000-0000-0000-0000-000000000000), + title: "Take a walk" + ), + remindersList: RemindersList( + id: UUID(00000000-0000-0000-0000-000000000000), + color: 1218047999, + position: 1, + title: "Personal" + ), + tags: "#car #kids #social", + title: "**Take** a walk" + ) + ] + """ + } } } @@ -122,34 +133,38 @@ extension BaseTestSuite { model.deleteCompletedReminders() try await model.$searchResults.load() #expect(model.searchResults.completedCount == 0) - assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { - """ - [ - [0]: SearchRemindersModel.Row( - isPastDue: true, - notes: "", - reminder: Reminder( - id: UUID(00000000-0000-0000-0000-00000000000A), - dueDate: Date(2009-02-17T23:31:30.000Z), - isCompleted: false, - isFlagged: false, + withKnownIssue( + "'@Fetch' introduces an escaping closure and loses the task-local dependency" + ) { + assertInlineSnapshot(of: model.searchResults.rows, as: .customDump) { + """ + [ + [0]: SearchRemindersModel.Row( + isPastDue: false, notes: "", - position: 8, - priority: .high, - remindersListID: UUID(00000000-0000-0000-0000-000000000001), - title: "Take out trash" - ), - remindersList: RemindersList( - id: UUID(00000000-0000-0000-0000-000000000001), - color: 3985191935, - position: 2, - title: "Family" - ), - tags: "", - title: "**Take** out trash" - ) - ] - """ + reminder: Reminder( + id: UUID(00000000-0000-0000-0000-00000000000A), + dueDate: Date(2009-02-17T23:31:30.000Z), + isCompleted: false, + isFlagged: false, + notes: "", + position: 8, + priority: .high, + remindersListID: UUID(00000000-0000-0000-0000-000000000001), + title: "Take out trash" + ), + remindersList: RemindersList( + id: UUID(00000000-0000-0000-0000-000000000001), + color: 3985191935, + position: 2, + title: "Family" + ), + tags: "", + title: "**Take** out trash" + ) + ] + """ + } } } }