├── .github
├── .codecov.yml
├── CODEOWNERS
├── CONTRIBUTING.md
└── workflows
│ ├── api-docs.yml
│ └── test.yml
├── .gitignore
├── .spi.yml
├── LICENSE
├── Package.swift
├── README.md
├── Sources
├── FluentBenchmark
│ ├── Exports.swift
│ ├── FluentBenchmarker.swift
│ ├── SolarSystem
│ │ ├── GalacticJurisdiction.swift
│ │ ├── Galaxy.swift
│ │ ├── Governor.swift
│ │ ├── Jurisdiction.swift
│ │ ├── Moon.swift
│ │ ├── Planet.swift
│ │ ├── PlanetTag.swift
│ │ ├── SolarSystem.swift
│ │ ├── Star.swift
│ │ └── Tag.swift
│ └── Tests
│ │ ├── AggregateTests.swift
│ │ ├── ArrayTests.swift
│ │ ├── BatchTests.swift
│ │ ├── CRUDTests.swift
│ │ ├── ChildTests.swift
│ │ ├── ChildrenTests.swift
│ │ ├── ChunkTests.swift
│ │ ├── CodableTests.swift
│ │ ├── CompositeIDTests.swift
│ │ ├── CompositeRelationTests.swift
│ │ ├── EagerLoadTests.swift
│ │ ├── EnumTests.swift
│ │ ├── FilterTests.swift
│ │ ├── GroupTests.swift
│ │ ├── IDTests.swift
│ │ ├── JoinTests.swift
│ │ ├── MiddlewareTests.swift
│ │ ├── MigratorTests.swift
│ │ ├── ModelTests.swift
│ │ ├── OptionalParentTests.swift
│ │ ├── PaginationTests.swift
│ │ ├── ParentTests.swift
│ │ ├── PerformanceTests+Siblings.swift
│ │ ├── PerformanceTests.swift
│ │ ├── RangeTests.swift
│ │ ├── SQLTests.swift
│ │ ├── SchemaTests.swift
│ │ ├── SetTests.swift
│ │ ├── SiblingsTests.swift
│ │ ├── SoftDeleteTests.swift
│ │ ├── SortTests.swift
│ │ ├── TimestampTests.swift
│ │ ├── TransactionTests.swift
│ │ └── UniqueTests.swift
├── FluentKit
│ ├── Concurrency
│ │ ├── AsyncMigration.swift
│ │ ├── AsyncModelMiddleware.swift
│ │ ├── Children+Concurrency.swift
│ │ ├── Database+Concurrency.swift
│ │ ├── EnumBuilder+Concurrency.swift
│ │ ├── Model+Concurrency.swift
│ │ ├── ModelResponder+Concurrency.swift
│ │ ├── OptionalChild+Concurrency.swift
│ │ ├── OptionalParent+Concurrency.swift
│ │ ├── Parent+Concurrency.swift
│ │ ├── QueryBuilder+Concurrency.swift
│ │ ├── Relation+Concurrency.swift
│ │ ├── SchemaBuilder+Concurrency.swift
│ │ └── Siblings+Concurrency.swift
│ ├── Database
│ │ ├── Database+Logging.swift
│ │ ├── Database.swift
│ │ ├── DatabaseID.swift
│ │ ├── DatabaseInput.swift
│ │ ├── DatabaseOutput.swift
│ │ ├── Databases.swift
│ │ ├── KeyPrefixingStrategy.swift
│ │ └── TransactionControlDatabase.swift
│ ├── Docs.docc
│ │ ├── Resources
│ │ │ └── vapor-fluentkit-logo.svg
│ │ ├── index.md
│ │ └── theme-settings.json
│ ├── Enum
│ │ ├── DatabaseEnum.swift
│ │ ├── EnumBuilder.swift
│ │ ├── EnumMetadata.swift
│ │ ├── EnumProperty.swift
│ │ └── OptionalEnumProperty.swift
│ ├── Exports.swift
│ ├── FluentError.swift
│ ├── Middleware
│ │ ├── ModelMiddleware.swift
│ │ └── ModelResponder.swift
│ ├── Migration
│ │ ├── Migration.swift
│ │ ├── MigrationLog.swift
│ │ ├── Migrations.swift
│ │ └── Migrator.swift
│ ├── Model
│ │ ├── AnyModel.swift
│ │ ├── EagerLoad.swift
│ │ ├── Fields+Codable.swift
│ │ ├── Fields.swift
│ │ ├── MirrorBypass.swift
│ │ ├── Model+CRUD.swift
│ │ ├── Model.swift
│ │ ├── ModelAlias.swift
│ │ └── Schema.swift
│ ├── Operators
│ │ ├── FieldOperators.swift
│ │ ├── Operators.swift
│ │ ├── ValueOperators+Array.swift
│ │ ├── ValueOperators+String.swift
│ │ └── ValueOperators.swift
│ ├── Properties
│ │ ├── Boolean.swift
│ │ ├── BooleanPropertyFormat.swift
│ │ ├── Children.swift
│ │ ├── CompositeChildren.swift
│ │ ├── CompositeID.swift
│ │ ├── CompositeOptionalChild.swift
│ │ ├── CompositeOptionalParent.swift
│ │ ├── CompositeParent.swift
│ │ ├── Field.swift
│ │ ├── FieldKey.swift
│ │ ├── Group.swift
│ │ ├── ID.swift
│ │ ├── OptionalBoolean.swift
│ │ ├── OptionalChild.swift
│ │ ├── OptionalField.swift
│ │ ├── OptionalParent.swift
│ │ ├── Parent.swift
│ │ ├── Property.swift
│ │ ├── Relation.swift
│ │ ├── Siblings.swift
│ │ ├── Timestamp.swift
│ │ └── TimestampFormat.swift
│ ├── Query
│ │ ├── Builder
│ │ │ ├── QueryBuilder+Aggregate.swift
│ │ │ ├── QueryBuilder+EagerLoad.swift
│ │ │ ├── QueryBuilder+Filter.swift
│ │ │ ├── QueryBuilder+Group.swift
│ │ │ ├── QueryBuilder+Join+DirectRelations.swift
│ │ │ ├── QueryBuilder+Join.swift
│ │ │ ├── QueryBuilder+Paginate.swift
│ │ │ ├── QueryBuilder+Range.swift
│ │ │ ├── QueryBuilder+Set.swift
│ │ │ ├── QueryBuilder+Sort.swift
│ │ │ └── QueryBuilder.swift
│ │ ├── Database
│ │ │ ├── DatabaseQuery+Action.swift
│ │ │ ├── DatabaseQuery+Aggregate.swift
│ │ │ ├── DatabaseQuery+Field.swift
│ │ │ ├── DatabaseQuery+Filter.swift
│ │ │ ├── DatabaseQuery+Join.swift
│ │ │ ├── DatabaseQuery+Range.swift
│ │ │ ├── DatabaseQuery+Sort.swift
│ │ │ ├── DatabaseQuery+Value.swift
│ │ │ └── DatabaseQuery.swift
│ │ └── QueryHistory.swift
│ ├── Schema
│ │ ├── DatabaseSchema.swift
│ │ └── SchemaBuilder.swift
│ └── Utilities
│ │ ├── OptionalType.swift
│ │ ├── RandomGeneratable.swift
│ │ ├── SomeCodingKey.swift
│ │ └── UnsafeMutableTransferBox.swift
├── FluentSQL
│ ├── ConverterUtilities.swift
│ ├── DatabaseQuery+SQL.swift
│ ├── DatabaseSchema+SQL.swift
│ ├── Docs.docc
│ │ ├── Resources
│ │ │ └── vapor-fluentkit-logo.svg
│ │ ├── index.md
│ │ └── theme-settings.json
│ ├── Exports.swift
│ ├── SQLDatabase+Model+Concurrency.swift
│ ├── SQLDatabase+Model.swift
│ ├── SQLJSONColumnPath+Deprecated.swift
│ ├── SQLList+Deprecated.swift
│ ├── SQLQualifiedTable+Deprecated.swift
│ ├── SQLQueryConverter.swift
│ └── SQLSchemaConverter.swift
└── XCTFluent
│ ├── Docs.docc
│ ├── Resources
│ │ └── vapor-fluentkit-logo.svg
│ ├── index.md
│ └── theme-settings.json
│ ├── DummyDatabase.swift
│ └── TestDatabase.swift
└── Tests
└── FluentKitTests
├── AsyncTests
├── AsyncFilterQueryTests.swift
├── AsyncFluentKitTests.swift
└── AsyncQueryBuilderTests.swift
├── CompositeIDTests.swift
├── DummyDatabaseForTestSQLSerializer.swift
├── FilterQueryTests.swift
├── FluentKitTests.swift
├── OptionalEnumQueryTests.swift
├── OptionalFieldQueryTests.swift
├── QueryBuilderTests.swift
├── SQLTests.swift
└── TestUtilities.swift
/.github/.codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | notify:
3 | after_n_builds: 1
4 | wait_for_ci: false
5 | require_ci_to_pass: false
6 | comment:
7 | behavior: default
8 | layout: diff, files
9 | require_changes: true
10 | coverage:
11 | status:
12 | patch:
13 | default:
14 | branches:
15 | - ^main$
16 | informational: true
17 | only_pulls: false
18 | paths:
19 | - ^Sources.*
20 | target: auto
21 | project:
22 | default:
23 | branches:
24 | - ^main$
25 | informational: true
26 | only_pulls: false
27 | paths:
28 | - ^Sources.*
29 | target: auto
30 | github_checks:
31 | annotations: true
32 | ignore:
33 | - ^Sources/XCTFluent/.*
34 | - ^Sources/FluentBenchmarks/.*
35 | - ^Tests/.*
36 | - ^.build/.*
37 | slack_app: false
38 |
39 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @gwynne
2 | /.github/CONTRIBUTING.md @gwynne @0xTim
3 | /.github/workflows/*.yml @gwynne @0xTim
4 | /.github/workflows/test.yml @gwynne
5 | /.spi.yml @gwynne @0xTim
6 | /.gitignore @gwynne @0xTim
7 | /LICENSE @gwynne @0xTim
8 | /README.md @gwynne @0xTim
9 |
--------------------------------------------------------------------------------
/.github/workflows/api-docs.yml:
--------------------------------------------------------------------------------
1 | name: deploy-api-docs
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | build-and-deploy:
9 | uses: vapor/api-docs/.github/workflows/build-and-deploy-docs-workflow.yml@main
10 | secrets: inherit
11 | with:
12 | package_name: fluent-kit
13 | modules: FluentKit,FluentSQL,XCTFluent
14 | pathsToInvalidate: /fluentkit/* /fluentsql/* /xctfluent/*
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Packages
2 | .build
3 | .DS_Store
4 | *.xcodeproj
5 | Package.pins
6 | Package.resolved
7 | DerivedData/
8 | .swiftpm
9 | Tests/LinuxMain.swift
10 | .vscode
11 |
12 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | metadata:
3 | authors: "Maintained by the Vapor Core Team with hundreds of contributions from the Vapor Community."
4 | external_links:
5 | documentation: "https://api.vapor.codes/fluentkit/documentation/fluentkit/"
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Qutheory, LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.10
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "fluent-kit",
6 | platforms: [
7 | .macOS(.v10_15),
8 | .iOS(.v13),
9 | .watchOS(.v6),
10 | .tvOS(.v13),
11 | ],
12 | products: [
13 | .library(name: "FluentKit", targets: ["FluentKit"]),
14 | .library(name: "FluentBenchmark", targets: ["FluentBenchmark"]),
15 | .library(name: "FluentSQL", targets: ["FluentSQL"]),
16 | .library(name: "XCTFluent", targets: ["XCTFluent"]),
17 | ],
18 | dependencies: [
19 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
20 | .package(url: "https://github.com/apple/swift-log.git", from: "1.6.3"),
21 | .package(url: "https://github.com/vapor/sql-kit.git", from: "3.32.0"),
22 | ],
23 | targets: [
24 | .target(
25 | name: "FluentKit",
26 | dependencies: [
27 | .product(name: "NIO", package: "swift-nio"),
28 | .product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
29 | .product(name: "NIOCore", package: "swift-nio"),
30 | .product(name: "NIOPosix", package: "swift-nio"),
31 | .product(name: "Logging", package: "swift-log"),
32 | .product(name: "SQLKit", package: "sql-kit"),
33 | ],
34 | swiftSettings: swiftSettings
35 | ),
36 | .target(
37 | name: "FluentBenchmark",
38 | dependencies: [
39 | .target(name: "FluentKit"),
40 | .target(name: "FluentSQL"),
41 | .product(name: "SQLKit", package: "sql-kit"),
42 | .product(name: "SQLKitBenchmark", package: "sql-kit"),
43 | ],
44 | swiftSettings: swiftSettings
45 | ),
46 | .target(
47 | name: "FluentSQL",
48 | dependencies: [
49 | .product(name: "SQLKit", package: "sql-kit"),
50 | .target(name: "FluentKit"),
51 | ],
52 | swiftSettings: swiftSettings
53 | ),
54 | .target(
55 | name: "XCTFluent",
56 | dependencies: [
57 | .product(name: "NIOEmbedded", package: "swift-nio"),
58 | .target(name: "FluentKit"),
59 | ],
60 | swiftSettings: swiftSettings
61 | ),
62 | .testTarget(
63 | name: "FluentKitTests",
64 | dependencies: [
65 | .target(name: "FluentBenchmark"),
66 | .target(name: "FluentSQL"),
67 | .target(name: "XCTFluent"),
68 | ],
69 | swiftSettings: swiftSettings
70 | ),
71 | ]
72 | )
73 |
74 | var swiftSettings: [SwiftSetting] { [
75 | .enableUpcomingFeature("ExistentialAny"),
76 | .enableUpcomingFeature("ConciseMagicFile"),
77 | .enableUpcomingFeature("ForwardTrailingClosures"),
78 | .enableUpcomingFeature("DisableOutwardActorInference"),
79 | .enableUpcomingFeature("MemberImportVisibility"),
80 | .enableExperimentalFeature("StrictConcurrency=complete"),
81 | ] }
82 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | An Object-Relational Mapper (ORM) for Swift. It allows you to write type safe, database agnostic models and queries. It takes advantage of Swift's type system to provide a powerful, yet easy to use API.
16 |
17 | An example query looks like:
18 |
19 | ```swift
20 | let planets = try await Planet.query(on: database)
21 | .filter(\.$type == .gasGiant)
22 | .sort(\.$name)
23 | .with(\.$star)
24 | .all()
25 | ```
26 |
27 | For more information, see the [Fluent documentation](https://docs.vapor.codes/fluent/overview/).
28 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Exports.swift:
--------------------------------------------------------------------------------
1 | @_documentation(visibility: internal) @_exported import FluentKit
2 | @_documentation(visibility: internal) @_exported import XCTest
3 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/FluentBenchmarker.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import XCTest
4 |
5 | public final class FluentBenchmarker {
6 | public let databases: Databases
7 | public var database: any Database
8 |
9 | public init(databases: Databases) {
10 | precondition(databases.ids().count >= 2, "FluentBenchmarker Databases instance must have 2 or more registered databases")
11 |
12 | self.databases = databases
13 | self.database = self.databases.database(
14 | logger: .init(label: "codes.vapor.fluent.benchmarker"),
15 | on: self.databases.eventLoopGroup.any()
16 | )!
17 | }
18 |
19 | public func testAll() throws {
20 | try self.testAggregate()
21 | try self.testArray()
22 | try self.testBatch()
23 | try self.testChild()
24 | try self.testChildren()
25 | try self.testCodable()
26 | try self.testChunk()
27 | try self.testCompositeID()
28 | try self.testCRUD()
29 | try self.testEagerLoad()
30 | try self.testEnum()
31 | try self.testFilter()
32 | try self.testGroup()
33 | try self.testID()
34 | try self.testJoin()
35 | try self.testMiddleware()
36 | try self.testMigrator()
37 | try self.testModel()
38 | try self.testOptionalParent()
39 | try self.testPagination()
40 | try self.testParent()
41 | try self.testPerformance()
42 | try self.testRange()
43 | try self.testSchema()
44 | try self.testSet()
45 | try self.testSiblings()
46 | try self.testSoftDelete()
47 | try self.testSort()
48 | try self.testSQL()
49 | try self.testTimestamp()
50 | try self.testTransaction()
51 | try self.testUnique()
52 | }
53 |
54 | // MARK: Utilities
55 |
56 | func runTest(
57 | _ name: String,
58 | _ migrations: [any Migration],
59 | _ test: () throws -> ()
60 | ) throws {
61 | try self.runTest(name, migrations, { _ in try test() })
62 | }
63 |
64 | func runTest(
65 | _ name: String,
66 | _ migrations: [any Migration],
67 | _ test: (any Database) throws -> ()
68 | ) throws {
69 | // This re-initialization is required to make the middleware tests work thanks to ridiculous design flaws
70 | self.database = self.databases.database(
71 | logger: .init(label: "codes.vapor.fluent.benchmarker"),
72 | on: self.databases.eventLoopGroup.any()
73 | )!
74 | try self.runTest(name, migrations, on: self.database, test)
75 | }
76 |
77 | func runTest(
78 | _ name: String,
79 | _ migrations: [any Migration],
80 | on database: any Database,
81 | _ test: (any Database) throws -> ()
82 | ) throws {
83 | database.logger.notice("Running \(name)...")
84 |
85 | // Prepare migrations.
86 | do {
87 | for migration in migrations {
88 | try migration.prepare(on: database).wait()
89 | }
90 | } catch {
91 | database.logger.error("\(name): Error: \(String(reflecting: error))")
92 | throw error
93 | }
94 |
95 | let result = Result { try test(database) }
96 |
97 | // Revert migrations
98 | do {
99 | for migration in migrations.reversed() {
100 | try migration.revert(on: database).wait()
101 | }
102 | } catch {
103 | // ignore revert errors if the test itself failed
104 | guard case .failure(_) = result else {
105 | database.logger.error("\(name): Error: \(String(reflecting: error))")
106 | throw error
107 | }
108 | }
109 |
110 | if case .failure(let error) = result {
111 | database.logger.error("\(name): Error: \(String(reflecting: error))")
112 | throw error
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/SolarSystem/GalacticJurisdiction.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 |
5 | public final class GalacticJurisdiction: Model, @unchecked Sendable {
6 | public static let schema = "galaxy_jurisdictions"
7 |
8 | public final class IDValue: Fields, Hashable, @unchecked Sendable {
9 | @Parent(key: "galaxy_id")
10 | public var galaxy: Galaxy
11 |
12 | @Parent(key: "jurisdiction_id")
13 | public var jurisdiction: Jurisdiction
14 |
15 | @Field(key: "rank")
16 | public var rank: Int
17 |
18 | public init() {}
19 |
20 | public convenience init(galaxy: Galaxy, jurisdiction: Jurisdiction, rank: Int) throws {
21 | try self.init(galaxyId: galaxy.requireID(), jurisdictionId: jurisdiction.requireID(), rank: rank)
22 | }
23 |
24 | public init(galaxyId: Galaxy.IDValue, jurisdictionId: Jurisdiction.IDValue, rank: Int) {
25 | self.$galaxy.id = galaxyId
26 | self.$jurisdiction.id = jurisdictionId
27 | self.rank = rank
28 | }
29 |
30 | public static func == (lhs: IDValue, rhs: IDValue) -> Bool {
31 | lhs.$galaxy.id == rhs.$galaxy.id && lhs.$jurisdiction.id == rhs.$jurisdiction.id && lhs.rank == rhs.rank
32 | }
33 |
34 | public func hash(into hasher: inout Hasher) {
35 | hasher.combine(self.$galaxy.id)
36 | hasher.combine(self.$jurisdiction.id)
37 | hasher.combine(self.rank)
38 | }
39 | }
40 |
41 | @CompositeID()
42 | public var id: IDValue?
43 |
44 | public init() {}
45 |
46 | public init(id: IDValue) {
47 | self.id = id
48 | }
49 | }
50 |
51 | public struct GalacticJurisdictionMigration: Migration {
52 | public init() {}
53 |
54 | public func prepare(on database: any Database) -> EventLoopFuture {
55 | database.schema(GalacticJurisdiction.schema)
56 | .field("galaxy_id", .uuid, .required, .references(Galaxy.schema, .id, onDelete: .cascade, onUpdate: .cascade))
57 | .field("jurisdiction_id", .uuid, .required, .references(Jurisdiction.schema, .id, onDelete: .cascade, onUpdate: .cascade))
58 | .field("rank", .int, .required)
59 | .compositeIdentifier(over: "galaxy_id", "jurisdiction_id", "rank")
60 | .create()
61 | }
62 |
63 | public func revert(on database: any Database) -> EventLoopFuture {
64 | database.schema(GalacticJurisdiction.schema)
65 | .delete()
66 | }
67 | }
68 |
69 | public struct GalacticJurisdictionSeed: Migration {
70 | public init() {}
71 |
72 | public func prepare(on database: any Database) -> EventLoopFuture {
73 | database.eventLoop.flatSubmit {
74 | Galaxy.query(on: database).all().and(
75 | Jurisdiction.query(on: database).all())
76 | }.flatMap { galaxies, jurisdictions in
77 | [
78 | ("Milky Way", "Old", 0),
79 | ("Milky Way", "Corporate", 1),
80 | ("Andromeda", "Military", 0),
81 | ("Andromeda", "Corporate", 1),
82 | ("Andromeda", "None", 2),
83 | ("Pinwheel Galaxy", "Q", 0),
84 | ("Messier 82", "None", 0),
85 | ("Messier 82", "Military", 1),
86 | ]
87 | .reduce(database.eventLoop.makeSucceededVoidFuture()) { future, data in
88 | future.flatMap {
89 | GalacticJurisdiction.init(id: try! .init(
90 | galaxy: galaxies.first(where: { $0.name == data.0 })!,
91 | jurisdiction: jurisdictions.first(where: { $0.title == data.1 })!,
92 | rank: data.2
93 | )).create(on: database)
94 | }
95 | }
96 | }
97 | }
98 |
99 | public func revert(on database: any Database) -> EventLoopFuture {
100 | GalacticJurisdiction.query(on: database).delete()
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/SolarSystem/Galaxy.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 | import XCTest
5 |
6 | public final class Galaxy: Model, @unchecked Sendable {
7 | public static let schema = "galaxies"
8 |
9 | @ID
10 | public var id: UUID?
11 |
12 | @Field(key: "name")
13 | public var name: String
14 |
15 | @Children(for: \.$galaxy)
16 | public var stars: [Star]
17 |
18 | @Siblings(through: GalacticJurisdiction.self, from: \.$id.$galaxy, to: \.$id.$jurisdiction)
19 | public var jurisdictions: [Jurisdiction]
20 |
21 | public init() {}
22 |
23 | public init(id: UUID? = nil, name: String) {
24 | self.id = id
25 | self.name = name
26 | }
27 | }
28 |
29 | public struct GalaxyMigration: AsyncMigration {
30 | public init() {}
31 |
32 | public func prepare(on database: any Database) async throws {
33 | try await database.schema("galaxies")
34 | .id()
35 | .field("name", .string, .required)
36 | .create()
37 | }
38 |
39 | public func revert(on database: any Database) async throws {
40 | try await database.schema("galaxies").delete()
41 | }
42 | }
43 |
44 | public struct GalaxySeed: AsyncMigration {
45 | public init() {}
46 |
47 | public func prepare(on database: any Database) async throws {
48 | try await [
49 | "Andromeda",
50 | "Milky Way",
51 | "Pinwheel Galaxy",
52 | "Messier 82"
53 | ]
54 | .map { Galaxy(name: $0) }
55 | .create(on: database)
56 | }
57 |
58 | public func revert(on database: any Database) async throws {
59 | try await Galaxy.query(on: database).delete()
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/SolarSystem/Governor.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 | import XCTest
5 |
6 | public final class Governor: Model, @unchecked Sendable {
7 | public static let schema = "governors"
8 |
9 | @ID(key: .id)
10 | public var id: UUID?
11 |
12 | @Field(key: "name")
13 | public var name: String
14 |
15 | @Parent(key: "planet_id")
16 | public var planet: Planet
17 |
18 | public init() { }
19 |
20 | public init(id: IDValue? = nil, name: String) {
21 | self.id = id
22 | self.name = name
23 | }
24 |
25 | public init(id: IDValue? = nil, name: String, planetId: UUID) {
26 | self.id = id
27 | self.name = name
28 | self.$planet.id = planetId
29 | }
30 | }
31 |
32 | public struct GovernorMigration: Migration {
33 | public func prepare(on database: any Database) -> EventLoopFuture {
34 | database.schema(Governor.schema)
35 | .field(.id, .uuid, .identifier(auto: false), .required)
36 | .field("name", .string, .required)
37 | .field("planet_id", .uuid, .required, .references("planets", "id"))
38 | .unique(on: "planet_id")
39 | .create()
40 | }
41 |
42 | public func revert(on database: any Database) -> EventLoopFuture {
43 | database.schema(Governor.schema).delete()
44 | }
45 | }
46 |
47 | public struct GovernorSeed: Migration {
48 | public init() { }
49 |
50 | public func prepare(on database: any Database) -> EventLoopFuture {
51 | Planet.query(on: database).all().flatMap { planets in
52 | .andAllSucceed(planets.map { planet in
53 | let governor: Governor?
54 | switch planet.name {
55 | case "Mars":
56 | governor = .init(name: "John Doe")
57 | case "Earth":
58 | governor = .init(name: "Jane Doe")
59 | default:
60 | return database.eventLoop.makeSucceededVoidFuture()
61 | }
62 | return planet.$governor.create(governor!, on: database)
63 | }, on: database.eventLoop)
64 | }
65 | }
66 |
67 | public func revert(on database: any Database) -> EventLoopFuture {
68 | Governor.query(on: database).delete()
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/SolarSystem/Jurisdiction.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 | import XCTest
5 |
6 | public final class Jurisdiction: Model, @unchecked Sendable {
7 | public static let schema = "jurisdictions"
8 |
9 | @ID(key: .id)
10 | public var id: UUID?
11 |
12 | @Field(key: "title")
13 | public var title: String
14 |
15 | @Siblings(through: GalacticJurisdiction.self, from: \.$id.$jurisdiction, to: \.$id.$galaxy)
16 | public var galaxies: [Galaxy]
17 |
18 | public init() {}
19 |
20 | public init(id: IDValue? = nil, title: String) {
21 | self.id = id
22 | self.title = title
23 | }
24 | }
25 |
26 | public struct JurisdictionMigration: Migration {
27 | public init() {}
28 |
29 | public func prepare(on database: any Database) -> EventLoopFuture {
30 | database.schema(Jurisdiction.schema)
31 | .field(.id, .uuid, .identifier(auto: false), .required)
32 | .field("title", .string, .required)
33 | .unique(on: "title")
34 | .create()
35 | }
36 |
37 | public func revert(on database: any Database) -> EventLoopFuture {
38 | database.schema(Jurisdiction.schema)
39 | .delete()
40 | }
41 | }
42 |
43 | public struct JurisdictionSeed: Migration {
44 | public init() {}
45 |
46 | public func prepare(on database: any Database) -> EventLoopFuture {
47 | [
48 | "Old",
49 | "Corporate",
50 | "Military",
51 | "None",
52 | "Q",
53 | ]
54 | .map { Jurisdiction(title: $0) }
55 | .create(on: database)
56 | }
57 |
58 | public func revert(on database: any Database) -> EventLoopFuture {
59 | Jurisdiction.query(on: database)
60 | .delete()
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/SolarSystem/Moon.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 |
5 | public final class Moon: Model, @unchecked Sendable {
6 | public static let schema = "moons"
7 |
8 | @ID(key: .id)
9 | public var id: UUID?
10 |
11 | @Field(key: "name")
12 | public var name: String
13 |
14 | @Field(key: "craters")
15 | public var craters: Int
16 |
17 | @Field(key: "comets")
18 | public var comets: Int
19 |
20 | @Parent(key: "planet_id")
21 | public var planet: Planet
22 |
23 | public init() { }
24 |
25 | public init(
26 | id: IDValue? = nil,
27 | name: String,
28 | craters: Int,
29 | comets: Int
30 | ) {
31 | self.id = id
32 | self.name = name
33 | self.craters = craters
34 | self.comets = comets
35 | }
36 | }
37 |
38 | public struct MoonMigration: Migration {
39 | public init() { }
40 |
41 | public func prepare(on database: any Database) -> EventLoopFuture {
42 | database.schema("moons")
43 | .field("id", .uuid, .identifier(auto: false))
44 | .field("name", .string, .required)
45 | .field("craters", .int, .required)
46 | .field("comets", .int, .required)
47 | .field("planet_id", .uuid, .required, .references("planets", "id"))
48 | .create()
49 | }
50 |
51 | public func revert(on database: any Database) -> EventLoopFuture {
52 | database.schema("moons").delete()
53 | }
54 | }
55 |
56 | public final class MoonSeed: Migration {
57 | public init() { }
58 |
59 | public func prepare(on database: any Database) -> EventLoopFuture {
60 | Planet.query(on: database).all().flatMap { planets in
61 | .andAllSucceed(planets.map { planet in
62 | let moons: [Moon]
63 | switch planet.name {
64 | case "Earth":
65 | moons = [
66 | .init(name: "Moon", craters: 10, comets: 10)
67 | ]
68 | case "Mars":
69 | moons = [
70 | .init(name: "Deimos", craters: 1, comets: 5),
71 | .init(name: "Phobos", craters: 20, comets: 3)
72 | ]
73 | case "Jupiter":
74 | moons = [
75 | .init(name: "Io", craters: 10, comets: 10),
76 | .init(name: "Europa", craters: 10, comets: 10),
77 | .init(name: "Ganymede", craters: 10, comets: 10),
78 | .init(name: "callisto", craters: 10, comets: 10),
79 | ]
80 | case "Saturn":
81 | moons = [
82 | .init(name: "Titan", craters: 10, comets: 10),
83 | .init(name: "Prometheus", craters: 10, comets: 10),
84 | .init(name: "Atlas", craters: 9, comets: 8),
85 | .init(name: "Janus", craters: 15, comets: 9)
86 | ]
87 | default:
88 | moons = []
89 | }
90 | return planet.$moons.create(moons, on: database)
91 | }, on: database.eventLoop)
92 | }
93 | }
94 |
95 | public func revert(on database: any Database) -> EventLoopFuture {
96 | Moon.query(on: database).delete()
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/SolarSystem/Planet.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 | import XCTest
5 |
6 | public final class Planet: Model, @unchecked Sendable {
7 | public static let schema = "planets"
8 |
9 | @ID(key: .id)
10 | public var id: UUID?
11 |
12 | @Field(key: "name")
13 | public var name: String
14 |
15 | @Parent(key: "star_id")
16 | public var star: Star
17 |
18 | @OptionalParent(key: "possible_star_id")
19 | public var possibleStar: Star?
20 |
21 | @Children(for: \.$planet)
22 | public var moons: [Moon]
23 |
24 | @OptionalChild(for: \.$planet)
25 | public var governor: Governor?
26 |
27 | @Siblings(through: PlanetTag.self, from: \.$planet, to: \.$tag)
28 | public var tags: [Tag]
29 |
30 | @Timestamp(key: "deleted_at", on: .delete)
31 | var deletedAt: Date?
32 |
33 | public init() { }
34 |
35 | public init(id: IDValue? = nil, name: String) {
36 | self.id = id
37 | self.name = name
38 | }
39 |
40 | public init(id: IDValue? = nil, name: String, starId: UUID) {
41 | self.id = id
42 | self.name = name
43 | self.$star.id = starId
44 | }
45 | }
46 |
47 | public struct PlanetMigration: Migration {
48 | public func prepare(on database: any Database) -> EventLoopFuture {
49 | database.schema("planets")
50 | .field("id", .uuid, .identifier(auto: false))
51 | .field("name", .string, .required)
52 | .field("star_id", .uuid, .required, .references("stars", "id"))
53 | .field("possible_star_id", .uuid, .references("stars", "id"))
54 | .field("deleted_at", .datetime)
55 | .create()
56 | }
57 |
58 | public func revert(on database: any Database) -> EventLoopFuture {
59 | database.schema("planets").delete()
60 | }
61 | }
62 |
63 | public struct PlanetSeed: Migration {
64 | public init() { }
65 |
66 | public func prepare(on database: any Database) -> EventLoopFuture {
67 | Star.query(on: database).all().flatMap { stars in
68 | .andAllSucceed(stars.map { star in
69 | let planets: [Planet]
70 | switch star.name {
71 | case "Sol":
72 | planets = [
73 | .init(name: "Mercury"),
74 | .init(name: "Venus"),
75 | .init(name: "Earth"),
76 | .init(name: "Mars"),
77 | .init(name: "Jupiter"),
78 | .init(name: "Saturn"),
79 | .init(name: "Uranus"),
80 | .init(name: "Nepture"),
81 | ]
82 | case "Alpha Centauri":
83 | planets = [
84 | .init(name: "Proxima Centauri b")
85 | ]
86 | default:
87 | planets = []
88 | }
89 | return star.$planets.create(planets, on: database)
90 | }, on: database.eventLoop)
91 | }
92 | }
93 |
94 | public func revert(on database: any Database) -> EventLoopFuture {
95 | Planet.query(on: database).delete(force: true)
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/SolarSystem/PlanetTag.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 | import XCTest
5 |
6 | public final class PlanetTag: Model, @unchecked Sendable {
7 | public static let schema = "planet+tag"
8 |
9 | @ID(key: .id)
10 | public var id: UUID?
11 |
12 | @Parent(key: "planet_id")
13 | public var planet: Planet
14 |
15 | @Parent(key: "tag_id")
16 | public var tag: Tag
17 |
18 | @OptionalField(key: "comments")
19 | public var comments: String?
20 |
21 | public init() { }
22 |
23 | public init(id: IDValue? = nil, planetID: Planet.IDValue, tagID: Tag.IDValue, comments: String? = nil) {
24 | self.id = id
25 | self.$planet.id = planetID
26 | self.$tag.id = tagID
27 | self.comments = comments
28 | }
29 | }
30 |
31 | public struct PlanetTagMigration: Migration {
32 | public init() { }
33 |
34 | public func prepare(on database: any Database) -> EventLoopFuture {
35 | database.schema(PlanetTag.schema)
36 | .id()
37 | .field("planet_id", .uuid, .required)
38 | .field("tag_id", .uuid, .required)
39 | .field("comments", .string)
40 | .foreignKey("planet_id", references: Planet.schema, .id)
41 | .foreignKey("tag_id", references: Tag.schema, .id)
42 | .create()
43 | }
44 |
45 | public func revert(on database: any Database) -> EventLoopFuture {
46 | database.schema(PlanetTag.schema).delete()
47 | }
48 | }
49 |
50 | public struct PlanetTagSeed: Migration {
51 | public init() { }
52 |
53 | public func prepare(on database: any Database) -> EventLoopFuture {
54 | let planets = Planet.query(on: database).all()
55 | let tags = Tag.query(on: database).all()
56 | return planets.and(tags).flatMap { (planets, tags) in
57 | let inhabited = tags.filter { $0.name == "Inhabited" }.first!
58 | let gasGiant = tags.filter { $0.name == "Gas Giant" }.first!
59 | let smallRocky = tags.filter { $0.name == "Small Rocky" }.first!
60 |
61 | return .andAllSucceed(planets.map { planet in
62 | let tags: [Tag]
63 | switch planet.name {
64 | case "Mercury", "Venus", "Mars", "Proxima Centauri b":
65 | tags = [smallRocky]
66 | case "Earth":
67 | tags = [inhabited, smallRocky]
68 | case "Jupiter", "Saturn", "Uranus", "Neptune":
69 | tags = [gasGiant]
70 | default:
71 | tags = []
72 | }
73 | return planet.$tags.attach(tags, on: database)
74 | }, on: database.eventLoop)
75 | }
76 | }
77 |
78 | public func revert(on database: any Database) -> EventLoopFuture {
79 | PlanetTag.query(on: database).delete()
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/SolarSystem/SolarSystem.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import NIOCore
3 |
4 | private let migrations: [any Migration] = [
5 | GalaxyMigration(),
6 | StarMigration(),
7 | PlanetMigration(),
8 | GovernorMigration(),
9 | MoonMigration(),
10 | TagMigration(),
11 | PlanetTagMigration(),
12 | ]
13 |
14 | private let seeds: [any Migration] = [
15 | GalaxySeed(),
16 | StarSeed(),
17 | PlanetSeed(),
18 | GovernorSeed(),
19 | MoonSeed(),
20 | TagSeed(),
21 | PlanetTagSeed(),
22 | ]
23 |
24 | public struct SolarSystem: Migration {
25 | let seed: Bool
26 | public init(seed: Bool = true) {
27 | self.seed = seed
28 | }
29 |
30 | public func prepare(on database: any Database) -> EventLoopFuture {
31 | let all: [any Migration]
32 | if self.seed {
33 | all = migrations + seeds
34 | } else {
35 | all = migrations
36 | }
37 |
38 | return all.reduce(database.eventLoop.makeSucceededVoidFuture()) { f, m in f.flatMap { m.prepare(on: database) } }
39 | }
40 |
41 | public func revert(on database: any Database) -> EventLoopFuture {
42 | let all: [any Migration]
43 | if self.seed {
44 | all = migrations + seeds
45 | } else {
46 | all = migrations
47 | }
48 |
49 | return all.reversed().reduce(database.eventLoop.makeSucceededVoidFuture()) { f, m in f.flatMap { m.revert(on: database) } }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/SolarSystem/Star.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 | import XCTest
5 |
6 | public final class Star: Model, @unchecked Sendable {
7 | public static let schema = "stars"
8 |
9 | @ID
10 | public var id: UUID?
11 |
12 | @Field(key: "name")
13 | public var name: String
14 |
15 | @Parent(key: "galaxy_id")
16 | public var galaxy: Galaxy
17 |
18 | @Children(for: \.$star)
19 | public var planets: [Planet]
20 |
21 | @Timestamp(key: "deleted_at", on: .delete)
22 | var deletedAt: Date?
23 |
24 | public init() { }
25 |
26 | public init(id: IDValue? = nil, name: String, galaxyId: Galaxy.IDValue? = nil) {
27 | self.id = id
28 | self.name = name
29 | if let galaxyId {
30 | self.$galaxy.id = galaxyId
31 | }
32 | }
33 | }
34 |
35 | public struct StarMigration: AsyncMigration {
36 | public func prepare(on database: any Database) async throws {
37 | try await database.schema("stars")
38 | .id()
39 | .field("name", .string, .required)
40 | .field("galaxy_id", .uuid, .required, .references("galaxies", "id"))
41 | .field("deleted_at", .datetime)
42 | .create()
43 | }
44 |
45 | public func revert(on database: any Database) async throws {
46 | try await database.schema("stars").delete()
47 | }
48 | }
49 |
50 | public final class StarSeed: AsyncMigration {
51 | public init() {}
52 |
53 | public func prepare(on database: any Database) async throws {
54 | var stars: [Star] = []
55 |
56 | for galaxy in try await Galaxy.query(on: database).all() {
57 | switch galaxy.name {
58 | case "Milky Way":
59 | stars.append(contentsOf: [
60 | .init(name: "Sol", galaxyId: galaxy.id!),
61 | .init(name: "Alpha Centauri", galaxyId: galaxy.id!)
62 | ])
63 | case "Andromeda":
64 | stars.append(.init(name: "Alpheratz", galaxyId: galaxy.id!))
65 | default:
66 | break
67 | }
68 | }
69 | try await stars.create(on: database)
70 | }
71 |
72 | public func revert(on database: any Database) async throws {
73 | try await Star.query(on: database).delete(force: true)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/SolarSystem/Tag.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 | import XCTest
5 |
6 | public final class Tag: Model, @unchecked Sendable {
7 | public static let schema = "tags"
8 |
9 | @ID(key: .id)
10 | public var id: UUID?
11 |
12 | @Field(key: "name")
13 | public var name: String
14 |
15 | @Siblings(through: PlanetTag.self, from: \.$tag, to: \.$planet)
16 | public var planets: [Planet]
17 |
18 | public init() { }
19 |
20 | public init(id: UUID? = nil, name: String) {
21 | self.id = id
22 | self.name = name
23 | }
24 | }
25 |
26 | public struct TagMigration: Migration {
27 | public init() { }
28 |
29 | public func prepare(on database: any Database) -> EventLoopFuture {
30 | database.schema("tags")
31 | .field("id", .uuid, .identifier(auto: false))
32 | .field("name", .string, .required)
33 | .create()
34 | }
35 |
36 | public func revert(on database: any Database) -> EventLoopFuture {
37 | database.schema("tags").delete()
38 | }
39 | }
40 |
41 | public final class TagSeed: Migration {
42 | public init() { }
43 |
44 | public func prepare(on database: any Database) -> EventLoopFuture {
45 | .andAllSucceed([
46 | "Small Rocky", "Gas Giant", "Inhabited"
47 | ].map {
48 | Tag(name: $0)
49 | .create(on: database)
50 | }, on: database.eventLoop)
51 | }
52 |
53 | public func revert(on database: any Database) -> EventLoopFuture {
54 | Tag.query(on: database).delete()
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/AggregateTests.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import XCTest
3 |
4 | extension FluentBenchmarker {
5 | public func testAggregate(max: Bool = true) throws {
6 | try self.testAggregate_all(max: max)
7 | try self.testAggregate_emptyDatabase(max: max)
8 | }
9 |
10 | private func testAggregate_all(max: Bool) throws {
11 | try self.runTest(#function, [
12 | SolarSystem()
13 | ]) {
14 | // whole table
15 | let count = try Planet.query(on: self.database)
16 | .count().wait()
17 | XCTAssertEqual(count, 9)
18 |
19 | // filtered w/ results
20 | let filteredCount = try Planet.query(on: self.database)
21 | .filter(\.$name == "Earth")
22 | .count().wait()
23 | XCTAssertEqual(filteredCount, 1)
24 |
25 | // filtered empty
26 | let emptyCount = try Planet.query(on: self.database)
27 | .filter(\.$name == "Pluto")
28 | .count().wait()
29 | XCTAssertEqual(emptyCount, 0)
30 |
31 | // max
32 | if max {
33 | let maxName = try Planet.query(on: self.database)
34 | .max(\.$name).wait()
35 | XCTAssertEqual(maxName, "Venus")
36 | }
37 |
38 | // eager loads ignored
39 | let countWithEagerLoads = try Galaxy.query(on: self.database)
40 | .with(\.$stars)
41 | .count().wait()
42 | XCTAssertEqual(countWithEagerLoads, 4)
43 |
44 | // eager loads ignored again
45 | if max {
46 | let maxNameWithEagerLoads = try Galaxy.query(on: self.database)
47 | .with(\.$stars)
48 | .max(\.$name).wait()
49 | XCTAssertEqual(maxNameWithEagerLoads, "Pinwheel Galaxy")
50 | }
51 | }
52 | }
53 |
54 | private func testAggregate_emptyDatabase(max: Bool) throws {
55 | try self.runTest(#function, [
56 | SolarSystem(seed: false)
57 | ]) {
58 | // whole table
59 | let count = try Planet.query(on: self.database)
60 | .count().wait()
61 | XCTAssertEqual(count, 0)
62 |
63 | // max
64 | if max {
65 | let maxName = try Planet.query(on: self.database)
66 | .max(\.$name).wait()
67 | // expect error?
68 | XCTAssertNil(maxName)
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/BatchTests.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 | import XCTest
3 |
4 | extension FluentBenchmarker {
5 | public func testBatch() throws {
6 | try self.testBatch_create()
7 | try self.testBatch_update()
8 | try self.testBatch_delete()
9 | }
10 |
11 | private func testBatch_create() throws {
12 | try runTest(#function, [
13 | GalaxyMigration()
14 | ]) {
15 | let galaxies = Array("abcdefghijklmnopqrstuvwxyz").map { letter in
16 | return Galaxy(name: .init(letter))
17 | }
18 |
19 | try galaxies.create(on: self.database).wait()
20 | let count = try Galaxy.query(on: self.database).count().wait()
21 | XCTAssertEqual(count, 26)
22 | }
23 | }
24 |
25 | private func testBatch_update() throws {
26 | try runTest(#function, [
27 | GalaxyMigration(),
28 | GalaxySeed()
29 | ]) {
30 | try Galaxy.query(on: self.database).set(\.$name, to: "Foo")
31 | .update().wait()
32 |
33 | let galaxies = try Galaxy.query(on: self.database).all().wait()
34 | for galaxy in galaxies {
35 | XCTAssertEqual(galaxy.name, "Foo")
36 | }
37 | }
38 | }
39 |
40 | private func testBatch_delete() throws {
41 | try runTest(#function, [
42 | GalaxyMigration(),
43 | ]) {
44 | let galaxies = Array("abcdefghijklmnopqrstuvwxyz").map { letter in
45 | return Galaxy(name: .init(letter))
46 | }
47 | try EventLoopFuture.andAllSucceed(galaxies.map {
48 | $0.create(on: self.database)
49 | }, on: self.database.eventLoop).wait()
50 |
51 | let count = try Galaxy.query(on: self.database).count().wait()
52 | XCTAssertEqual(count, 26)
53 |
54 | try galaxies[..<5].delete(on: self.database).wait()
55 |
56 | let postDeleteCount = try Galaxy.query(on: self.database).count().wait()
57 | XCTAssertEqual(postDeleteCount, 21)
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/CRUDTests.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import NIOCore
3 | import XCTest
4 |
5 | extension FluentBenchmarker {
6 | public func testCRUD() throws {
7 | try self.testCRUD_create()
8 | try self.testCRUD_read()
9 | try self.testCRUD_update()
10 | try self.testCRUD_delete()
11 | }
12 |
13 | private func testCRUD_create() throws {
14 | try self.runTest(#function, [
15 | GalaxyMigration()
16 | ]) {
17 | let galaxy = Galaxy(name: "Messier")
18 | galaxy.name += " 82"
19 | try! galaxy.save(on: self.database).wait()
20 | XCTAssertNotNil(galaxy.id)
21 |
22 | guard let fetched = try Galaxy.query(on: self.database)
23 | .filter(\.$name == "Messier 82")
24 | .first()
25 | .wait()
26 | else {
27 | XCTFail("unexpected empty result set")
28 | return
29 | }
30 |
31 | if fetched.name != galaxy.name {
32 | XCTFail("unexpected name: \(galaxy) \(fetched)")
33 | }
34 | if fetched.id != galaxy.id {
35 | XCTFail("unexpected id: \(galaxy) \(fetched)")
36 | }
37 | }
38 | }
39 |
40 | private func testCRUD_read() throws {
41 | try runTest(#function, [
42 | GalaxyMigration(),
43 | GalaxySeed()
44 | ]) {
45 | guard let milkyWay = try Galaxy.query(on: self.database)
46 | .filter(\.$name == "Milky Way")
47 | .first().wait()
48 | else {
49 | XCTFail("unpexected missing galaxy")
50 | return
51 | }
52 | guard milkyWay.name == "Milky Way" else {
53 | XCTFail("unexpected name")
54 | return
55 | }
56 | }
57 | }
58 |
59 | private func testCRUD_update() throws {
60 | try runTest(#function, [
61 | GalaxyMigration()
62 | ]) {
63 | let galaxy = Galaxy(name: "Milkey Way")
64 | try galaxy.save(on: self.database).wait()
65 | galaxy.name = "Milky Way"
66 | try galaxy.save(on: self.database).wait()
67 | // Test save without changes.
68 | try galaxy.save(on: self.database).wait()
69 |
70 | // verify
71 | let galaxies = try Galaxy.query(on: self.database).filter(\.$name == "Milky Way").all().wait()
72 | guard galaxies.count == 1 else {
73 | XCTFail("unexpected galaxy count: \(galaxies)")
74 | return
75 | }
76 | guard galaxies[0].name == "Milky Way" else {
77 | XCTFail("unexpected galaxy name")
78 | return
79 | }
80 | }
81 | }
82 |
83 | private func testCRUD_delete() throws {
84 | try runTest(#function, [
85 | GalaxyMigration(),
86 | ]) {
87 | let galaxy = Galaxy(name: "Milky Way")
88 | try galaxy.save(on: self.database).wait()
89 | try galaxy.delete(on: self.database).wait()
90 |
91 | // verify
92 | let galaxies = try Galaxy.query(on: self.database).all().wait()
93 | guard galaxies.count == 0 else {
94 | XCTFail("unexpected galaxy count: \(galaxies)")
95 | return
96 | }
97 | }
98 | }
99 |
100 | public func testAsyncCreate() throws {
101 | try runTest(#function, [
102 | GalaxyMigration()
103 | ]) {
104 | let a = Galaxy(name: "a")
105 | let b = Galaxy(name: "b")
106 | _ = try a.save(on: self.database).and(b.save(on: self.database)).wait()
107 | let galaxies = try Galaxy.query(on: self.database).all().wait()
108 | guard galaxies.count == 2 else {
109 | XCTFail("both galaxies did not save")
110 | return
111 | }
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/ChildrenTests.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 | import XCTest
5 |
6 | extension FluentBenchmarker {
7 | public func testChildren() throws {
8 | try self.testChildren_with()
9 | }
10 |
11 | private func testChildren_with() throws {
12 | try self.runTest(#function, [
13 | FooMigration(),
14 | BarMigration(),
15 | BazMigration()
16 | ]) {
17 | let foo = Foo(name: "a")
18 | try foo.save(on: self.database).wait()
19 | let bar = Bar(bar: 42, fooID: foo.id!)
20 | try bar.save(on: self.database).wait()
21 | let baz = Baz(baz: 3.14, fooID: foo.id!)
22 | try baz.save(on: self.database).wait()
23 |
24 | let foos = try Foo.query(on: self.database)
25 | .with(\.$bars)
26 | .with(\.$bazs)
27 | .all().wait()
28 |
29 | for foo in foos {
30 | XCTAssertEqual(foo.bars[0].bar, 42)
31 | XCTAssertEqual(foo.bazs[0].baz, 3.14)
32 | }
33 | }
34 | }
35 | }
36 |
37 |
38 | private final class Foo: Model, @unchecked Sendable {
39 | static let schema = "foos"
40 |
41 | @ID(key: .id)
42 | var id: UUID?
43 |
44 | @Field(key: "name")
45 | var name: String
46 |
47 | @Children(for: \.$foo)
48 | var bars: [Bar]
49 |
50 | @Children(for: \.$foo)
51 | var bazs: [Baz]
52 |
53 | init() { }
54 |
55 | init(id: IDValue? = nil, name: String) {
56 | self.id = id
57 | self.name = name
58 | }
59 | }
60 |
61 | private struct FooMigration: Migration {
62 | func prepare(on database: any Database) -> EventLoopFuture {
63 | database.schema("foos")
64 | .field("id", .uuid, .identifier(auto: false))
65 | .field("name", .string, .required)
66 | .create()
67 | }
68 |
69 | func revert(on database: any Database) -> EventLoopFuture {
70 | database.schema("foos").delete()
71 | }
72 | }
73 |
74 | private final class Bar: Model, @unchecked Sendable {
75 | static let schema = "bars"
76 |
77 | @ID(key: .id)
78 | var id: UUID?
79 |
80 | @Field(key: "bar")
81 | var bar: Int
82 |
83 | @Parent(key: "foo_id")
84 | var foo: Foo
85 |
86 | init() { }
87 |
88 | init(id: IDValue? = nil, bar: Int, fooID: Foo.IDValue) {
89 | self.id = id
90 | self.bar = bar
91 | self.$foo.id = fooID
92 | }
93 | }
94 |
95 | private struct BarMigration: Migration {
96 | func prepare(on database: any Database) -> EventLoopFuture {
97 | database.schema("bars")
98 | .field("id", .uuid, .identifier(auto: false))
99 | .field("bar", .int, .required)
100 | .field("foo_id", .uuid, .required)
101 | .create()
102 | }
103 |
104 | func revert(on database: any Database) -> EventLoopFuture {
105 | database.schema("bars").delete()
106 | }
107 | }
108 |
109 | private final class Baz: Model, @unchecked Sendable {
110 | static let schema = "bazs"
111 |
112 | @ID(key: .id)
113 | var id: UUID?
114 |
115 | @Field(key: "baz")
116 | var baz: Double
117 |
118 | @Parent(key: "foo_id")
119 | var foo: Foo
120 |
121 | init() { }
122 |
123 | init(id: IDValue? = nil, baz: Double, fooID: Foo.IDValue) {
124 | self.id = id
125 | self.baz = baz
126 | self.$foo.id = fooID
127 | }
128 | }
129 |
130 | private struct BazMigration: Migration {
131 | func prepare(on database: any Database) -> EventLoopFuture {
132 | database.schema("bazs")
133 | .field("id", .uuid, .identifier(auto: false))
134 | .field("baz", .double, .required)
135 | .field("foo_id", .uuid, .required)
136 | .create()
137 | }
138 |
139 | func revert(on database: any Database) -> EventLoopFuture {
140 | database.schema("bazs").delete()
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/ChunkTests.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 | import NIOConcurrencyHelpers
3 | import XCTest
4 |
5 | extension FluentBenchmarker {
6 | public func testChunk() throws {
7 | try self.testChunk_fetch()
8 | }
9 |
10 | private func testChunk_fetch() throws {
11 | try runTest(#function, [
12 | GalaxyMigration(),
13 | ]) {
14 |
15 | let saves = (1...512).map { i -> EventLoopFuture in
16 | return Galaxy(name: "Milky Way \(i)")
17 | .save(on: self.database)
18 | }
19 | try EventLoopFuture.andAllSucceed(saves, on: self.database.eventLoop).wait()
20 |
21 | let fetched64 = NIOLockedValueBox(0)
22 |
23 | try Galaxy.query(on: self.database).chunk(max: 64) { chunk in
24 | guard chunk.count == 64 else {
25 | XCTFail("bad chunk count")
26 | return
27 | }
28 | fetched64.withLockedValue { $0 += chunk.count }
29 | }.wait()
30 |
31 | guard fetched64.withLockedValue({ $0 }) == 512 else {
32 | XCTFail("did not fetch all - only \(fetched64.withLockedValue { $0 }) out of 512")
33 | return
34 | }
35 |
36 | let fetched511 = NIOLockedValueBox(0)
37 |
38 | try Galaxy.query(on: self.database).chunk(max: 511) { chunk in
39 | guard chunk.count == 511 || chunk.count == 1 else {
40 | XCTFail("bad chunk count")
41 | return
42 | }
43 | fetched511.withLockedValue { $0 += chunk.count }
44 | }.wait()
45 |
46 | guard fetched511.withLockedValue({ $0 }) == 512 else {
47 | XCTFail("did not fetch all - only \(fetched511.withLockedValue { $0 }) out of 512")
48 | return
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/CodableTests.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 | import XCTest
5 |
6 | extension FluentBenchmarker {
7 | public func testCodable() throws {
8 | try self.testCodable_decodeError()
9 | }
10 |
11 | private func testCodable_decodeError() throws {
12 | let json = """
13 | {
14 | "title": "Test Question",
15 | "body": "Test Question Body",
16 | "answer": "Test Answer",
17 | "project": "TestProject"
18 | }
19 | """
20 |
21 | do {
22 | _ = try JSONDecoder().decode(Question.self, from: .init(json.utf8))
23 | XCTFail("expected error")
24 | } catch DecodingError.typeMismatch(let type, let context) {
25 | XCTAssertEqual(ObjectIdentifier(type), ObjectIdentifier(Project.self))
26 | XCTAssertEqual(context.codingPath.map(\.stringValue), ["project"])
27 | }
28 | }
29 | }
30 |
31 | final class Question: Model, @unchecked Sendable {
32 | static let schema = "questions"
33 |
34 | @ID(custom: "id")
35 | var id: Int?
36 |
37 | @Field(key: "title")
38 | var title: String
39 |
40 | @Field(key: "body")
41 | var body: String
42 |
43 | @Field(key: "answer")
44 | var answer: String
45 |
46 | @Parent(key: "project_id")
47 | var project: Project
48 |
49 | init() { }
50 |
51 | init(id: Int? = nil, title: String, body: String = "", answer: String = "", projectId: Project.IDValue) {
52 | self.id = id
53 | self.title = title
54 | self.body = body
55 | self.answer = answer
56 | self.$project.id = projectId
57 | }
58 | }
59 |
60 | final class Project: Model, @unchecked Sendable {
61 | static let schema = "projects"
62 |
63 | @ID(custom: "id")
64 | var id: String?
65 |
66 | @Field(key: "title")
67 | var title: String
68 |
69 | @Field(key: "links")
70 | var links: [String]
71 |
72 | @Children(for: \.$project)
73 | var questions: [Question]
74 |
75 | init() { }
76 |
77 | init(id: String, title: String, links: [String] = []) {
78 | self.id = id
79 | self.title = title
80 | self.links = links
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/PaginationTests.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import XCTest
3 |
4 | extension FluentBenchmarker {
5 | public func testPagination() throws {
6 | try self.runTest(#function, [
7 | SolarSystem()
8 | ]) {
9 | do {
10 | let planetsPage1 = try Planet.query(on: self.database)
11 | .sort(\.$name)
12 | .paginate(PageRequest(page: 1, per: 2))
13 | .wait()
14 |
15 | XCTAssertEqual(planetsPage1.metadata.page, 1)
16 | XCTAssertEqual(planetsPage1.metadata.per, 2)
17 | XCTAssertEqual(planetsPage1.metadata.total, 9)
18 | XCTAssertEqual(planetsPage1.metadata.pageCount, 5)
19 | XCTAssertEqual(planetsPage1.items.count, 2)
20 | XCTAssertEqual(planetsPage1.items.dropFirst(0).first?.name, "Earth")
21 | XCTAssertEqual(planetsPage1.items.dropFirst(1).first?.name, "Jupiter")
22 | }
23 | do {
24 | let planetsPage2 = try Planet.query(on: self.database)
25 | .sort(\.$name)
26 | .paginate(PageRequest(page: 2, per: 2))
27 | .wait()
28 |
29 | XCTAssertEqual(planetsPage2.metadata.page, 2)
30 | XCTAssertEqual(planetsPage2.metadata.per, 2)
31 | XCTAssertEqual(planetsPage2.metadata.total, 9)
32 | XCTAssertEqual(planetsPage2.metadata.pageCount, 5)
33 | XCTAssertEqual(planetsPage2.items.count, 2)
34 | XCTAssertEqual(planetsPage2.items.dropFirst(0).first?.name, "Mars")
35 | XCTAssertEqual(planetsPage2.items.dropFirst(1).first?.name, "Mercury")
36 | }
37 | do {
38 | let galaxiesPage = try Galaxy.query(on: self.database)
39 | .filter(\.$name == "Milky Way")
40 | .with(\.$stars)
41 | .sort(\.$name)
42 | .paginate(PageRequest(page: 1, per: 1))
43 | .wait()
44 |
45 | XCTAssertEqual(galaxiesPage.metadata.page, 1)
46 | XCTAssertEqual(galaxiesPage.metadata.per, 1)
47 |
48 | let milkyWay = galaxiesPage.items.first
49 | XCTAssertEqual(milkyWay?.name, "Milky Way")
50 | XCTAssertEqual(milkyWay?.stars.count, 2)
51 | }
52 | }
53 | }
54 |
55 | public func testPaginationDoesntCrashWithInvalidValues() throws {
56 | try self.runTest(#function, [
57 | SolarSystem()
58 | ]) {
59 | do {
60 | _ = try Planet.query(on: self.database)
61 | .sort(\.$name)
62 | .paginate(PageRequest(page: -1, per: 2))
63 | .wait()
64 | }
65 | do {
66 | _ = try Planet.query(on: self.database)
67 | .sort(\.$name)
68 | .paginate(PageRequest(page: 2, per: -2))
69 | .wait()
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/ParentTests.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 | import XCTest
5 |
6 | extension FluentBenchmarker {
7 | public func testParent() throws {
8 | try self.testParent_serialization()
9 | try self.testParent_get()
10 | try self.testParent_value()
11 | }
12 |
13 | private func testParent_serialization() throws {
14 | try self.runTest(#function, [
15 | SolarSystem()
16 | ]) {
17 | let stars = try Star.query(on: self.database).all().wait()
18 |
19 | let encoded = try JSONEncoder().encode(stars)
20 | self.database.logger.trace("\(String(decoding: encoded, as: UTF8.self)))")
21 | let decoded = try JSONDecoder().decode([StarJSON].self, from: encoded)
22 | XCTAssertEqual(stars.map { $0.id }, decoded.map { $0.id })
23 | XCTAssertEqual(stars.map { $0.name }, decoded.map { $0.name })
24 | XCTAssertEqual(stars.map { $0.$galaxy.id }, decoded.map { $0.galaxy.id })
25 | }
26 | }
27 |
28 | private func testParent_get() throws {
29 | try self.runTest(#function, [
30 | SolarSystem()
31 | ]) {
32 | let planets = try Planet.query(on: self.database)
33 | .all().wait()
34 |
35 | for planet in planets {
36 | let star = try planet.$star.get(on: self.database).wait()
37 | switch planet.name {
38 | case "Earth", "Jupiter":
39 | XCTAssertEqual(star.name, "Sol")
40 | case "Proxima Centauri b":
41 | XCTAssertEqual(star.name, "Alpha Centauri")
42 | default: break
43 | }
44 | }
45 | }
46 | }
47 |
48 | private func testParent_value() throws {
49 | try runTest(#function, [
50 | SolarSystem()
51 | ]) {
52 | guard let earth = try Planet.query(on: self.database)
53 | .filter(\.$name == "Earth")
54 | .first().wait()
55 | else {
56 | XCTFail("Could not load Planet earth")
57 | return
58 | }
59 |
60 | // test loading relation manually
61 | XCTAssertNil(earth.$star.value)
62 | try earth.$star.load(on: self.database).wait()
63 | XCTAssertNotNil(earth.$star.value)
64 | XCTAssertEqual(earth.star.name, "Sol")
65 |
66 | let test = Star(name: "Foo")
67 | earth.$star.value = test
68 | XCTAssertEqual(earth.star.name, "Foo")
69 | // test get uses cached value
70 | try XCTAssertEqual(earth.$star.get(on: self.database).wait().name, "Foo")
71 | // test get can reload relation
72 | try XCTAssertEqual(earth.$star.get(reload: true, on: self.database).wait().name, "Sol")
73 |
74 | // test clearing loaded relation
75 | earth.$star.value = nil
76 | XCTAssertNil(earth.$star.value)
77 |
78 | // test get loads relation if nil
79 | try XCTAssertEqual(earth.$star.get(on: self.database).wait().name, "Sol")
80 | }
81 | }
82 | }
83 |
84 | private struct StarJSON: Codable {
85 | var id: UUID
86 | var name: String
87 | struct GalaxyJSON: Codable { var id: UUID }
88 | var galaxy: GalaxyJSON
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/PerformanceTests.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | #if !canImport(Darwin)
3 | @preconcurrency import Foundation
4 | #else
5 | import Foundation
6 | #endif
7 | import NIOCore
8 | import XCTest
9 |
10 | extension FluentBenchmarker {
11 | public func testPerformance(decimalType: DatabaseSchema.DataType = .string) throws {
12 | try self.testPerformance_largeModel(decimalType: decimalType)
13 | try self.testPerformance_siblings()
14 | }
15 |
16 | private func testPerformance_largeModel(decimalType: DatabaseSchema.DataType) throws {
17 | try runTest(#function, [
18 | FooMigration(decimalType: decimalType)
19 | ]) {
20 | for _ in 0..<100 {
21 | let foo = Foo(
22 | bar: 42,
23 | baz: 3.14159,
24 | qux: "foobar",
25 | quux: .init(),
26 | quuz: 2.71828,
27 | corge: [1, 2, 3],
28 | grault: [4, 5, 6],
29 | garply: ["foo", "bar", "baz"],
30 | fred: 1.4142135623730950,
31 | plugh: 1337,
32 | xyzzy: 9.94987437106,
33 | thud: .init(foo: 5, bar: 23, baz: "1994")
34 | )
35 | try foo.save(on: self.database).wait()
36 | }
37 | let foos = try Foo.query(on: self.database).all().wait()
38 | for foo in foos {
39 | XCTAssertNotNil(foo.id)
40 | }
41 | XCTAssertEqual(foos.count, 100)
42 | }
43 | }
44 | }
45 |
46 | private final class Foo: Model, @unchecked Sendable {
47 | static let schema = "foos"
48 |
49 | struct Thud: Codable {
50 | var foo: Int
51 | var bar: Double
52 | var baz: String
53 | }
54 |
55 | @ID(key: .id) var id: UUID?
56 | @Field(key: "bar") var bar: Int
57 | @Field(key: "baz") var baz: Double
58 | @Field(key: "qux") var qux: String
59 | @Field(key: "quux") var quux: Date
60 | @Field(key: "quuz") var quuz: Float
61 | @Field(key: "corge") var corge: [Int]
62 | @Field(key: "grault") var grault: [Double]
63 | @Field(key: "garply") var garply: [String]
64 | @Field(key: "fred") var fred: Decimal
65 | @Field(key: "plugh") var plugh: Int?
66 | @Field(key: "xyzzy") var xyzzy: Double?
67 | @Field(key: "thud") var thud: Thud
68 |
69 | init() { }
70 |
71 | init(
72 | id: UUID? = nil,
73 | bar: Int,
74 | baz: Double,
75 | qux: String,
76 | quux: Date,
77 | quuz: Float,
78 | corge: [Int],
79 | grault: [Double],
80 | garply: [String],
81 | fred: Decimal,
82 | plugh: Int?,
83 | xyzzy: Double?,
84 | thud: Thud
85 | ) {
86 | self.id = id
87 | self.bar = bar
88 | self.baz = baz
89 | self.qux = qux
90 | self.quux = quux
91 | self.quuz = quuz
92 | self.corge = corge
93 | self.grault = grault
94 | self.garply = garply
95 | self.fred = fred
96 | self.plugh = plugh
97 | self.xyzzy = xyzzy
98 | self.thud = thud
99 | }
100 | }
101 |
102 | private struct FooMigration: Migration {
103 | let decimalType: DatabaseSchema.DataType
104 |
105 | func prepare(on database: any Database) -> EventLoopFuture {
106 | database.schema("foos")
107 | .field("id", .uuid, .identifier(auto: false))
108 | .field("bar", .int, .required)
109 | .field("baz", .double, .required)
110 | .field("qux", .string, .required)
111 | .field("quux", .datetime, .required)
112 | .field("quuz", .float, .required)
113 | .field("corge", .array(of: .int), .required)
114 | .field("grault", .array(of: .double), .required)
115 | .field("garply", .array(of: .string), .required)
116 | .field("fred", self.decimalType, .required)
117 | .field("plugh", .int)
118 | .field("xyzzy", .double)
119 | .field("thud", .json, .required)
120 | .create()
121 | }
122 |
123 | func revert(on database: any Database) -> EventLoopFuture {
124 | database.schema("foos").delete()
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/RangeTests.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import XCTest
3 |
4 | extension FluentBenchmarker {
5 | public func testRange() throws {
6 | try self.testRange_basic()
7 | }
8 |
9 | private func testRange_basic() throws {
10 | try self.runTest(#function, [
11 | SolarSystem()
12 | ]) {
13 | do {
14 | let planets = try Planet.query(on: self.database)
15 | .range(2..<5)
16 | .sort(\.$name)
17 | .all().wait()
18 | XCTAssertEqual(planets.count, 3)
19 | XCTAssertEqual(planets.first?.name, "Mars")
20 | }
21 | do {
22 | let planets = try Planet.query(on: self.database)
23 | .range(...5)
24 | .sort(\.$name)
25 | .all().wait()
26 | XCTAssertEqual(planets.count, 6)
27 | }
28 | do {
29 | let planets = try Planet.query(on: self.database)
30 | .range(..<5)
31 | .sort(\.$name)
32 | .all().wait()
33 | XCTAssertEqual(planets.count, 5)
34 | }
35 | do {
36 | let planets = try Planet.query(on: self.database)
37 | .range(..<5)
38 | .sort(\.$name)
39 | .all().wait()
40 | XCTAssertEqual(planets.count, 5)
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/SQLTests.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import FluentSQL
3 | import Foundation
4 | import NIOCore
5 | import NIOPosix
6 | import SQLKit
7 | import SQLKitBenchmark
8 | import XCTest
9 |
10 | extension FluentBenchmarker {
11 | public func testSQL() throws {
12 | guard let sql = self.database as? any SQLDatabase else {
13 | return
14 | }
15 | try self.testSQL_rawDecode(sql)
16 | try MultiThreadedEventLoopGroup.singleton.any().makeFutureWithTask {
17 | try await SQLBenchmarker(on: sql).runAllTests()
18 | }.wait()
19 | }
20 |
21 | private func testSQL_rawDecode(_ sql: any SQLDatabase) throws {
22 | try self.runTest(#function, [
23 | UserMigration()
24 | ]) {
25 | let tanner = User(firstName: "Tanner", lastName: "Nelson", parentID: UUID())
26 | try tanner.create(on: self.database).wait()
27 |
28 | // test db.first(decoding:)
29 | do {
30 | let user = try sql.raw("SELECT * FROM users").first(decodingFluent: User.self).wait()
31 | XCTAssertNotNil(user)
32 | if let user = user {
33 | XCTAssertEqual(user.id, tanner.id)
34 | XCTAssertEqual(user.firstName, tanner.firstName)
35 | XCTAssertEqual(user.lastName, tanner.lastName)
36 | XCTAssertEqual(user.$parent.id, tanner.$parent.id)
37 | }
38 | }
39 |
40 | // test db.all(decoding:)
41 | do {
42 | let users = try sql.raw("SELECT * FROM users").all(decodingFluent: User.self).wait()
43 | XCTAssertEqual(users.count, 1)
44 | if let user = users.first {
45 | XCTAssertEqual(user.id, tanner.id)
46 | XCTAssertEqual(user.firstName, tanner.firstName)
47 | XCTAssertEqual(user.lastName, tanner.lastName)
48 | XCTAssertEqual(user.$parent.id, tanner.$parent.id)
49 | }
50 | }
51 |
52 | // test row.decode()
53 | do {
54 | let users = try sql.raw("SELECT * FROM users").all().wait().map {
55 | try $0.decode(fluentModel: User.self)
56 | }
57 | XCTAssertEqual(users.count, 1)
58 | if let user = users.first {
59 | XCTAssertEqual(user.id, tanner.id)
60 | XCTAssertEqual(user.firstName, tanner.firstName)
61 | XCTAssertEqual(user.lastName, tanner.lastName)
62 | XCTAssertEqual(user.$parent.id, tanner.$parent.id)
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
69 |
70 | private final class User: Model, @unchecked Sendable {
71 | static let schema = "users"
72 |
73 | @ID(key: .id)
74 | var id: UUID?
75 |
76 | @Field(key: "first_name")
77 | var firstName: String
78 |
79 | @Field(key: "last_name")
80 | var lastName: String
81 |
82 | @OptionalParent(key: "parent_id")
83 | var parent: User?
84 |
85 | init() { }
86 |
87 | init(
88 | id: UUID? = nil,
89 | firstName: String,
90 | lastName: String,
91 | parentID: UUID? = nil
92 | ) {
93 | self.id = id
94 | self.firstName = firstName
95 | self.lastName = lastName
96 | self.$parent.id = parentID
97 | }
98 | }
99 |
100 | private struct UserMigration: Migration {
101 | func prepare(on database: any Database) -> EventLoopFuture {
102 | database.schema("users")
103 | .id()
104 | .field("first_name", .string, .required)
105 | .field("last_name", .string, .required)
106 | .field("parent_id", .uuid)
107 | .create()
108 | }
109 |
110 | func revert(on database: any Database) -> EventLoopFuture {
111 | database.schema("users").delete()
112 | }
113 | }
114 |
115 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/SetTests.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 | import XCTest
5 |
6 | extension FluentBenchmarker {
7 | public func testSet() throws {
8 | try self.testSet_multiple()
9 | try self.testSet_optional()
10 | try self.testSet_enum()
11 | }
12 |
13 | private func testSet_multiple() throws {
14 | try runTest(#function, [
15 | TestMigration(),
16 | ]) {
17 | // Int value set first
18 | try Test.query(on: self.database)
19 | .set(\.$intValue, to: 1)
20 | .set(\.$stringValue, to: "a string")
21 | .update().wait()
22 |
23 | // String value set first
24 | try Test.query(on: self.database)
25 | .set(\.$stringValue, to: "a string")
26 | .set(\.$intValue, to: 1)
27 | .update().wait()
28 | }
29 | }
30 |
31 | private func testSet_optional() throws {
32 | try runTest(#function, [
33 | TestMigration(),
34 | ]) {
35 | try Test.query(on: self.database)
36 | .set(\.$intValue, to: nil)
37 | .set(\.$stringValue, to: nil)
38 | .update().wait()
39 | }
40 | }
41 |
42 | private func testSet_enum() throws {
43 | try runTest(#function, [
44 | Test2Migration(),
45 | ]) {
46 | try Test2.query(on: self.database)
47 | .set(\.$foo, to: .bar)
48 | .update().wait()
49 | }
50 | }
51 | }
52 |
53 | private final class Test: Model, @unchecked Sendable {
54 | static let schema = "test"
55 |
56 | @ID(key: .id)
57 | var id: UUID?
58 |
59 | @OptionalField(key: "int_value")
60 | var intValue: Int?
61 |
62 | @OptionalField(key: "string_value")
63 | var stringValue: String?
64 | }
65 |
66 | private struct TestMigration: FluentKit.Migration {
67 | func prepare(on database: any Database) -> EventLoopFuture {
68 | database.schema("test")
69 | .field("id", .uuid, .identifier(auto: false))
70 | .field("int_value", .int)
71 | .field("string_value", .string)
72 | .create()
73 | }
74 |
75 | func revert(on database: any Database) -> EventLoopFuture {
76 | database.schema("test").delete()
77 | }
78 | }
79 |
80 | private enum Foo: String, Codable {
81 | case bar, baz
82 | }
83 |
84 | private final class Test2: Model, @unchecked Sendable {
85 | static let schema = "test"
86 |
87 | @ID(key: .id)
88 | var id: UUID?
89 |
90 | @Enum(key: "foo")
91 | var foo: Foo
92 | }
93 |
94 | private struct Test2Migration: FluentKit.Migration {
95 | func prepare(on database: any Database) -> EventLoopFuture {
96 | database.enum("foo").case("bar").case("baz").create().flatMap { foo in
97 | database.schema("test")
98 | .id()
99 | .field("foo", foo)
100 | .create()
101 | }
102 | }
103 |
104 | func revert(on database: any Database) -> EventLoopFuture {
105 | database.schema("test").delete().flatMap {
106 | database.enum("foo").delete()
107 | }
108 | }
109 | }
110 |
111 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/SortTests.swift:
--------------------------------------------------------------------------------
1 | import FluentSQL
2 | import XCTest
3 | import SQLKit
4 |
5 | extension FluentBenchmarker {
6 | public func testSort(sql: Bool = true) throws {
7 | try self.testSort_basic()
8 | if sql {
9 | try self.testSort_sql()
10 | try self.testSort_embedSql()
11 | }
12 | }
13 |
14 | private func testSort_basic() throws {
15 | try self.runTest(#function, [
16 | SolarSystem()
17 | ]) {
18 | let ascending = try Galaxy.query(on: self.database)
19 | .sort(\.$name, .ascending)
20 | .all().wait()
21 | let descending = try Galaxy.query(on: self.database)
22 | .sort(\.$name, .descending)
23 | .all().wait()
24 | XCTAssertEqual(
25 | ascending.map(\.name),
26 | descending.reversed().map(\.name)
27 | )
28 | }
29 | }
30 |
31 | private func testSort_sql() throws {
32 | try self.runTest(#function, [
33 | SolarSystem()
34 | ]) {
35 | let planets = try Planet.query(on: self.database)
36 | .sort(.sql("name", .notEqual, "Earth"))
37 | .all().wait()
38 | XCTAssertEqual(planets.first?.name, "Earth")
39 | }
40 | }
41 |
42 | private func testSort_embedSql() throws {
43 | try self.runTest(#function, [
44 | SolarSystem()
45 | ]) {
46 | let planets = try Planet.query(on: self.database)
47 | .sort(.sql(embed: "\(ident: "name")\(SQLBinaryOperator.notEqual)\(literal: "Earth")"))
48 | .all().wait()
49 | XCTAssertEqual(planets.first?.name, "Earth")
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/TransactionTests.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 | import XCTest
3 | import FluentKit
4 |
5 | extension FluentBenchmarker {
6 | public func testTransaction() throws {
7 | try self.testTransaction_basic()
8 | try self.testTransaction_in()
9 | }
10 |
11 | private func testTransaction_basic() throws {
12 | try self.runTest(#function, [
13 | SolarSystem()
14 | ]) {
15 | let result = self.database.transaction { transaction in
16 | Star.query(on: transaction)
17 | .filter(\.$name == "Sol")
18 | .first()
19 | .flatMapWithEventLoop
20 | { sun, eventLoop -> EventLoopFuture in
21 | guard let sun else { return eventLoop.makeFailedFuture(FluentError.missingField(name: "Sol")) }
22 | let pluto = Planet(name: "Pluto")
23 | return sun.$planets.create(pluto, on: transaction).map {
24 | pluto
25 | }
26 | }.flatMap { pluto -> EventLoopFuture<(Planet, Tag)> in
27 | let tag = Tag(name: "Dwarf")
28 | return tag.create(on: transaction).map {
29 | (pluto, tag)
30 | }
31 | }.flatMap { (pluto, tag) in
32 | tag.$planets.attach(pluto, on: transaction)
33 | }.flatMapThrowing {
34 | throw Test()
35 | }
36 | }
37 | do {
38 | try result.wait()
39 | } catch is Test {
40 | // expected
41 | } catch {
42 | XCTFail("Unexpected error: \(error)")
43 | }
44 |
45 | let pluto = try Planet.query(on: self.database)
46 | .filter(\.$name == "Pluto")
47 | .first()
48 | .wait()
49 | XCTAssertNil(pluto)
50 | }
51 | }
52 |
53 | private func testTransaction_in() throws {
54 | try self.runTest(#function, [
55 | SolarSystem()
56 | ]) {
57 | try self.database.transaction { transaction in
58 | XCTAssertEqual(transaction.inTransaction, true)
59 | return transaction.transaction { nested in
60 | XCTAssertEqual(nested.inTransaction, true)
61 | return nested.eventLoop.makeSucceededFuture(())
62 | }
63 | }.wait()
64 | }
65 | }
66 | }
67 |
68 | private struct Test: Error { }
69 |
--------------------------------------------------------------------------------
/Sources/FluentBenchmark/Tests/UniqueTests.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import Foundation
3 | import NIOCore
4 | import XCTest
5 |
6 | extension FluentBenchmarker {
7 | public func testUnique() throws {
8 | try self.testUnique_fields()
9 | try self.testUnique_duplicateKey()
10 | }
11 |
12 | private func testUnique_fields() throws {
13 | try self.runTest(#function, [
14 | FooMigration(),
15 | ]) {
16 | try Foo(bar: "a", baz: 1).save(on: self.database).wait()
17 | try Foo(bar: "a", baz: 2).save(on: self.database).wait()
18 | do {
19 | try Foo(bar: "a", baz: 1).save(on: self.database).wait()
20 | XCTFail("should have failed")
21 | } catch let error where error is any DatabaseError {
22 | // pass
23 | }
24 | }
25 | }
26 |
27 | // https://github.com/vapor/fluent-kit/issues/112
28 | public func testUnique_duplicateKey() throws {
29 | try runTest(#function, [
30 | BarMigration(),
31 | BazMigration()
32 | ]) {
33 | //
34 | }
35 | }
36 | }
37 |
38 | private final class Foo: Model, @unchecked Sendable {
39 | static let schema = "foos"
40 |
41 | @ID(key: .id)
42 | var id: UUID?
43 |
44 | @Field(key: "bar")
45 | var bar: String
46 |
47 | @Field(key: "baz")
48 | var baz: Int
49 |
50 | init() { }
51 |
52 | init(id: IDValue? = nil, bar: String, baz: Int) {
53 | self.id = id
54 | self.bar = bar
55 | self.baz = baz
56 | }
57 | }
58 |
59 | private struct FooMigration: Migration {
60 | func prepare(on database: any Database) -> EventLoopFuture {
61 | database.schema("foos")
62 | .field("id", .uuid, .identifier(auto: false))
63 | .field("bar", .string, .required)
64 | .field("baz", .int, .required)
65 | .unique(on: "bar", "baz")
66 | .create()
67 | }
68 |
69 | func revert(on database: any Database) -> EventLoopFuture {
70 | database.schema("foos").delete()
71 | }
72 | }
73 |
74 | private struct BarMigration: Migration {
75 | func prepare(on database: any Database) -> EventLoopFuture {
76 | database.schema("bars")
77 | .field("name", .string)
78 | .unique(on: "name")
79 | .create()
80 | }
81 |
82 | func revert(on database: any Database) -> EventLoopFuture {
83 | database.schema("bars").delete()
84 | }
85 | }
86 |
87 | private struct BazMigration: Migration {
88 | func prepare(on database: any Database) -> EventLoopFuture {
89 | database.schema("bazs")
90 | .field("name", .string)
91 | .unique(on: "name")
92 | .create()
93 | }
94 |
95 | func revert(on database: any Database) -> EventLoopFuture {
96 | database.schema("bazs").delete()
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/AsyncMigration.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public protocol AsyncMigration: Migration {
4 | func prepare(on database: any Database) async throws
5 | func revert(on database: any Database) async throws
6 | }
7 |
8 | public extension AsyncMigration {
9 | func prepare(on database: any Database) -> EventLoopFuture {
10 | database.eventLoop.makeFutureWithTask {
11 | try await self.prepare(on: database)
12 | }
13 | }
14 |
15 | func revert(on database: any Database) -> EventLoopFuture {
16 | database.eventLoop.makeFutureWithTask {
17 | try await self.revert(on: database)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/AsyncModelMiddleware.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public protocol AsyncModelMiddleware: AnyModelMiddleware {
4 | associatedtype Model: FluentKit.Model
5 |
6 | func create(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws
7 | func update(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws
8 | func delete(model: Model, force: Bool, on db: any Database, next: any AnyAsyncModelResponder) async throws
9 | func softDelete(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws
10 | func restore(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws
11 | }
12 |
13 | extension AsyncModelMiddleware {
14 | public func handle(
15 | _ event: ModelEvent,
16 | _ model: any AnyModel,
17 | on db: any Database,
18 | chainingTo next: any AnyModelResponder
19 | ) -> EventLoopFuture {
20 | guard let modelType = (model as? Model) else {
21 | return next.handle(event, model, on: db)
22 | }
23 |
24 | return db.eventLoop.makeFutureWithTask {
25 | let responder = AsyncBasicModelResponder { responderEvent, responderModel, responderDB in
26 | try await next.handle(responderEvent, responderModel, on: responderDB).get()
27 | }
28 |
29 | switch event {
30 | case .create:
31 | try await self.create(model: modelType, on: db, next: responder)
32 | case .update:
33 | try await self.update(model: modelType, on: db, next: responder)
34 | case .delete(let force):
35 | try await self.delete(model: modelType, force: force, on: db, next: responder)
36 | case .softDelete:
37 | try await self.softDelete(model: modelType, on: db, next: responder)
38 | case .restore:
39 | try await self.restore(model: modelType, on: db, next: responder)
40 | }
41 | }
42 | }
43 |
44 | public func create(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws {
45 | try await next.create(model, on: db)
46 | }
47 |
48 | public func update(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws {
49 | try await next.update(model, on: db)
50 | }
51 |
52 | public func delete(model: Model, force: Bool, on db: any Database, next: any AnyAsyncModelResponder) async throws {
53 | try await next.delete(model, force: force, on: db)
54 | }
55 |
56 | public func softDelete(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws {
57 | try await next.softDelete(model, on: db)
58 | }
59 |
60 | public func restore(model: Model, on db: any Database, next: any AnyAsyncModelResponder) async throws {
61 | try await next.restore(model, on: db)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/Children+Concurrency.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public extension ChildrenProperty {
4 | func load(on database: any Database) async throws {
5 | try await self.load(on: database).get()
6 | }
7 |
8 | func create(_ to: To, on database: any Database) async throws {
9 | try await self.create(to, on: database).get()
10 | }
11 |
12 | func create(_ to: [To], on database: any Database) async throws {
13 | try await self.create(to, on: database).get()
14 | }
15 | }
16 |
17 | public extension CompositeChildrenProperty {
18 | func load(on database: any Database) async throws {
19 | try await self.load(on: database).get()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/Database+Concurrency.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public extension Database {
4 | func execute(
5 | query: DatabaseQuery,
6 | onOutput: @escaping @Sendable (any DatabaseOutput) -> ()
7 | ) async throws {
8 | try await self.execute(query: query, onOutput: onOutput).get()
9 | }
10 |
11 | func execute(
12 | schema: DatabaseSchema
13 | ) async throws {
14 | try await self.execute(schema: schema).get()
15 | }
16 |
17 | func execute(
18 | enum: DatabaseEnum
19 | ) async throws {
20 | try await self.execute(enum: `enum`).get()
21 | }
22 |
23 | func transaction(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T {
24 | try await self.transaction { db in
25 | self.eventLoop.makeFutureWithTask {
26 | try await closure(db)
27 | }
28 | }.get()
29 | }
30 |
31 | func withConnection(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T {
32 | try await self.withConnection { db in
33 | self.eventLoop.makeFutureWithTask {
34 | try await closure(db)
35 | }
36 | }.get()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/EnumBuilder+Concurrency.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public extension EnumBuilder {
4 | func create() async throws -> DatabaseSchema.DataType {
5 | try await self.create().get()
6 | }
7 |
8 | func read() async throws -> DatabaseSchema.DataType {
9 | try await self.read().get()
10 | }
11 |
12 | func update() async throws -> DatabaseSchema.DataType {
13 | try await self.update().get()
14 | }
15 |
16 | func delete() async throws {
17 | try await self.delete().get()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/Model+Concurrency.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public extension Model {
4 | static func find(
5 | _ id: Self.IDValue?,
6 | on database: any Database
7 | ) async throws -> Self? {
8 | try await self.find(id, on: database).get()
9 | }
10 |
11 | // MARK: - CRUD
12 | func save(on database: any Database) async throws {
13 | try await self.save(on: database).get()
14 | }
15 |
16 | func create(on database: any Database) async throws {
17 | try await self.create(on: database).get()
18 | }
19 |
20 | func update(on database: any Database) async throws {
21 | try await self.update(on: database).get()
22 | }
23 |
24 | func delete(force: Bool = false, on database: any Database) async throws {
25 | try await self.delete(force: force, on: database).get()
26 | }
27 |
28 | func restore(on database: any Database) async throws {
29 | try await self.restore(on: database).get()
30 | }
31 | }
32 |
33 | public extension Collection where Element: FluentKit.Model, Self: Sendable {
34 | func delete(force: Bool = false, on database: any Database) async throws {
35 | try await self.delete(force: force, on: database).get()
36 | }
37 |
38 | func create(on database: any Database) async throws {
39 | try await self.create(on: database).get()
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/ModelResponder+Concurrency.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public protocol AnyAsyncModelResponder: AnyModelResponder {
4 | func handle(
5 | _ event: ModelEvent,
6 | _ model: any AnyModel,
7 | on db: any Database
8 | ) async throws
9 | }
10 |
11 | extension AnyAsyncModelResponder {
12 | func handle(_ event: ModelEvent, _ model: any AnyModel, on db: any Database) -> EventLoopFuture {
13 | db.eventLoop.makeFutureWithTask {
14 | try await self.handle(event, model, on: db)
15 | }
16 | }
17 | }
18 |
19 | extension AnyAsyncModelResponder {
20 | public func create(_ model: any AnyModel, on db: any Database) async throws {
21 | try await handle(.create, model, on: db)
22 | }
23 |
24 | public func update(_ model: any AnyModel, on db: any Database) async throws {
25 | try await handle(.update, model, on: db)
26 | }
27 |
28 | public func restore(_ model: any AnyModel, on db: any Database) async throws {
29 | try await handle(.restore, model, on: db)
30 | }
31 |
32 | public func softDelete(_ model: any AnyModel, on db: any Database) async throws {
33 | try await handle(.softDelete, model, on: db)
34 | }
35 |
36 | public func delete(_ model: any AnyModel, force: Bool, on db: any Database) async throws {
37 | try await handle(.delete(force), model, on: db)
38 | }
39 | }
40 |
41 | internal struct AsyncBasicModelResponder: AnyAsyncModelResponder {
42 | private let _handle: @Sendable (ModelEvent, any AnyModel, any Database) async throws -> Void
43 |
44 | internal func handle(_ event: ModelEvent, _ model: any AnyModel, on db: any Database) async throws {
45 | try await _handle(event, model, db)
46 | }
47 |
48 | init(handle: @escaping @Sendable (ModelEvent, any AnyModel, any Database) async throws -> Void) {
49 | self._handle = handle
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/OptionalChild+Concurrency.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public extension OptionalChildProperty {
4 | func load(on database: any Database) async throws {
5 | try await self.load(on: database).get()
6 | }
7 |
8 | func create(_ to: To, on database: any Database) async throws {
9 | try await self.create(to, on: database).get()
10 | }
11 | }
12 |
13 | public extension CompositeOptionalChildProperty {
14 | func load(on database: any Database) async throws {
15 | try await self.load(on: database).get()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/OptionalParent+Concurrency.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public extension OptionalParentProperty {
4 | func load(on database: any Database) async throws {
5 | try await self.load(on: database).get()
6 | }
7 | }
8 |
9 | public extension CompositeOptionalParentProperty {
10 | func load(on database: any Database) async throws {
11 | try await self.load(on: database).get()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/Parent+Concurrency.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public extension ParentProperty {
4 | func load(on database: any Database) async throws {
5 | try await self.load(on: database).get()
6 | }
7 | }
8 |
9 | public extension CompositeParentProperty {
10 | func load(on database: any Database) async throws {
11 | try await self.load(on: database).get()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/Relation+Concurrency.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public extension Relation {
4 | func get(reload: Bool = false, on database: any Database) async throws -> RelatedValue {
5 | try await self.get(reload: reload, on: database).get()
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/SchemaBuilder+Concurrency.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public extension SchemaBuilder {
4 | func create() async throws {
5 | try await self.create().get()
6 | }
7 |
8 | func update() async throws {
9 | try await self.update().get()
10 | }
11 |
12 | func delete() async throws {
13 | try await self.delete().get()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Concurrency/Siblings+Concurrency.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public extension SiblingsProperty {
4 |
5 | func load(on database: any Database) async throws {
6 | try await self.load(on: database).get()
7 | }
8 |
9 | // MARK: Checking state
10 |
11 | func isAttached(to: To, on database: any Database) async throws -> Bool {
12 | try await self.isAttached(to: to, on: database).get()
13 | }
14 |
15 | func isAttached(toID: To.IDValue, on database: any Database) async throws -> Bool {
16 | try await self.isAttached(toID: toID, on: database).get()
17 | }
18 |
19 | // MARK: Operations
20 |
21 | /// Attach multiple models with plain edit closure.
22 | func attach(_ tos: [To], on database: any Database, _ edit: @escaping @Sendable (Through) -> () = { _ in }) async throws {
23 | try await self.attach(tos, on: database, edit).get()
24 | }
25 |
26 | /// Attach single model with plain edit closure.
27 | func attach(_ to: To, on database: any Database, _ edit: @escaping @Sendable (Through) -> () = { _ in }) async throws {
28 | try await self.attach(to, method: .always, on: database, edit)
29 | }
30 |
31 | /// Attach single model by specific method with plain edit closure.
32 | func attach(
33 | _ to: To, method: AttachMethod, on database: any Database,
34 | _ edit: @escaping @Sendable (Through) -> () = { _ in }
35 | ) async throws {
36 | try await self.attach(to, method: method, on: database, edit).get()
37 | }
38 |
39 | /// A version of ``attach(_:on:_:)-791gu`` whose edit closure is async and can throw.
40 | ///
41 | /// This method provides "all or none" semantics- if the edit closure throws an error, any already-
42 | /// processed pivots are discarded. Only if all pivots are successfully edited are any of them saved.
43 | ///
44 | /// These semantics require us to reimplement, rather than calling through to, the ELF version.
45 | func attach(
46 | _ tos: [To],
47 | on database: any Database,
48 | _ edit: @escaping @Sendable (Through) async throws -> ()
49 | ) async throws {
50 | guard let fromID = self.idValue else {
51 | throw SiblingsPropertyError.owningModelIdRequired(property: self.name)
52 | }
53 |
54 | var pivots: [Through] = []
55 | pivots.reserveCapacity(tos.count)
56 |
57 | for to in tos {
58 | guard let toID = to.id else {
59 | throw SiblingsPropertyError.operandModelIdRequired(property: self.name)
60 | }
61 | let pivot = Through()
62 | pivot[keyPath: self.from].id = fromID
63 | pivot[keyPath: self.to].id = toID
64 | pivot[keyPath: self.to].value = to
65 | try await edit(pivot)
66 | pivots.append(pivot)
67 | }
68 | try await pivots.create(on: database)
69 | }
70 |
71 | /// A version of ``attach(_:on:_:)-791gu`` whose edit closure is async and can throw.
72 | ///
73 | /// These semantics require us to reimplement, rather than calling through to, the ELF version.
74 | func attach(_ to: To, on database: any Database, _ edit: @escaping @Sendable (Through) async throws -> ()) async throws {
75 | try await self.attach(to, method: .always, on: database, edit)
76 | }
77 |
78 | /// A version of ``attach(_:method:on:_:)-20vs`` whose edit closure is async and can throw.
79 | ///
80 | /// These semantics require us to reimplement, rather than calling through to, the ELF version.
81 | func attach(
82 | _ to: To, method: AttachMethod, on database: any Database,
83 | _ edit: @escaping @Sendable (Through) async throws -> ()
84 | ) async throws {
85 | switch method {
86 | case .ifNotExists:
87 | guard try await !self.isAttached(to: to, on: database) else { return }
88 | fallthrough
89 | case .always:
90 | try await self.attach([to], on: database, edit)
91 | }
92 | }
93 |
94 | func detach(_ tos: [To], on database: any Database) async throws {
95 | try await self.detach(tos, on: database).get()
96 | }
97 |
98 | func detach(_ to: To, on database: any Database) async throws {
99 | try await self.detach(to, on: database).get()
100 | }
101 |
102 | func detachAll(on database: any Database) async throws {
103 | try await self.detachAll(on: database).get()
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Database/Database+Logging.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 | import Logging
3 | import SQLKit
4 |
5 | extension Database {
6 | public func logging(to logger: Logger) -> any Database {
7 | LoggingOverrideDatabase(database: self, logger: logger)
8 | }
9 | }
10 |
11 | private struct LoggingOverrideDatabase {
12 | let database: D
13 | let logger: Logger
14 | }
15 |
16 | extension LoggingOverrideDatabase: Database {
17 | var context: DatabaseContext {
18 | .init(
19 | configuration: self.database.context.configuration,
20 | logger: self.logger,
21 | eventLoop: self.database.context.eventLoop,
22 | history: self.database.context.history
23 | )
24 | }
25 |
26 | func execute(
27 | query: DatabaseQuery,
28 | onOutput: @escaping @Sendable (any DatabaseOutput) -> ()
29 | ) -> EventLoopFuture {
30 | self.database.execute(query: query, onOutput: onOutput)
31 | }
32 |
33 | func execute(
34 | schema: DatabaseSchema
35 | ) -> EventLoopFuture {
36 | self.database.execute(schema: schema)
37 | }
38 |
39 | func execute(
40 | enum: DatabaseEnum
41 | ) -> EventLoopFuture {
42 | self.database.execute(enum: `enum`)
43 | }
44 |
45 | var inTransaction: Bool {
46 | self.database.inTransaction
47 | }
48 |
49 | func transaction(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture {
50 | self.database.transaction(closure)
51 | }
52 |
53 | func withConnection(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture {
54 | self.database.withConnection(closure)
55 | }
56 | }
57 |
58 | extension LoggingOverrideDatabase: SQLDatabase where D: SQLDatabase {
59 | func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) -> EventLoopFuture {
60 | self.database.execute(sql: query, onRow)
61 | }
62 | func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) async throws {
63 | try await self.database.execute(sql: query, onRow)
64 | }
65 | func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R {
66 | try await self.database.withSession(closure)
67 | }
68 | var dialect: any SQLDialect { self.database.dialect }
69 | var version: (any SQLDatabaseReportedVersion)? { self.database.version }
70 | var queryLogLevel: Logger.Level? { self.database.queryLogLevel }
71 | }
72 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Database/Database.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 | import Logging
3 |
4 | public protocol Database: Sendable {
5 | var context: DatabaseContext { get }
6 |
7 | func execute(
8 | query: DatabaseQuery,
9 | onOutput: @escaping @Sendable (any DatabaseOutput) -> ()
10 | ) -> EventLoopFuture
11 |
12 | func execute(
13 | schema: DatabaseSchema
14 | ) -> EventLoopFuture
15 |
16 | func execute(
17 | enum: DatabaseEnum
18 | ) -> EventLoopFuture
19 |
20 | var inTransaction: Bool { get }
21 |
22 | func transaction(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture
23 |
24 | func withConnection(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture
25 | }
26 |
27 | extension Database {
28 | public func query(_ model: Model.Type) -> QueryBuilder
29 | where Model: FluentKit.Model
30 | {
31 | .init(database: self)
32 | }
33 | }
34 |
35 | extension Database {
36 | public var configuration: any DatabaseConfiguration {
37 | self.context.configuration
38 | }
39 |
40 | public var logger: Logger {
41 | self.context.logger
42 | }
43 |
44 | public var eventLoop: any EventLoop {
45 | self.context.eventLoop
46 | }
47 |
48 | public var history: QueryHistory? {
49 | self.context.history
50 | }
51 |
52 | public var pageSizeLimit: Int? {
53 | self.context.pageSizeLimit
54 | }
55 | }
56 |
57 | public protocol DatabaseDriver: Sendable {
58 | func makeDatabase(with context: DatabaseContext) -> any Database
59 | func shutdown()
60 | func shutdownAsync() async
61 | }
62 |
63 | public extension DatabaseDriver {
64 | func shutdownAsync() async {
65 | shutdown()
66 | }
67 | }
68 |
69 | public protocol DatabaseConfiguration: Sendable {
70 | var middleware: [any AnyModelMiddleware] { get set }
71 | func makeDriver(for databases: Databases) -> any DatabaseDriver
72 | }
73 |
74 | public struct DatabaseContext: Sendable {
75 | public let configuration: any DatabaseConfiguration
76 | public let logger: Logger
77 | public let eventLoop: any EventLoop
78 | public let history: QueryHistory?
79 | public let pageSizeLimit: Int?
80 |
81 | public init(
82 | configuration: any DatabaseConfiguration,
83 | logger: Logger,
84 | eventLoop: any EventLoop,
85 | history: QueryHistory? = nil,
86 | pageSizeLimit: Int? = nil
87 | ) {
88 | self.configuration = configuration
89 | self.logger = logger
90 | self.eventLoop = eventLoop
91 | self.history = history
92 | self.pageSizeLimit = pageSizeLimit
93 | }
94 | }
95 |
96 | public protocol DatabaseError {
97 | var isSyntaxError: Bool { get }
98 | var isConstraintFailure: Bool { get }
99 | var isConnectionClosed: Bool { get }
100 | }
101 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Database/DatabaseID.swift:
--------------------------------------------------------------------------------
1 | public struct DatabaseID: Hashable, Codable, Sendable {
2 | public let string: String
3 |
4 | public init(string: String) {
5 | self.string = string
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Database/DatabaseOutput.swift:
--------------------------------------------------------------------------------
1 | public protocol DatabaseOutput: CustomStringConvertible, Sendable {
2 | func schema(_ schema: String) -> any DatabaseOutput
3 | func contains(_ key: FieldKey) -> Bool
4 | func decodeNil(_ key: FieldKey) throws -> Bool
5 | func decode(_ key: FieldKey, as type: T.Type) throws -> T
6 | where T: Decodable
7 | }
8 |
9 | extension DatabaseOutput {
10 | public func decode(_ key: FieldKey) throws -> T
11 | where T: Decodable
12 | {
13 | try self.decode(key, as: T.self)
14 | }
15 |
16 | public func qualifiedSchema(space: String?, _ schema: String) -> any DatabaseOutput {
17 | self.schema([space, schema].compactMap({ $0 }).joined(separator: "_"))
18 | }
19 | }
20 |
21 | extension DatabaseOutput {
22 | public func prefixed(by prefix: FieldKey) -> any DatabaseOutput {
23 | PrefixedDatabaseOutput(prefix: prefix, strategy: .none, base: self)
24 | }
25 |
26 | public func prefixed(by prefix: FieldKey, using stratgey: KeyPrefixingStrategy) -> any DatabaseOutput {
27 | PrefixedDatabaseOutput(prefix: prefix, strategy: stratgey, base: self)
28 | }
29 |
30 | public func cascading(to output: any DatabaseOutput) -> any DatabaseOutput {
31 | CombinedOutput(first: self, second: output)
32 | }
33 | }
34 |
35 | private struct CombinedOutput: DatabaseOutput {
36 | let first: any DatabaseOutput, second: any DatabaseOutput
37 |
38 | func schema(_ schema: String) -> any DatabaseOutput {
39 | CombinedOutput(first: self.first.schema(schema), second: self.second.schema(schema))
40 | }
41 |
42 | func contains(_ key: FieldKey) -> Bool {
43 | self.first.contains(key) || self.second.contains(key)
44 | }
45 |
46 | func decodeNil(_ key: FieldKey) throws -> Bool {
47 | try self.first.contains(key) ? self.first.decodeNil(key) : self.second.decodeNil(key)
48 | }
49 |
50 | func decode(_ key: FieldKey, as type: T.Type) throws -> T where T: Decodable {
51 | try self.first.contains(key) ? self.first.decode(key, as: T.self) : self.second.decode(key, as: T.self)
52 | }
53 |
54 | var description: String {
55 | self.first.description + " -> " + self.second.description
56 | }
57 | }
58 |
59 | private struct PrefixedDatabaseOutput: DatabaseOutput {
60 | let prefix: FieldKey, strategy: KeyPrefixingStrategy
61 | let base: any DatabaseOutput
62 |
63 | func schema(_ schema: String) -> any DatabaseOutput {
64 | PrefixedDatabaseOutput(prefix: self.prefix, strategy: self.strategy, base: self.base.schema(schema))
65 | }
66 |
67 | func contains(_ key: FieldKey) -> Bool {
68 | self.base.contains(self.strategy.apply(prefix: self.prefix, to: key))
69 | }
70 |
71 | func decodeNil(_ key: FieldKey) throws -> Bool {
72 | try self.base.decodeNil(self.strategy.apply(prefix: self.prefix, to: key))
73 | }
74 |
75 | func decode(_ key: FieldKey, as type: T.Type) throws -> T where T : Decodable {
76 | try self.base.decode(self.strategy.apply(prefix: self.prefix, to: key), as: T.self)
77 | }
78 |
79 | var description: String {
80 | "Prefix(\(self.prefix) by \(self.strategy), of: \(self.base.description))"
81 | }
82 | }
83 |
84 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Database/KeyPrefixingStrategy.swift:
--------------------------------------------------------------------------------
1 | /// A strategy describing how to apply a prefix to a ``FieldKey``.
2 | public enum KeyPrefixingStrategy: CustomStringConvertible, Sendable {
3 | /// The "do nothing" strategy - the prefix is applied to each key by simple concatenation.
4 | case none
5 |
6 | /// Each key has its first character capitalized and the prefix is applied to the result.
7 | case camelCase
8 |
9 | /// An underscore is placed between the prefix and each key.
10 | case snakeCase
11 |
12 | /// A custom strategy - for each key, the closure is called with that key and the prefix with which the
13 | /// wrapper was initialized, and must return the field key to actually use. The closure must be "pure"
14 | /// (i.e. for any given pair of inputs it must always return the same result, in the same way that hash
15 | /// values must be consistent within a single execution context).
16 | case custom(@Sendable (_ prefix: FieldKey, _ idFieldKey: FieldKey) -> FieldKey)
17 |
18 | // See `CustomStringConvertible.description`.
19 | public var description: String {
20 | switch self {
21 | case .none:
22 | ".useDefaultKeys"
23 | case .camelCase:
24 | ".camelCase"
25 | case .snakeCase:
26 | ".snakeCase"
27 | case .custom(_):
28 | ".custom(...)"
29 | }
30 | }
31 |
32 | /// Apply this prefixing strategy and the given prefix to the given key, and return the result.
33 | public func apply(prefix: FieldKey, to key: FieldKey) -> FieldKey {
34 | switch self {
35 | case .none:
36 | .prefix(prefix, key)
37 |
38 | // This strategy converts `.id` and `.aggregate` keys (but not prefixes) into generic `.string()`s.
39 | case .camelCase:
40 | switch key {
41 | case .id, .aggregate, .string(_):
42 | .prefix(prefix, .string(key.description.withUppercasedFirstCharacter()))
43 |
44 | case .prefix(let originalPrefix, let originalSuffix):
45 | .prefix(self.apply(prefix: prefix, to: originalPrefix), originalSuffix)
46 | }
47 |
48 | case .snakeCase:
49 | .prefix(.prefix(prefix, .string("_")), key)
50 |
51 | case .custom(let closure):
52 | closure(prefix, key)
53 | }
54 | }
55 | }
56 |
57 | fileprivate extension String {
58 | func withUppercasedFirstCharacter() -> String {
59 | guard !self.isEmpty else { return self }
60 |
61 | var result = self
62 | result.replaceSubrange(result.startIndex ... result.startIndex, with: result[result.startIndex].uppercased())
63 | return result
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Database/TransactionControlDatabase.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | /// Protocol for describing a database that allows fine-grained control over transcactions
4 | /// when you need more control than provided by ``Database/transaction(_:)-1x3ds``
5 | ///
6 | /// > Warning: ⚠️ It is the developer's responsiblity to get hold of a ``Database``, execute the
7 | /// > transaction functions on that connection, and ensure that the functions aren't called across
8 | /// > different conenctions. You are also responsible for ensuring that you commit or rollback
9 | /// > queries when you're ready.
10 | ///
11 | /// Do not mix these functions and ``Database/transaction(_:)-1x3ds``.
12 | public protocol TransactionControlDatabase: Database {
13 | /// Start the transaction on the current connection. This is equivalent to an SQL `BEGIN`
14 | /// - Returns: future `Void` when the transaction has been started
15 | func beginTransaction() -> EventLoopFuture
16 |
17 | /// Commit the queries executed for the transaction and write them to the database
18 | /// This is equivalent to an SQL `COMMIT`
19 | /// - Returns: future `Void` when the transaction has been committed
20 | func commitTransaction() -> EventLoopFuture
21 |
22 | /// Rollback the current transaction's queries. You may want to trigger this when handling an error
23 | /// when trying to create models.
24 | /// This is equivalent to an SQL `ROLLBACK`
25 | /// - Returns: future `Void` when the transaction has been rollbacked
26 | func rollbackTransaction() -> EventLoopFuture
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Docs.docc/Resources/vapor-fluentkit-logo.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Docs.docc/index.md:
--------------------------------------------------------------------------------
1 | # ``FluentKit``
2 |
3 | FluentKit is an ORM framework for Swift. It allows you to write type safe, database agnostic models and queries. It takes advantage of Swift's type system to provide a powerful, yet easy to use API.
4 |
5 | An example query looks like:
6 |
7 | ```swift
8 | let planets = try await Planet.query(on: database)
9 | .filter(\.$type == .gasGiant)
10 | .sort(\.$name)
11 | .with(\.$star)
12 | .all()
13 | ```
14 |
15 | For more information, see the [Fluent documentation](https://docs.vapor.codes/fluent/overview/).
16 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Docs.docc/theme-settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme": {
3 | "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" },
4 | "border-radius": "0",
5 | "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
6 | "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
7 | "color": {
8 | "fluentkit": { "dark": "hsl(200, 75%, 85%)", "light": "hsl(200, 75%, 75%)" },
9 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-fluentkit) 30%, #000 100%)",
10 | "documentation-intro-accent": "var(--color-fluentkit)",
11 | "documentation-intro-eyebrow": "white",
12 | "documentation-intro-figure": "white",
13 | "documentation-intro-title": "white",
14 | "logo-base": { "dark": "#fff", "light": "#000" },
15 | "logo-shape": { "dark": "#000", "light": "#fff" },
16 | "fill": { "dark": "#000", "light": "#fff" }
17 | },
18 | "icons": { "technology": "/fluentkit/images/FluentKit/vapor-fluentkit-logo.svg" }
19 | },
20 | "features": {
21 | "quickNavigation": { "enable": true },
22 | "i18n": { "enable": true }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Enum/DatabaseEnum.swift:
--------------------------------------------------------------------------------
1 | public struct DatabaseEnum: Sendable {
2 | public enum Action: Sendable {
3 | case create
4 | case update
5 | case delete
6 | }
7 |
8 | public var action: Action
9 | public var name: String
10 |
11 | public var createCases: [String]
12 | public var deleteCases: [String]
13 |
14 | public init(name: String) {
15 | self.action = .create
16 | self.name = name
17 | self.createCases = []
18 | self.deleteCases = []
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Enum/EnumBuilder.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 | import NIOConcurrencyHelpers
3 | import SQLKit
4 |
5 | extension Database {
6 | public func `enum`(_ name: String) -> EnumBuilder {
7 | .init(database: self, name: name)
8 | }
9 | }
10 |
11 | public final class EnumBuilder: Sendable {
12 | let database: any Database
13 | let lockedEnum: NIOLockedValueBox
14 |
15 | public var `enum`: DatabaseEnum {
16 | get { self.lockedEnum.withLockedValue { $0 } }
17 | set { self.lockedEnum.withLockedValue { $0 = newValue } }
18 | }
19 |
20 | init(database: any Database, name: String) {
21 | self.database = database
22 | self.lockedEnum = .init(.init(name: name))
23 | }
24 |
25 | public func `case`(_ name: String) -> Self {
26 | self.enum.createCases.append(name)
27 | return self
28 | }
29 |
30 | public func deleteCase(_ name: String) -> Self {
31 | self.enum.deleteCases.append(name)
32 | return self
33 | }
34 |
35 | public func create() -> EventLoopFuture {
36 | self.enum.action = .create
37 | return self.database.execute(enum: self.enum).flatMap {
38 | self.generateDatatype()
39 | }
40 | }
41 |
42 | public func read() -> EventLoopFuture {
43 | self.generateDatatype()
44 | }
45 |
46 | public func update() -> EventLoopFuture {
47 | self.enum.action = .update
48 | return self.database.execute(enum: self.enum).flatMap {
49 | self.generateDatatype()
50 | }
51 | }
52 |
53 | public func delete() -> EventLoopFuture {
54 | self.enum.action = .delete
55 | return self.database.execute(enum: self.enum).flatMap {
56 | self.deleteMetadata()
57 | }
58 | }
59 |
60 | // MARK: Private
61 |
62 | private func generateDatatype() -> EventLoopFuture {
63 | EnumMetadata.migration.prepare(on: self.database).flatMap {
64 | self.updateMetadata()
65 | }.flatMap { _ in
66 | // Fetch the latest cases.
67 | EnumMetadata.query(on: self.database).filter(\.$name == self.enum.name).all()
68 | }.map { cases in
69 | // Convert latest cases to usable DataType.
70 | .enum(.init(
71 | name: self.enum.name,
72 | cases: cases.map { $0.case }
73 | ))
74 | }
75 | }
76 |
77 | private func updateMetadata() -> EventLoopFuture {
78 | // Create all new enum cases.
79 | let create = self.enum.createCases.map {
80 | EnumMetadata(name: self.enum.name, case: $0)
81 | }.create(on: self.database)
82 | // Delete all old enum cases.
83 | let delete = EnumMetadata.query(on: self.database)
84 | .filter(\.$name == self.enum.name)
85 | .filter(\.$case ~~ self.enum.deleteCases)
86 | .delete()
87 | return create.and(delete).map { _ in }
88 | }
89 |
90 | private func deleteMetadata() -> EventLoopFuture {
91 | // Delete all cases for this enum.
92 | EnumMetadata.query(on: self.database)
93 | .filter(\.$name == self.enum.name)
94 | .delete()
95 | .flatMap
96 | { _ in
97 | EnumMetadata.query(on: self.database).count()
98 | }.flatMap { count in
99 | // If no enums are left, remove table.
100 | if count == 0 {
101 | return EnumMetadata.migration.revert(on: self.database)
102 | } else {
103 | return self.database.eventLoop.makeSucceededFuture(())
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Enum/EnumMetadata.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 | import Foundation
3 |
4 | final class EnumMetadata: Model, @unchecked Sendable {
5 | static let schema = "_fluent_enums"
6 |
7 | static var migration: any Migration {
8 | EnumMetadataMigration()
9 | }
10 |
11 | @ID(key: .id)
12 | var id: UUID?
13 |
14 | @Field(key: "name")
15 | var name: String
16 |
17 | @Field(key: "case")
18 | var `case`: String
19 |
20 | init() {}
21 |
22 | init(id: IDValue? = nil, name: String, `case`: String) {
23 | self.id = id
24 | self.name = name
25 | self.case = `case`
26 | }
27 | }
28 |
29 | private struct EnumMetadataMigration: Migration {
30 | func prepare(on database: any Database) -> EventLoopFuture {
31 | database.schema(EnumMetadata.schema)
32 | .id()
33 | .field("name", .string, .required)
34 | .field("case", .string, .required)
35 | .unique(on: "name", "case")
36 | .ignoreExisting()
37 | .create()
38 | }
39 |
40 | func revert(on database: any Database) -> EventLoopFuture {
41 | database.schema(EnumMetadata.schema).delete()
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Enum/EnumProperty.swift:
--------------------------------------------------------------------------------
1 | extension Fields {
2 | public typealias Enum = EnumProperty
3 | where Value: Codable & Sendable,
4 | Value: RawRepresentable,
5 | Value.RawValue == String
6 | }
7 |
8 | // MARK: Type
9 |
10 | @propertyWrapper
11 | public final class EnumProperty
12 | where Model: FluentKit.Fields,
13 | Value: Codable & Sendable,
14 | Value: RawRepresentable,
15 | Value.RawValue == String
16 | {
17 | public let field: FieldProperty
18 |
19 | public var projectedValue: EnumProperty {
20 | self
21 | }
22 |
23 | public var wrappedValue: Value {
24 | get {
25 | guard let value = self.value else {
26 | fatalError("Cannot access enum field before it is initialized or fetched: \(self.field.key)")
27 | }
28 | return value
29 | }
30 | set {
31 | self.value = newValue
32 | }
33 | }
34 |
35 | public init(key: FieldKey) {
36 | self.field = .init(key: key)
37 | }
38 | }
39 |
40 | // MARK: Property
41 |
42 | extension EnumProperty: AnyProperty { }
43 |
44 | extension EnumProperty: Property {
45 | public var value: Value? {
46 | get {
47 | self.field.value.map {
48 | Value(rawValue: $0)!
49 | }
50 | }
51 | set {
52 | self.field.value = newValue?.rawValue
53 | }
54 | }
55 | }
56 |
57 | // MARK: Queryable
58 |
59 | extension EnumProperty: AnyQueryableProperty {
60 | public var path: [FieldKey] {
61 | self.field.path
62 | }
63 | }
64 |
65 | extension EnumProperty: QueryableProperty {
66 | public static func queryValue(_ value: Value) -> DatabaseQuery.Value {
67 | .enumCase(value.rawValue)
68 | }
69 | }
70 |
71 | // MARK: Query-addressable
72 |
73 | extension EnumProperty: AnyQueryAddressableProperty {
74 | public var anyQueryableProperty: any AnyQueryableProperty { self }
75 | public var queryablePath: [FieldKey] { self.path }
76 | }
77 |
78 | extension EnumProperty: QueryAddressableProperty {
79 | public var queryableProperty: EnumProperty { self }
80 | }
81 |
82 | // MARK: Database
83 |
84 | extension EnumProperty: AnyDatabaseProperty {
85 | public var keys: [FieldKey] {
86 | self.field.keys
87 | }
88 |
89 | public func input(to input: any DatabaseInput) {
90 | let value: DatabaseQuery.Value
91 | if !input.wantsUnmodifiedKeys {
92 | guard let ivalue = self.field.inputValue else { return }
93 | value = ivalue
94 | } else {
95 | value = self.field.inputValue ?? .default
96 | }
97 |
98 | switch value {
99 | case .bind(let bind as String):
100 | input.set(.enumCase(bind), at: self.field.key)
101 | case .enumCase(let string):
102 | input.set(.enumCase(string), at: self.field.key)
103 | case .default:
104 | input.set(.default, at: self.field.key)
105 | default:
106 | fatalError("Unexpected input value type for '\(Model.self)'.'\(self.field.key)': \(value)")
107 | }
108 | }
109 |
110 | public func output(from output: any DatabaseOutput) throws {
111 | try self.field.output(from: output)
112 | }
113 | }
114 |
115 | // MARK: Codable
116 |
117 | extension EnumProperty: AnyCodableProperty {
118 | public func encode(to encoder: any Encoder) throws {
119 | var container = encoder.singleValueContainer()
120 | try container.encode(self.wrappedValue)
121 | }
122 |
123 | public func decode(from decoder: any Decoder) throws {
124 | let container = try decoder.singleValueContainer()
125 | self.value = try container.decode(Value.self)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Enum/OptionalEnumProperty.swift:
--------------------------------------------------------------------------------
1 | extension Fields {
2 | public typealias OptionalEnum = OptionalEnumProperty
3 | where Value: Codable & Sendable,
4 | Value: RawRepresentable,
5 | Value.RawValue == String
6 | }
7 |
8 | // MARK: Type
9 |
10 | @propertyWrapper
11 | public final class OptionalEnumProperty
12 | where Model: FluentKit.Fields,
13 | WrappedValue: Codable & Sendable,
14 | WrappedValue: RawRepresentable,
15 | WrappedValue.RawValue == String
16 | {
17 | public let field: OptionalFieldProperty
18 |
19 | public var projectedValue: OptionalEnumProperty {
20 | self
21 | }
22 |
23 | public var wrappedValue: WrappedValue? {
24 | get {
25 | self.value ?? nil
26 | }
27 | set {
28 | self.value = .some(newValue)
29 | }
30 | }
31 |
32 | public init(key: FieldKey) {
33 | self.field = .init(key: key)
34 | }
35 | }
36 |
37 | // MARK: Property
38 |
39 | extension OptionalEnumProperty: AnyProperty { }
40 |
41 | extension OptionalEnumProperty: Property {
42 | public var value: WrappedValue?? {
43 | get {
44 | self.field.value.map {
45 | $0.map {
46 | WrappedValue(rawValue: $0)!
47 | }
48 | }
49 | }
50 | set {
51 | switch newValue {
52 | case .some(.some(let newValue)):
53 | self.field.value = .some(.some(newValue.rawValue))
54 | case .some(.none):
55 | self.field.value = .some(.none)
56 | case .none:
57 | self.field.value = .none
58 | }
59 | }
60 | }
61 | }
62 |
63 | // MARK: Queryable
64 |
65 | extension OptionalEnumProperty: AnyQueryableProperty {
66 | public var path: [FieldKey] {
67 | self.field.path
68 | }
69 | }
70 |
71 | extension OptionalEnumProperty: QueryableProperty {
72 | public static func queryValue(_ value: Value) -> DatabaseQuery.Value {
73 | value.flatMap { .enumCase($0.rawValue) } ?? .null
74 | }
75 | }
76 |
77 | // MARK: Query-addressable
78 |
79 | extension OptionalEnumProperty: AnyQueryAddressableProperty {
80 | public var anyQueryableProperty: any AnyQueryableProperty { self }
81 | public var queryablePath: [FieldKey] { self.path }
82 | }
83 |
84 | extension OptionalEnumProperty: QueryAddressableProperty {
85 | public var queryableProperty: OptionalEnumProperty { self }
86 | }
87 |
88 | // MARK: Database
89 |
90 | extension OptionalEnumProperty: AnyDatabaseProperty {
91 | public var keys: [FieldKey] {
92 | self.field.keys
93 | }
94 |
95 | public func input(to input: any DatabaseInput) {
96 | let value: DatabaseQuery.Value
97 | if !input.wantsUnmodifiedKeys {
98 | guard let ivalue = self.field.inputValue else { return }
99 | value = ivalue
100 | } else {
101 | value = self.field.inputValue ?? .default
102 | }
103 |
104 |
105 | switch value {
106 | case .bind(let bind as String):
107 | input.set(.enumCase(bind), at: self.field.key)
108 | case .enumCase(let string):
109 | input.set(.enumCase(string), at: self.field.key)
110 | case .null:
111 | input.set(.null, at: self.field.key)
112 | case .default:
113 | input.set(.default, at: self.field.key)
114 | default:
115 | fatalError("Unexpected input value type for '\(Model.self)'.'\(self.field.key)': \(value)")
116 | }
117 | }
118 |
119 | public func output(from output: any DatabaseOutput) throws {
120 | try self.field.output(from: output)
121 | }
122 | }
123 |
124 | // MARK: Codable
125 |
126 | extension OptionalEnumProperty: AnyCodableProperty {
127 | public func encode(to encoder: any Encoder) throws {
128 | var container = encoder.singleValueContainer()
129 | try container.encode(self.wrappedValue)
130 | }
131 |
132 | public func decode(from decoder: any Decoder) throws {
133 | let container = try decoder.singleValueContainer()
134 | if container.decodeNil() {
135 | self.value = nil
136 | } else {
137 | self.value = try container.decode(Value.self)
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Exports.swift:
--------------------------------------------------------------------------------
1 | @_documentation(visibility: internal) @_exported import struct Foundation.Date
2 | @_documentation(visibility: internal) @_exported import struct Foundation.UUID
3 |
4 | @_documentation(visibility: internal) @_exported import Logging
5 |
6 | @_documentation(visibility: internal) @_exported import protocol NIO.EventLoop
7 | @_documentation(visibility: internal) @_exported import class NIO.EventLoopFuture
8 | @_documentation(visibility: internal) @_exported import struct NIO.EventLoopPromise
9 | @_documentation(visibility: internal) @_exported import protocol NIO.EventLoopGroup
10 | @_documentation(visibility: internal) @_exported import class NIO.NIOThreadPool
11 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Middleware/ModelMiddleware.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public protocol AnyModelMiddleware: Sendable {
4 | func handle(
5 | _ event: ModelEvent,
6 | _ model: any AnyModel,
7 | on db: any Database,
8 | chainingTo next: any AnyModelResponder
9 | ) -> EventLoopFuture
10 | }
11 |
12 | public protocol ModelMiddleware: AnyModelMiddleware {
13 | associatedtype Model: FluentKit.Model
14 |
15 | func create(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture
16 | func update(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture
17 | func delete(model: Model, force: Bool, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture
18 | func softDelete(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture
19 | func restore(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture
20 | }
21 |
22 | extension ModelMiddleware {
23 | public func handle(_ event: ModelEvent, _ model: any AnyModel, on db: any Database, chainingTo next: any AnyModelResponder) -> EventLoopFuture {
24 | guard let modelType = model as? Model else {
25 | return next.handle(event, model, on: db)
26 | }
27 |
28 | switch event {
29 | case .create:
30 | return create(model: modelType, on: db, next: next)
31 | case .update:
32 | return update(model: modelType, on: db, next: next)
33 | case .delete(let force):
34 | return delete(model: modelType, force: force, on: db, next: next)
35 | case .softDelete:
36 | return softDelete(model: modelType, on: db, next: next)
37 | case .restore:
38 | return restore(model: modelType, on: db, next: next)
39 | }
40 | }
41 |
42 | public func create(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture {
43 | next.create(model, on: db)
44 | }
45 |
46 | public func update(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture {
47 | next.update(model, on: db)
48 | }
49 |
50 | public func delete(model: Model, force: Bool, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture {
51 | next.delete(model, force: force, on: db)
52 | }
53 |
54 | public func softDelete(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture {
55 | next.softDelete(model, on: db)
56 | }
57 |
58 | public func restore(model: Model, on db: any Database, next: any AnyModelResponder) -> EventLoopFuture {
59 | next.restore(model, on: db)
60 | }
61 | }
62 |
63 | extension AnyModelMiddleware {
64 | func makeResponder(chainingTo responder: any AnyModelResponder) -> any AnyModelResponder {
65 | ModelMiddlewareResponder(middleware: self, responder: responder)
66 | }
67 | }
68 |
69 | extension Array where Element == any AnyModelMiddleware {
70 | internal func chainingTo(
71 | _ type: Model.Type,
72 | closure: @escaping @Sendable (ModelEvent, Model, any Database) throws -> EventLoopFuture
73 | ) -> any AnyModelResponder where Model: FluentKit.Model {
74 | var responder: any AnyModelResponder = BasicModelResponder(handle: closure)
75 | for middleware in reversed() {
76 | responder = middleware.makeResponder(chainingTo: responder)
77 | }
78 | return responder
79 | }
80 | }
81 |
82 | private struct ModelMiddlewareResponder: AnyModelResponder {
83 | var middleware: any AnyModelMiddleware
84 | var responder: any AnyModelResponder
85 |
86 | func handle(_ event: ModelEvent, _ model: any AnyModel, on db: any Database) -> EventLoopFuture {
87 | self.middleware.handle(event, model, on: db, chainingTo: responder)
88 | }
89 | }
90 |
91 | public enum ModelEvent: Sendable {
92 | case create
93 | case update
94 | case delete(Bool)
95 | case restore
96 | case softDelete
97 | }
98 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Middleware/ModelResponder.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public protocol AnyModelResponder: Sendable {
4 | func handle(
5 | _ event: ModelEvent,
6 | _ model: any AnyModel,
7 | on db: any Database
8 | ) -> EventLoopFuture
9 | }
10 |
11 | extension AnyModelResponder {
12 | public func create(_ model: any AnyModel, on db: any Database) -> EventLoopFuture {
13 | self.handle(.create, model, on: db)
14 | }
15 |
16 | public func update(_ model: any AnyModel, on db: any Database) -> EventLoopFuture {
17 | self.handle(.update, model, on: db)
18 | }
19 |
20 | public func restore(_ model: any AnyModel, on db: any Database) -> EventLoopFuture {
21 | self.handle(.restore, model, on: db)
22 | }
23 |
24 | public func softDelete(_ model: any AnyModel, on db: any Database) -> EventLoopFuture {
25 | self.handle(.softDelete, model, on: db)
26 | }
27 |
28 | public func delete(_ model: any AnyModel, force: Bool, on db: any Database) -> EventLoopFuture {
29 | self.handle(.delete(force), model, on: db)
30 | }
31 | }
32 |
33 | internal struct BasicModelResponder: AnyModelResponder where Model: FluentKit.Model {
34 | private let _handle: @Sendable (ModelEvent, Model, any Database) throws -> EventLoopFuture
35 |
36 | internal func handle(_ event: ModelEvent, _ model: any AnyModel, on db: any Database) -> EventLoopFuture {
37 | guard let modelType = model as? Model else {
38 | fatalError("Could not convert type AnyModel to \(Model.self)")
39 | }
40 |
41 | do {
42 | return try self._handle(event, modelType, db)
43 | } catch {
44 | return db.eventLoop.makeFailedFuture(error)
45 | }
46 | }
47 |
48 | init(handle: @escaping @Sendable (ModelEvent, Model, any Database) throws -> EventLoopFuture) {
49 | self._handle = handle
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Migration/Migration.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | /// Fluent's `Migration` can handle database migrations, which can include
4 | /// adding new table, changing existing tables or adding
5 | /// seed data. These actions are executed only once.
6 | public protocol Migration: Sendable {
7 |
8 | /// The name of the migration which Fluent uses to track the state of.
9 | var name: String { get }
10 |
11 | /// Called when a migration is executed.
12 | /// - Parameters:
13 | /// - database: `Database` to run the migration on,
14 | /// - returns: An asynchronous `Void`.
15 |
16 | func prepare(on database: any Database) -> EventLoopFuture
17 |
18 | /// Called when the changes from a migration are reverted.
19 | /// - Parameters:
20 | /// - database: `Database` to revert the migration on.
21 | /// - returns: An asynchronous `Void`.
22 | func revert(on database: any Database) -> EventLoopFuture
23 | }
24 |
25 | extension Migration {
26 | public var name: String {
27 | self.defaultName
28 | }
29 |
30 | internal var defaultName: String {
31 | #if compiler(<6.2)
32 | /// `String.init(reflecting:)` creates a `Mirror` unconditionally, but
33 | /// when the parameter is a metatype (such as is the case here), that
34 | /// mirror is never actually used for anything. Unfortunately, just
35 | /// creating it slows this accessor down by at least 30% at best, and
36 | /// as much as 50% in some cases. Given that it's already incorrect to
37 | /// be depending on the stability of `String(reflecting:)`'s output
38 | /// anyway, it seems to make more sense to just call the underlying
39 | /// runtime function directly instead of taking the huge speed hit just
40 | /// because the leading underscore makes it harder to ignore the
41 | /// fragility of the usage.
42 | Swift._typeName(Self.self, qualified: true)
43 | #else
44 | String(reflecting: Self.self)
45 | #endif
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Migration/MigrationLog.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 | import Foundation
3 |
4 | /// Stores information about `Migration`s that have been run.
5 | public final class MigrationLog: Model, @unchecked Sendable {
6 | public static let schema = "_fluent_migrations"
7 |
8 | public static var migration: any Migration {
9 | MigrationLogMigration()
10 | }
11 |
12 | @ID(key: .id)
13 | public var id: UUID?
14 |
15 | @Field(key: "name")
16 | public var name: String
17 |
18 | @Field(key: "batch")
19 | public var batch: Int
20 |
21 | @Timestamp(key: "created_at", on: .create)
22 | public var createdAt: Date?
23 |
24 | @Timestamp(key: "updated_at", on: .update)
25 | public var updatedAt: Date?
26 |
27 | public init() {}
28 |
29 | public init(id: IDValue? = nil, name: String, batch: Int) {
30 | self.id = id
31 | self.name = name
32 | self.batch = batch
33 | self.createdAt = nil
34 | self.updatedAt = nil
35 | }
36 | }
37 |
38 | private struct MigrationLogMigration: Migration {
39 | func prepare(on database: any Database) -> EventLoopFuture {
40 | database.schema(MigrationLog.schema)
41 | .field(.id, .uuid, .identifier(auto: false))
42 | .field("name", .string, .required)
43 | .field("batch", .int, .required)
44 | .field("created_at", .datetime)
45 | .field("updated_at", .datetime)
46 | .unique(on: "name")
47 | .ignoreExisting()
48 | .create()
49 | }
50 |
51 | func revert(on database: any Database) -> EventLoopFuture {
52 | database.schema(MigrationLog.schema).delete()
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Migration/Migrations.swift:
--------------------------------------------------------------------------------
1 | import NIOConcurrencyHelpers
2 |
3 | public final class Migrations: Sendable {
4 | let storage: NIOLockedValueBox<[DatabaseID?: [any Migration]]>
5 |
6 | public init() {
7 | self.storage = .init([:])
8 | }
9 |
10 | public func add(_ migration: any Migration, to id: DatabaseID? = nil) {
11 | self.storage.withLockedValue { $0[id, default: []].append(migration) }
12 | }
13 |
14 | @inlinable
15 | public func add(_ migrations: any Migration..., to id: DatabaseID? = nil) {
16 | self.add(migrations, to: id)
17 | }
18 |
19 | public func add(_ migrations: [any Migration], to id: DatabaseID? = nil) {
20 | self.storage.withLockedValue { $0[id, default: []].append(contentsOf: migrations) }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Model/AnyModel.swift:
--------------------------------------------------------------------------------
1 | public protocol AnyModel: Schema, CustomStringConvertible { }
2 |
3 | extension AnyModel {
4 | public static var alias: String? { nil }
5 | }
6 |
7 | extension AnyModel {
8 | public var description: String {
9 | let input = self.collectInput()
10 | let info = [
11 | "input": !input.isEmpty ? input.description : nil,
12 | "output": self.anyID.cachedOutput?.description
13 | ].compactMapValues({ $0 })
14 |
15 | return "\(Self.self)(\(info.isEmpty ? ":" : info.map { "\($0): \($1)" }.joined(separator: ", ")))"
16 | }
17 |
18 | // MARK: Joined
19 |
20 | public func joined(_ model: Joined.Type) throws -> Joined
21 | where Joined: Schema
22 | {
23 | guard let output = self.anyID.cachedOutput else {
24 | fatalError("Can only access joined models using models fetched from database (from \(Self.self) to \(Joined.self)).")
25 | }
26 | let joined = Joined()
27 | try joined.output(from: output.qualifiedSchema(space: Joined.spaceIfNotAliased, Joined.schemaOrAlias))
28 | return joined
29 | }
30 |
31 | var anyID: any AnyID {
32 | for (nameC, child) in _FastChildSequence(subject: self) {
33 | /// Match a property named `_id` which conforms to `AnyID`. `as?` is expensive, so check that last.
34 | if nameC?[0] == 0x5f/* '_' */,
35 | nameC?[1] == 0x69/* 'i' */,
36 | nameC?[2] == 0x64/* 'd' */,
37 | nameC?[3] == 0x00/* '\0' */,
38 | let idChild = child as? any AnyID
39 | {
40 | return idChild
41 | }
42 | }
43 | fatalError("id property for model \(Self.self) must be declared using @ID or @CompositeID")
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Model/EagerLoad.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public protocol EagerLoader: AnyEagerLoader {
4 | associatedtype Model: FluentKit.Model
5 | func run(models: [Model], on database: any Database) -> EventLoopFuture
6 | }
7 |
8 | extension EagerLoader {
9 | func anyRun(models: [any AnyModel], on database: any Database) -> EventLoopFuture {
10 | self.run(models: models.map { $0 as! Model }, on: database)
11 | }
12 | }
13 |
14 | public protocol AnyEagerLoader: Sendable {
15 | func anyRun(models: [any AnyModel], on database: any Database) -> EventLoopFuture
16 | }
17 |
18 | public protocol EagerLoadable {
19 | associatedtype From: Model
20 | associatedtype To: Model
21 |
22 | static func eagerLoad(
23 | _ relationKey: KeyPath,
24 | to builder: Builder
25 | ) where Builder: EagerLoadBuilder, Builder.Model == From
26 |
27 | static func eagerLoad(
28 | _ relationKey: KeyPath,
29 | withDeleted: Bool,
30 | to builder: Builder
31 | ) where Builder: EagerLoadBuilder, Builder.Model == From
32 |
33 | static func eagerLoad(
34 | _ loader: Loader,
35 | through: KeyPath,
36 | to builder: Builder
37 | ) where Loader: EagerLoader,
38 | Builder: EagerLoadBuilder,
39 | Loader.Model == To,
40 | Builder.Model == From
41 | }
42 |
43 | extension EagerLoadable {
44 | public static func eagerLoad(
45 | _ relationKey: KeyPath,
46 | withDeleted: Bool,
47 | to builder: Builder
48 | ) where Builder: EagerLoadBuilder, Builder.Model == From {
49 | Self.eagerLoad(relationKey, to: builder)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Model/Fields+Codable.swift:
--------------------------------------------------------------------------------
1 | import struct SQLKit.SomeCodingKey
2 |
3 | extension Fields {
4 | public init(from decoder: any Decoder) throws {
5 | self.init()
6 |
7 | let container = try decoder.container(keyedBy: SomeCodingKey.self)
8 |
9 | for (key, property) in self.codableProperties {
10 | let propDecoder = try container.superDecoder(forKey: key)
11 | do {
12 | try property.decode(from: propDecoder)
13 | } catch {
14 | throw DecodingError.typeMismatch(type(of: property).anyValueType, .init(
15 | codingPath: container.codingPath + [key],
16 | debugDescription: "Could not decode property",
17 | underlyingError: error
18 | ))
19 | }
20 | }
21 | }
22 |
23 | public func encode(to encoder: any Encoder) throws {
24 | var container = encoder.container(keyedBy: SomeCodingKey.self)
25 |
26 | for (key, property) in self.codableProperties where !property.skipPropertyEncoding {
27 | do {
28 | try property.encode(to: container.superEncoder(forKey: key))
29 | } catch let error where error is EncodingError { // trapping all errors breaks value handling logic in database driver layers
30 | throw EncodingError.invalidValue(property.anyValue ?? "null", .init(
31 | codingPath: container.codingPath + [key],
32 | debugDescription: "Could not encode property",
33 | underlyingError: error
34 | ))
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Model/Model.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 |
3 | public protocol Model: AnyModel {
4 | associatedtype IDValue: Codable, Hashable, Sendable
5 | var id: IDValue? { get set }
6 | }
7 |
8 | extension Model {
9 | public static func query(on database: any Database) -> QueryBuilder {
10 | .init(database: database)
11 | }
12 |
13 | public static func find(
14 | _ id: Self.IDValue?,
15 | on database: any Database
16 | ) -> EventLoopFuture {
17 | guard let id = id else {
18 | return database.eventLoop.makeSucceededFuture(nil)
19 | }
20 | return Self.query(on: database)
21 | .filter(id: id)
22 | .first()
23 | }
24 |
25 | public func requireID() throws -> IDValue {
26 | guard let id = self.id else {
27 | throw FluentError.idRequired
28 | }
29 | return id
30 | }
31 |
32 | /// Replaces the existing common usage of `model._$id.exists`, which indicates whether any
33 | /// particular generic model has a non-`nil` ID that was loaded from a database query (or
34 | /// was overridden to allow Fluent to assume as such without having to check first). This
35 | /// version works for models which use `@CompositeID()`. It would not be necessary if
36 | /// support existed for property wrappers in protocols.
37 | ///
38 | /// > Note: Adding this property to ``Model`` rather than making the ``AnyID`` protocol
39 | /// > and ``anyID`` property public was chosen because implementing a new conformance for
40 | /// > ``AnyID`` can not be done correctly from outside FluentKit; it would be mostly useless
41 | /// > and potentially confusing public API surface.
42 | public var _$idExists: Bool {
43 | get { self.anyID.exists }
44 | set { self.anyID.exists = newValue }
45 | }
46 |
47 | public var _$id: ID {
48 | self.anyID as! ID
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Model/Schema.swift:
--------------------------------------------------------------------------------
1 | public protocol Schema: Fields {
2 | static var space: String? { get }
3 | static var schema: String { get }
4 | static var alias: String? { get }
5 | }
6 |
7 | extension Schema {
8 | public static var space: String? { nil }
9 |
10 | public static var schemaOrAlias: String {
11 | self.alias ?? self.schema
12 | }
13 |
14 | public static var spaceIfNotAliased: String? {
15 | self.alias == nil ? self.space : nil
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Operators/Operators.swift:
--------------------------------------------------------------------------------
1 | infix operator ~~
2 | infix operator !~
3 | infix operator =~
4 | infix operator !~=
5 | infix operator !=~
6 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Operators/ValueOperators+Array.swift:
--------------------------------------------------------------------------------
1 | // MARK: Field.Value
2 |
3 | public func ~~ (lhs: KeyPath, rhs: Values) -> ModelValueFilter
4 | where Model: FluentKit.Schema,
5 | Field: QueryableProperty,
6 | Values: Collection,
7 | Values.Element == Field.Value
8 | {
9 | lhs ~~ .array(rhs.map { Field.queryValue($0) })
10 | }
11 |
12 | public func ~~ (lhs: KeyPath, rhs: Values) -> ModelValueFilter
13 | where Model: FluentKit.Schema,
14 | Field: QueryableProperty,
15 | Field.Value: OptionalType,
16 | Field.Value.Wrapped: Codable,
17 | Values: Collection,
18 | Values.Element == Field.Value.Wrapped
19 | {
20 | lhs ~~ .array(rhs.map { Field.queryValue(.init($0)) })
21 | }
22 |
23 | public func !~ (lhs: KeyPath, rhs: Values) -> ModelValueFilter
24 | where Model: FluentKit.Schema,
25 | Field: QueryableProperty,
26 | Values: Collection,
27 | Values.Element == Field.Value
28 | {
29 | lhs !~ .array(rhs.map { Field.queryValue($0) })
30 | }
31 |
32 | public func !~ (lhs: KeyPath, rhs: Values) -> ModelValueFilter
33 | where Model: FluentKit.Schema,
34 | Field: QueryableProperty,
35 | Field.Value: OptionalType,
36 | Field.Value.Wrapped: Codable,
37 | Values: Collection,
38 | Values.Element == Field.Value.Wrapped
39 | {
40 | lhs !~ .array(rhs.map { Field.queryValue(.init($0)) })
41 | }
42 |
43 | // MARK: DatabaseQuery.Value
44 |
45 | public func ~~ (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter
46 | where Model: FluentKit.Schema, Field: QueryableProperty
47 | {
48 | .init(lhs, .subset(inverse: false), rhs)
49 | }
50 |
51 | public func !~ (lhs: KeyPath, rhs: DatabaseQuery.Value) -> ModelValueFilter
52 | where Model: FluentKit.Schema, Field: QueryableProperty
53 | {
54 | .init(lhs, .subset(inverse: true), rhs)
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Properties/CompositeID.swift:
--------------------------------------------------------------------------------
1 | import NIOConcurrencyHelpers
2 |
3 | extension Model {
4 | public typealias CompositeID = CompositeIDProperty
5 | where Value: Fields
6 | }
7 |
8 | // MARK: Type
9 |
10 | @propertyWrapper @dynamicMemberLookup
11 | public final class CompositeIDProperty: @unchecked Sendable
12 | where Model: FluentKit.Model, Value: FluentKit.Fields
13 | {
14 | public var value: Value? = .init(.init())
15 | public var exists: Bool = false
16 | var cachedOutput: (any DatabaseOutput)?
17 |
18 | public var projectedValue: CompositeIDProperty { self }
19 |
20 | public var wrappedValue: Value? {
21 | get { self.value }
22 | set { self.value = newValue }
23 | }
24 |
25 | public init() {}
26 |
27 | public subscript(
28 | dynamicMember keyPath: KeyPath
29 | ) -> Nested
30 | where Nested: Property
31 | {
32 | self.value![keyPath: keyPath]
33 | }
34 | }
35 |
36 | extension CompositeIDProperty: CustomStringConvertible {
37 | public var description: String {
38 | "@\(Model.self).CompositeID<\(Value.self))>()"
39 | }
40 | }
41 |
42 | // MARK: Property
43 |
44 | extension CompositeIDProperty: AnyProperty, Property {}
45 |
46 | // MARK: Database
47 |
48 | extension CompositeIDProperty: AnyDatabaseProperty {
49 | public var keys: [FieldKey] {
50 | Value.keys
51 | }
52 |
53 | public func input(to input: any DatabaseInput) {
54 | if let value = self.value {
55 | value.input(to: input)
56 | }
57 | }
58 |
59 | public func output(from output: any DatabaseOutput) throws {
60 | self.exists = true
61 | self.cachedOutput = output
62 | try self.value!.output(from: output)
63 | }
64 | }
65 |
66 | // MARK: Codable
67 |
68 | extension CompositeIDProperty: AnyCodableProperty {
69 | public func encode(to encoder: any Encoder) throws {
70 | var container = encoder.singleValueContainer()
71 | try container.encode(self.value)
72 | }
73 |
74 | public func decode(from decoder: any Decoder) throws {
75 | let container = try decoder.singleValueContainer()
76 | self.value = try container.decode(Value?.self)
77 | }
78 | }
79 |
80 | // MARK: AnyID
81 |
82 | extension CompositeIDProperty: AnyID {
83 | func generate() {}
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Properties/FieldKey.swift:
--------------------------------------------------------------------------------
1 | public indirect enum FieldKey: Sendable {
2 | case id
3 | case string(String)
4 | case aggregate
5 | case prefix(FieldKey, FieldKey)
6 | }
7 |
8 | extension FieldKey: ExpressibleByStringLiteral {
9 | public init(stringLiteral value: String) {
10 | switch value {
11 | case "id", "_id":
12 | self = .id
13 | default:
14 | self = .string(value)
15 | }
16 | }
17 | }
18 |
19 | extension FieldKey: CustomStringConvertible {
20 | public var description: String {
21 | switch self {
22 | case .id:
23 | "id"
24 | case .string(let name):
25 | name
26 | case .aggregate:
27 | "aggregate"
28 | case .prefix(let prefix, let key):
29 | prefix.description + key.description
30 | }
31 | }
32 | }
33 |
34 | extension FieldKey: Equatable { }
35 |
36 | extension FieldKey: Hashable { }
37 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Properties/OptionalField.swift:
--------------------------------------------------------------------------------
1 | import NIOConcurrencyHelpers
2 |
3 | extension Fields {
4 | public typealias OptionalField = OptionalFieldProperty
5 | where Value: Codable & Sendable
6 | }
7 |
8 | // MARK: Type
9 |
10 | @propertyWrapper
11 | public final class OptionalFieldProperty: @unchecked Sendable
12 | where Model: FluentKit.Fields, WrappedValue: Codable & Sendable
13 | {
14 | public let key: FieldKey
15 | var outputValue: WrappedValue??
16 | var inputValue: DatabaseQuery.Value?
17 |
18 | public var projectedValue: OptionalFieldProperty {
19 | self
20 | }
21 |
22 | public var wrappedValue: WrappedValue? {
23 | get { self.value ?? nil }
24 | set { self.value = .some(newValue) }
25 | }
26 |
27 | public init(key: FieldKey) {
28 | self.key = key
29 | }
30 | }
31 |
32 | // MARK: Property
33 |
34 | extension OptionalFieldProperty: AnyProperty { }
35 |
36 | extension OptionalFieldProperty: Property {
37 | public var value: WrappedValue?? {
38 | get {
39 | if let value = self.inputValue {
40 | switch value {
41 | case .bind(let bind):
42 | .some(bind as? WrappedValue)
43 | case .enumCase(let string):
44 | .some(string as? WrappedValue)
45 | case .default:
46 | fatalError("Cannot access default field for '\(Model.self).\(key)' before it is initialized or fetched")
47 | case .null:
48 | .some(.none)
49 | default:
50 | fatalError("Unexpected input value type for '\(Model.self).\(key)': \(value)")
51 | }
52 | } else if let value = self.outputValue {
53 | .some(value)
54 | } else {
55 | .none
56 | }
57 | }
58 | set {
59 | if let value = newValue {
60 | self.inputValue = value
61 | .flatMap { .bind($0) }
62 | ?? .null
63 | } else {
64 | self.inputValue = nil
65 | }
66 | }
67 | }
68 | }
69 |
70 | // MARK: Queryable
71 |
72 | extension OptionalFieldProperty: AnyQueryableProperty {
73 | public var path: [FieldKey] {
74 | [self.key]
75 | }
76 | }
77 |
78 | extension OptionalFieldProperty: QueryableProperty { }
79 |
80 | // MARK: Query-addressable
81 |
82 | extension OptionalFieldProperty: AnyQueryAddressableProperty {
83 | public var anyQueryableProperty: any AnyQueryableProperty { self }
84 | public var queryablePath: [FieldKey] { self.path }
85 | }
86 |
87 | extension OptionalFieldProperty: QueryAddressableProperty {
88 | public var queryableProperty: OptionalFieldProperty { self }
89 | }
90 |
91 | // MARK: Database
92 |
93 | extension OptionalFieldProperty: AnyDatabaseProperty {
94 | public var keys: [FieldKey] {
95 | [self.key]
96 | }
97 |
98 | public func input(to input: any DatabaseInput) {
99 | if input.wantsUnmodifiedKeys {
100 | input.set(self.inputValue ?? self.outputValue.map { $0.map { .bind($0) } ?? .null } ?? .default, at: self.key)
101 | } else if let inputValue = self.inputValue {
102 | input.set(inputValue, at: self.key)
103 | }
104 | }
105 |
106 | public func output(from output: any DatabaseOutput) throws {
107 | if output.contains(self.key) {
108 | self.inputValue = nil
109 | do {
110 | if try output.decodeNil(self.key) {
111 | self.outputValue = .some(nil)
112 | } else {
113 | self.outputValue = try .some(output.decode(self.key, as: Value.self))
114 | }
115 | } catch {
116 | throw FluentError.invalidField(
117 | name: self.key.description,
118 | valueType: Value.self,
119 | error: error
120 | )
121 | }
122 | }
123 | }
124 | }
125 |
126 | // MARK: Codable
127 |
128 | extension OptionalFieldProperty: AnyCodableProperty {
129 | public func encode(to encoder: any Encoder) throws {
130 | var container = encoder.singleValueContainer()
131 | try container.encode(self.wrappedValue)
132 | }
133 |
134 | public func decode(from decoder: any Decoder) throws {
135 | let container = try decoder.singleValueContainer()
136 | if container.decodeNil() {
137 | self.value = nil
138 | } else {
139 | self.value = try container.decode(Value.self)
140 | }
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Properties/TimestampFormat.swift:
--------------------------------------------------------------------------------
1 | import NIOConcurrencyHelpers
2 | import class NIOPosix.ThreadSpecificVariable
3 | import Foundation
4 |
5 | // MARK: Format
6 |
7 | public protocol TimestampFormat: Sendable {
8 | associatedtype Value: Codable & Sendable
9 |
10 | func parse(_ value: Value) -> Date?
11 | func serialize(_ date: Date) -> Value?
12 | }
13 |
14 | public struct TimestampFormatFactory {
15 | public let makeFormat: () -> Format
16 |
17 | public init(_ makeFormat: @escaping () -> Format) {
18 | self.makeFormat = makeFormat
19 | }
20 | }
21 |
22 | // MARK: Default
23 |
24 | extension TimestampFormatFactory {
25 | public static var `default`: TimestampFormatFactory {
26 | .init {
27 | DefaultTimestampFormat()
28 | }
29 | }
30 | }
31 |
32 | public struct DefaultTimestampFormat: TimestampFormat {
33 | public typealias Value = Date
34 |
35 | public func parse(_ value: Date) -> Date? {
36 | value
37 | }
38 |
39 | public func serialize(_ date: Date) -> Date? {
40 | date
41 | }
42 | }
43 |
44 |
45 | // MARK: ISO8601
46 |
47 | extension TimestampFormatFactory {
48 | public static var iso8601: TimestampFormatFactory {
49 | .iso8601(withMilliseconds: false)
50 | }
51 |
52 | public static func iso8601(
53 | withMilliseconds: Bool
54 | ) -> TimestampFormatFactory {
55 | .init {
56 | ISO8601TimestampFormat(formatter: (withMilliseconds ?
57 | ISO8601DateFormatter.sharedWithMs :
58 | ISO8601DateFormatter.sharedWithoutMs
59 | ).value)
60 | }
61 | }
62 | }
63 |
64 | extension ISO8601DateFormatter {
65 | // We use this to suppress warnings about ISO8601DateFormatter not being Sendable. It's safe to do so because we
66 | // know that in reality, the formatter is safe to use simultaneously from multiple threads as long as the options
67 | // are not changed, and we never change the options after the formatter is first created.
68 | fileprivate struct FakeSendable: @unchecked Sendable { let value: T }
69 |
70 | fileprivate static let sharedWithoutMs: FakeSendable = .init(value: .init())
71 | fileprivate static let sharedWithMs: FakeSendable = {
72 | let formatter = ISO8601DateFormatter()
73 | formatter.formatOptions.insert(.withFractionalSeconds)
74 | return .init(value: formatter)
75 | }()
76 | }
77 |
78 | public struct ISO8601TimestampFormat: TimestampFormat, @unchecked Sendable {
79 | public typealias Value = String
80 |
81 | let formatter: ISO8601DateFormatter
82 |
83 | public func parse(_ value: String) -> Date? {
84 | self.formatter.date(from: value)
85 | }
86 |
87 | public func serialize(_ date: Date) -> String? {
88 | self.formatter.string(from: date)
89 | }
90 | }
91 |
92 | // MARK: Unix
93 |
94 |
95 | extension TimestampFormatFactory {
96 | public static var unix: TimestampFormatFactory {
97 | .init {
98 | UnixTimestampFormat()
99 | }
100 | }
101 | }
102 |
103 | public struct UnixTimestampFormat: TimestampFormat {
104 | public typealias Value = Double
105 |
106 | public func parse(_ value: Double) -> Date? {
107 | Date(timeIntervalSince1970: value)
108 | }
109 |
110 | public func serialize(_ date: Date) -> Double? {
111 | date.timeIntervalSince1970
112 | }
113 | }
114 |
115 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Builder/QueryBuilder+EagerLoad.swift:
--------------------------------------------------------------------------------
1 | extension QueryBuilder: EagerLoadBuilder {
2 | public func add(loader: Loader)
3 | where Loader: EagerLoader, Loader.Model == Model
4 | {
5 | self.eagerLoaders.append(loader)
6 | }
7 | }
8 |
9 | public protocol EagerLoadBuilder {
10 | associatedtype Model: FluentKit.Model
11 | func add(loader: Loader)
12 | where Loader: EagerLoader, Loader.Model == Model
13 | }
14 |
15 |
16 | extension EagerLoadBuilder {
17 | // MARK: Eager Load
18 |
19 | @discardableResult
20 | public func with(_ relationKey: KeyPath) -> Self
21 | where Relation: EagerLoadable, Relation.From == Model
22 | {
23 | Relation.eagerLoad(relationKey, to: self)
24 | return self
25 | }
26 |
27 | @discardableResult
28 | public func with(
29 | _ throughKey: KeyPath,
30 | _ nested: (NestedEagerLoadBuilder) throws -> ()
31 | ) rethrows -> Self
32 | where Relation: EagerLoadable, Relation.From == Model
33 | {
34 | Relation.eagerLoad(throughKey, to: self)
35 | let builder = NestedEagerLoadBuilder(builder: self, throughKey)
36 | try nested(builder)
37 | return self
38 | }
39 |
40 | @discardableResult
41 | public func with(
42 | _ relationKey: KeyPath,
43 | withDeleted: Bool
44 | ) -> Self
45 | where Relation: EagerLoadable, Relation.From == Model
46 | {
47 | Relation.eagerLoad(relationKey, withDeleted: withDeleted, to: self)
48 | return self
49 | }
50 |
51 | @discardableResult
52 | public func with(
53 | _ throughKey: KeyPath,
54 | withDeleted: Bool,
55 | _ nested: (NestedEagerLoadBuilder) throws -> ()
56 | ) rethrows -> Self
57 | where Relation: EagerLoadable, Relation.From == Model
58 | {
59 | Relation.eagerLoad(throughKey, withDeleted: withDeleted, to: self)
60 | let builder = NestedEagerLoadBuilder(builder: self, throughKey)
61 | try nested(builder)
62 | return self
63 | }
64 | }
65 |
66 | public struct NestedEagerLoadBuilder: EagerLoadBuilder
67 | where Builder: EagerLoadBuilder,
68 | Relation: EagerLoadable,
69 | Builder.Model == Relation.From
70 | {
71 | public typealias Model = Relation.To
72 | let builder: Builder
73 | let relationKey: KeyPath
74 |
75 | init(builder: Builder, _ relationKey: KeyPath) {
76 | self.builder = builder
77 | self.relationKey = relationKey
78 | }
79 |
80 | public func add(loader: Loader)
81 | where Loader: EagerLoader, Loader.Model == Relation.To
82 | {
83 | Relation.eagerLoad(loader, through: self.relationKey, to: self.builder)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Builder/QueryBuilder+Filter.swift:
--------------------------------------------------------------------------------
1 | extension QueryBuilder {
2 | // MARK: Filter
3 |
4 | @discardableResult
5 | func filter(id: Model.IDValue) -> Self {
6 | if let fields = id as? any Fields {
7 | self.group(.and) { fields.input(to: QueryFilterInput(builder: $0)) }
8 | } else {
9 | self.filter(\Model._$id == id)
10 | }
11 | }
12 |
13 | @discardableResult
14 | func filter(ids: [Model.IDValue]) -> Self {
15 | guard let firstId = ids.first else { return self.limit(0) }
16 | return if firstId is any Fields {
17 | self.group(.or) { q in ids.forEach { id in q.group(.and) { (id as! any Fields).input(to: QueryFilterInput(builder: $0)) } } }
18 | } else {
19 | self.filter(\Model._$id ~~ ids)
20 | }
21 | }
22 |
23 | @discardableResult
24 | public func filter(
25 | _ field: KeyPath,
26 | _ method: DatabaseQuery.Filter.Method,
27 | _ value: Field.Value
28 | ) -> Self
29 | where Field: QueryableProperty, Field.Model == Model
30 | {
31 | self.filter(.extendedPath(
32 | Model.path(for: field),
33 | schema: Model.schemaOrAlias,
34 | space: Model.spaceIfNotAliased
35 | ), method, Field.queryValue(value))
36 | }
37 |
38 | @discardableResult
39 | public func filter(
40 | _ joined: Joined.Type,
41 | _ field: KeyPath,
42 | _ method: DatabaseQuery.Filter.Method,
43 | _ value: Field.Value
44 | ) -> Self
45 | where Joined: Schema, Field: QueryableProperty, Field.Model == Joined
46 | {
47 | self.filter(.extendedPath(
48 | Joined.path(for: field),
49 | schema: Joined.schemaOrAlias,
50 | space: Joined.spaceIfNotAliased
51 | ), method, Field.queryValue(value))
52 | }
53 |
54 | @discardableResult
55 | public func filter(
56 | _ lhsField: KeyPath,
57 | _ method: DatabaseQuery.Filter.Method,
58 | _ rhsField: KeyPath
59 | ) -> Self
60 | where Left: QueryableProperty,
61 | Left.Model == Model,
62 | Right: QueryableProperty,
63 | Right.Model == Model
64 | {
65 | self.filter(Model.path(for: lhsField), method, Model.path(for: rhsField))
66 | }
67 |
68 | @discardableResult
69 | public func filter(
70 | _ fieldName: FieldKey,
71 | _ method: DatabaseQuery.Filter.Method,
72 | _ value: Value
73 | ) -> Self
74 | where Value: Codable & Sendable
75 | {
76 | self.filter([fieldName], method, value)
77 | }
78 |
79 | @discardableResult
80 | public func filter(
81 | _ fieldPath: [FieldKey],
82 | _ method: DatabaseQuery.Filter.Method,
83 | _ value: Value
84 | ) -> Self
85 | where Value: Codable & Sendable
86 | {
87 | self.filter(
88 | .extendedPath(fieldPath, schema: Model.schemaOrAlias, space: Model.spaceIfNotAliased),
89 | method,
90 | .bind(value)
91 | )
92 | }
93 |
94 | @discardableResult
95 | public func filter(
96 | _ leftName: FieldKey,
97 | _ method: DatabaseQuery.Filter.Method,
98 | _ rightName: FieldKey
99 | ) -> Self {
100 | self.filter([leftName], method, [rightName])
101 | }
102 |
103 | @discardableResult
104 | public func filter(
105 | _ leftPath: [FieldKey],
106 | _ method: DatabaseQuery.Filter.Method,
107 | _ rightPath: [FieldKey]
108 | ) -> Self {
109 | self.filter(
110 | .extendedPath(leftPath, schema: Model.schemaOrAlias, space: Model.spaceIfNotAliased),
111 | method,
112 | .extendedPath(rightPath, schema: Model.schemaOrAlias, space: Model.spaceIfNotAliased)
113 | )
114 | }
115 |
116 | @discardableResult
117 | public func filter(
118 | _ field: DatabaseQuery.Field,
119 | _ method: DatabaseQuery.Filter.Method,
120 | _ value: DatabaseQuery.Value
121 | ) -> Self {
122 | self.filter(.value(field, method, value))
123 | }
124 |
125 | @discardableResult
126 | public func filter(
127 | _ lhsField: DatabaseQuery.Field,
128 | _ method: DatabaseQuery.Filter.Method,
129 | _ rhsField: DatabaseQuery.Field
130 | ) -> Self {
131 | self.filter(.field(lhsField, method, rhsField))
132 | }
133 |
134 | @discardableResult
135 | public func filter(_ filter: DatabaseQuery.Filter) -> Self {
136 | self.query.filters.append(filter)
137 | return self
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Builder/QueryBuilder+Group.swift:
--------------------------------------------------------------------------------
1 | extension QueryBuilder {
2 | @discardableResult
3 | public func group(
4 | _ relation: DatabaseQuery.Filter.Relation = .and,
5 | _ closure: (QueryBuilder) throws -> ()
6 | ) rethrows -> Self {
7 | let group = QueryBuilder(database: self.database)
8 | try closure(group)
9 | if !group.query.filters.isEmpty {
10 | self.query.filters.append(.group(group.query.filters, relation))
11 | }
12 | return self
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Builder/QueryBuilder+Range.swift:
--------------------------------------------------------------------------------
1 | extension QueryBuilder {
2 | // MARK: Range
3 |
4 | /// Limits the results of this query to the specified range.
5 | ///
6 | /// query.range(2..<5) // returns at most 3 results, offset by 2
7 | ///
8 | /// - returns: Query builder for chaining.
9 | @discardableResult
10 | public func range(_ range: Range) -> Self {
11 | self.range(lower: range.lowerBound, upper: range.upperBound - 1)
12 | }
13 |
14 | /// Limits the results of this query to the specified range.
15 | ///
16 | /// query.range(...5) // returns at most 6 results
17 | ///
18 | /// - returns: Query builder for chaining.
19 | @discardableResult
20 | public func range(_ range: PartialRangeThrough) -> Self {
21 | self.range(upper: range.upperBound)
22 | }
23 |
24 | /// Limits the results of this query to the specified range.
25 | ///
26 | /// query.range(..<5) // returns at most 5 results
27 | ///
28 | /// - returns: Query builder for chaining.
29 | @discardableResult
30 | public func range(_ range: PartialRangeUpTo) -> Self {
31 | self.range(upper: range.upperBound - 1)
32 | }
33 |
34 | /// Limits the results of this query to the specified range.
35 | ///
36 | /// query.range(5...) // offsets the result by 5
37 | ///
38 | /// - returns: Query builder for chaining.
39 | @discardableResult
40 | public func range(_ range: PartialRangeFrom) -> Self {
41 | self.range(lower: range.lowerBound)
42 | }
43 |
44 | /// Limits the results of this query to the specified range.
45 | ///
46 | /// query.range(2..<5) // returns at most 3 results, offset by 2
47 | ///
48 | /// - returns: Query builder for chaining.
49 | @discardableResult
50 | public func range(_ range: ClosedRange) -> Self {
51 | self.range(lower: range.lowerBound, upper: range.upperBound)
52 | }
53 |
54 | /// Limits the results of this query to the specified range.
55 | ///
56 | /// - parameters:
57 | /// - lower: Amount to offset the query by.
58 | /// - upper: `upper` - `lower` = maximum results.
59 | /// - returns: Query builder for chaining.
60 | @discardableResult
61 | public func range(lower: Int = 0, upper: Int? = nil) -> Self {
62 | self.query.offsets.append(.count(lower))
63 | upper.flatMap { upper in
64 | self.query.limits.append(.count((upper - lower) + 1))
65 | }
66 | return self
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Builder/QueryBuilder+Set.swift:
--------------------------------------------------------------------------------
1 | extension QueryBuilder {
2 | // MARK: Set
3 |
4 | @discardableResult
5 | public func set(_ data: [FieldKey: DatabaseQuery.Value]) -> Self {
6 | self.set([data])
7 | }
8 |
9 | @discardableResult
10 | public func set(_ data: [[FieldKey: DatabaseQuery.Value]]) -> Self {
11 | self.query.input = data.map {
12 | .dictionary($0)
13 | }
14 | return self
15 | }
16 |
17 | // MARK: Set
18 |
19 | @discardableResult
20 | public func set(
21 | _ field: KeyPath,
22 | to value: Field.Value
23 | ) -> Self
24 | where Field: QueryableProperty, Field.Model == Model.IDValue
25 | {
26 | if self.query.input.isEmpty {
27 | self.query.input = [.dictionary([:])]
28 | }
29 |
30 | switch self.query.input[0] {
31 | case .dictionary(var existing):
32 | let path = Model.path(for: field)
33 | assert(path.count == 1, "Set on nested properties is not yet supported.")
34 | existing[path[0]] = Field.queryValue(value)
35 | self.query.input[0] = .dictionary(existing)
36 | default:
37 | fatalError()
38 | }
39 |
40 | return self
41 | }
42 |
43 | @discardableResult
44 | public func set(
45 | _ field: KeyPath,
46 | to value: Field.Value
47 | ) -> Self
48 | where Field: QueryableProperty, Field.Model == Model
49 | {
50 | if self.query.input.isEmpty {
51 | self.query.input = [.dictionary([:])]
52 | }
53 |
54 | switch self.query.input[0] {
55 | case .dictionary(var existing):
56 | let path = Model.path(for: field)
57 | assert(path.count == 1, "Set on nested properties is not yet supported.")
58 | existing[path[0]] = Field.queryValue(value)
59 | self.query.input[0] = .dictionary(existing)
60 | default:
61 | fatalError()
62 | }
63 |
64 | return self
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Builder/QueryBuilder+Sort.swift:
--------------------------------------------------------------------------------
1 | extension QueryBuilder {
2 | // MARK: Sort
3 |
4 | @discardableResult
5 | public func sort(
6 | _ field: KeyPath,
7 | _ direction: DatabaseQuery.Sort.Direction = .ascending
8 | ) -> Self
9 | where
10 | Field: QueryableProperty,
11 | Field.Model == Model
12 | {
13 | self.sort(Model.path(for: field), direction)
14 | }
15 |
16 | @discardableResult
17 | public func sort(
18 | _ field: KeyPath>,
19 | _ direction: DatabaseQuery.Sort.Direction = .ascending
20 | ) -> Self
21 | where Field: QueryableProperty
22 | {
23 | self.sort(Model.path(for: field), direction)
24 | }
25 |
26 | @discardableResult
27 | public func sort(
28 | _ path: FieldKey,
29 | _ direction: DatabaseQuery.Sort.Direction = .ascending
30 | ) -> Self {
31 | self.sort([path], direction)
32 | }
33 |
34 | @discardableResult
35 | public func sort(
36 | _ path: [FieldKey],
37 | _ direction: DatabaseQuery.Sort.Direction = .ascending
38 | ) -> Self {
39 | self.sort(.extendedPath(path, schema: Model.schemaOrAlias, space: Model.spaceIfNotAliased), direction)
40 | }
41 |
42 | @discardableResult
43 | public func sort(
44 | _ joined: Joined.Type,
45 | _ field: KeyPath,
46 | _ direction: DatabaseQuery.Sort.Direction = .ascending,
47 | alias: String? = nil
48 | ) -> Self
49 | where
50 | Field: QueryableProperty,
51 | Field.Model == Joined,
52 | Joined: Schema
53 | {
54 | self.sort(Joined.self, Joined.path(for: field), direction, alias: alias)
55 | }
56 |
57 | @discardableResult
58 | public func sort(
59 | _ model: Joined.Type,
60 | _ path: FieldKey,
61 | _ direction: DatabaseQuery.Sort.Direction = .ascending,
62 | alias: String? = nil
63 | ) -> Self
64 | where Joined: Schema
65 | {
66 | self.sort(Joined.self, [path], direction)
67 | }
68 |
69 | @discardableResult
70 | public func sort(
71 | _ model: Joined.Type,
72 | _ path: [FieldKey],
73 | _ direction: DatabaseQuery.Sort.Direction = .ascending,
74 | alias: String? = nil
75 | ) -> Self
76 | where Joined: Schema
77 | {
78 | self.sort(.extendedPath(path, schema: Joined.schemaOrAlias, space: Joined.spaceIfNotAliased), direction)
79 | }
80 |
81 | @discardableResult
82 | public func sort(
83 | _ field: DatabaseQuery.Field,
84 | _ direction: DatabaseQuery.Sort.Direction
85 | ) -> Self {
86 | self.sort(.sort(field, direction))
87 | }
88 |
89 | @discardableResult
90 | public func sort(_ sort: DatabaseQuery.Sort) -> Self {
91 | self.query.sorts.append(sort)
92 | return self
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Database/DatabaseQuery+Action.swift:
--------------------------------------------------------------------------------
1 | extension DatabaseQuery {
2 | public enum Action: Sendable {
3 | case create
4 | case read
5 | case update
6 | case delete
7 | case aggregate(Aggregate)
8 | case custom(any Sendable)
9 | }
10 | }
11 |
12 | extension DatabaseQuery.Action: CustomStringConvertible {
13 | public var description: String {
14 | switch self {
15 | case .create:
16 | "create"
17 | case .read:
18 | "read"
19 | case .update:
20 | "update"
21 | case .delete:
22 | "delete"
23 | case .aggregate(let aggregate):
24 | "aggregate(\(aggregate))"
25 | case .custom(let custom):
26 | "custom(\(custom))"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Database/DatabaseQuery+Aggregate.swift:
--------------------------------------------------------------------------------
1 | extension DatabaseQuery {
2 | public enum Aggregate: Sendable {
3 | public enum Method: Sendable {
4 | case count
5 | case sum
6 | case average
7 | case minimum
8 | case maximum
9 | case custom(any Sendable)
10 | }
11 | case field(Field, Method)
12 | case custom(any Sendable)
13 | }
14 | }
15 |
16 | extension DatabaseQuery.Aggregate: CustomStringConvertible {
17 | public var description: String {
18 | switch self {
19 | case .field(let field, let method):
20 | "\(method)(\(field))"
21 | case .custom(let custom):
22 | "custom(\(custom))"
23 | }
24 | }
25 | }
26 |
27 | extension DatabaseQuery.Aggregate.Method: CustomStringConvertible {
28 | public var description: String {
29 | switch self {
30 | case .count:
31 | "count"
32 | case .sum:
33 | "sum"
34 | case .average:
35 | "average"
36 | case .minimum:
37 | "minimum"
38 | case .maximum:
39 | "maximum"
40 | case .custom(let custom):
41 | "custom(\(custom))"
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Database/DatabaseQuery+Field.swift:
--------------------------------------------------------------------------------
1 | extension DatabaseQuery {
2 | public enum Field: Sendable {
3 | case path([FieldKey], schema: String)
4 | case extendedPath([FieldKey], schema: String, space: String?)
5 | case custom(any Sendable)
6 | }
7 | }
8 |
9 | extension DatabaseQuery.Field: CustomStringConvertible {
10 | public var description: String {
11 | switch self {
12 | case .path(let path, let schema):
13 | "\(schema)\(path)"
14 | case .extendedPath(let path, let schema, let space):
15 | "\(space.map { "\($0)." } ?? "")\(schema)\(path)"
16 | case .custom(let custom):
17 | "custom(\(custom))"
18 | }
19 | }
20 |
21 | var describedByLoggingMetadata: Logger.MetadataValue {
22 | switch self {
23 | case .path(let array, let schema):
24 | "\(schema).\(array.map(\.description).joined(separator: "->"))"
25 | case .extendedPath(let array, let schema, let space):
26 | "\(space.map { "\($0)." } ?? "")\(schema).\(array.map(\.description).joined(separator: "->"))"
27 | case .custom:
28 | .stringConvertible(self)
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Database/DatabaseQuery+Filter.swift:
--------------------------------------------------------------------------------
1 | extension DatabaseQuery {
2 | public enum Filter: Sendable {
3 | public enum Method: Sendable {
4 | public static var equal: Method {
5 | .equality(inverse: false)
6 | }
7 |
8 | public static var notEqual: Method {
9 | .equality(inverse: true)
10 | }
11 |
12 | public static var greaterThan: Method {
13 | .order(inverse: false, equality: false)
14 | }
15 |
16 | public static var greaterThanOrEqual: Method {
17 | .order(inverse: false, equality: true)
18 | }
19 |
20 | public static var lessThan: Method {
21 | .order(inverse: true, equality: false)
22 | }
23 |
24 | public static var lessThanOrEqual: Method {
25 | .order(inverse: true, equality: true)
26 | }
27 |
28 | /// LHS is equal/not equal to RHS
29 | case equality(inverse: Bool)
30 |
31 | /// LHS is greater/less than [or equal to] RHS
32 | case order(inverse: Bool, equality: Bool)
33 |
34 | /// LHS exists in/doesn't exist in RHS
35 | case subset(inverse: Bool)
36 |
37 | public enum Contains: Sendable {
38 | case prefix
39 | case suffix
40 | case anywhere
41 | }
42 |
43 | /// RHS is [anchored] substring/isn't [anchored] substring of LHS
44 | case contains(inverse: Bool, Contains)
45 |
46 | /// Custom method
47 | case custom(any Sendable)
48 | }
49 |
50 | public enum Relation: Sendable {
51 | case and
52 | case or
53 | case custom(any Sendable)
54 | }
55 |
56 | case value(Field, Method, Value)
57 | case field(Field, Method, Field)
58 | case group([Filter], Relation)
59 | case custom(any Sendable)
60 | }
61 | }
62 |
63 | extension DatabaseQuery.Filter: CustomStringConvertible {
64 | public var description: String {
65 | switch self {
66 | case .value(let field, let method, let value):
67 | "\(field) \(method) \(value)"
68 | case .field(let fieldA, let method, let fieldB):
69 | "\(fieldA) \(method) \(fieldB)"
70 | case .group(let filters, let relation):
71 | filters.map { "(\($0))" }.joined(separator: " \(relation) ")
72 | case .custom(let any):
73 | "custom(\(any))"
74 | }
75 | }
76 |
77 | var describedByLoggingMetadata: Logger.MetadataValue {
78 | switch self {
79 | case .value(let field, let method, let value):
80 | ["field": field.describedByLoggingMetadata, "method": "\(method)", "value": value.describedByLoggingMetadata]
81 | case .field(let field, let method, let field2):
82 | ["field1": field.describedByLoggingMetadata, "method": "\(method)", "field2": field2.describedByLoggingMetadata]
83 | case .group(let array, let relation):
84 | ["group": .array(array.map(\.describedByLoggingMetadata)), "relation": "\(relation)"]
85 | case .custom:
86 | .stringConvertible(self)
87 | }
88 | }
89 | }
90 |
91 | extension DatabaseQuery.Filter.Method: CustomStringConvertible {
92 | public var description: String {
93 | switch self {
94 | case .equality(let inverse):
95 | inverse ? "!=" : "="
96 | case .order(let inverse, let equality):
97 | if equality {
98 | inverse ? "<=" : ">="
99 | } else {
100 | inverse ? "<" : ">"
101 | }
102 | case .subset(let inverse):
103 | inverse ? "!~~" : "~~"
104 | case .contains(let inverse, let contains):
105 | inverse ? "!\(contains)" : "\(contains)"
106 | case .custom(let any):
107 | "custom(\(any))"
108 | }
109 | }
110 | }
111 |
112 | extension DatabaseQuery.Filter.Method.Contains: CustomStringConvertible {
113 | public var description: String {
114 | switch self {
115 | case .prefix:
116 | "startswith"
117 | case .suffix:
118 | "endswith"
119 | case .anywhere:
120 | "contains"
121 | }
122 | }
123 | }
124 |
125 | extension DatabaseQuery.Filter.Relation: CustomStringConvertible {
126 | public var description: String {
127 | switch self {
128 | case .and:
129 | "and"
130 | case .or:
131 | "or"
132 | case .custom(let custom):
133 | "custom(\(custom))"
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Database/DatabaseQuery+Join.swift:
--------------------------------------------------------------------------------
1 | extension DatabaseQuery {
2 | public enum Join: Sendable {
3 | public enum Method: Sendable {
4 | case inner
5 | case left
6 | case custom(any Sendable)
7 | }
8 |
9 | case join(
10 | schema: String,
11 | alias: String?,
12 | Method,
13 | foreign: Field,
14 | local: Field
15 | )
16 |
17 | case extendedJoin(
18 | schema: String,
19 | space: String?,
20 | alias: String?,
21 | Method,
22 | foreign: Field,
23 | local: Field
24 | )
25 |
26 | case advancedJoin(
27 | schema: String,
28 | space: String?,
29 | alias: String?,
30 | Method,
31 | filters: [Filter]
32 | )
33 |
34 | case custom(any Sendable)
35 | }
36 | }
37 |
38 | extension DatabaseQuery.Join: CustomStringConvertible {
39 | public var description: String {
40 | switch self {
41 | case .join(let schema, let alias, let method, let foreign, let local):
42 | "\(self.schemaDescription(schema: schema, alias: alias)) \(method) on \(foreign) == \(local)"
43 |
44 | case .extendedJoin(let schema, let space, let alias, let method, let foreign, let local):
45 | "\(self.schemaDescription(space: space, schema: schema, alias: alias)) \(method) on \(foreign) == \(local)"
46 |
47 | case .advancedJoin(let schema, let space, let alias, let method, let filters):
48 | "\(self.schemaDescription(space: space, schema: schema, alias: alias)) \(method) on \(filters)"
49 |
50 | case .custom(let custom):
51 | "custom(\(custom))"
52 | }
53 | }
54 |
55 | var describedByLoggingMetadata: Logger.MetadataValue {
56 | switch self {
57 | case .join(let schema, let alias, let method, let foreign, let local):
58 | .dictionary([
59 | "schema": "\(schema)", "alias": alias.map { "\($0)" }, "method": "\(method)",
60 | "foreign": foreign.describedByLoggingMetadata, "local": local.describedByLoggingMetadata
61 | ].compactMapValues { $0 })
62 | case .extendedJoin(let schema, let space, let alias, let method, let foreign, let local):
63 | .dictionary([
64 | "schema": "\(schema)", "space": space.map { "\($0)" }, "alias": alias.map { "\($0)" }, "method": "\(method)",
65 | "foreign": foreign.describedByLoggingMetadata, "local": local.describedByLoggingMetadata
66 | ].compactMapValues { $0 })
67 | case .advancedJoin(let schema, let space, let alias, let method, let filters):
68 | .dictionary([
69 | "schema": "\(schema)", "space": space.map { "\($0)" }, "alias": alias.map { "\($0)" }, "method": "\(method)",
70 | "filters": .array(filters.map(\.describedByLoggingMetadata))
71 | ].compactMapValues { $0 })
72 | case .custom:
73 | .stringConvertible(self)
74 | }
75 | }
76 |
77 | private func schemaDescription(space: String? = nil, schema: String, alias: String?) -> String {
78 | [space, schema].compactMap({ $0 }).joined(separator: ".") + (alias.map { " as \($0)" } ?? "")
79 | }
80 | }
81 |
82 | extension DatabaseQuery.Join.Method: CustomStringConvertible {
83 | public var description: String {
84 | switch self {
85 | case .inner:
86 | "inner"
87 | case .left:
88 | "left"
89 | case .custom(let custom):
90 | "custom(\(custom))"
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Database/DatabaseQuery+Range.swift:
--------------------------------------------------------------------------------
1 | extension DatabaseQuery {
2 | public enum Limit: Sendable {
3 | case count(Int)
4 |
5 | /// Due to design limitations in SQLKit, it is not possible to provide a custom limit expression;
6 | /// as such, it is in turn a design flaw of FluentKit that this enum exists at all. Any attempt to
7 | /// use ``custom(_:)`` in a query unconditionally triggers a fatal error at runtime.
8 | @available(*, deprecated, message: "`DatabaseQuery.Limit.custom(_:)` does not work.")
9 | case custom(any Sendable)
10 | }
11 |
12 | public enum Offset: Sendable {
13 | case count(Int)
14 |
15 | /// Due to design limitations in SQLKit, it is not possible to provide a custom offset expression;
16 | /// as such, it is in turn a design flaw of FluentKit that this enum exists at all. Any attempt to
17 | /// use ``custom(_:)`` in a query unconditionally triggers a fatal error at runtime.
18 | @available(*, deprecated, message: "`DatabaseQuery.Offset.custom(_:)` triggers a fatal error when used.")
19 | case custom(any Sendable)
20 | }
21 | }
22 |
23 | extension DatabaseQuery.Limit: CustomStringConvertible {
24 | public var description: String {
25 | switch self {
26 | case .count(let count):
27 | "count(\(count))"
28 | case .custom(let custom):
29 | "custom(\(custom))"
30 | }
31 | }
32 | }
33 |
34 | extension DatabaseQuery.Limit {
35 | var describedByLoggingMetadata: Logger.MetadataValue {
36 | switch self {
37 | case .count(let count): .stringConvertible(count)
38 | default: "custom"
39 | }
40 | }
41 | }
42 |
43 | extension DatabaseQuery.Offset: CustomStringConvertible {
44 | public var description: String {
45 | switch self {
46 | case .count(let count):
47 | "count(\(count))"
48 | case .custom(let custom):
49 | "custom(\(custom))"
50 | }
51 | }
52 | }
53 |
54 | extension DatabaseQuery.Offset {
55 | var describedByLoggingMetadata: Logger.MetadataValue {
56 | switch self {
57 | case .count(let count): .stringConvertible(count)
58 | default: "custom"
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Database/DatabaseQuery+Sort.swift:
--------------------------------------------------------------------------------
1 | extension DatabaseQuery {
2 | public enum Sort: Sendable {
3 | public enum Direction: Sendable {
4 | case ascending
5 | case descending
6 | case custom(any Sendable)
7 | }
8 | case sort(Field, Direction)
9 | case custom(any Sendable)
10 | }
11 | }
12 |
13 | extension DatabaseQuery.Sort: CustomStringConvertible {
14 | public var description: String {
15 | switch self {
16 | case .sort(let field, let direction):
17 | "\(field) \(direction)"
18 | case .custom(let custom):
19 | "custom(\(custom))"
20 | }
21 | }
22 |
23 | var describedByLoggingMetadata: Logger.MetadataValue {
24 | switch self {
25 | case .sort(let field, let direction):
26 | ["field": field.describedByLoggingMetadata, "direction": "\(direction)"]
27 | case .custom:
28 | .stringConvertible(self)
29 | }
30 | }
31 | }
32 |
33 | extension DatabaseQuery.Sort.Direction: CustomStringConvertible {
34 | public var description: String {
35 | switch self {
36 | case .ascending:
37 | "ascending"
38 | case .descending:
39 | "descending"
40 | case .custom(let custom):
41 | "custom(\(custom))"
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Database/DatabaseQuery+Value.swift:
--------------------------------------------------------------------------------
1 | extension DatabaseQuery {
2 | public enum Value: Sendable {
3 | case bind(any Encodable & Sendable)
4 | case dictionary([FieldKey: Value])
5 | case array([Value])
6 | case null
7 | case enumCase(String)
8 | case `default`
9 | case custom(any Sendable)
10 | }
11 | }
12 |
13 | extension DatabaseQuery.Value: CustomStringConvertible {
14 | public var description: String {
15 | switch self {
16 | case .bind(let encodable):
17 | if let convertible = encodable as? any CustomDebugStringConvertible {
18 | String(reflecting: convertible)
19 | } else {
20 | String(describing: encodable)
21 | }
22 | case .dictionary(let dictionary):
23 | String(describing: dictionary)
24 | case .array(let array):
25 | String(describing: array)
26 | case .enumCase(let string):
27 | string
28 | case .null:
29 | "nil"
30 | case .default:
31 | "default"
32 | case .custom(let custom):
33 | "custom(\(custom))"
34 | }
35 | }
36 |
37 | var describedByLoggingMetadata: Logger.MetadataValue {
38 | switch self {
39 | case .bind(let encodable):
40 | // N.B.: `is` is used instead of `as?` here because the latter irreversibly loses the `Sendable`-ness of the value.
41 | if encodable is any CustomStringConvertible {
42 | .stringConvertible(encodable as! any CustomStringConvertible & Sendable)
43 | } else {
44 | .string(String(describing: encodable))
45 | }
46 | case .dictionary(let d):
47 | .dictionary(.init(uniqueKeysWithValues: d.map { ($0.description, $1.describedByLoggingMetadata) }))
48 | case .array(let a):
49 | .array(a.map(\.describedByLoggingMetadata))
50 | case .enumCase(let string):
51 | .string(string)
52 | case .null:
53 | "nil"
54 | case .default:
55 | ""
56 | default:
57 | .stringConvertible(self)
58 | }
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/Database/DatabaseQuery.swift:
--------------------------------------------------------------------------------
1 | public struct DatabaseQuery: Sendable {
2 | public var schema: String
3 | public var space: String?
4 | public var customIDKey: FieldKey?
5 | public var isUnique: Bool
6 | public var fields: [Field]
7 | public var action: Action
8 | public var filters: [Filter]
9 | public var input: [Value]
10 | public var joins: [Join]
11 | public var sorts: [Sort]
12 | public var limits: [Limit]
13 | public var offsets: [Offset]
14 |
15 | init(schema: String, space: String? = nil) {
16 | self.schema = schema
17 | self.space = space
18 | self.isUnique = false
19 | self.fields = []
20 | self.action = .read
21 | self.filters = []
22 | self.input = []
23 | self.joins = []
24 | self.sorts = []
25 | self.limits = []
26 | self.offsets = []
27 | }
28 | }
29 |
30 | extension DatabaseQuery: CustomStringConvertible {
31 | public var description: String {
32 | var parts = [
33 | "query",
34 | "\(self.action)",
35 | "\(self.space.map { "\($0)." } ?? "")\(self.schema)",
36 | ]
37 | if self.isUnique {
38 | parts.append("unique")
39 | }
40 | if !self.fields.isEmpty {
41 | parts.append("fields=\(self.fields)")
42 | }
43 | if !self.joins.isEmpty {
44 | parts.append("joins=\(self.joins)")
45 | }
46 | if !self.filters.isEmpty {
47 | parts.append("filters=\(self.filters)")
48 | }
49 | if !self.input.isEmpty {
50 | parts.append("input=\(self.input)")
51 | }
52 | if !self.sorts.isEmpty {
53 | parts.append("sorts=\(self.sorts)")
54 | }
55 | if !self.limits.isEmpty {
56 | parts.append("limits=\(self.limits)")
57 | }
58 | if !self.offsets.isEmpty {
59 | parts.append("offsets=\(self.offsets)")
60 | }
61 | return parts.joined(separator: " ")
62 | }
63 |
64 | var describedByLoggingMetadata: Logger.Metadata {
65 | var result: Logger.Metadata = [
66 | "action": "\(self.action)",
67 | "schema": "\(self.space.map { "\($0)." } ?? "")\(self.schema)",
68 | ]
69 | switch self.action {
70 | case .create, .update, .custom: result["input"] = .array(self.input.map(\.describedByLoggingMetadata))
71 | default: break
72 | }
73 | switch self.action {
74 | case .read, .aggregate, .custom:
75 | result["unique"] = .stringConvertible(self.isUnique)
76 | result["fields"] = .array(self.fields.map(\.describedByLoggingMetadata))
77 | result["joins"] = .array(self.joins.map(\.describedByLoggingMetadata))
78 | fallthrough
79 | case .update, .delete:
80 | result["filters"] = .array(self.filters.map(\.describedByLoggingMetadata))
81 | result["sorts"] = .array(self.sorts.map(\.describedByLoggingMetadata))
82 | result["limits"] = .array(self.limits.map(\.describedByLoggingMetadata))
83 | result["offsets"] = .array(self.offsets.map(\.describedByLoggingMetadata))
84 | default: break
85 | }
86 | return result
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Query/QueryHistory.swift:
--------------------------------------------------------------------------------
1 | import struct NIOConcurrencyHelpers.NIOLockedValueBox
2 |
3 | /// Holds the history of queries for a database.
4 | public final class QueryHistory: @unchecked Sendable {
5 | /// The underlying (locked) storage.
6 | private let _queries: NIOLockedValueBox<[DatabaseQuery]> = .init([])
7 |
8 | /// The queries that have been executed.
9 | ///
10 | /// > Warning: This array can be modified aribtrarily by any code with access to the ``QueryHistory``
11 | /// > object; there is no guarantee that it represents a consistent and accurate history. This is an
12 | /// > accidental design flaw that can't be changed now without breaking the API.
13 | public var queries: [DatabaseQuery] {
14 | get { self._queries.withLockedValue { $0 } }
15 | set { self._queries.withLockedValue { $0 = newValue } }
16 | }
17 |
18 | /// Create a new ``QueryHistory`` with no existing history.
19 | public init() {}
20 |
21 | /// Add a query to the history.
22 | func add(_ query: DatabaseQuery) {
23 | self._queries.withLockedValue { $0.append(query) }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Utilities/OptionalType.swift:
--------------------------------------------------------------------------------
1 | public protocol AnyOptionalType {
2 | static var wrappedType: Any.Type { get }
3 | static var `nil`: Any { get }
4 | var wrappedValue: Any? { get }
5 | }
6 |
7 | public protocol OptionalType: AnyOptionalType {
8 | associatedtype Wrapped
9 | init(_ wrapped: Wrapped)
10 | }
11 |
12 | extension OptionalType {
13 | public static var wrappedType: Any.Type {
14 | Wrapped.self
15 | }
16 | }
17 |
18 | extension Optional: OptionalType {
19 | public static var `nil`: Any {
20 | Self.none as Any
21 | }
22 |
23 | public var wrappedValue: Any? {
24 | self
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Utilities/RandomGeneratable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol RandomGeneratable {
4 | static func generateRandom() -> Self
5 | }
6 |
7 | extension UUID: RandomGeneratable {
8 | public static func generateRandom() -> UUID {
9 | .init()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Utilities/SomeCodingKey.swift:
--------------------------------------------------------------------------------
1 | import SQLKit
2 |
3 | extension SQLKit.SomeCodingKey {
4 | public var description: String {
5 | "SomeCodingKey(\"\(self.stringValue)\"\(self.intValue.map { ", int: \($0)" } ?? ""))"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/FluentKit/Utilities/UnsafeMutableTransferBox.swift:
--------------------------------------------------------------------------------
1 | @usableFromInline
2 | final class UnsafeMutableTransferBox: @unchecked Sendable {
3 | @usableFromInline
4 | var wrappedValue: Wrapped
5 |
6 | @inlinable
7 | init(_ wrappedValue: Wrapped) {
8 | self.wrappedValue = wrappedValue
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/FluentSQL/ConverterUtilities.swift:
--------------------------------------------------------------------------------
1 | import SQLKit
2 |
3 | func custom(_ any: Any) -> any SQLExpression {
4 | if let sql = any as? any SQLExpression {
5 | sql
6 | } else if let string = any as? String {
7 | SQLRaw(string)
8 | } else if let stringConvertible = any as? any CustomStringConvertible {
9 | SQLRaw(stringConvertible.description)
10 | } else {
11 | fatalError("Could not convert \(any) to a SQL-compatible type.")
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/FluentSQL/Docs.docc/Resources/vapor-fluentkit-logo.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/Sources/FluentSQL/Docs.docc/index.md:
--------------------------------------------------------------------------------
1 | # ``FluentSQL``
2 |
3 | FluentSQL is a module containing the logic which maps FluentKit queries, schemata, models, and values to and from [SQLKit] types.
4 |
5 | [SQLKit]: https://api.vapor.codes/sqlkit/documentation/sqlkit
6 |
--------------------------------------------------------------------------------
/Sources/FluentSQL/Docs.docc/theme-settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme": {
3 | "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" },
4 | "border-radius": "0",
5 | "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
6 | "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
7 | "color": {
8 | "fluentkit": { "dark": "hsl(200, 75%, 85%)", "light": "hsl(200, 75%, 75%)" },
9 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-fluentkit) 30%, #000 100%)",
10 | "documentation-intro-accent": "var(--color-fluentkit)",
11 | "documentation-intro-eyebrow": "white",
12 | "documentation-intro-figure": "white",
13 | "documentation-intro-title": "white",
14 | "logo-base": { "dark": "#fff", "light": "#000" },
15 | "logo-shape": { "dark": "#000", "light": "#fff" },
16 | "fill": { "dark": "#000", "light": "#fff" }
17 | },
18 | "icons": { "technology": "/fluentsql/images/FluentSQL/vapor-fluentkit-logo.svg" }
19 | },
20 | "features": {
21 | "quickNavigation": { "enable": true },
22 | "i18n": { "enable": true }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/FluentSQL/Exports.swift:
--------------------------------------------------------------------------------
1 | @_documentation(visibility: internal) @_exported import FluentKit
2 | @_documentation(visibility: internal) @_exported import SQLKit
3 |
--------------------------------------------------------------------------------
/Sources/FluentSQL/SQLDatabase+Model+Concurrency.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 | import SQLKit
3 | import FluentKit
4 |
5 | extension SQLQueryFetcher {
6 | public func first(decodingFluent model: Model.Type) async throws -> Model? {
7 | try await self.first(decodingFluent: Model.self).get()
8 | }
9 |
10 | @available(*, deprecated, renamed: "first(decodingFluent:)", message: "Renamed to first(decodingFluent:)")
11 | public func first(decoding: Model.Type) async throws -> Model? {
12 | try await self.first(decodingFluent: Model.self)
13 | }
14 |
15 | public func all(decodingFluent: Model.Type) async throws -> [Model] {
16 | try await self.all(decodingFluent: Model.self).get()
17 | }
18 |
19 | @available(*, deprecated, renamed: "all(decodingFluent:)", message: "Renamed to all(decodingFluent:)")
20 | public func all(decoding: Model.Type) async throws -> [Model] {
21 | try await self.all(decodingFluent: Model.self)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/FluentSQL/SQLJSONColumnPath+Deprecated.swift:
--------------------------------------------------------------------------------
1 | import SQLKit
2 | import FluentKit
3 |
4 | /// A thin deprecated wrapper around `SQLNestedSubpathExpression`.
5 | @available(*, deprecated, message: "Replaced by `SQLNestedSubpathExpression` in SQLKit")
6 | public struct SQLJSONColumnPath: SQLExpression {
7 | private var realExpression: SQLNestedSubpathExpression
8 |
9 | public var column: String {
10 | get { (self.realExpression.column as? SQLIdentifier)?.string ?? "" }
11 | set { self.realExpression.column = SQLIdentifier(newValue) }
12 | }
13 |
14 | public var path: [String] {
15 | get { self.realExpression.path }
16 | set { self.realExpression.path = newValue }
17 | }
18 |
19 | public init(column: String, path: [String]) {
20 | self.realExpression = .init(column: column, path: path)
21 | }
22 |
23 | public func serialize(to serializer: inout SQLSerializer) {
24 | self.realExpression.serialize(to: &serializer)
25 | }
26 | }
27 |
28 | extension DatabaseQuery.Field {
29 | public static func sql(json column: String, _ path: String...) -> Self {
30 | .sql(json: column, path)
31 | }
32 |
33 | public static func sql(json column: String, _ path: [String]) -> Self {
34 | .sql(SQLNestedSubpathExpression(column: column, path: path))
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/FluentSQL/SQLList+Deprecated.swift:
--------------------------------------------------------------------------------
1 | import SQLKit
2 |
3 | /// This file provides a few extensions to SQLKit's `SQLList` which have the effect of mimicking
4 | /// the public API which was previously provided by a nearly-identical type of the same name in
5 | /// this module. The slightly differing behavior of the Fluent version had a tendency to cause
6 | /// confusion when both `FluentSQL` and `SQLKit` were imported in the same context; as such, the
7 | /// Fluent version was removed. To avoid breaking API that has been publicly available for a long
8 | /// time (no matter how incorrectly so), these deprecated extensions make the semantics of the removed
9 | /// Fluent implementation available. Whether the original or alternate serialization behavior is used
10 | /// is based on which initializer is used. The original SQLKit initializer, ``init(_:separator:)`` (or
11 | /// ``init(_:)``, taking the default value for the separator), gives the original and intended behavior
12 | /// (see `SQLList` for further details). The convenience intializer, ``init(items:separator:)``,
13 | /// enables the deprecated alternate behavior, which adds a space character before and after the separator.
14 | ///
15 | /// Examples:
16 | ///
17 | /// Expressions: [1, 2, 3, 4, 5]
18 | /// Separator: "AND"
19 | /// Original serialization: 1AND2AND3AND4AND5
20 | /// Alternate serialization: 1 AND 2 AND 3 AND 4 AND 5
21 | ///
22 | /// Expressions: [1, 2, 3, 4, 5]
23 | /// Separator: ", "
24 | /// Original serialization: 1, 2, 3, 4, 5
25 | /// Alternate serialization: 1 , 2 , 3 , 4 , 5
26 | ///
27 | /// > Warning: These extensions are not recommended, as it was never intended for this behavior to be
28 | /// > public. Convert code using these extensions to invoke the original `SQLList` directly.
29 | extension SQLKit.SQLList {
30 | @available(*, deprecated, message: "Use `expressions` instead.")
31 | public var items: [any SQLExpression] {
32 | get { self.expressions }
33 | set { self.expressions = newValue }
34 | }
35 |
36 | @available(*, deprecated, message: "Use `init(_:separator:)` and include whitespace in the separator as needed instead.")
37 | public init(items: [any SQLExpression], separator: any SQLExpression) {
38 | self.init(items, separator: " \(separator) " as SQLQueryString)
39 | }
40 | }
41 |
42 | @available(*, deprecated, message: "Import `SQLList` from the SQLKit module instead.")
43 | public typealias SQLList = SQLKit.SQLList
44 |
--------------------------------------------------------------------------------
/Sources/FluentSQL/SQLQualifiedTable+Deprecated.swift:
--------------------------------------------------------------------------------
1 | import SQLKit
2 |
3 | /// Formerly a complete implementation of a qualified table SQL expression, now an alias for the equivalent
4 | /// type available in SQLKit.
5 | @available(*, deprecated, message: "This type is now provided by SQLKit; use that module's version.")
6 | public typealias SQLQualifiedTable = SQLKit.SQLQualifiedTable
7 |
--------------------------------------------------------------------------------
/Sources/XCTFluent/Docs.docc/Resources/vapor-fluentkit-logo.svg:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/Sources/XCTFluent/Docs.docc/index.md:
--------------------------------------------------------------------------------
1 | # ``XCTFluent``
2 |
3 | XCTFluent provides XCTest extensions to make it easy to write tests using FluentKit.
4 |
--------------------------------------------------------------------------------
/Sources/XCTFluent/Docs.docc/theme-settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme": {
3 | "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" },
4 | "border-radius": "0",
5 | "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
6 | "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
7 | "color": {
8 | "fluentkit": { "dark": "hsl(200, 75%, 85%)", "light": "hsl(200, 75%, 75%)" },
9 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-fluentkit) 30%, #000 100%)",
10 | "documentation-intro-accent": "var(--color-fluentkit)",
11 | "documentation-intro-eyebrow": "white",
12 | "documentation-intro-figure": "white",
13 | "documentation-intro-title": "white",
14 | "logo-base": { "dark": "#fff", "light": "#000" },
15 | "logo-shape": { "dark": "#000", "light": "#fff" },
16 | "fill": { "dark": "#000", "light": "#fff" }
17 | },
18 | "icons": { "technology": "/xctfluent/images/XCTFluent/vapor-fluentkit-logo.svg" }
19 | },
20 | "features": {
21 | "quickNavigation": { "enable": true },
22 | "i18n": { "enable": true }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Tests/FluentKitTests/OptionalFieldQueryTests.swift:
--------------------------------------------------------------------------------
1 | import FluentKit
2 | import FluentSQL
3 | import Foundation
4 | import XCTest
5 |
6 | final class OptionalFieldQueryTests: DbQueryTestCase {
7 | func testInsertNonNull() throws {
8 | db.fakedRows.append([.init(["id": UUID()])])
9 | _ = try Thing(id: 1, name: "Jared").create(on: db).wait()
10 | assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, $2)"#)
11 | }
12 |
13 | func testInsertNull() throws {
14 | db.fakedRows.append([.init(["id": UUID()])])
15 | _ = try Thing(id: 1, name: nil).create(on: db).wait()
16 | assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, NULL)"#)
17 | }
18 |
19 | func testInsertAfterMutatingNullableField() throws {
20 | let thing = Thing(id: 1, name: nil)
21 | thing.name = "Jared"
22 | db.fakedRows.append([.init(["id": UUID()])])
23 | _ = try thing.create(on: db).wait()
24 | assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, $2)"#)
25 |
26 | db.reset()
27 |
28 | let thing2 = Thing(id: 1, name: "Jared")
29 | thing2.name = nil
30 | db.fakedRows.append([.init(["id": UUID()])])
31 | _ = try thing2.create(on: db).wait()
32 | assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, NULL)"#)
33 | }
34 |
35 | func testSaveReplacingNonNull() throws {
36 | let thing = Thing(id: 1, name: "Jared")
37 | db.fakedRows.append([.init(["id": UUID()])])
38 | _ = try thing.create(on: db).wait()
39 | thing.name = "Bob"
40 | _ = try thing.save(on: db).wait()
41 | assertLastQuery(db, #"UPDATE "things" SET "name" = $1 WHERE "things"."id" = $2"#)
42 | }
43 |
44 | func testSaveReplacingNull() throws {
45 | let thing = Thing(id: 1, name: nil)
46 | db.fakedRows.append([.init(["id": UUID()])])
47 | _ = try thing.create(on: db).wait()
48 | thing.name = "Bob"
49 | _ = try thing.save(on: db).wait()
50 | assertLastQuery(db, #"UPDATE "things" SET "name" = $1 WHERE "things"."id" = $2"#)
51 | }
52 |
53 | func testSaveNullReplacingNonNull() throws {
54 | let thing = Thing(id: 1, name: "Jared")
55 | db.fakedRows.append([.init(["id": UUID()])])
56 | _ = try thing.create(on: db).wait()
57 | thing.name = nil
58 | _ = try thing.save(on: db).wait()
59 | assertLastQuery(db, #"UPDATE "things" SET "name" = NULL WHERE "things"."id" = $1"#)
60 | }
61 |
62 | func testBulkInsertWithoutNulls() throws {
63 | let things = [Thing(id: 1, name: "Jared"), Thing(id: 2, name: "Bob")]
64 | db.fakedRows.append([.init(["id": UUID()])])
65 | _ = try things.create(on: db).wait()
66 | assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, $2), ($3, $4)"#)
67 | }
68 |
69 | func testBulkInsertWithOnlyNulls() throws {
70 | let things = [Thing(id: 1, name: nil), Thing(id: 2, name: nil)]
71 | db.fakedRows.append([.init(["id": UUID()])])
72 | _ = try things.create(on: db).wait()
73 | assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, NULL), ($2, NULL)"#)
74 | }
75 |
76 | func testBulkInsertWithMixedNulls() throws {
77 | let things = [Thing(id: 1, name: "Jared"), Thing(id: 2, name: nil)]
78 | db.fakedRows.append([.init(["id": UUID()])])
79 | _ = try things.create(on: db).wait()
80 | assertQuery(db, #"INSERT INTO "things" ("id", "name") VALUES ($1, $2), ($3, NULL)"#)
81 | }
82 | }
83 |
84 | private final class Thing: Model, @unchecked Sendable {
85 | static let schema = "things"
86 |
87 | @ID(custom: "id", generatedBy: .user)
88 | var id: Int?
89 |
90 | @OptionalField(key: "name")
91 | var name: String?
92 |
93 | init() {}
94 |
95 | init(id: Int, name: String? = nil) {
96 | self.id = id
97 | self.name = name
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Tests/FluentKitTests/TestUtilities.swift:
--------------------------------------------------------------------------------
1 | import Logging
2 | import SQLKit
3 | import XCTest
4 |
5 | class DbQueryTestCase: XCTestCase {
6 | var db = DummyDatabaseForTestSQLSerializer()
7 |
8 | override class func setUp() {
9 | super.setUp()
10 | XCTAssertTrue(isLoggingConfigured)
11 | }
12 |
13 | override func setUp() {
14 | self.db = DummyDatabaseForTestSQLSerializer()
15 | }
16 |
17 | override func tearDown() {
18 | self.db.reset()
19 | }
20 | }
21 |
22 | func assertQuery(
23 | _ db: DummyDatabaseForTestSQLSerializer,
24 | _ query: String,
25 | file: StaticString = #filePath,
26 | line: UInt = #line
27 | ) {
28 | XCTAssertEqual(db.sqlSerializers.count, 1, file: file, line: line)
29 | XCTAssertEqual(db.sqlSerializers.first?.sql, query, file: file, line: line)
30 | }
31 |
32 | func assertLastQuery(
33 | _ db: DummyDatabaseForTestSQLSerializer,
34 | _ query: String,
35 | file: StaticString = #filePath,
36 | line: UInt = #line
37 | ) {
38 | XCTAssertEqual(db.sqlSerializers.last?.sql, query, file: file, line: line)
39 | }
40 |
41 | func env(_ name: String) -> String? {
42 | ProcessInfo.processInfo.environment[name]
43 | }
44 |
45 | let isLoggingConfigured: Bool = {
46 | LoggingSystem.bootstrap {
47 | var handler = StreamLogHandler.standardOutput(label: $0)
48 | handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .info
49 | return handler
50 | }
51 | return true
52 | }()
53 |
--------------------------------------------------------------------------------