From 908374d9b7242e023f93f8c85d3e837d113df122 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 1 May 2025 09:17:35 -0500 Subject: [PATCH 1/6] Manually order reminders. --- Examples/Reminders/README.md | 2 +- ...ListDetail.swift => RemindersDetail.swift} | 15 +++++--- Examples/Reminders/RemindersLists.swift | 8 ++-- Examples/Reminders/Schema.swift | 37 +++++++++++++++++++ 4 files changed, 51 insertions(+), 11 deletions(-) rename Examples/Reminders/{RemindersListDetail.swift => RemindersDetail.swift} (95%) diff --git a/Examples/Reminders/README.md b/Examples/Reminders/README.md index 31e9c8d7..cc9a4679 100644 --- a/Examples/Reminders/README.md +++ b/Examples/Reminders/README.md @@ -10,4 +10,4 @@ comma-separated list of all of its tags. SQLite is an incredibly powerful langua not embrace abstractions that keep you from querying SQLite directly as SwiftData does. [reminders-app-store]: https://apps.apple.com/us/app/reminders/id1108187841 -[tags-concat]: https://github.com/pointfreeco/sharing-grdb/blob/0391201992241f62e7bd10c8d1ece63b078c16ad/Examples/Reminders/RemindersListDetail.swift#L146-L147 +[tags-concat]: https://github.com/pointfreeco/sharing-grdb/blob/0391201992241f62e7bd10c8d1ece63b078c16ad/Examples/Reminders/RemindersDetail.swift#L146-L147 diff --git a/Examples/Reminders/RemindersListDetail.swift b/Examples/Reminders/RemindersDetail.swift similarity index 95% rename from Examples/Reminders/RemindersListDetail.swift rename to Examples/Reminders/RemindersDetail.swift index 93483d22..f9e5c232 100644 --- a/Examples/Reminders/RemindersListDetail.swift +++ b/Examples/Reminders/RemindersDetail.swift @@ -2,7 +2,7 @@ import CasePaths import SharingGRDB import SwiftUI -struct RemindersListDetailView: View { +struct RemindersDetailView: View { @FetchAll private var reminderStates: [ReminderState] @AppStorage private var ordering: Ordering @AppStorage private var showCompleted: Bool @@ -128,11 +128,13 @@ struct RemindersListDetailView: View { private enum Ordering: String, CaseIterable { case dueDate = "Due Date" + case manual = "Manual" case priority = "Priority" case title = "Title" var icon: Image { switch self { case .dueDate: Image(systemName: "calendar") + case .manual: Image(systemName: "hand.draw") case .priority: Image(systemName: "chart.bar.fill") case .title: Image(systemName: "textformat.characters") } @@ -157,7 +159,7 @@ struct RemindersListDetailView: View { fileprivate var remindersQuery: some StructuredQueriesCore.Statement { let query = - Reminder + Reminder .where { if !showCompleted { !$0.isCompleted @@ -167,6 +169,7 @@ struct RemindersListDetailView: View { .order { switch ordering { case .dueDate: $0.dueDate + case .manual: $0.dueDate // TODO: position case .priority: ($0.priority.desc(), $0.isFlagged.desc()) case .title: $0.title } @@ -208,7 +211,7 @@ struct RemindersListDetailView: View { } } -extension RemindersListDetailView.DetailType { +extension RemindersDetailView.DetailType { fileprivate var id: String { switch self { case .all: "all" @@ -249,7 +252,7 @@ extension RemindersListDetailView.DetailType { } } -struct RemindersListDetailPreview: PreviewProvider { +struct RemindersDetailPreview: PreviewProvider { static var previews: some View { let (remindersList, tag) = try! prepareDependencies { $0.defaultDatabase = try Reminders.appDatabase() @@ -260,14 +263,14 @@ struct RemindersListDetailPreview: PreviewProvider { ) } } - let detailTypes: [RemindersListDetailView.DetailType] = [ + let detailTypes: [RemindersDetailView.DetailType] = [ .all, .list(remindersList), .tags([tag]), ] ForEach(detailTypes, id: \.self) { detailType in NavigationStack { - RemindersListDetailView(detailType: detailType) + RemindersDetailView(detailType: detailType) } .previewDisplayName(detailType.navigationTitle) } diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 35a030e4..df77c11f 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -37,7 +37,7 @@ struct RemindersListsView: View { private var stats = Stats() @State private var destination: Destination? - @State private var remindersDetailType: RemindersListDetailView.DetailType? + @State private var remindersDetailType: RemindersDetailView.DetailType? @State private var searchText = "" @Dependency(\.defaultDatabase) private var database @@ -124,7 +124,7 @@ struct RemindersListsView: View { Section { ForEach(remindersLists) { state in NavigationLink { - RemindersListDetailView(detailType: .list(state.remindersList)) + RemindersDetailView(detailType: .list(state.remindersList)) } label: { RemindersListRow( remindersCount: state.remindersCount, @@ -148,7 +148,7 @@ struct RemindersListsView: View { Section { ForEach(tags) { tag in NavigationLink { - RemindersListDetailView(detailType: .tags([tag])) + RemindersDetailView(detailType: .tags([tag])) } label: { TagRow(tag: tag) } @@ -211,7 +211,7 @@ struct RemindersListsView: View { } .searchable(text: $searchText) .navigationDestination(item: $remindersDetailType) { detailType in - RemindersListDetailView(detailType: detailType) + RemindersDetailView(detailType: detailType) } } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index a0e89132..98a802e6 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -23,6 +23,7 @@ struct Reminder: Equatable, Identifiable { var notes = "" var priority: Priority? var remindersListID: Int +// var position = 0 var title = "" } @@ -179,6 +180,42 @@ func appDatabase() throws -> any DatabaseWriter { ) .execute(db) } + migrator.registerMigration("Add 'position' column to 'reminders'") { db in + try #sql( + """ + ALTER TABLE "reminders" + ADD COLUMN "position" INTEGER NOT NULL DEFAULT 0 + """ + ) + .execute(db) + try #sql( + """ + WITH "reminderPositions" AS ( + SELECT + id, + ROW_NUMBER() OVER (PARTITION BY "remindersListID" ORDER BY id) - 1 AS "position" + FROM "reminders" + ) + UPDATE "reminders" + SET "position" = "reminderPositions"."position" + FROM "reminderPositions" + WHERE "reminders"."id" = "reminderPositions"."id" + """ + ) + .execute(db) + try #sql( + """ + CREATE TRIGGER "default_position_reminders" + AFTER INSERT ON "reminders" + FOR EACH ROW BEGIN + UPDATE "reminders" + SET "position" = (SELECT max("position") + 1 FROM "reminders") + WHERE "id" = NEW."id"; + END + """ + ) + .execute(db) + } #if DEBUG && targetEnvironment(simulator) if context != .test { migrator.registerMigration("Seed sample data") { db in From 3f5d4eff9a14e04ccc61221db7d2e84625420fc0 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 1 May 2025 10:17:17 -0500 Subject: [PATCH 2/6] wip --- Examples/Reminders/RemindersDetail.swift | 29 ++++++++++++++++++- Examples/Reminders/RemindersLists.swift | 5 ++-- Examples/Reminders/Schema.swift | 12 ++++++-- .../DefaultDatabase.swift | 13 ++++----- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/Examples/Reminders/RemindersDetail.swift b/Examples/Reminders/RemindersDetail.swift index f9e5c232..401ffed7 100644 --- a/Examples/Reminders/RemindersDetail.swift +++ b/Examples/Reminders/RemindersDetail.swift @@ -46,6 +46,9 @@ struct RemindersDetailView: View { tags: reminderState.tags ) } + .onMove { indexSet, index in + move(from: indexSet, to: index) + } } .onScrollGeometryChange(for: Bool.self) { geometry in geometry.contentOffset.y + geometry.contentInsets.top > navigationTitleHeight @@ -126,6 +129,30 @@ struct RemindersDetailView: View { } } + func move(from source: IndexSet, to destination: Int) { + print("?!?!!?") + withErrorReporting { + try database.write { db in + var ids = reminderStates.map(\.reminder.id) + ids.move(fromOffsets: source, toOffset: destination) + try Reminder + .where { $0.id.in(ids) } + .update { + let ids = Array(ids.enumerated()) + let (first, rest) = (ids.first!, ids.dropFirst()) + $0.position = + rest + .reduce(Case($0.id).when(first.element, then: first.offset)) { cases, id in + cases.when(id.element, then: id.offset) + } + .else($0.position) + } + .execute(db) + } + } + ordering = .manual + } + private enum Ordering: String, CaseIterable { case dueDate = "Due Date" case manual = "Manual" @@ -169,7 +196,7 @@ struct RemindersDetailView: View { .order { switch ordering { case .dueDate: $0.dueDate - case .manual: $0.dueDate // TODO: position + case .manual: $0.position case .priority: ($0.priority.desc(), $0.isFlagged.desc()) case .title: $0.title } diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index df77c11f..e9e6a241 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -130,7 +130,7 @@ struct RemindersListsView: View { remindersCount: state.remindersCount, remindersList: state.remindersList ) - } + } } .onMove { indexSet, index in move(from: indexSet, to: index) @@ -225,7 +225,8 @@ struct RemindersListsView: View { .update { let ids = Array(ids.enumerated()) let (first, rest) = (ids.first!, ids.dropFirst()) - $0.position = rest + $0.position = + rest .reduce(Case($0.id).when(first.element, then: first.offset)) { cases, id in cases.when(id.element, then: id.offset) } diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index 98a802e6..afc9aa72 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -23,7 +23,7 @@ struct Reminder: Equatable, Identifiable { var notes = "" var priority: Priority? var remindersListID: Int -// var position = 0 + var position = 0 var title = "" } @@ -87,17 +87,21 @@ struct ReminderTag: Hashable, Identifiable { } func appDatabase() throws -> any DatabaseWriter { + @Dependency(\.context) var context let database: any DatabaseWriter var configuration = Configuration() configuration.foreignKeysEnabled = true configuration.prepareDatabase { db in #if DEBUG db.trace(options: .profile) { - logger.debug("\($0.expandedDescription)") + if context == .live { + logger.debug("\($0.expandedDescription)") + } else { + print("\($0.expandedDescription)") + } } #endif } - @Dependency(\.context) var context if context == .live { let path = URL.documentsDirectory.appending(component: "db.sqlite").path() logger.info("open \(path)") @@ -188,6 +192,7 @@ func appDatabase() throws -> any DatabaseWriter { """ ) .execute(db) + // Backfill position of reminders based on their completion status and due date. try #sql( """ WITH "reminderPositions" AS ( @@ -195,6 +200,7 @@ func appDatabase() throws -> any DatabaseWriter { id, ROW_NUMBER() OVER (PARTITION BY "remindersListID" ORDER BY id) - 1 AS "position" FROM "reminders" + ORDER BY NOT "isCompleted", "dueDate" DESC ) UPDATE "reminders" SET "position" = "reminderPositions"."position" diff --git a/Sources/StructuredQueriesGRDBCore/DefaultDatabase.swift b/Sources/StructuredQueriesGRDBCore/DefaultDatabase.swift index 1e1d06af..43dfab57 100644 --- a/Sources/StructuredQueriesGRDBCore/DefaultDatabase.swift +++ b/Sources/StructuredQueriesGRDBCore/DefaultDatabase.swift @@ -51,10 +51,7 @@ extension DependencyValues { switch context { case .live: return """ - A blank, in-memory database is being used. To set the database that is used by \ - 'SharingGRDB', use the 'prepareDependencies' tool as early as possible in the lifetime \ - of your app, such as in your app or scene delegate in UIKit, or the app entry point in \ - SwiftUI: + A blank, in-memory database is being used. To set the database that is used by 'SharingGRDB', use the 'prepareDependencies' tool as early as possible in the lifetime of your app, such as in your app or scene delegate in UIKit, or the app entry point in SwiftUI: @main struct MyApp: App { @@ -70,8 +67,7 @@ extension DependencyValues { case .preview: return #""" - A blank, in-memory database is being used. To set the database that is used by \ - 'SharingGRDB' in a preview, use a tool like the 'dependency' trait: + A blank, in-memory database is being used. To set the database that is used by 'SharingGRDB' in a preview, use a tool like the 'dependency' trait: #Preview( trait: .dependency(\.defaultDatabase, try DatabaseQueue(/* ... */)) @@ -82,8 +78,7 @@ extension DependencyValues { case .test: return #""" - A blank, in-memory database is being used. To set the database that is used by \ - 'SharingGRDB' in a test, use a tool like the 'dependency' trait: + A blank, in-memory database is being used. To set the database that is used by 'SharingGRDB' in a test, use a tool like the 'dependency' trait: @Suite(.dependency(\.defaultDatabase, try DatabaseQueue(/* ... */))) struct MyTests { @@ -110,3 +105,5 @@ extension DependencyValues { package static let defaultDatabaseLabel = "co.pointfree.SharingGRDB.testValue" } #endif + +import Foundation From 8347d0bf4d635bf9dbafe267f6d82b546863507d Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 1 May 2025 10:20:38 -0500 Subject: [PATCH 3/6] wip; --- Examples/Reminders/RemindersDetail.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/Reminders/RemindersDetail.swift b/Examples/Reminders/RemindersDetail.swift index 401ffed7..94b702de 100644 --- a/Examples/Reminders/RemindersDetail.swift +++ b/Examples/Reminders/RemindersDetail.swift @@ -130,7 +130,6 @@ struct RemindersDetailView: View { } func move(from source: IndexSet, to destination: Int) { - print("?!?!!?") withErrorReporting { try database.write { db in var ids = reminderStates.map(\.reminder.id) From f4dc94a04d05c38a287f2a93499c3bdb51fea237 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 1 May 2025 10:25:19 -0500 Subject: [PATCH 4/6] wip --- Examples/Reminders/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Reminders/README.md b/Examples/Reminders/README.md index cc9a4679..31e9c8d7 100644 --- a/Examples/Reminders/README.md +++ b/Examples/Reminders/README.md @@ -10,4 +10,4 @@ comma-separated list of all of its tags. SQLite is an incredibly powerful langua not embrace abstractions that keep you from querying SQLite directly as SwiftData does. [reminders-app-store]: https://apps.apple.com/us/app/reminders/id1108187841 -[tags-concat]: https://github.com/pointfreeco/sharing-grdb/blob/0391201992241f62e7bd10c8d1ece63b078c16ad/Examples/Reminders/RemindersDetail.swift#L146-L147 +[tags-concat]: https://github.com/pointfreeco/sharing-grdb/blob/0391201992241f62e7bd10c8d1ece63b078c16ad/Examples/Reminders/RemindersListDetail.swift#L146-L147 From e6ce22db6301092b695989a129fb58717c59d917 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Thu, 1 May 2025 10:36:12 -0500 Subject: [PATCH 5/6] wip --- Examples/Reminders/Schema.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index afc9aa72..bfd7d26d 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -197,7 +197,7 @@ func appDatabase() throws -> any DatabaseWriter { """ WITH "reminderPositions" AS ( SELECT - id, + "reminders"."id", ROW_NUMBER() OVER (PARTITION BY "remindersListID" ORDER BY id) - 1 AS "position" FROM "reminders" ORDER BY NOT "isCompleted", "dueDate" DESC From aadaef1dac5a1372fc99d9148f8de033ad0bda99 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 5 May 2025 12:00:36 -0500 Subject: [PATCH 6/6] wip --- .../StructuredQueriesGRDBCore/DefaultDatabase.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Sources/StructuredQueriesGRDBCore/DefaultDatabase.swift b/Sources/StructuredQueriesGRDBCore/DefaultDatabase.swift index cb0aefca..a6195961 100644 --- a/Sources/StructuredQueriesGRDBCore/DefaultDatabase.swift +++ b/Sources/StructuredQueriesGRDBCore/DefaultDatabase.swift @@ -51,7 +51,10 @@ extension DependencyValues { switch context { case .live: return """ - A blank, in-memory database is being used. To set the database that is used by 'SharingGRDB', use the 'prepareDependencies' tool as early as possible in the lifetime of your app, such as in your app or scene delegate in UIKit, or the app entry point in SwiftUI: + A blank, in-memory database is being used. To set the database that is used by \ + 'SharingGRDB', use the 'prepareDependencies' tool as early as possible in the lifetime \ + of your app, such as in your app or scene delegate in UIKit, or the app entry point in \ + SwiftUI: @main struct MyApp: App { @@ -67,7 +70,8 @@ extension DependencyValues { case .preview: return """ - A blank, in-memory database is being used. To set the database that is used by 'SharingGRDB' in a preview, use a tool like the 'dependency' trait: + A blank, in-memory database is being used. To set the database that is used by \ + 'SharingGRDB' in a preview, use a tool like the 'dependency' trait: #Preview( traits: .dependency(\\.defaultDatabase, try DatabaseQueue(/* ... */)) @@ -78,7 +82,9 @@ extension DependencyValues { case .test: return """ - A blank, in-memory database is being used. To set the database that is used by 'SharingGRDB' in a test, use a tool like the 'dependency' trait from 'DependenciesTestSupport': + A blank, in-memory database is being used. To set the database that is used by \ + 'SharingGRDB' in a test, use a tool like the 'dependency' trait from \ + 'DependenciesTestSupport': import DependenciesTestSupport