diff --git a/Package.resolved b/Package.resolved index 2cc93a52..c7f91d8f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -60,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { - "revision" : "706feb7858a7f6c242879d137b8ee30926aa5b26", - "version" : "1.12.0" + "revision" : "8dc1fbf2f6255a73dec53b4648164884898db4c5", + "version" : "1.14.1" } }, { diff --git a/Sources/SQLiteData/FetchAll+Sections.swift b/Sources/SQLiteData/FetchAll+Sections.swift index 4cae5fe1..1d78bd89 100644 --- a/Sources/SQLiteData/FetchAll+Sections.swift +++ b/Sources/SQLiteData/FetchAll+Sections.swift @@ -37,52 +37,6 @@ extension FetchAll { return sectionedReader.wrappedValue } - fileprivate init( - wrappedValue: [Element], - statement: Select<(), From, ()>, - sectionBy: _Sectioning, - database: (any DatabaseReader)?, - scheduler: (any ValueObservationScheduler & Hashable)? - ) - where - Element == From.QueryOutput, - From.QueryOutput: Sendable - { - self.init( - wrappedValue: wrappedValue, - request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectionBy), - sectionBy: sectionBy, - database: database, - scheduler: scheduler - ) - } - - fileprivate init( - wrappedValue: [Element], - request: FetchAllSectionedStatementValueRequest, - sectionBy: _Sectioning, - database: (any DatabaseReader)?, - scheduler: (any ValueObservationScheduler & Hashable)? - ) - where - Element == Value.QueryOutput, - Value.QueryOutput: Sendable - { - let sectionedReader = SharedReader( - wrappedValue: ResultsSectionCollection(elements: wrappedValue, sectionName: nil), - FetchKey( - request: request, - database: database, - scheduler: scheduler - ) - ) - self.sectionedReader = sectionedReader - self.sharedReader = sectionedReader.elements - self.sectioning.setValue(sectionBy) - } -} - -extension FetchAll { /// Initializes this property with a query that fetches every row from a table, grouping results /// into sections. /// @@ -179,30 +133,21 @@ extension FetchAll { /// /// - Parameters: /// - wrappedValue: A default collection to associate with this property. - /// - sectioning: A closure that returns a string expression, or an ordering of one, to group - /// results by, or `nil` for no grouping. + /// - sectionKeyPath: A key path to a string column to group results by. /// - database: The database to read from. A value of `nil` will use the default database /// (`@Dependency(\.defaultDatabase)`). - /// - scheduler: The scheduler to observe from. By default, database observation is performed - /// asynchronously on the main queue. public init( wrappedValue: [Element] = [], - @_SectionBuilder sectionBy sectioning: (Element.TableColumns) -> _Sectioning?, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable + sectionBy sectionKeyPath: KeyPath< + Element.TableColumns, some QueryExpression> + >, + database: (any DatabaseReader)? = nil ) where Element: StructuredQueriesCore.Table, Element.QueryOutput == Element { - let statement: Select<(), Element, ()> = Element.all.asSelect() - guard let sectioning = sectioning(Element.columns) else { - self.init(wrappedValue: wrappedValue, statement, database: database, scheduler: scheduler) - return - } self.init( wrappedValue: wrappedValue, - statement: statement, - sectionBy: sectioning, - database: database, - scheduler: scheduler + sectionBy: { $0[keyPath: sectionKeyPath] }, + database: database ) } @@ -212,18 +157,16 @@ extension FetchAll { /// - Parameters: /// - wrappedValue: A default collection to associate with this property. /// - statement: A query associated with the wrapped value. - /// - sectioning: A closure that returns a string expression, or an ordering of one, to group - /// results by, or `nil` for no grouping. + /// - sectionKeyPath: A key path to a string column to group results by. /// - database: The database to read from. A value of `nil` will use the default database /// (`@Dependency(\.defaultDatabase)`). - /// - scheduler: The scheduler to observe from. By default, database observation is performed - /// asynchronously on the main queue. public init( wrappedValue: [Element] = [], _ statement: S, - @_SectionBuilder sectionBy sectioning: (S.From.TableColumns) -> _Sectioning?, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable + sectionBy sectionKeyPath: KeyPath< + S.From.TableColumns, some QueryExpression> + >, + database: (any DatabaseReader)? = nil ) where Element == S.From.QueryOutput, @@ -231,88 +174,94 @@ extension FetchAll { S.From.QueryOutput: Sendable, S.Joins == () { - let statement: Select<(), S.From, ()> = statement.asSelect() - guard let sectioning = sectioning(S.From.columns) else { - self.init(wrappedValue: wrappedValue, statement, database: database, scheduler: scheduler) + self.init( + wrappedValue: wrappedValue, + statement, + sectionBy: { $0[keyPath: sectionKeyPath] }, + database: database + ) + } + + public init< + V: QueryRepresentable, From: StructuredQueriesCore.Table + >( + wrappedValue: [Element] = [], + _ statement: Select, + @_SectionBuilder sectionBy sectioning: (From.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil + ) + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + guard let sectioning = sectioning(From.columns) else { + self.init(wrappedValue: wrappedValue, statement, database: database) return } self.init( wrappedValue: wrappedValue, - statement: statement, + request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), sectionBy: sectioning, database: database, - scheduler: scheduler + scheduler: nil ) } -} -#if canImport(SwiftUI) - extension FetchAll { - /// Initializes this property with a query that fetches every row from a table, grouping - /// results into sections. - /// - /// - Parameters: - /// - wrappedValue: A default collection to associate with this property. - /// - sectioning: A closure that returns a string expression, or an ordering of one, to - /// group results by, or `nil` for no grouping. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - public init( - wrappedValue: [Element] = [], - @_SectionBuilder sectionBy sectioning: (Element.TableColumns) -> _Sectioning?, - database: (any DatabaseReader)? = nil, - animation: Animation - ) - where Element: StructuredQueriesCore.Table, Element.QueryOutput == Element { - self.init( - wrappedValue: wrappedValue, - sectionBy: sectioning, - database: database, - scheduler: .animation(animation) - ) + public init< + V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table + >( + wrappedValue: [Element] = [], + _ statement: Select, + @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil + ) + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + guard let sectioning = sectioning(From.columns, J.columns) else { + self.init(wrappedValue: wrappedValue, statement, database: database) + return } - - /// Initializes this property with a query associated with the wrapped value, grouping results - /// into sections. - /// - /// - Parameters: - /// - wrappedValue: A default collection to associate with this property. - /// - statement: A query associated with the wrapped value. - /// - sectioning: A closure that returns a string expression, or an ordering of one, to - /// group results by, or `nil` for no grouping. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - public init( - wrappedValue: [Element] = [], - _ statement: S, - @_SectionBuilder sectionBy sectioning: (S.From.TableColumns) -> _Sectioning?, - database: (any DatabaseReader)? = nil, - animation: Animation + self.init( + wrappedValue: wrappedValue, + request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), + sectionBy: sectioning, + database: database, + scheduler: nil ) - where - Element == S.From.QueryOutput, - S.QueryValue == (), - S.From.QueryOutput: Sendable, - S.Joins == () - { - self.init( - wrappedValue: wrappedValue, - statement, - sectionBy: sectioning, - database: database, - scheduler: .animation(animation) - ) + } + + public init< + V: QueryRepresentable, + From: StructuredQueriesCore.Table, + J1: StructuredQueriesCore.Table, + each J2: StructuredQueriesCore.Table + >( + wrappedValue: [Element] = [], + _ statement: Select, + @_SectionBuilder sectionBy sectioning: ( + From.TableColumns, J1.TableColumns, repeat (each J2).TableColumns + ) -> _Sectioning?, + database: (any DatabaseReader)? = nil + ) + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + guard let sectioning = sectioning(From.columns, J1.columns, repeat (each J2).columns) else { + self.init(wrappedValue: wrappedValue, statement, database: database) + return } + self.init( + wrappedValue: wrappedValue, + request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), + sectionBy: sectioning, + database: database, + scheduler: nil + ) } -#endif -extension FetchAll { /// Replaces the wrapped value with data from the given query, grouping results into sections. /// /// - Parameters: @@ -347,19 +296,17 @@ extension FetchAll { /// /// - Parameters: /// - statement: A query associated with the wrapped value. - /// - sectioning: A closure that returns a string expression, or an ordering of one, to group - /// results by, or `nil` for no grouping. + /// - sectionKeyPath: A key path to a string column to group results by. /// - database: The database to read from. A value of `nil` will use the default database /// (`@Dependency(\.defaultDatabase)`). - /// - scheduler: The scheduler to observe from. By default, database observation is performed - /// asynchronously on the main queue. /// - Returns: A subscription associated with the observation. @discardableResult public func load( _ statement: S, - @_SectionBuilder sectionBy sectioning: (S.From.TableColumns) -> _Sectioning?, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable + sectionBy sectionKeyPath: KeyPath< + S.From.TableColumns, some QueryExpression> + >, + database: (any DatabaseReader)? = nil ) async throws -> FetchSubscription where Element == S.From.QueryOutput, @@ -367,34 +314,150 @@ extension FetchAll { S.From.QueryOutput: Sendable, S.Joins == () { - let statement: Select<(), S.From, ()> = statement.asSelect() - return try await loadSections( - statement: statement, - sectionBy: sectioning(S.From.columns), - database: database, - scheduler: scheduler + try await load( + statement, + sectionBy: { $0[keyPath: sectionKeyPath] }, + database: database ) } - func loadSections( - statement: Select<(), From, ()>, - sectionBy sectioning: _Sectioning?, - database: (any DatabaseReader)?, - scheduler: (any ValueObservationScheduler & Hashable)? + @discardableResult + public func load< + V: QueryRepresentable, From: StructuredQueriesCore.Table + >( + _ statement: Select, + @_SectionBuilder sectionBy sectioning: (From.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil ) async throws -> FetchSubscription where - Element == From.QueryOutput, - From.QueryOutput: Sendable + Element == V.QueryOutput, + V.QueryOutput: Sendable { - guard let sectioning else { - removeSections() - let statement: Select = statement.selectStar() - try await sharedReader.load( - FetchKey( - request: FetchAllStatementValueRequest(statement: statement), - database: database, - scheduler: scheduler - ) + guard let sectioning = sectioning(From.columns) else { + return try await load(statement, database: database) + } + return try await loadSections( + request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), + sectionBy: sectioning, + database: database, + scheduler: nil + ) + } + + @discardableResult + public func load< + V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table + >( + _ statement: Select, + @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil + ) async throws -> FetchSubscription + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + guard let sectioning = sectioning(From.columns, J.columns) else { + return try await load(statement, database: database) + } + return try await loadSections( + request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), + sectionBy: sectioning, + database: database, + scheduler: nil + ) + } + + @discardableResult + public func load< + V: QueryRepresentable, + From: StructuredQueriesCore.Table, + J1: StructuredQueriesCore.Table, + each J2: StructuredQueriesCore.Table + >( + _ statement: Select, + @_SectionBuilder sectionBy sectioning: ( + From.TableColumns, J1.TableColumns, repeat (each J2).TableColumns + ) -> _Sectioning?, + database: (any DatabaseReader)? = nil + ) async throws -> FetchSubscription + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + guard let sectioning = sectioning(From.columns, J1.columns, repeat (each J2).columns) else { + return try await load(statement, database: database) + } + return try await loadSections( + request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), + sectionBy: sectioning, + database: database, + scheduler: nil + ) + } + + fileprivate init( + wrappedValue: [Element], + statement: Select<(), From, ()>, + sectionBy: _Sectioning, + database: (any DatabaseReader)?, + scheduler: (any ValueObservationScheduler & Hashable)? + ) + where + Element == From.QueryOutput, + From.QueryOutput: Sendable + { + self.init( + wrappedValue: wrappedValue, + request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectionBy), + sectionBy: sectionBy, + database: database, + scheduler: scheduler + ) + } + + fileprivate init( + wrappedValue: [Element], + request: FetchAllSectionedStatementValueRequest, + sectionBy: _Sectioning, + database: (any DatabaseReader)?, + scheduler: (any ValueObservationScheduler & Hashable)? + ) + where + Element == Value.QueryOutput, + Value.QueryOutput: Sendable + { + let sectionedReader = SharedReader( + wrappedValue: ResultsSectionCollection(elements: wrappedValue, sectionName: nil), + FetchKey( + request: request, + database: database, + scheduler: scheduler + ) + ) + self.sectionedReader = sectionedReader + self.sharedReader = sectionedReader.elements + self.sectioning.setValue(sectionBy) + } + + func loadSections( + statement: Select<(), From, ()>, + sectionBy sectioning: _Sectioning?, + database: (any DatabaseReader)?, + scheduler: (any ValueObservationScheduler & Hashable)? + ) async throws -> FetchSubscription + where + Element == From.QueryOutput, + From.QueryOutput: Sendable + { + guard let sectioning else { + removeSections() + let statement: Select = statement.selectStar() + try await sharedReader.load( + FetchKey( + request: FetchAllStatementValueRequest(statement: statement), + database: database, + scheduler: scheduler + ) ) return FetchSubscription(sharedReader: sharedReader) } @@ -431,64 +494,36 @@ extension FetchAll { } } -#if canImport(SwiftUI) - extension FetchAll { - /// Replaces the wrapped value with data from the given query, grouping results into sections. - /// - /// - Parameters: - /// - statement: A query associated with the wrapped value. - /// - sectioning: A closure that returns a string expression, or an ordering of one, to - /// group results by, or `nil` for no grouping. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - /// - Returns: A subscription associated with the observation. - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @discardableResult - public func load( - _ statement: S, - @_SectionBuilder sectionBy sectioning: (S.From.TableColumns) -> _Sectioning?, - database: (any DatabaseReader)? = nil, - animation: Animation? - ) async throws -> FetchSubscription - where - Element == S.From.QueryOutput, - S.QueryValue == (), - S.From.QueryOutput: Sendable, - S.Joins == () - { - try await load( - statement, - sectionBy: sectioning, - database: database, - scheduler: .animation(animation) - ) - } - } -#endif - extension FetchAll { /// Initializes this property with a query that fetches every row from a table, grouping results /// into sections. /// /// - Parameters: /// - wrappedValue: A default collection to associate with this property. - /// - sectionKeyPath: A key path to a string column to group results by. + /// - sectioning: A closure that returns a string expression, or an ordering of one, to group + /// results by, or `nil` for no grouping. /// - database: The database to read from. A value of `nil` will use the default database /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. public init( wrappedValue: [Element] = [], - sectionBy sectionKeyPath: KeyPath< - Element.TableColumns, some QueryExpression> - >, - database: (any DatabaseReader)? = nil + @_SectionBuilder sectionBy sectioning: (Element.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable ) where Element: StructuredQueriesCore.Table, Element.QueryOutput == Element { + let statement: Select<(), Element, ()> = Element.all.asSelect() + guard let sectioning = sectioning(Element.columns) else { + self.init(wrappedValue: wrappedValue, statement, database: database, scheduler: scheduler) + return + } self.init( wrappedValue: wrappedValue, - sectionBy: { $0[keyPath: sectionKeyPath] }, - database: database + statement: statement, + sectionBy: sectioning, + database: database, + scheduler: scheduler ) } @@ -498,16 +533,18 @@ extension FetchAll { /// - Parameters: /// - wrappedValue: A default collection to associate with this property. /// - statement: A query associated with the wrapped value. - /// - sectionKeyPath: A key path to a string column to group results by. + /// - sectioning: A closure that returns a string expression, or an ordering of one, to group + /// results by, or `nil` for no grouping. /// - database: The database to read from. A value of `nil` will use the default database /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. public init( wrappedValue: [Element] = [], _ statement: S, - sectionBy sectionKeyPath: KeyPath< - S.From.TableColumns, some QueryExpression> - >, - database: (any DatabaseReader)? = nil + @_SectionBuilder sectionBy sectioning: (S.From.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable ) where Element == S.From.QueryOutput, @@ -515,11 +552,17 @@ extension FetchAll { S.From.QueryOutput: Sendable, S.Joins == () { + let statement: Select<(), S.From, ()> = statement.asSelect() + guard let sectioning = sectioning(S.From.columns) else { + self.init(wrappedValue: wrappedValue, statement, database: database, scheduler: scheduler) + return + } self.init( wrappedValue: wrappedValue, - statement, - sectionBy: { $0[keyPath: sectionKeyPath] }, - database: database + statement: statement, + sectionBy: sectioning, + database: database, + scheduler: scheduler ) } @@ -585,189 +628,47 @@ extension FetchAll { ) } - /// Replaces the wrapped value with data from the given query, grouping results into sections. - /// - /// - Parameters: - /// - statement: A query associated with the wrapped value. - /// - sectionKeyPath: A key path to a string column to group results by. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - Returns: A subscription associated with the observation. - @discardableResult - public func load( - _ statement: S, - sectionBy sectionKeyPath: KeyPath< - S.From.TableColumns, some QueryExpression> - >, - database: (any DatabaseReader)? = nil - ) async throws -> FetchSubscription + public init< + V: QueryRepresentable, From: StructuredQueriesCore.Table + >( + wrappedValue: [Element] = [], + _ statement: Select, + @_SectionBuilder sectionBy sectioning: (From.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) where - Element == S.From.QueryOutput, - S.QueryValue == (), - S.From.QueryOutput: Sendable, - S.Joins == () + Element == V.QueryOutput, + V.QueryOutput: Sendable { - try await load( - statement, - sectionBy: { $0[keyPath: sectionKeyPath] }, - database: database + guard let sectioning = sectioning(From.columns) else { + self.init(wrappedValue: wrappedValue, statement, database: database, scheduler: scheduler) + return + } + self.init( + wrappedValue: wrappedValue, + request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), + sectionBy: sectioning, + database: database, + scheduler: scheduler ) } - /// Replaces the wrapped value with data from the given query, grouping results into sections. - /// - /// - Parameters: - /// - statement: A query associated with the wrapped value. - /// - sectionKeyPath: A key path to a string column to group results by. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - scheduler: The scheduler to observe from. By default, database observation is performed - /// asynchronously on the main queue. - /// - Returns: A subscription associated with the observation. - @discardableResult - public func load( - _ statement: S, - sectionBy sectionKeyPath: KeyPath< - S.From.TableColumns, some QueryExpression> - >, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable - ) async throws -> FetchSubscription - where - Element == S.From.QueryOutput, - S.QueryValue == (), - S.From.QueryOutput: Sendable, - S.Joins == () - { - try await load( - statement, - sectionBy: { $0[keyPath: sectionKeyPath] }, - database: database, - scheduler: scheduler - ) - } -} - -#if canImport(SwiftUI) - extension FetchAll { - /// Initializes this property with a query that fetches every row from a table, grouping - /// results into sections. - /// - /// - Parameters: - /// - wrappedValue: A default collection to associate with this property. - /// - sectionKeyPath: A key path to a string column to group results by. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - public init( - wrappedValue: [Element] = [], - sectionBy sectionKeyPath: KeyPath< - Element.TableColumns, some QueryExpression> - >, - database: (any DatabaseReader)? = nil, - animation: Animation - ) - where Element: StructuredQueriesCore.Table, Element.QueryOutput == Element { - self.init( - wrappedValue: wrappedValue, - sectionBy: { $0[keyPath: sectionKeyPath] }, - database: database, - animation: animation - ) - } - - /// Initializes this property with a query associated with the wrapped value, grouping results - /// into sections. - /// - /// - Parameters: - /// - wrappedValue: A default collection to associate with this property. - /// - statement: A query associated with the wrapped value. - /// - sectionKeyPath: A key path to a string column to group results by. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - public init( - wrappedValue: [Element] = [], - _ statement: S, - sectionBy sectionKeyPath: KeyPath< - S.From.TableColumns, some QueryExpression> - >, - database: (any DatabaseReader)? = nil, - animation: Animation - ) - where - Element == S.From.QueryOutput, - S.QueryValue == (), - S.From.QueryOutput: Sendable, - S.Joins == () - { - self.init( - wrappedValue: wrappedValue, - statement, - sectionBy: { $0[keyPath: sectionKeyPath] }, - database: database, - animation: animation - ) - } - - /// Replaces the wrapped value with data from the given query, grouping results into sections. - /// - /// - Parameters: - /// - statement: A query associated with the wrapped value. - /// - sectionKeyPath: A key path to a string column to group results by. - /// - database: The database to read from. A value of `nil` will use the default database - /// (`@Dependency(\.defaultDatabase)`). - /// - animation: The animation to use for user interface changes that result from changes to - /// the fetched results. - /// - Returns: A subscription associated with the observation. - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @discardableResult - public func load( - _ statement: S, - sectionBy sectionKeyPath: KeyPath< - S.From.TableColumns, some QueryExpression> - >, - database: (any DatabaseReader)? = nil, - animation: Animation? - ) async throws -> FetchSubscription - where - Element == S.From.QueryOutput, - S.QueryValue == (), - S.From.QueryOutput: Sendable, - S.Joins == () - { - try await load( - statement, - sectionBy: { $0[keyPath: sectionKeyPath] }, - database: database, - animation: animation - ) - } - } -#endif - -extension FetchAll { public init< - V: QueryRepresentable, From: StructuredQueriesCore.Table, each J: StructuredQueriesCore.Table + V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table >( wrappedValue: [Element] = [], - _ statement: some SelectStatement, - @_SectionBuilder sectionBy sectioning: ( - From.TableColumns, repeat (each J).TableColumns - ) -> _Sectioning?, - database: (any DatabaseReader)? = nil + _ statement: Select, + @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable ) where Element == V.QueryOutput, V.QueryOutput: Sendable { - let statement: Select = statement.asSelect() - guard let sectioning = sectioning(From.columns, repeat (each J).columns) else { - self.init(wrappedValue: wrappedValue, statement, database: database) + guard let sectioning = sectioning(From.columns, J.columns) else { + self.init(wrappedValue: wrappedValue, statement, database: database, scheduler: scheduler) return } self.init( @@ -775,17 +676,20 @@ extension FetchAll { request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), sectionBy: sectioning, database: database, - scheduler: nil + scheduler: scheduler ) } public init< - V: QueryRepresentable, From: StructuredQueriesCore.Table, each J: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table, + J1: StructuredQueriesCore.Table, + each J2: StructuredQueriesCore.Table >( wrappedValue: [Element] = [], - _ statement: some SelectStatement, + _ statement: Select, @_SectionBuilder sectionBy sectioning: ( - From.TableColumns, repeat (each J).TableColumns + From.TableColumns, J1.TableColumns, repeat (each J2).TableColumns ) -> _Sectioning?, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable @@ -794,8 +698,7 @@ extension FetchAll { Element == V.QueryOutput, V.QueryOutput: Sendable { - let statement: Select = statement.asSelect() - guard let sectioning = sectioning(From.columns, repeat (each J).columns) else { + guard let sectioning = sectioning(From.columns, J1.columns, repeat (each J2).columns) else { self.init(wrappedValue: wrappedValue, statement, database: database, scheduler: scheduler) return } @@ -808,105 +711,113 @@ extension FetchAll { ) } + /// Replaces the wrapped value with data from the given query, grouping results into sections. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - sectioning: A closure that returns a string expression, or an ordering of one, to group + /// results by, or `nil` for no grouping. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. + /// - Returns: A subscription associated with the observation. @discardableResult - public func load< - V: QueryRepresentable, From: StructuredQueriesCore.Table, each J: StructuredQueriesCore.Table - >( - _ statement: some SelectStatement, - @_SectionBuilder sectionBy sectioning: ( - From.TableColumns, repeat (each J).TableColumns - ) -> _Sectioning?, - database: (any DatabaseReader)? = nil + public func load( + _ statement: S, + @_SectionBuilder sectionBy sectioning: (S.From.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable ) async throws -> FetchSubscription where - Element == V.QueryOutput, - V.QueryOutput: Sendable + Element == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () { - let statement: Select = statement.asSelect() - guard let sectioning = sectioning(From.columns, repeat (each J).columns) else { - return try await load(statement, database: database) - } + let statement: Select<(), S.From, ()> = statement.asSelect() return try await loadSections( - request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), - sectionBy: sectioning, + statement: statement, + sectionBy: sectioning(S.From.columns), database: database, - scheduler: nil + scheduler: scheduler ) } + /// Replaces the wrapped value with data from the given query, grouping results into sections. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - sectionKeyPath: A key path to a string column to group results by. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - scheduler: The scheduler to observe from. By default, database observation is performed + /// asynchronously on the main queue. + /// - Returns: A subscription associated with the observation. @discardableResult - public func load< - V: QueryRepresentable, From: StructuredQueriesCore.Table, each J: StructuredQueriesCore.Table - >( - _ statement: some SelectStatement, - @_SectionBuilder sectionBy sectioning: ( - From.TableColumns, repeat (each J).TableColumns - ) -> _Sectioning?, + public func load( + _ statement: S, + sectionBy sectionKeyPath: KeyPath< + S.From.TableColumns, some QueryExpression> + >, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable ) async throws -> FetchSubscription where - Element == V.QueryOutput, - V.QueryOutput: Sendable + Element == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () { - let statement: Select = statement.asSelect() - guard let sectioning = sectioning(From.columns, repeat (each J).columns) else { - return try await load(statement, database: database, scheduler: scheduler) - } - return try await loadSections( - request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), - sectionBy: sectioning, + try await load( + statement, + sectionBy: { $0[keyPath: sectionKeyPath] }, database: database, scheduler: scheduler ) } -} -extension FetchAll { - public init< - V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table + @discardableResult + public func load< + V: QueryRepresentable, From: StructuredQueriesCore.Table >( - wrappedValue: [Element] = [], - _ statement: Select, - @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> _Sectioning?, - database: (any DatabaseReader)? = nil - ) + _ statement: Select, + @_SectionBuilder sectionBy sectioning: (From.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable + ) async throws -> FetchSubscription where Element == V.QueryOutput, V.QueryOutput: Sendable { - guard let sectioning = sectioning(From.columns, J.columns) else { - self.init(wrappedValue: wrappedValue, statement, database: database) - return + guard let sectioning = sectioning(From.columns) else { + return try await load(statement, database: database, scheduler: scheduler) } - self.init( - wrappedValue: wrappedValue, + return try await loadSections( request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), sectionBy: sectioning, database: database, - scheduler: nil + scheduler: scheduler ) } - public init< + @discardableResult + public func load< V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table >( - wrappedValue: [Element] = [], _ statement: Select, @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> _Sectioning?, database: (any DatabaseReader)? = nil, scheduler: some ValueObservationScheduler & Hashable - ) + ) async throws -> FetchSubscription where Element == V.QueryOutput, V.QueryOutput: Sendable { guard let sectioning = sectioning(From.columns, J.columns) else { - self.init(wrappedValue: wrappedValue, statement, database: database, scheduler: scheduler) - return + return try await load(statement, database: database, scheduler: scheduler) } - self.init( - wrappedValue: wrappedValue, + return try await loadSections( request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), sectionBy: sectioning, database: database, @@ -916,63 +827,219 @@ extension FetchAll { @discardableResult public func load< - V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table, + J1: StructuredQueriesCore.Table, + each J2: StructuredQueriesCore.Table >( - _ statement: Select, - @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> _Sectioning?, - database: (any DatabaseReader)? = nil + _ statement: Select, + @_SectionBuilder sectionBy sectioning: ( + From.TableColumns, J1.TableColumns, repeat (each J2).TableColumns + ) -> _Sectioning?, + database: (any DatabaseReader)? = nil, + scheduler: some ValueObservationScheduler & Hashable ) async throws -> FetchSubscription where Element == V.QueryOutput, V.QueryOutput: Sendable { - guard let sectioning = sectioning(From.columns, J.columns) else { - return try await load(statement, database: database) + guard let sectioning = sectioning(From.columns, J1.columns, repeat (each J2).columns) else { + return try await load(statement, database: database, scheduler: scheduler) } return try await loadSections( request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), sectionBy: sectioning, database: database, - scheduler: nil + scheduler: scheduler ) } +} - @discardableResult - public func load< - V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table - >( - _ statement: Select, - @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> _Sectioning?, - database: (any DatabaseReader)? = nil, - scheduler: some ValueObservationScheduler & Hashable - ) async throws -> FetchSubscription - where - Element == V.QueryOutput, - V.QueryOutput: Sendable - { - guard let sectioning = sectioning(From.columns, J.columns) else { - return try await load(statement, database: database, scheduler: scheduler) +#if canImport(SwiftUI) + extension FetchAll { + /// Initializes this property with a query that fetches every row from a table, grouping + /// results into sections. + /// + /// - Parameters: + /// - wrappedValue: A default collection to associate with this property. + /// - sectioning: A closure that returns a string expression, or an ordering of one, to + /// group results by, or `nil` for no grouping. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + wrappedValue: [Element] = [], + @_SectionBuilder sectionBy sectioning: (Element.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where Element: StructuredQueriesCore.Table, Element.QueryOutput == Element { + self.init( + wrappedValue: wrappedValue, + sectionBy: sectioning, + database: database, + scheduler: .animation(animation) + ) } - return try await loadSections( - request: FetchAllSectionedStatementValueRequest(statement: statement, sectionBy: sectioning), - sectionBy: sectioning, - database: database, - scheduler: scheduler + + /// Initializes this property with a query associated with the wrapped value, grouping results + /// into sections. + /// + /// - Parameters: + /// - wrappedValue: A default collection to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - sectioning: A closure that returns a string expression, or an ordering of one, to + /// group results by, or `nil` for no grouping. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + wrappedValue: [Element] = [], + _ statement: S, + @_SectionBuilder sectionBy sectioning: (S.From.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil, + animation: Animation ) - } -} + where + Element == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + self.init( + wrappedValue: wrappedValue, + statement, + sectionBy: sectioning, + database: database, + scheduler: .animation(animation) + ) + } + + /// Initializes this property with a query that fetches every row from a table, grouping + /// results into sections. + /// + /// - Parameters: + /// - wrappedValue: A default collection to associate with this property. + /// - sectionKeyPath: A key path to a string column to group results by. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + wrappedValue: [Element] = [], + sectionBy sectionKeyPath: KeyPath< + Element.TableColumns, some QueryExpression> + >, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where Element: StructuredQueriesCore.Table, Element.QueryOutput == Element { + self.init( + wrappedValue: wrappedValue, + sectionBy: { $0[keyPath: sectionKeyPath] }, + database: database, + animation: animation + ) + } + + /// Initializes this property with a query associated with the wrapped value, grouping results + /// into sections. + /// + /// - Parameters: + /// - wrappedValue: A default collection to associate with this property. + /// - statement: A query associated with the wrapped value. + /// - sectionKeyPath: A key path to a string column to group results by. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init( + wrappedValue: [Element] = [], + _ statement: S, + sectionBy sectionKeyPath: KeyPath< + S.From.TableColumns, some QueryExpression> + >, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where + Element == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + self.init( + wrappedValue: wrappedValue, + statement, + sectionBy: { $0[keyPath: sectionKeyPath] }, + database: database, + animation: animation + ) + } -#if canImport(SwiftUI) - extension FetchAll { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public init< - V: QueryRepresentable, From: StructuredQueriesCore.Table, - each J: StructuredQueriesCore.Table + V: QueryRepresentable, From: StructuredQueriesCore.Table >( wrappedValue: [Element] = [], - _ statement: some SelectStatement, + _ statement: Select, + @_SectionBuilder sectionBy sectioning: (From.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + self.init( + wrappedValue: wrappedValue, + statement, + sectionBy: sectioning, + database: database, + scheduler: .animation(animation) + ) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init< + V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table + >( + wrappedValue: [Element] = [], + _ statement: Select, + @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil, + animation: Animation + ) + where + Element == V.QueryOutput, + V.QueryOutput: Sendable + { + self.init( + wrappedValue: wrappedValue, + statement, + sectionBy: sectioning, + database: database, + scheduler: .animation(animation) + ) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + public init< + V: QueryRepresentable, + From: StructuredQueriesCore.Table, + J1: StructuredQueriesCore.Table, + each J2: StructuredQueriesCore.Table + >( + wrappedValue: [Element] = [], + _ statement: Select, @_SectionBuilder sectionBy sectioning: ( - From.TableColumns, repeat (each J).TableColumns + From.TableColumns, J1.TableColumns, repeat (each J2).TableColumns ) -> _Sectioning?, database: (any DatabaseReader)? = nil, animation: Animation @@ -990,16 +1057,80 @@ extension FetchAll { ) } + /// Replaces the wrapped value with data from the given query, grouping results into sections. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - sectioning: A closure that returns a string expression, or an ordering of one, to + /// group results by, or `nil` for no grouping. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + /// - Returns: A subscription associated with the observation. + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @discardableResult + public func load( + _ statement: S, + @_SectionBuilder sectionBy sectioning: (S.From.TableColumns) -> _Sectioning?, + database: (any DatabaseReader)? = nil, + animation: Animation? + ) async throws -> FetchSubscription + where + Element == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + try await load( + statement, + sectionBy: sectioning, + database: database, + scheduler: .animation(animation) + ) + } + + /// Replaces the wrapped value with data from the given query, grouping results into sections. + /// + /// - Parameters: + /// - statement: A query associated with the wrapped value. + /// - sectionKeyPath: A key path to a string column to group results by. + /// - database: The database to read from. A value of `nil` will use the default database + /// (`@Dependency(\.defaultDatabase)`). + /// - animation: The animation to use for user interface changes that result from changes to + /// the fetched results. + /// - Returns: A subscription associated with the observation. + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @discardableResult + public func load( + _ statement: S, + sectionBy sectionKeyPath: KeyPath< + S.From.TableColumns, some QueryExpression> + >, + database: (any DatabaseReader)? = nil, + animation: Animation? + ) async throws -> FetchSubscription + where + Element == S.From.QueryOutput, + S.QueryValue == (), + S.From.QueryOutput: Sendable, + S.Joins == () + { + try await load( + statement, + sectionBy: { $0[keyPath: sectionKeyPath] }, + database: database, + animation: animation + ) + } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @discardableResult public func load< - V: QueryRepresentable, From: StructuredQueriesCore.Table, - each J: StructuredQueriesCore.Table + V: QueryRepresentable, From: StructuredQueriesCore.Table >( - _ statement: some SelectStatement, - @_SectionBuilder sectionBy sectioning: ( - From.TableColumns, repeat (each J).TableColumns - ) -> _Sectioning?, + _ statement: Select, + @_SectionBuilder sectionBy sectioning: (From.TableColumns) -> _Sectioning?, database: (any DatabaseReader)? = nil, animation: Animation? ) async throws -> FetchSubscription @@ -1016,21 +1147,20 @@ extension FetchAll { } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - public init< + @discardableResult + public func load< V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table >( - wrappedValue: [Element] = [], _ statement: Select, @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> _Sectioning?, database: (any DatabaseReader)? = nil, - animation: Animation - ) + animation: Animation? + ) async throws -> FetchSubscription where Element == V.QueryOutput, V.QueryOutput: Sendable { - self.init( - wrappedValue: wrappedValue, + try await load( statement, sectionBy: sectioning, database: database, @@ -1041,10 +1171,15 @@ extension FetchAll { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @discardableResult public func load< - V: QueryRepresentable, From: StructuredQueriesCore.Table, J: StructuredQueriesCore.Table + V: QueryRepresentable, + From: StructuredQueriesCore.Table, + J1: StructuredQueriesCore.Table, + each J2: StructuredQueriesCore.Table >( - _ statement: Select, - @_SectionBuilder sectionBy sectioning: (From.TableColumns, J.TableColumns) -> _Sectioning?, + _ statement: Select, + @_SectionBuilder sectionBy sectioning: ( + From.TableColumns, J1.TableColumns, repeat (each J2).TableColumns + ) -> _Sectioning?, database: (any DatabaseReader)? = nil, animation: Animation? ) async throws -> FetchSubscription @@ -1097,9 +1232,7 @@ public struct _Sectioning: Hashable, Sendable { } package init(_ orderingTerm: _OrderingTerm>) { - var orderingTerm = orderingTerm self.select = orderingTerm.baseQueryFragment - orderingTerm.baseQueryFragment = orderingTerm.base.queryFragment self.order = orderingTerm.queryFragment } } diff --git a/Tests/SQLiteDataTests/FetchAllSectionsTests.swift b/Tests/SQLiteDataTests/FetchAllSectionsTests.swift index 89180123..1e26ba7d 100644 --- a/Tests/SQLiteDataTests/FetchAllSectionsTests.swift +++ b/Tests/SQLiteDataTests/FetchAllSectionsTests.swift @@ -341,11 +341,13 @@ struct FetchAllSectionsTests { } @Test func selectionSectionBy() async throws { - let statement = + @FetchAll( SectionedReminder - .order(by: \.id) - .select { SectionedRow.Columns(title: $0.title, label: $0.category) } - @FetchAll(statement, sectionBy: { $0.category }) var rows + .order(by: \.id) + .select { SectionedRow.Columns(title: $0.title, label: $0.category) }, + sectionBy: { $0.category } + ) + var rows try await $rows.load() #expect(rows.map(\.title) == ["Groceries", "Dishes", "Laundry", "Standup", "Review"]) @@ -354,12 +356,14 @@ struct FetchAllSectionsTests { } @Test func joinedSectionBy() async throws { - let statement = + @FetchAll( SectionedReminder - .order(by: \.id) - .join(SectionedCategory.all) { $0.category.eq($1.name) } - .select { SectionedRow.Columns(title: $0.title, label: $1.label) } - @FetchAll(statement, sectionBy: { $1.label }) var rows + .order(by: \.id) + .join(SectionedCategory.all) { $0.category.eq($1.name) } + .select { SectionedRow.Columns(title: $0.title, label: $1.label) }, + sectionBy: { $1.label } + ) + var rows try await $rows.load() #expect(rows.map(\.title) == ["Dishes", "Laundry", "Standup", "Review", "Groceries"]) @@ -369,40 +373,47 @@ struct FetchAllSectionsTests { } @Test func joinedDescendingSectionBy() async throws { - let statement = + @FetchAll( SectionedReminder - .order(by: \.id) - .join(SectionedCategory.all) { $0.category.eq($1.name) } - .select { SectionedRow.Columns(title: $0.title, label: $1.label) } - @FetchAll(statement, sectionBy: { $1.label.desc() }) var rows + .order(by: \.id) + .join(SectionedCategory.all) { $0.category.eq($1.name) } + .select { SectionedRow.Columns(title: $0.title, label: $1.label) }, + sectionBy: { $1.label.desc() } + ) + var rows try await $rows.load() #expect($rows.sections.sectionNames == ["Out & About", "At Work", "At Home"]) } @Test func loadJoinedSectionBy() async throws { - let statement = + @FetchAll( SectionedReminder - .order(by: \.id) - .join(SectionedCategory.all) { $0.category.eq($1.name) } - .select { SectionedRow.Columns(title: $0.title, label: $1.label) } - @FetchAll(statement) var rows + .order(by: \.id) + .join(SectionedCategory.all) { $0.category.eq($1.name) } + .select { SectionedRow.Columns(title: $0.title, label: $1.label) } + ) + var rows try await $rows.load() #expect($rows.sections.sectionNames == [nil]) - try await $rows.load(statement, sectionBy: { $1.label }) + try await $rows.load( + SectionedReminder + .order(by: \.id) + .join(SectionedCategory.all) { $0.category.eq($1.name) } + .select { SectionedRow.Columns(title: $0.title, label: $1.label) }, + sectionBy: { $1.label } + ) #expect(rows.map(\.title) == ["Dishes", "Laundry", "Standup", "Review", "Groceries"]) #expect($rows.sections.sectionNames == ["At Home", "At Work", "Out & About"]) } @Test(arguments: [true, false]) func dynamicJoinedSectionBy(isSectioned: Bool) async throws { - let statement = - SectionedReminder - .order(by: \.id) - .join(SectionedCategory.all) { $0.category.eq($1.name) } - .select { SectionedRow.Columns(title: $0.title, label: $1.label) } @FetchAll( - statement, + SectionedReminder + .order(by: \.id) + .join(SectionedCategory.all) { $0.category.eq($1.name) } + .select { SectionedRow.Columns(title: $0.title, label: $1.label) }, sectionBy: { if isSectioned { $1.label @@ -420,13 +431,16 @@ struct FetchAllSectionsTests { } @Test func twoJoinsSectionBy() async throws { - let statement = + @FetchAll( SectionedReminder - .order(by: \.id) - .join(SectionedCategory.all) { $0.category.eq($1.name) } - .join(SectionedPriority.all) { reminder, _, priority in reminder.priority.eq(priority.name) } - .select { SectionedRow.Columns(title: $0.title, label: $2.label) } - @FetchAll(statement, sectionBy: { $2.label }) var rows + .order(by: \.id) + .join(SectionedCategory.all) { $0.category.eq($1.name) } + .join(SectionedPriority.all) { reminder, _, priority in reminder.priority.eq(priority.name) + } + .select { SectionedRow.Columns(title: $0.title, label: $2.label) }, + sectionBy: { $2.label } + ) + var rows try await $rows.load() #expect(rows.map(\.title) == ["Dishes", "Standup", "Groceries"]) @@ -436,16 +450,19 @@ struct FetchAllSectionsTests { } @Test func threeJoinsSectionBy() async throws { - let statement = + @FetchAll( SectionedReminder - .order(by: \.id) - .join(SectionedCategory.all) { $0.category.eq($1.name) } - .join(SectionedPriority.all) { reminder, _, priority in reminder.priority.eq(priority.name) } - .join(SectionedUrgency.all) { reminder, _, _, urgency in - reminder.priority.eq(urgency.name) - } - .select { SectionedRow.Columns(title: $0.title, label: $3.label) } - @FetchAll(statement, sectionBy: { $3.label }) var rows + .order(by: \.id) + .join(SectionedCategory.all) { $0.category.eq($1.name) } + .join(SectionedPriority.all) { reminder, _, priority in reminder.priority.eq(priority.name) + } + .join(SectionedUrgency.all) { reminder, _, _, urgency in + reminder.priority.eq(urgency.name) + } + .select { SectionedRow.Columns(title: $0.title, label: $3.label) }, + sectionBy: { $3.label } + ) + var rows try await $rows.load() #expect(rows.map(\.title) == ["Groceries", "Dishes", "Standup"])