Skip to content

Commit 8e9bb22

Browse files
authored
Merge pull request #1 from vapor/beta
fluent psql beta
2 parents 69319e6 + 726bbf4 commit 8e9bb22

22 files changed

+844
-2
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
Package.resolved
6+

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2018 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.

Package.swift

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// swift-tools-version:4.0
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "FluentPostgreSQL",
6+
products: [
7+
// Swift ORM for PostgreSQL (built on top of Fluent ORM framework)
8+
.library(name: "FluentPostgreSQL", targets: ["FluentPostgreSQL"]),
9+
],
10+
dependencies: [
11+
// ⏱ Promises and reactive-streams in Swift built for high-performance and scalability.
12+
.package(url: "https://github.com/vapor/async.git", from: "1.0.0-rc"),
13+
14+
// 🌎 Utility package containing tools for byte manipulation, Codable, OS APIs, and debugging.
15+
.package(url: "https://github.com/vapor/core.git", from: "3.0.0-rc"),
16+
17+
// Swift ORM framework (queries, models, and relations) for building NoSQL and SQL database integrations.
18+
.package(url: "https://github.com/vapor/fluent.git", from: "3.0.0-rc"),
19+
20+
// 🐘 Non-blocking, event-driven Swift client for PostgreSQL.
21+
.package(url: "https://github.com/vapor/postgresql.git", from: "1.0.0-rc"),
22+
],
23+
targets: [
24+
.target(name: "FluentPostgreSQL", dependencies: ["Async", "CodableKit", "Fluent", "FluentSQL", "PostgreSQL"]),
25+
.testTarget(name: "FluentPostgreSQLTests", dependencies: ["FluentBenchmark", "FluentPostgreSQL"]),
26+
]
27+
)

