Skip to content

Commit

Permalink
Allow eager-loading of deleted models (#555)
Browse files Browse the repository at this point in the history
  • Loading branch information
tarag authored Mar 23, 2023
1 parent dd6b7f8 commit 21d99b0
Show file tree
Hide file tree
Showing 21 changed files with 299 additions and 39 deletions.
6 changes: 5 additions & 1 deletion Sources/FluentBenchmark/SolarSystem/Planet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public final class Planet: Model {

@Siblings(through: PlanetTag.self, from: \.$planet, to: \.$tag)
public var tags: [Tag]

@Timestamp(key: "deleted_at", on: .delete)
var deletedAt: Date?

public init() { }

Expand All @@ -48,6 +51,7 @@ public struct PlanetMigration: Migration {
.field("name", .string, .required)
.field("star_id", .uuid, .required, .references("stars", "id"))
.field("possible_star_id", .uuid, .references("stars", "id"))
.field("deleted_at", .datetime)
.create()
}

Expand Down Expand Up @@ -88,6 +92,6 @@ public struct PlanetSeed: Migration {
}

public func revert(on database: Database) -> EventLoopFuture<Void> {
Planet.query(on: database).delete()
Planet.query(on: database).delete(force: true)
}
}
6 changes: 5 additions & 1 deletion Sources/FluentBenchmark/SolarSystem/Star.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public final class Star: Model {

@Children(for: \.$star)
public var planets: [Planet]

@Timestamp(key: "deleted_at", on: .delete)
var deletedAt: Date?

public init() { }

Expand All @@ -32,6 +35,7 @@ public struct StarMigration: Migration {
.field("id", .uuid, .identifier(auto: false))
.field("name", .string, .required)
.field("galaxy_id", .uuid, .required, .references("galaxies", "id"))
.field("deleted_at", .datetime)
.create()
}

Expand Down Expand Up @@ -61,6 +65,6 @@ public final class StarSeed: Migration {
}

public func revert(on database: Database) -> EventLoopFuture<Void> {
Star.query(on: database).delete()
Star.query(on: database).delete(force: true)
}
}
77 changes: 77 additions & 0 deletions Sources/FluentBenchmark/Tests/EagerLoadTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ extension FluentBenchmarker {
public func testEagerLoad() throws {
try self.testEagerLoad_nesting()
try self.testEagerLoad_children()
try self.testEagerLoad_childrenDeleted()
try self.testEagerLoad_parent()
try self.testEagerLoad_parentDeleted()
try self.testEagerLoad_siblings()
try self.testEagerLoad_siblingsDeleted()
try self.testEagerLoad_parentJSON()
try self.testEagerLoad_childrenJSON()
try self.testEagerLoad_emptyChildren()
Expand Down Expand Up @@ -57,6 +60,30 @@ extension FluentBenchmarker {
}
}
}

private func testEagerLoad_childrenDeleted() throws {
try self.runTest(#function, [
SolarSystem()
]) {
try Planet.query(on: self.database).filter(\.$name == "Jupiter").delete().wait()

let sun1 = try XCTUnwrap(Star.query(on: self.database)
.filter(\.$name == "Sun")
.with(\.$planets, withDeleted: true)
.first().wait()
)
XCTAssertTrue(sun1.planets.contains { $0.name == "Earth" })
XCTAssertTrue(sun1.planets.contains { $0.name == "Jupiter" })

let sun2 = try XCTUnwrap(Star.query(on: self.database)
.filter(\.$name == "Sun")
.with(\.$planets)
.first().wait()
)
XCTAssertTrue(sun2.planets.contains { $0.name == "Earth" })
XCTAssertFalse(sun2.planets.contains { $0.name == "Jupiter" })
}
}

private func testEagerLoad_parent() throws {
try self.runTest(#function, [
Expand All @@ -77,6 +104,34 @@ extension FluentBenchmarker {
}
}
}

