diff --git a/Sources/FluentKit/Properties/Timestamp.swift b/Sources/FluentKit/Properties/Timestamp.swift index 780353fd..776a367a 100644 --- a/Sources/FluentKit/Properties/Timestamp.swift +++ b/Sources/FluentKit/Properties/Timestamp.swift @@ -222,4 +222,25 @@ extension Schema { .value(deletedAtField, .greaterThan, timestamp.currentTimestampInput) ], .or)) } + + + static func excludeDeleted(from filters: [DatabaseQuery.Filter]) -> [DatabaseQuery.Filter] { + guard let timestamp = self.init().deletedTimestamp else { + return filters + } + + let deletedAtField = DatabaseQuery.Field.extendedPath( + [timestamp.key], + schema: self.schemaOrAlias, + space: self.space + ) + + var copy = filters + copy.append(.group([ + .value(deletedAtField, .equal, .null), + .value(deletedAtField, .greaterThan, timestamp.currentTimestampInput) + ], .or)) + + return copy + } } diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder+Join.swift b/Sources/FluentKit/Query/Builder/QueryBuilder+Join.swift index 8a3553c4..58ff2f6e 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder+Join.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder+Join.swift @@ -36,7 +36,16 @@ extension QueryBuilder { ) -> Self where Foreign: Schema { - self.join(Foreign.self, on: .advancedJoin(schema: Foreign.schema, space: Foreign.space, alias: Foreign.alias, method, filters: filters)) + let finalFilters: [DatabaseQuery.Filter] + + // If deleted models aren't included, add filters to exclude them for each model being queried. + if !self.includeDeleted { + finalFilters = foreign.excludeDeleted(from: filters) + } else { + finalFilters = filters + } + + return self.join(Foreign.self, on: .advancedJoin(schema: Foreign.schema, space: Foreign.space, alias: Foreign.alias, method, filters: finalFilters)) } /// `.join(Foreign.self, on: databaseJoin)` diff --git a/Sources/FluentKit/Query/Builder/QueryBuilder.swift b/Sources/FluentKit/Query/Builder/QueryBuilder.swift index c1b8d9ca..45a14948 100644 --- a/Sources/FluentKit/Query/Builder/QueryBuilder.swift +++ b/Sources/FluentKit/Query/Builder/QueryBuilder.swift @@ -303,7 +303,7 @@ public final class QueryBuilder self.addFields(for: model, to: &query) } } - + // If deleted models aren't included, add filters // to exclude them for each model being queried. if !self.includeDeleted { diff --git a/Tests/FluentKitTests/AsyncTests/AsyncFluentKitTests.swift b/Tests/FluentKitTests/AsyncTests/AsyncFluentKitTests.swift index 3fcf9199..b2a7a420 100644 --- a/Tests/FluentKitTests/AsyncTests/AsyncFluentKitTests.swift +++ b/Tests/FluentKitTests/AsyncTests/AsyncFluentKitTests.swift @@ -75,17 +75,17 @@ final class AsyncFluentKitTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try await Planet.query(on: db).join(child: \Planet.$governor).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "governors" ON "planets"."id" = "governors"."planet_id"#), true) + XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "governors" ON "planets"."id" = "governors"."planet_id""#), true) db.reset() _ = try await Planet.query(on: db).join(children: \Planet.$moons).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "moons" ON "planets"."id" = "moons"."planet_id"#), true) + XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "moons" ON "planets"."id" = "moons"."planet_id""#), true) db.reset() _ = try await Planet.query(on: db).join(parent: \Planet.$star).all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "stars" ON "planets"."star_id" = "stars"."id"#), true) + XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "stars" ON "planets"."star_id" = "stars"."id" AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $1)"#), true) db.reset() _ = try await Planet.query(on: db).join(siblings: \Planet.$tags).all() diff --git a/Tests/FluentKitTests/AsyncTests/AsyncQueryBuilderTests.swift b/Tests/FluentKitTests/AsyncTests/AsyncQueryBuilderTests.swift index d9da3716..3b68d73f 100644 --- a/Tests/FluentKitTests/AsyncTests/AsyncQueryBuilderTests.swift +++ b/Tests/FluentKitTests/AsyncTests/AsyncQueryBuilderTests.swift @@ -304,6 +304,6 @@ final class AsyncQueryBuilderTests: XCTestCase { .join(Star.self, on: \Star.$id == \Planet.$star.$id && \Star.$name != \Planet.$name) .all() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(try db.sqlSerializers.xctAt(0).sql, #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at", "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "planets" INNER JOIN "stars" ON "stars"."id" = "planets"."star_id" AND "stars"."name" <> "planets"."name" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1) AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $2)"#) + XCTAssertEqual(try db.sqlSerializers.xctAt(0).sql, #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at", "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "planets" INNER JOIN "stars" ON "stars"."id" = "planets"."star_id" AND "stars"."name" <> "planets"."name" AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $1) WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $2) AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $3)"#) } } diff --git a/Tests/FluentKitTests/CompositeIDTests.swift b/Tests/FluentKitTests/CompositeIDTests.swift index ace4e0fb..5f85b40c 100644 --- a/Tests/FluentKitTests/CompositeIDTests.swift +++ b/Tests/FluentKitTests/CompositeIDTests.swift @@ -120,7 +120,7 @@ final class CompositeIDTests: XCTestCase { XCTAssertEqual(db.sqlSerializers.count, 9) XCTAssertEqual(try db.sqlSerializers.xctAt(0).sql, #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id" FROM "planets" WHERE "planets"."id" = $1 LIMIT 1"#) XCTAssertEqual(try db.sqlSerializers.xctAt(1).sql, #"SELECT "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $2)"#) - XCTAssertEqual(try db.sqlSerializers.xctAt(2).sql, #"SELECT "tags"."id" AS "tags_id", "tags"."name" AS "tags_name", "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "tags" INNER JOIN "composite+planet+tag" ON "tags"."id" = "composite+planet+tag"."tag_id" WHERE "composite+planet+tag"."planet_id" = $1 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $2)"#) + XCTAssertEqual(try db.sqlSerializers.xctAt(2).sql, #"SELECT "tags"."id" AS "tags_id", "tags"."name" AS "tags_name", "composite+planet+tag"."planet_id" AS "composite+planet+tag_planet_id", "composite+planet+tag"."tag_id" AS "composite+planet+tag_tag_id", "composite+planet+tag"."notation" AS "composite+planet+tag_notation", "composite+planet+tag"."createdAt" AS "composite+planet+tag_createdAt", "composite+planet+tag"."updatedAt" AS "composite+planet+tag_updatedAt", "composite+planet+tag"."deletedAt" AS "composite+planet+tag_deletedAt" FROM "tags" INNER JOIN "composite+planet+tag" ON "tags"."id" = "composite+planet+tag"."tag_id" AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $1) WHERE "composite+planet+tag"."planet_id" = $2 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"#) XCTAssertEqual(try db.sqlSerializers.xctAt(3).sql, #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, DEFAULT, $3, $4, DEFAULT)"#) XCTAssertEqual(try db.sqlSerializers.xctAt(4).sql, #"INSERT INTO "composite+planet+tag" ("planet_id", "tag_id", "notation", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, DEFAULT, $3, $4, DEFAULT)"#) XCTAssertEqual(try db.sqlSerializers.xctAt(5).sql, #"SELECT COUNT("composite+planet+tag"."planet_id") AS "aggregate" FROM "composite+planet+tag" WHERE "composite+planet+tag"."planet_id" = $1 AND "composite+planet+tag"."tag_id" = $2 AND ("composite+planet+tag"."deletedAt" IS NULL OR "composite+planet+tag"."deletedAt" > $3)"#) diff --git a/Tests/FluentKitTests/FluentKitTests.swift b/Tests/FluentKitTests/FluentKitTests.swift index ace5d467..8103320c 100644 --- a/Tests/FluentKitTests/FluentKitTests.swift +++ b/Tests/FluentKitTests/FluentKitTests.swift @@ -134,22 +134,22 @@ final class FluentKitTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try Planet.query(on: db).join(child: \Planet.$governor).all().wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "governors" ON "planets"."id" = "governors"."planet_id"#), true) + XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "governors" ON "planets"."id" = "governors"."planet_id""#), true) db.reset() _ = try Planet.query(on: db).join(children: \Planet.$moons).all().wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "moons" ON "planets"."id" = "moons"."planet_id"#), true) + XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "moons" ON "planets"."id" = "moons"."planet_id""#), true) db.reset() _ = try Planet.query(on: db).join(parent: \Planet.$star).all().wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "stars" ON "planets"."star_id" = "stars"."id"#), true) + XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "stars" ON "planets"."star_id" = "stars"."id" AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $1)"#), true) db.reset() _ = try Planet.query(on: db).join(parent: \Planet.$possibleStar).all().wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "stars" ON "planets"."possible_star_id" = "stars"."id"#), true) + XCTAssertEqual(db.sqlSerializers.first?.sql.contains(#"INNER JOIN "stars" ON "planets"."possible_star_id" = "stars"."id" AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $1)"#), true) db.reset() _ = try Planet.query(on: db).join(siblings: \Planet.$tags).all().wait() @@ -744,7 +744,7 @@ final class FluentKitTests: XCTestCase { XCTAssertEqual(db.sqlSerializers.dropFirst(2).first?.sql, #"INSERT INTO "mirror_universe"."planets" ("id", "name", "star_id", "possible_star_id", "createdAt", "updatedAt", "deletedAt") VALUES ($1, $2, DEFAULT, DEFAULT, $3, $4, DEFAULT)"#) XCTAssertEqual(db.sqlSerializers.dropFirst(3).first?.sql, #"UPDATE "mirror_universe"."planets" SET "id" = $1, "name" = $2, "updatedAt" = $3 WHERE "mirror_universe"."planets"."id" = $4 AND ("mirror_universe"."planets"."deletedAt" IS NULL OR "mirror_universe"."planets"."deletedAt" > $5)"#) XCTAssertEqual(db.sqlSerializers.dropFirst(4).first?.sql, #"DELETE FROM "mirror_universe"."planets" WHERE "mirror_universe"."planets"."name" <> $1"#) - XCTAssertEqual(db.sqlSerializers.dropFirst(5).first?.sql, #"SELECT "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "stars" INNER JOIN "mirror_universe"."planets" ON "mirror_universe"."planets"."star_id" = "stars"."id" LIMIT 1"#) + XCTAssertEqual(db.sqlSerializers.dropFirst(5).first?.sql, #"SELECT "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "stars" INNER JOIN "mirror_universe"."planets" ON "mirror_universe"."planets"."star_id" = "stars"."id" AND ("mirror_universe"."planets"."deletedAt" IS NULL OR "mirror_universe"."planets"."deletedAt" > $1) LIMIT 1"#) } func testKeyPrefixingStrategies() throws { diff --git a/Tests/FluentKitTests/QueryBuilderTests.swift b/Tests/FluentKitTests/QueryBuilderTests.swift index 437cd9a7..7bbbcfa7 100644 --- a/Tests/FluentKitTests/QueryBuilderTests.swift +++ b/Tests/FluentKitTests/QueryBuilderTests.swift @@ -193,10 +193,10 @@ final class QueryBuilderTests: XCTestCase { let db = DummyDatabaseForTestSQLSerializer() _ = try Planet.query(on: db) .join(Planet.self, Star.self, - on: .custom(#"LEFT JOIN "stars" ON "stars"."id" = "planets"."id" AND "stars"."name" = 'Sol'"#)) + on: .custom(#"LEFT JOIN "stars" ON "stars"."id" = "planets"."id" AND "stars"."name" = 'Sol' AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $1)"#)) .all().wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at", "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "planets" LEFT JOIN "stars" ON "stars"."id" = "planets"."id" AND "stars"."name" = 'Sol' WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1) AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $2)"#) + XCTAssertEqual(db.sqlSerializers.first?.sql, #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at", "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "planets" LEFT JOIN "stars" ON "stars"."id" = "planets"."id" AND "stars"."name" = 'Sol' AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $1) WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1) AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $2)"#) } func testComplexJoinOperators() throws { @@ -206,6 +206,6 @@ final class QueryBuilderTests: XCTestCase { .join(Star.self, on: \Star.$id == \Planet.$star.$id && \Star.$name != \Planet.$name) .all().wait() XCTAssertEqual(db.sqlSerializers.count, 1) - XCTAssertEqual(try db.sqlSerializers.xctAt(0).sql, #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at", "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "planets" INNER JOIN "stars" ON "stars"."id" = "planets"."star_id" AND "stars"."name" <> "planets"."name" WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $1) AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $2)"#) + XCTAssertEqual(try db.sqlSerializers.xctAt(0).sql, #"SELECT "planets"."id" AS "planets_id", "planets"."name" AS "planets_name", "planets"."star_id" AS "planets_star_id", "planets"."possible_star_id" AS "planets_possible_star_id", "planets"."deleted_at" AS "planets_deleted_at", "stars"."id" AS "stars_id", "stars"."name" AS "stars_name", "stars"."galaxy_id" AS "stars_galaxy_id", "stars"."deleted_at" AS "stars_deleted_at" FROM "planets" INNER JOIN "stars" ON "stars"."id" = "planets"."star_id" AND "stars"."name" <> "planets"."name" AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $1) WHERE ("planets"."deleted_at" IS NULL OR "planets"."deleted_at" > $2) AND ("stars"."deleted_at" IS NULL OR "stars"."deleted_at" > $3)"#) } }