README.md

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,20 @@
1-
# fluent-postgresql
2-
Swift ORM for PostgreSQL (built on top of Fluent ORM framework)
1+
<p align="center">
2+
<img src="https://user-images.githubusercontent.com/1342803/36623958-dc6f07f0-18d7-11e8-8c6c-01737f496de9.png" height="64" alt="Fluent PostgreSQL">
3+
<br>
4+
<br>
5+
<a href="http://docs.vapor.codes/3.0/">
6+
<img src="http://img.shields.io/badge/read_the-docs-2196f3.svg" alt="Documentation">
7+
</a>
8+
<a href="http://vapor.team">
9+
<img src="http://vapor.team/badge.svg" alt="Slack Team">
10+
</a>
11+
<a href="LICENSE">
12+
<img src="http://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License">
13+
</a>
14+
<a href="https://circleci.com/gh/vapor/fluent-postgresql">
15+
<img src="https://circleci.com/gh/vapor/fluent-postgresql.svg?style=shield" alt="Continuous Integration">
16+
</a>
17+
<a href="https://swift.org">
18+
<img src="http://img.shields.io/badge/swift-4.1-brightgreen.svg" alt="Swift 4.1">
19+
</a>
20+
</p>
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@_exported import Fluent
2+
@_exported import PostgreSQL
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Service
2+
3+
/// Adds Fluent PostgreSQL's services to your project.
4+
public final class FluentPostgreSQLProvider: Provider {
5+
/// See `Provider.repositoryName`
6+
public static let repositoryName = "fluent-postgresql"
7+
8+
/// Creates a new `FluentPostgreSQLProvider`
9+
///
10+
/// - enableIdentityColumns: If true, `GENERATED BY DEFAULT AS IDENTITY` will be used.
11+
/// `true` by default.
12+
public init(enableIdentityColumns: Bool? = nil) {
13+
if let enableIdentityColumns = enableIdentityColumns {
14+
_globalEnableIdentityColumns = enableIdentityColumns
15+
}
16+
}
17+
18+
/// See `Provider.register(_:)`
19+
public func register(_ services: inout Services) throws {
20+
try services.register(FluentProvider())
21+
try services.register(PostgreSQLProvider())
22+
}
23+
24+
/// See `Provider.boot(_:)`
25+
public func boot(_ worker: Container) throws { }
26+
}
27+
28+
/// Enabled by default
29+
internal var _globalEnableIdentityColumns: Bool = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// A PostgreSQL column type and size.
2+
public struct PostgreSQLColumn {
3+
/// The columns data type.
4+
public let type: PostgreSQLDataType
5+
6+
/// The columns size. Negative values mean varying size.
7+
public let size: Int16
8+
9+
/// Creates a new `PostgreSQLColumn`.
10+
public init(type: PostgreSQLDataType, size: Int16? = nil) {
11+
self.type = type
12+
self.size = size ?? -1
13+
}
14+
}
15+
16+
/// MARK: Representable
17+
18+
/// Capable of being represented statically by a `PostgreSQLColumn`
19+
public protocol PostgreSQLColumnStaticRepresentable {
20+
/// The `PostgreSQLColumn` type that best represents this type.
21+
static var postgreSQLColumn: PostgreSQLColumn { get }
22+
}
23+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
extension PostgreSQLDatabase: JoinSupporting { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
extension PostgreSQLDatabase: LogSupporting {
2+
/// See `LogSupporting.enableLogging(using:)`
3+
public func enableLogging(using logger: DatabaseLogger) {
4+
self.logger = logger
5+
}
6+
}
7+
8+
extension DatabaseLogger: PostgreSQLLogger {
9+
/// See `PostgreSQLLogger.log(query:parameters:)`
10+
public func log(query: String, parameters: [PostgreSQLData]) {
11+
let log = DatabaseLog(query: query, values: parameters.map { $0.data?.description ?? "nil" }, dbID: "postgresql", date: .init())
12+
self.record(log: log)
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import Async
2+
import CodableKit
3+
import FluentSQL
4+
import Foundation
5+
6+
/// Adds ability to do basic Fluent queries using a `PostgreSQLDatabase`.
7+
extension PostgreSQLDatabase: QuerySupporting {
8+
/// See `QuerySupporting.execute`
9+
public static func execute<I, D>(query: DatabaseQuery<PostgreSQLDatabase>, into stream: I, on connection: PostgreSQLConnection)
10+
where I: Async.InputStream, D: Decodable, D == I.Input
11+
{
12+
let future = Future<Void>.flatMap {
13+
// Convert Fluent `DatabaseQuery` to generic FluentSQL `DataQuery`
14+
var (sqlQuery, bindValues) = query.makeDataQuery()
15+
16+
// If the query has an Encodable model attached serialize it.
17+
// Dictionary keys should be added to the DataQuery as columns.
18+
// Dictionary values should be added to the parameterized array.
19+
let modelData: [PostgreSQLData]
20+
if let model = query.data {
21+
let encoder = PostgreSQLRowEncoder()
22+
try model.encode(to: encoder)
23+
sqlQuery.columns += encoder.data.keys.map { key in
24+
return DataColumn(table: query.entity, name: key)
25+
}
26+
modelData = .init(encoder.data.values)
27+
} else {
28+
modelData = []
29+
}
30+
31+
// Create a PostgreSQL-flavored SQL serializer to create a SQL string
32+
let sqlSerializer = PostgreSQLSQLSerializer()
33+
let sqlString = sqlSerializer.serialize(data: sqlQuery)
34+
35+
// Combine the query data with bind values from filters.
36+
// All bind values must come _after_ the columns section of the query.
37+
let parameters = try modelData + bindValues.map { bind in
38+
let encodable = bind.encodable
39+
guard let convertible = encodable as? PostgreSQLDataCustomConvertible else {
40+
let type = Swift.type(of: encodable)
41+
throw PostgreSQLError(
42+
identifier: "convertible",
43+
reason: "Unsupported encodable type: \(type)",
44+
suggestedFixes: [
45+
"Conform \(type) to PostgreSQLDataCustomConvertible"
46+
],
47+
source: .capture()
48+
)
49+
}
50+
return try convertible.convertToPostgreSQLData()
51+
}
52+
53+
// Create a push stream to accept the psql output
54+
// FIXME: connect streams directly instead?
55+
let pushStream = PushStream<D>()
56+
pushStream.output(to: stream)
57+
58+
// Run the query
59+
return try connection.query(sqlString, parameters) { row in
60+
do {
61+
let decoded = try D.init(from: PostgreSQLRowDecoder(row: row))
62+
pushStream.push(decoded)
63+
} catch {
64+
pushStream.error(error)
65+
}
66+
}
67+
}
68+
69+
/// Convert Future completion / error to stream
70+
future.do {
71+
// Query is complete
72+
stream.close()
73+
}.catch { error in
74+
// Query failed
75+
stream.error(error)
76+
stream.close()
77+
}
78+
}
79+
80+
/// See `QuerySupporting.modelEvent`
81+
public static func modelEvent<M>(event: ModelEvent, model: M, on connection: PostgreSQLConnection) -> Future<M>
82+
where PostgreSQLDatabase == M.Database, M: Model
83+
{
84+
switch event {
85+
case .willCreate:
86+
if M.ID.self == UUID.self {
87+
var model = model
88+
model.fluentID = UUID() as? M.ID
89+
return Future(model)
90+
}
91+
case .didCreate:
92+
if M.ID.self == Int.self {
93+
return connection.simpleQuery("SELECT LASTVAL();").map(to: M.self) { row in
94+
var model = model
95+
try model.fluentID = row[0]["lastval"]?.decode(Int.self) as? M.ID
96+
return model
97+
}
98+
}
99+
default: break
100+
}
101+
102+
return Future(model)
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Async
2+
3+
extension PostgreSQLDatabase: ReferenceSupporting {
4+
/// See `ReferenceSupporting.enableReferences(on:)`
5+
public static func enableReferences(on connection: PostgreSQLConnection) -> Future<Void> {
6+
// enabled by default
7+
return .done
8+
}
9+
10+
/// See `ReferenceSupporting.disableReferences(on:)`
11+
public static func disableReferences(on connection: PostgreSQLConnection) -> Future<Void> {
12+
return Future(
13+
error: PostgreSQLError(identifier: "disableReferences", reason: "PostgreSQL does not support disabling foreign key checks.", source: .capture())
14+
)
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import Async
2+
import Foundation
3+
4+
/// Adds ability to create, update, and delete schemas using a `PostgreSQLDatabase`.
5+
extension PostgreSQLDatabase: SchemaSupporting, IndexSupporting {
6+
/// See `SchemaSupporting.dataType`
7+
public static func dataType(for field: SchemaField<PostgreSQLDatabase>) -> String {
8+
var string: String
9+
if let knownSQLName = field.type.type.knownSQLName {
10+
string = knownSQLName
11+
} else {
12+
string = "VOID"
13+
}
14+
15+
if field.type.size >= 0 {
16+
string += "(\(field.type.size))"
17+
}
18+
19+
if field.isIdentifier {
20+
switch field.type.type {
21+
case .int8, .int4, .int2:
22+
if _globalEnableIdentityColumns {
23+
string += " GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY"
24+
} else {
25+
// not appending!
26+
string = "SERIAL PRIMARY KEY"
27+
}
28+
default: string += " PRIMARY KEY"
29+
}
30+
} else if !field.isOptional {
31+
string += " NOT NULL"
32+
}
33+
34+
return string
35+
}
36+
37+
/// See `SchemaSupporting.fieldType`
38+
public static func fieldType(for type: Any.Type) throws -> PostgreSQLColumn {
39+
if let representable = type as? PostgreSQLColumnStaticRepresentable.Type {
40+
return representable.postgreSQLColumn
41+
} else {
42+
throw PostgreSQLError(
43+
identifier: "fieldType",
44+
reason: "No PostgreSQL column type known for \(type).",
45+
suggestedFixes: [
46+
"Conform \(type) to `PostgreSQLColumnStaticRepresentable` to specify field type or implement a custom migration.",
47+
"Specify the `PostgreSQLColumn` manually using the schema builder in a migration."
48+
],
49+
source: .capture()
50+
)
51+
}
52+
}
53+
54+
/// See `SchemaSupporting.execute`
55+
public static func execute(schema: DatabaseSchema<PostgreSQLDatabase>, on connection: PostgreSQLConnection) -> Future<Void> {
56+
do {
57+
var schemaQuery = schema.makeSchemaQuery(dataTypeFactory: dataType)
58+
schema.applyReferences(to: &schemaQuery)
59+
let sqlString = PostgreSQLSQLSerializer().serialize(schema: schemaQuery)
60+
return try connection.query(sqlString).map(to: Void.self) { rows in
61+
assert(rows.count == 0)
62+
}.flatMap(to: Void.self) {
63+
/// handle indexes as separate query
64+
var indexFutures: [Future<Void>] = []
65+
for addIndex in schema.addIndexes {
66+
let fields = addIndex.fields.map { "\"\($0.name)\"" }.joined(separator: ", ")
67+
let name = addIndex.psqlName(for: schema.entity)
68+
let add = connection.simpleQuery("CREATE \(addIndex.isUnique ? "UNIQUE " : "")INDEX \"\(name)\" ON \"\(schema.entity)\" (\(fields))").map(to: Void.self) { rows in
69+
assert(rows.count == 0)
70+
}
71+
indexFutures.append(add)
72+
}
73+
for removeIndex in schema.removeIndexes {
74+
let name = removeIndex.psqlName(for: schema.entity)
75+
let remove = connection.simpleQuery("DROP INDEX \"\(name)\"").map(to: Void.self) { rows in
76+
assert(rows.count == 0)
77+
}
78+
indexFutures.append(remove)
79+
}
80+
return indexFutures.flatten()
81+
}
82+
83+
84+
} catch {
85+
return Future(error: error)
86+
}
87+
}
88+
}
89+
90+
extension SchemaIndex {
91+
func psqlName(for entity: String) -> String {
92+
return "_fluent_index_\(entity)_" + fields.map { $0.name }.joined(separator: "_")
93+
}
94+
}
95+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Async
2+
3+
extension PostgreSQLDatabase: TransactionSupporting {
4+
/// See `TransactionSupporting.execute(transaction:on:)`
5+
public static func execute(transaction: DatabaseTransaction<PostgreSQLDatabase>, on connection: PostgreSQLConnection) -> Future<Void> {
6+
return connection.simpleQuery("BEGIN TRANSACTION").flatMap(to: Void.self) { results in
7+
return transaction.run(on: connection).flatMap(to: Void.self) { void in
8+
return connection.simpleQuery("END TRANSACTION").transform(to: ())
9+
}.catchFlatMap { error in
10+
return connection.simpleQuery("ROLLBACK").map(to: Void.self) { results in
11+
throw error
12+
}
13+
}
14+
}
15+
}
16+
}

0 commit comments

Comments
 (0)