private func testEagerLoad_parentDeleted() throws {
try self.runTest(#function, [
SolarSystem()
]) {
try Star.query(on: self.database).filter(\.$name == "Sun").delete().wait()

let planet = try XCTUnwrap(Planet.query(on: self.database)
.filter(\.$name == "Earth")
.with(\.$star, withDeleted: true)
.first().wait()
)
XCTAssertEqual(planet.star.name, "Sun")

XCTAssertThrowsError(
try Planet.query(on: self.database)
.with(\.$star)
.all().wait()
) { error in
guard case let .missingParent(from, to, key, _) = error as? FluentError else {
return XCTFail("Unexpected error \(error) thrown")
}
XCTAssertEqual(from, "Planet")
XCTAssertEqual(to, "Star")
XCTAssertEqual(key, "star_id")
}
}
}

private func testEagerLoad_siblings() throws {
try self.runTest(#function, [
Expand All @@ -103,6 +158,28 @@ extension FluentBenchmarker {
}
}
}

private func testEagerLoad_siblingsDeleted() throws {
try self.runTest(#function, [
SolarSystem()
]) {
try Planet.query(on: self.database).filter(\.$name == "Earth").delete().wait()

let tag1 = try XCTUnwrap(Tag.query(on: self.database)
.filter(\.$name == "Inhabited")
.with(\.$planets, withDeleted: true)
.first().wait()
)
XCTAssertEqual(Set(tag1.planets.map(\.name)), ["Earth"])

let tag2 = try XCTUnwrap(Tag.query(on: self.database)
.filter(\.$name == "Inhabited")
.with(\.$planets)
.first().wait()
)
XCTAssertEqual(Set(tag2.planets.map(\.name)), [])
}
}

