diff --git a/frameworks/Swift/vapor/Package.resolved b/frameworks/Swift/vapor/Package.resolved
index a6e676b873d..ede714a04e4 100644
--- a/frameworks/Swift/vapor/Package.resolved
+++ b/frameworks/Swift/vapor/Package.resolved
@@ -37,6 +37,24 @@
"version": "1.0.1"
}
},
+ {
+ "package": "Fluent",
+ "repositoryURL": "https://github.com/vapor/fluent.git",
+ "state": {
+ "branch": null,
+ "revision": "8f22ef2dfeb278179a2d6e91769a5c1ceccf31ef",
+ "version": "3.0.0-rc.2.4.1"
+ }
+ },
+ {
+ "package": "FluentMySQL",
+ "repositoryURL": "https://github.com/vapor/fluent-mysql.git",
+ "state": {
+ "branch": null,
+ "revision": "19956a04b1983f20879d9c770a49d7948c42b270",
+ "version": "3.0.0-rc.2.4"
+ }
+ },
{
"package": "HTTP",
"repositoryURL": "https://github.com/vapor/http.git",
@@ -46,6 +64,15 @@
"version": "3.0.5"
}
},
+ {
+ "package": "Leaf",
+ "repositoryURL": "https://github.com/vapor/leaf.git",
+ "state": {
+ "branch": null,
+ "revision": "a71cb0cd29ec88ee72baa00668c05e327114f27c",
+ "version": "3.0.0-rc.2.2"
+ }
+ },
{
"package": "Multipart",
"repositoryURL": "https://github.com/vapor/multipart.git",
@@ -55,6 +82,15 @@
"version": "3.0.1"
}
},
+ {
+ "package": "MySQL",
+ "repositoryURL": "https://github.com/vapor/mysql.git",
+ "state": {
+ "branch": null,
+ "revision": "cda473257572c8e02706d0c65fd145765c5a414d",
+ "version": "3.0.0-rc.2.2"
+ }
+ },
{
"package": "Routing",
"repositoryURL": "https://github.com/vapor/routing.git",
@@ -73,6 +109,15 @@
"version": "1.0.0"
}
},
+ {
+ "package": "SQL",
+ "repositoryURL": "https://github.com/vapor/sql.git",
+ "state": {
+ "branch": null,
+ "revision": "1bb94271d8645b6d6983721080ad642aa35d749c",
+ "version": "1.0.0"
+ }
+ },
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio.git",
diff --git a/frameworks/Swift/vapor/Package.swift b/frameworks/Swift/vapor/Package.swift
index 42871ee11ef..2fa7b5a6a43 100644
--- a/frameworks/Swift/vapor/Package.swift
+++ b/frameworks/Swift/vapor/Package.swift
@@ -5,8 +5,11 @@ let package = Package(
name: "tfb",
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"),
+ .package(url: "https://github.com/vapor/fluent-mysql.git", from: "3.0.0-rc"),
+ .package(url: "https://github.com/vapor/leaf.git", from: "3.0.0-rc.1")
],
targets: [
.target(name: "vapor-tfb", dependencies: ["Vapor"], exclude: ["Resources"]),
+ .target(name: "vapor-tfb-mysql", dependencies: ["FluentMySQL", "Leaf", "Vapor"]),
]
)
diff --git a/frameworks/Swift/vapor/Resources/Views/fortune.leaf b/frameworks/Swift/vapor/Resources/Views/fortune.leaf
new file mode 100644
index 00000000000..861552c0437
--- /dev/null
+++ b/frameworks/Swift/vapor/Resources/Views/fortune.leaf
@@ -0,0 +1,10 @@
+
+
+
Fortunes
+
+
+id | message |
+#for(fortune in fortunes) { #(fortune.id) | #(fortune.message) |
}
+
+
+
diff --git a/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/Models/Fortune.swift b/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/Models/Fortune.swift
new file mode 100644
index 00000000000..35b81f2bf87
--- /dev/null
+++ b/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/Models/Fortune.swift
@@ -0,0 +1,15 @@
+import Vapor
+import FluentMySQL
+
+final class Fortune: MySQLModel, Migration, Content {
+ static let entity = "fortune"
+
+ var id: Int?
+ var message: String
+
+ init(id: Int?, message: String) {
+ self.id = id
+ self.message = message
+ }
+
+}
diff --git a/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/Models/Message.swift b/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/Models/Message.swift
new file mode 100644
index 00000000000..f1dd8e25c6d
--- /dev/null
+++ b/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/Models/Message.swift
@@ -0,0 +1,9 @@
+import Vapor
+
+struct Message: Content {
+ let message: String?
+
+ init(message: String? = nil) {
+ self.message = message
+ }
+}
diff --git a/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/Models/World.swift b/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/Models/World.swift
new file mode 100644
index 00000000000..7897d6bc0fa
--- /dev/null
+++ b/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/Models/World.swift
@@ -0,0 +1,15 @@
+import Vapor
+import FluentMySQL
+
+final class World: MySQLModel, Migration, Content {
+ static let entity = "world"
+
+ var id: Int?
+ var randomNumber: Int
+
+ /// Creates a new World
+ init(id: Int?, randomNumber: Int) {
+ self.id = id
+ self.randomNumber = randomNumber
+ }
+}
diff --git a/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/ParameterUtils.swift b/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/ParameterUtils.swift
new file mode 100644
index 00000000000..d489e09fbee
--- /dev/null
+++ b/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/ParameterUtils.swift
@@ -0,0 +1,26 @@
+import Vapor
+
+public let validQueriesRange: ClosedRange = 1...500
+
+/// Tests 3 and 5 are parameterized with `queries`.
+///
+/// The queries parameter must be bounded to between 1 and 500.
+/// If the parameter is missing, is not an integer, or is an integer less than 1,
+/// the value should be interpreted as 1; if greater than 500, the value should be interpreted as 500.
+///
+/// - Parameter request: HTTP request
+/// - Returns: queries
+public func queriesParam(for request: Request) throws -> Int {
+ // TODO: throw instead of using !
+ let rangeMax = try request.parameters.next(String.self)
+ .removingPercentEncoding? // convert url-encoded chars
+ .components(separatedBy: "...")
+ .last? // ignore lower bound, only retain last part which contains upper bound
+ .dropLast() // remove ]
+ let paramNormalized = rangeMax.flatMap(String.init).flatMap(Int.init) ?? validQueriesRange.upperBound
+ return clamp(paramNormalized, to: validQueriesRange)
+}
+
+func clamp(_ value: Int, to: ClosedRange) -> Int {
+ return max(to.lowerBound, min(to.upperBound, value))
+}
diff --git a/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/WorldMeta.swift b/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/WorldMeta.swift
new file mode 100644
index 00000000000..2964ad92e8a
--- /dev/null
+++ b/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/WorldMeta.swift
@@ -0,0 +1,29 @@
+import Vapor
+#if os(Linux)
+import Glibc
+#endif
+
+private extension Int {
+ static func random(_ max: T) -> Int where T: BinaryInteger {
+ #if os(Linux)
+ return Int(SwiftGlibc.random() % (Int(max) + 1))
+ #else
+ return Int(arc4random_uniform(UInt32(max)))
+ #endif
+ }
+}
+
+public struct WorldMeta {
+ private init() { }
+
+ public static let maxId: UInt32 = 10_000
+
+ public static func randomId() -> Int {
+ return Int.random(maxId)
+ }
+
+ public static let maxRandomNumber: Int32 = 10_000
+ public static func randomRandomNumber() -> Int {
+ return Int.random(maxRandomNumber)
+ }
+}
diff --git a/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/main.swift b/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/main.swift
new file mode 100644
index 00000000000..6f5c003840b
--- /dev/null
+++ b/frameworks/Swift/vapor/Sources/vapor-tfb-mysql/main.swift
@@ -0,0 +1,122 @@
+import FluentMySQL
+import Leaf
+import Vapor
+
+// Services
+var services = Services.default()
+
+// Router
+let router = EngineRouter.default()
+
+// Config
+var config = Config.default()
+
+// Routes
+
+var jsonRes = HTTPResponse(status: .ok, headers: ["Server": "Vapor"])
+let jsonEncoder = JSONEncoder()
+
+// Test type 2: Single database query
+router.get("db") { req -> Future in
+ let worldId = WorldMeta.randomId()
+ return try World.find(worldId, on: req)
+ .map(to: Response.self) { world in
+ let res = Response(http: jsonRes, using: req.sharedContainer)
+
+ if let world = world {
+ try res.content.encode(json: world, using: jsonEncoder)
+ } else {
+ try res.content.encode(json: Message(), using: jsonEncoder)
+ }
+
+ return res
+ }
+}
+
+// Test type 3: Multiple database queries
+router.get("queries", String.parameter) { req -> Future in
+ let queries = try queriesParam(for: req)
+ let ids = (1...queries).lazy.map({ _ in WorldMeta.randomId() })
+
+ return try ids.map { id in
+ return try World.find(id, on: req)
+ }
+ .flatten(on: req)
+ .map { result in
+ return result.compactMap({ $0 })
+ }.map { worlds in
+ let res = Response(http: jsonRes, using: req.sharedContainer)
+ try res.content.encode(json: worlds, using: jsonEncoder)
+ return res
+ }
+}
+
+// Test type 4: Fortunes
+router.get("fortunes") { req -> Future in
+ let posixLocale = Locale(identifier: "en_US_POSIX")
+
+ return Fortune.query(on: req).all()
+ .map { fortunes -> [Fortune] in
+ var newFortunes = fortunes
+ let additional = Fortune(id: 0, message: "Additional fortune added at request time.")
+ newFortunes.insert(additional, at: 0)
+ newFortunes.sort(by: { lhs, rhs -> Bool in
+ return lhs.message.compare(rhs.message, locale: posixLocale) == .orderedAscending
+ })
+ return newFortunes
+ }
+ .flatMap { fortunes in
+ // FIXME: Add headers
+ return try req.view().render("fortune", ["fortunes": fortunes])
+ }
+}
+
+// Test type 5: Database updates
+router.get("updates", String.parameter) { req -> Future in
+ let queries = try queriesParam(for: req)
+ let ids = (1...queries).lazy.map({ _ in WorldMeta.randomId() })
+
+ return try ids.map { id in
+ return try World.find(id, on: req)
+ }
+ .flatten(on: req)
+ .map { result -> [World] in
+ let worlds = result.compactMap({ $0 })
+ worlds.forEach { $0.randomNumber = WorldMeta.randomRandomNumber() }
+ return worlds
+ }
+ .flatMap { worlds -> Future<[World]> in
+ return worlds
+ .map { $0.save(on: req) }
+ .flatten(on: req)
+ }.flatMap { _ in
+ return World.query(on: req).all()
+ }.map { worlds in
+ let res = Response(http: jsonRes, using: req.sharedContainer)
+ try res.content.encode(json: worlds, using: jsonEncoder)
+ return res
+ }
+}
+
+services.register(router, as: Router.self)
+
+try services.register(LeafProvider())
+config.prefer(LeafRenderer.self, for: ViewRenderer.self)
+try services.register(FluentMySQLProvider())
+/// Register custom MySQL Config
+let mySQLUrl = URL(string: "mysql://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world")!
+let mysqlConfig = MySQLDatabaseConfig(hostname: mySQLUrl.host!, port: mySQLUrl.port!, username: mySQLUrl.user!, password: mySQLUrl.password, database: "hello_world")
+services.register(mysqlConfig)
+
+/// Configure migrations
+services.register(MigrationConfig())
+/// Allow requests as DatabaseConnectible
+Fortune.defaultDatabase = .mysql
+World.defaultDatabase = .mysql
+
+// Middlewares (remove unused ErrorMiddleware)
+var middlewares = MiddlewareConfig()
+services.register(middlewares)
+
+let app = try Application(config: config, environment: .detect(), services: services)
+try app.run()
diff --git a/frameworks/Swift/vapor/benchmark_config.json b/frameworks/Swift/vapor/benchmark_config.json
index b68a405a582..28f7ffe0cb8 100644
--- a/frameworks/Swift/vapor/benchmark_config.json
+++ b/frameworks/Swift/vapor/benchmark_config.json
@@ -19,6 +19,27 @@
"display_name": "Vapor",
"notes": "",
"versus": ""
+ },
+ "mysql": {
+ "db_url": "/db",
+ "query_url": "/queries/",
+ "fortune_url": "/fortunes",
+ "update_url": "/updates/",
+ "port": 8080,
+ "approach": "Realistic",
+ "classification": "Fullstack",
+ "database": "MySQL",
+ "framework": "Vapor",
+ "language": "Swift",
+ "flavor": "None",
+ "orm": "Full",
+ "platform": "None",
+ "webserver": "None",
+ "os": "Linux",
+ "database_os": "Linux",
+ "display_name": "Vapor",
+ "notes": "",
+ "versus": ""
}
}]
}
diff --git a/frameworks/Swift/vapor/vapor-mysql.dockerfile b/frameworks/Swift/vapor/vapor-mysql.dockerfile
new file mode 100644
index 00000000000..d32c75ce30c
--- /dev/null
+++ b/frameworks/Swift/vapor/vapor-mysql.dockerfile
@@ -0,0 +1,8 @@
+FROM swift:4.1
+
+ADD ./ /vapor
+WORKDIR /vapor
+RUN apt update -yqq && apt install -yqq cmysql libmysqlclient-dev
+RUN swift build -c release
+
+CMD .build/release/vapor-tfb-mysql -e production -b 0.0.0.0:8080
\ No newline at end of file