private func testEagerLoad_parentJSON() throws {
try self.runTest(#function, [
Expand Down
24 changes: 24 additions & 0 deletions Sources/FluentBenchmark/Tests/OptionalParentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,26 @@ extension FluentBenchmarker {
.all().wait()
XCTAssertEqual(users2.count, 1)
XCTAssert(users2.first?.bestFriend == nil)

// Test deleted OptionalParent
try User.query(on: self.database).filter(\.$name == "Swift").delete().wait()

let users3 = try User.query(on: self.database)
.with(\.$bestFriend, withDeleted: true)
.all().wait()
XCTAssertEqual(users3.first?.bestFriend?.name, "Swift")

XCTAssertThrowsError(try User.query(on: self.database)
.with(\.$bestFriend)
.all().wait()
) { error in
guard case let .missingParent(from, to, key, _) = error as? FluentError else {
return XCTFail("Unexpected error \(error) thrown")
}
XCTAssertEqual(from, "User")
XCTAssertEqual(to, "User")
XCTAssertEqual(key, "bf_id")
}
}
}
}
Expand Down Expand Up @@ -93,6 +113,9 @@ private final class User: Model {

@Children(for: \.$bestFriend)
var friends: [User]

@Timestamp(key: "deleted_at", on: .delete)
var deletedAt: Date?

init() { }

Expand All @@ -111,6 +134,7 @@ private struct UserMigration: Migration {
.field("name", .string, .required)
.field("pet", .json, .required)
.field("bf_id", .uuid)
.field("deleted_at", .datetime)
.create()
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/FluentBenchmark/Tests/SchemaTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ extension FluentBenchmarker {
XCTAssertThrowsError(
try Star.query(on: self.database)
.filter(\.$name == "Sun")
.delete().wait()
.delete(force: true).wait()
)
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/FluentBenchmark/Tests/SoftDeleteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ extension FluentBenchmarker {
XCTAssertEqual(key, "bar")
XCTAssertEqual(id, "\(bar1.id!)")
}

XCTAssertNoThrow(try Foo.query(on: self.database).with(\.$bar, withDeleted: true).all().wait())
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions Sources/FluentKit/Model/EagerLoad.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ public protocol EagerLoadable {
_ relationKey: KeyPath<From, Self>,
to builder: Builder
) where Builder: EagerLoadBuilder, Builder.Model == From

static func eagerLoad<Builder>(
_ relationKey: KeyPath<From, Self>,
withDeleted: Bool,
to builder: Builder
) where Builder: EagerLoadBuilder, Builder.Model == From

static func eagerLoad<Loader, Builder>(
_ loader: Loader,
Expand All @@ -33,3 +39,13 @@ public protocol EagerLoadable {
Loader.Model == To,
Builder.Model == From
}

extension EagerLoadable {
public static func eagerLoad<Builder>(
_ relationKey: KeyPath<From, Self>,
withDeleted: Bool,
to builder: Builder
) where Builder: EagerLoadBuilder, Builder.Model == From {
return Self.eagerLoad(relationKey, to: builder)
}
}
18 changes: 16 additions & 2 deletions Sources/FluentKit/Properties/Children.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,23 @@ extension ChildrenProperty: Relation {
// MARK: Eager Loadable

extension ChildrenProperty: EagerLoadable {
public static func eagerLoad<Builder>(
_ relationKey: KeyPath<From, ChildrenProperty<From, To>>,
to builder: Builder
)
where Builder : EagerLoadBuilder, From == Builder.Model
{
self.eagerLoad(relationKey, withDeleted: false, to: builder)
}

public static func eagerLoad<Builder>(
_ relationKey: KeyPath<From, From.Children<To>>,
withDeleted: Bool,
to builder: Builder
)
where Builder: EagerLoadBuilder, Builder.Model == From
{
let loader = ChildrenEagerLoader(relationKey: relationKey)
let loader = ChildrenEagerLoader(relationKey: relationKey, withDeleted: withDeleted)
builder.add(loader: loader)
}

Expand All @@ -194,7 +204,8 @@ private struct ChildrenEagerLoader<From, To>: EagerLoader
where From: Model, To: Model
{
let relationKey: KeyPath<From, From.Children<To>>

let withDeleted: Bool

func run(models: [From], on database: Database) -> EventLoopFuture<Void> {
let ids = models.map { $0.id! }

Expand All @@ -206,6 +217,9 @@ private struct ChildrenEagerLoader<From, To>: EagerLoader
case .required(let required):
builder.filter(required.appending(path: \.$id) ~~ Set(ids))
}
if (self.withDeleted) {
builder.withDeleted()
}
return builder.all().map {
for model in models {
let id = model[keyPath: self.relationKey].idValue!
Expand Down
11 changes: 9 additions & 2 deletions Sources/FluentKit/Properties/CompositeChildren.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,16 @@ extension CompositeChildrenProperty: Relation {
}

extension CompositeChildrenProperty: EagerLoadable {
public static func eagerLoad<Builder>(_ relationKey: KeyPath<From, From.CompositeChildren<To>>, to builder: Builder)
public static func eagerLoad<Builder>(_ relationKey: KeyPath<From, CompositeChildrenProperty<From, To>>, to builder: Builder)
where Builder : EagerLoadBuilder, From == Builder.Model
{
self.eagerLoad(relationKey, withDeleted: false, to: builder)
}

public static func eagerLoad<Builder>(_ relationKey: KeyPath<From, From.CompositeChildren<To>>, withDeleted: Bool, to builder: Builder)
where Builder: EagerLoadBuilder, Builder.Model == From
{
let loader = CompositeChildrenEagerLoader(relationKey: relationKey)
let loader = CompositeChildrenEagerLoader(relationKey: relationKey, withDeleted: withDeleted)
builder.add(loader: loader)
}

Expand All @@ -183,6 +189,7 @@ private struct CompositeChildrenEagerLoader<From, To>: EagerLoader
where From: Model, To: Model, From.IDValue: Fields
{
let relationKey: KeyPath<From, From.CompositeChildren<To>>
let withDeleted: Bool

func run(models: [From], on database: Database) -> EventLoopFuture<Void> {
let ids = Set(models.map(\.id!))
Expand Down
15 changes: 12 additions & 3 deletions Sources/FluentKit/Properties/CompositeOptionalChild.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,16 @@ extension CompositeOptionalChildProperty: Relation {
}

extension CompositeOptionalChildProperty: EagerLoadable {
public static func eagerLoad<Builder>(_ relationKey: KeyPath<From, From.CompositeOptionalChild<To>>, to builder: Builder)
public static func eagerLoad<Builder>(_ relationKey: KeyPath<From, CompositeOptionalChildProperty<From, To>>, to builder: Builder)
where Builder : EagerLoadBuilder, From == Builder.Model
{
self.eagerLoad(relationKey, withDeleted: false, to: builder)
}

public static func eagerLoad<Builder>(_ relationKey: KeyPath<From, From.CompositeOptionalChild<To>>, withDeleted: Bool, to builder: Builder)
where Builder: EagerLoadBuilder, Builder.Model == From
{
let loader = CompositeOptionalChildEagerLoader(relationKey: relationKey)
let loader = CompositeOptionalChildEagerLoader(relationKey: relationKey, withDeleted: withDeleted)
builder.add(loader: loader)
}

Expand All @@ -168,6 +174,7 @@ private struct CompositeOptionalChildEagerLoader<From, To>: EagerLoader
where From: Model, To: Model, From.IDValue: Fields
{
let relationKey: KeyPath<From, From.CompositeOptionalChild<To>>
let withDeleted: Bool

func run(models: [From], on database: Database) -> EventLoopFuture<Void> {
let ids = Set(models.map(\.id!))
Expand All @@ -177,7 +184,9 @@ private struct CompositeOptionalChildEagerLoader<From, To>: EagerLoader
builder.group(.or) { query in
_ = parentKey.queryFilterIds(ids, in: query)
}

if (self.withDeleted) {
builder.withDeleted()
}
return builder.all().map {
let indexedResults = Dictionary(grouping: $0, by: { parentKey.referencedId(in: $0)! })

Expand Down
18 changes: 14 additions & 4 deletions Sources/FluentKit/Properties/CompositeOptionalParent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,16 @@ extension CompositeOptionalParentProperty: AnyCodableProperty {
}

extension CompositeOptionalParentProperty: EagerLoadable {
public static func eagerLoad<Builder>(_ relationKey: KeyPath<From, From.CompositeOptionalParent<To>>, to builder: Builder)
public static func eagerLoad<Builder>(_ relationKey: KeyPath<From, CompositeOptionalParentProperty<From, To>>, to builder: Builder)
where Builder : EagerLoadBuilder, From == Builder.Model
{
self.eagerLoad(relationKey, withDeleted: false, to: builder)
}

public static func eagerLoad<Builder>(_ relationKey: KeyPath<From, From.CompositeOptionalParent<To>>, withDeleted: Bool, to builder: Builder)
where Builder: EagerLoadBuilder, Builder.Model == From
{
builder.add(loader: CompositeOptionalParentEagerLoader(relationKey: relationKey))
builder.add(loader: CompositeOptionalParentEagerLoader(relationKey: relationKey, withDeleted: withDeleted))
}

public static func eagerLoad<Loader, Builder>(_ loader: Loader, through: KeyPath<From, From.CompositeOptionalParent<To>>, to builder: Builder)
Expand All @@ -208,14 +214,18 @@ private struct CompositeOptionalParentEagerLoader<From, To>: EagerLoader
where From: Model, To: Model, To.IDValue: Fields
{
let relationKey: KeyPath<From, From.CompositeOptionalParent<To>>
let withDeleted: Bool

func run(models: [From], on database: Database) -> EventLoopFuture<Void> {
var sets = Dictionary(grouping: models, by: { $0[keyPath: self.relationKey].id })
let nilParentModels = sets.removeValue(forKey: nil) ?? []

return To.query(on: database)
let builder = To.query(on: database)
.group(.or) { _ = sets.keys.reduce($0) { query, id in query.group(.and) { id!.input(to: QueryFilterInput(builder: $0)) } } }
.all().flatMapThrowing {
if (self.withDeleted) {
builder.withDeleted()
}
return builder.all().flatMapThrowing {
let parents = Dictionary(uniqueKeysWithValues: $0.map { ($0.id!, $0) })

for (parentId, models) in sets {
Expand Down
Loading

0 comments on commit 21d99b0

Please sign in to comment.