├── .gitignore
├── .jazzy.yaml
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── PerfectPostgreSQL
│ ├── PerfectPostgreSQL.swift
│ ├── PostgresCRUD.swift
│ ├── PostgresCRUDInsert.swift
│ └── PostgresCRUDUpdate.swift
└── Tests
├── LinuxMain.swift
└── PerfectPostgreSQLTests
├── PerfectPostgreSQLTests.swift
└── XCTestManifests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata
19 |
20 | ## Other
21 | *.xccheckout
22 | *.moved-aside
23 | *.xcuserstate
24 | *.xcscmblueprint
25 | *.resolved
26 | *.pins
27 | *.xcode*
28 | .build*
29 |
30 | ## Obj-C/Swift specific
31 | *.hmap
32 | *.ipa
33 |
34 | ## Playgrounds
35 | timeline.xctimeline
36 | playground.xcworkspace
37 |
38 | # Swift Package Manager
39 | #
40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
41 | # Packages/
42 | .build/
43 |
44 | # CocoaPods
45 | #
46 | # We recommend against adding the Pods directory to your .gitignore. However
47 | # you should judge for yourself, the pros and cons are mentioned at:
48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
49 | #
50 | # Pods/
51 |
52 | # Carthage
53 | #
54 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
55 | # Carthage/Checkouts
56 |
57 | Carthage/Build
58 |
59 | # fastlane
60 | #
61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
62 | # screenshots whenever they are needed.
63 | # For more information about the recommended setup visit:
64 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
65 |
66 | fastlane/report.xml
67 | fastlane/screenshots
68 |
69 | Packages/
70 | PostgreSQL.xcodeproj/
71 | .DS_Store
72 |
--------------------------------------------------------------------------------
/.jazzy.yaml:
--------------------------------------------------------------------------------
1 | module: PerfectPostgreSQL
2 | author: PerfectlySoft
3 | author_url: https://perfect.org
4 | github_url: https://github.com/PerfectlySoft/Perfect-PostgreSQL
5 | copyright: '© 2016 PerfectlySoft Inc. and the Perfect project authors'
6 | theme: fullwidth
7 | xcodebuild_arguments: [-target, PostgreSQL, -toolchain, org.swift.3020160620a]
8 | clean: true
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // Package.swift
3 | // Perfect-PostgreSQL
4 | //
5 | // Created by Kyle Jessup on 3/22/16.
6 | // Copyright (C) 2016 PerfectlySoft, Inc.
7 | //
8 | //===----------------------------------------------------------------------===//
9 | //
10 | // This source file is part of the Perfect.org open source project
11 | //
12 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors
13 | // Licensed under Apache License v2.0
14 | //
15 | // See http://perfect.org/licensing.html for license information
16 | //
17 | //===----------------------------------------------------------------------===//
18 | //
19 |
20 | import PackageDescription
21 |
22 | #if os(macOS)
23 | let package = Package(
24 | name: "PerfectPostgreSQL",
25 | platforms: [
26 | .macOS(.v10_15)
27 | ],
28 | products: [
29 | .library(name: "PerfectPostgreSQL", targets: ["PerfectPostgreSQL"])
30 | ],
31 | dependencies: [
32 | .package(url: "https://github.com/PerfectlySoft/Perfect-CRUD.git", from: "2.0.0"),
33 | .package(url: "https://github.com/PerfectlySoft/Perfect-libpq.git", from: "2.0.0"),
34 | ],
35 | targets: [
36 | .target(name: "PerfectPostgreSQL", dependencies: ["PerfectCRUD"]),
37 | .testTarget(name: "PerfectPostgreSQLTests", dependencies: ["PerfectPostgreSQL"])
38 | ]
39 | )
40 | #else
41 | let package = Package(
42 | name: "PerfectPostgreSQL",
43 | products: [
44 | .library(name: "PerfectPostgreSQL", targets: ["PerfectPostgreSQL"])
45 | ],
46 | dependencies: [
47 | .package(url: "https://github.com/PerfectlySoft/Perfect-CRUD.git", from: "2.0.0"),
48 | .package(url: "https://github.com/PerfectlySoft/Perfect-libpq-linux.git", from: "2.0.0"),
49 | ],
50 | targets: [
51 | .target(name: "PerfectPostgreSQL", dependencies: ["PerfectCRUD"]),
52 | .testTarget(name: "PerfectPostgreSQLTests", dependencies: ["PerfectPostgreSQL"])
53 | ]
54 | )
55 | #endif
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Perfect - PostgreSQL Connector
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | This project provides a Swift wrapper around the libpq client library, enabling access to PostgreSQL servers.
44 |
45 | This package builds with Swift Package Manager and is part of the [Perfect](https://github.com/PerfectlySoft/Perfect) project. It was written to be stand-alone and so does not require PerfectLib or any other components.
46 |
47 | Ensure you have installed and activated the latest Swift 4.0 tool chain.
48 |
49 | ## macOS Build Notes
50 |
51 | This package requires the [Home Brew](http://brew.sh) build of PostgreSQL.
52 |
53 | To install Home Brew:
54 |
55 | ```
56 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
57 | ```
58 |
59 | To install postgres:
60 |
61 | ```
62 | brew install postgres
63 | ```
64 |
65 | ## Linux Build Notes
66 |
67 | Ensure that you have installed libpq-dev.
68 |
69 | ```
70 | sudo apt-get install libpq-dev
71 | ```
72 |
73 | ## Building
74 |
75 | Add this project as a dependency in your Package.swift file.
76 |
77 | ```
78 | .Package(url: "https://github.com/PerfectlySoft/Perfect-PostgreSQL.git", majorVersion: 3)
79 | ```
80 |
81 | ## Documentation
82 |
83 | For more information, please visit [perfect.org](http://www.perfect.org/docs/PostgreSQL.html).
84 |
--------------------------------------------------------------------------------
/Sources/PerfectPostgreSQL/PerfectPostgreSQL.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostgreSQL.swift
3 | // PostgreSQL
4 | //
5 | // Created by Kyle Jessup on 2015-07-29.
6 | // Copyright (C) 2015 PerfectlySoft, Inc.
7 | //
8 | //===----------------------------------------------------------------------===//
9 | //
10 | // This source file is part of the Perfect.org open source project
11 | //
12 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors
13 | // Licensed under Apache License v2.0
14 | //
15 | // See http://perfect.org/licensing.html for license information
16 | //
17 | //===----------------------------------------------------------------------===//
18 | //
19 |
20 | import Foundation
21 | import libpq
22 |
23 | /// This enum type indicates an exception when dealing with a PostgreSQL database
24 | public enum PostgreSQLError : Error {
25 | /// Error with detail message.
26 | case error(String)
27 | }
28 |
29 | /// result object
30 | public final class PGResult {
31 |
32 | /// Result Status enum
33 | public enum StatusType {
34 | case emptyQuery
35 | case commandOK
36 | case tuplesOK
37 | case badResponse
38 | case nonFatalError
39 | case fatalError
40 | case singleTuple
41 | case unknown
42 | }
43 |
44 | private var res: OpaquePointer? = OpaquePointer(bitPattern: 0)
45 | private var borrowed = false
46 |
47 | init(_ res: OpaquePointer?, isBorrowed: Bool = false) {
48 | self.res = res
49 | self.borrowed = isBorrowed
50 | }
51 |
52 | deinit {
53 | close()
54 | }
55 |
56 | func isValid() -> Bool {
57 | return res != nil
58 | }
59 |
60 | /// close result object
61 | public func close() {
62 | clear()
63 | }
64 |
65 | /// clear and disconnect result object
66 | public func clear() {
67 | if let res = self.res {
68 | if !self.borrowed {
69 | PQclear(res)
70 | }
71 | self.res = OpaquePointer(bitPattern: 0)
72 | }
73 | }
74 |
75 | /// Result Status number as Int
76 | public func statusInt() -> Int {
77 | guard let res = self.res else {
78 | return 0
79 | }
80 | let s = PQresultStatus(res)
81 | return Int(s.rawValue)
82 | }
83 |
84 | /// Result Status Value
85 | public func status() -> StatusType {
86 | guard let res = self.res else {
87 | return .unknown
88 | }
89 | let s = PQresultStatus(res)
90 | switch(s.rawValue) {
91 | case PGRES_EMPTY_QUERY.rawValue:
92 | return .emptyQuery
93 | case PGRES_COMMAND_OK.rawValue:
94 | return .commandOK
95 | case PGRES_TUPLES_OK.rawValue:
96 | return .tuplesOK
97 | case PGRES_BAD_RESPONSE.rawValue:
98 | return .badResponse
99 | case PGRES_NONFATAL_ERROR.rawValue:
100 | return .nonFatalError
101 | case PGRES_FATAL_ERROR.rawValue:
102 | return .fatalError
103 | case PGRES_SINGLE_TUPLE.rawValue:
104 | return .singleTuple
105 | default:
106 | print("Unhandled PQresult status type \(s.rawValue)")
107 | }
108 | return .unknown
109 | }
110 |
111 | /// Result Status Message
112 | public func errorMessage() -> String {
113 | guard let res = self.res else {
114 | return ""
115 | }
116 | return String(validatingUTF8: PQresultErrorMessage(res)) ?? ""
117 | }
118 |
119 | /// Result field count
120 | public func numFields() -> Int {
121 | guard let res = self.res else {
122 | return 0
123 | }
124 | return Int(PQnfields(res))
125 | }
126 |
127 | /// Field name for index value
128 | public func fieldName(index: Int) -> String? {
129 | guard let res = self.res,
130 | let fn = PQfname(res, Int32(index)),
131 | let ret = String(validatingUTF8: fn) else {
132 | return nil
133 | }
134 | return ret
135 | }
136 |
137 | /// Field type for index value
138 | public func fieldType(index: Int) -> Oid? {
139 | guard let res = self.res else {
140 | return nil
141 | }
142 | let fn = PQftype(res, Int32(index))
143 | return fn
144 | }
145 |
146 | /// number of rows (Tuples) returned in result
147 | public func numTuples() -> Int {
148 | guard let res = self.res else {
149 | return 0
150 | }
151 | return Int(PQntuples(res))
152 | }
153 |
154 | /// test null field at row index for field index
155 | public func fieldIsNull(tupleIndex: Int, fieldIndex: Int) -> Bool {
156 | return 1 == PQgetisnull(res, Int32(tupleIndex), Int32(fieldIndex))
157 | }
158 |
159 | /// return value for String field type with row and field indexes provided
160 | public func getFieldString(tupleIndex: Int, fieldIndex: Int) -> String? {
161 | guard !fieldIsNull(tupleIndex: tupleIndex, fieldIndex: fieldIndex),
162 | let v = PQgetvalue(res, Int32(tupleIndex), Int32(fieldIndex)) else {
163 | return nil
164 | }
165 | return String(validatingUTF8: v)
166 | }
167 |
168 | /// return value for Bool field type with row and field indexes provided
169 | public func getFieldBool(tupleIndex: Int, fieldIndex: Int) -> Bool? {
170 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
171 | return nil
172 | }
173 | return s == "t"
174 | }
175 |
176 | /// return value for Int field type with row and field indexes provided
177 | public func getFieldInt(tupleIndex: Int, fieldIndex: Int) -> Int? {
178 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
179 | return nil
180 | }
181 | return Int(s)
182 | }
183 |
184 | /// return value for Int8 field type with row and field indexes provided
185 | public func getFieldInt8(tupleIndex: Int, fieldIndex: Int) -> Int8? {
186 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
187 | return nil
188 | }
189 | return Int8(s)
190 | }
191 |
192 | /// return value for Int16 field type with row and field indexes provided
193 | public func getFieldInt16(tupleIndex: Int, fieldIndex: Int) -> Int16? {
194 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
195 | return nil
196 | }
197 | return Int16(s)
198 | }
199 |
200 | /// return value for Int32 field type with row and field indexes provided
201 | public func getFieldInt32(tupleIndex: Int, fieldIndex: Int) -> Int32? {
202 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
203 | return nil
204 | }
205 | return Int32(s)
206 | }
207 |
208 | /// return value for Int64 field type with row and field indexes provided
209 | public func getFieldInt64(tupleIndex: Int, fieldIndex: Int) -> Int64? {
210 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
211 | return nil
212 | }
213 | return Int64(s)
214 | }
215 |
216 | /// return value for Int field type with row and field indexes provided
217 | public func getFieldUInt(tupleIndex: Int, fieldIndex: Int) -> UInt? {
218 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
219 | return nil
220 | }
221 | return UInt(s)
222 | }
223 |
224 | /// return value for Int8 field type with row and field indexes provided
225 | public func getFieldUInt8(tupleIndex: Int, fieldIndex: Int) -> UInt8? {
226 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
227 | return nil
228 | }
229 | return UInt8(s)
230 | }
231 |
232 | /// return value for Int16 field type with row and field indexes provided
233 | public func getFieldUInt16(tupleIndex: Int, fieldIndex: Int) -> UInt16? {
234 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
235 | return nil
236 | }
237 | return UInt16(s)
238 | }
239 |
240 | /// return value for Int32 field type with row and field indexes provided
241 | public func getFieldUInt32(tupleIndex: Int, fieldIndex: Int) -> UInt32? {
242 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
243 | return nil
244 | }
245 | return UInt32(s)
246 | }
247 |
248 | /// return value for Int64 field type with row and field indexes provided
249 | public func getFieldUInt64(tupleIndex: Int, fieldIndex: Int) -> UInt64? {
250 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
251 | return nil
252 | }
253 | return UInt64(s)
254 | }
255 |
256 | /// return value for Double field type with row and field indexes provided
257 | public func getFieldDouble(tupleIndex: Int, fieldIndex: Int) -> Double? {
258 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
259 | return nil
260 | }
261 | return Double(s)
262 | }
263 |
264 | /// return value for Float field type with row and field indexes provided
265 | public func getFieldFloat(tupleIndex: Int, fieldIndex: Int) -> Float? {
266 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
267 | return nil
268 | }
269 | return Float(s)
270 | }
271 |
272 | /// return value for Blob field type with row and field indexes provided
273 | public func getFieldBlob(tupleIndex: Int, fieldIndex: Int) -> [Int8]? {
274 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
275 | return nil
276 | }
277 | let sc = s.utf8
278 | guard sc.count % 2 == 0, sc.count >= 2, s[s.startIndex] == "\\", s[s.index(after: s.startIndex)] == "x" else {
279 | return nil
280 | }
281 | var ret = [Int8]()
282 | var index = sc.index(sc.startIndex, offsetBy: 2)
283 | while index != sc.endIndex {
284 | let c1 = Int8(sc[index])
285 | index = sc.index(after: index)
286 | let c2 = Int8(sc[index])
287 | guard let byte = byteFromHexDigits(one: c1, two: c2) else {
288 | return nil
289 | }
290 | ret.append(byte)
291 | index = sc.index(after: index)
292 | }
293 | return ret
294 | }
295 |
296 | private func byteFromHexDigits(one c1v: Int8, two c2v: Int8) -> Int8? {
297 |
298 | let capA: Int8 = 65
299 | let capF: Int8 = 70
300 | let lowA: Int8 = 97
301 | let lowF: Int8 = 102
302 | let zero: Int8 = 48
303 | let nine: Int8 = 57
304 |
305 | var newChar = Int8(0)
306 |
307 | if c1v >= capA && c1v <= capF {
308 | newChar = c1v - capA + 10
309 | } else if c1v >= lowA && c1v <= lowF {
310 | newChar = c1v - lowA + 10
311 | } else if c1v >= zero && c1v <= nine {
312 | newChar = c1v - zero
313 | } else {
314 | return nil
315 | }
316 |
317 | newChar = newChar &* 16
318 |
319 | if c2v >= capA && c2v <= capF {
320 | newChar += c2v - capA + 10
321 | } else if c2v >= lowA && c2v <= lowF {
322 | newChar += c2v - lowA + 10
323 | } else if c2v >= zero && c2v <= nine {
324 | newChar += c2v - zero
325 | } else {
326 | return nil
327 | }
328 | return newChar
329 | }
330 | }
331 |
332 | /// connection management class
333 | public final class PGConnection {
334 |
335 | /// Connection Status enum
336 | public enum StatusType {
337 | case ok
338 | case bad
339 | }
340 |
341 | var conn = OpaquePointer(bitPattern: 0)
342 | var connectInfo: String = ""
343 |
344 | /// empty init
345 | public init() {
346 |
347 | }
348 |
349 | deinit {
350 | close()
351 | }
352 |
353 | /// Makes a new connection to the database server.
354 | public func connectdb(_ info: String) -> StatusType {
355 | conn = PQconnectdb(info)
356 | connectInfo = info
357 | return status()
358 | }
359 |
360 | /// Close db connection
361 | public func close() {
362 | finish()
363 | }
364 |
365 | /// Closes the connection to the server. Also frees memory used by the PGconn object.
366 | public func finish() {
367 | if conn != nil {
368 | PQfinish(conn)
369 | conn = OpaquePointer(bitPattern: 0)
370 | }
371 | }
372 |
373 | /// Returns the status of the connection.
374 | public func status() -> StatusType {
375 | let status = PQstatus(conn)
376 | return status == CONNECTION_OK ? .ok : .bad
377 | }
378 |
379 | /// Returns the error message most recently generated by an operation on the connection.
380 | public func errorMessage() -> String {
381 | return String(validatingUTF8: PQerrorMessage(conn)) ?? ""
382 | }
383 |
384 | /// Submits a command to the server and waits for the result.
385 | public func exec(statement: String) -> PGResult {
386 | return PGResult(PQexec(conn, statement))
387 | }
388 |
389 | /// Sends data to the server during COPY_IN state.
390 | public func putCopyData(data: String) {
391 | PQputCopyData(self.conn, data, Int32(data.count))
392 | }
393 |
394 | /// Sends end-of-data indication to the server during COPY_IN state.
395 | /// If withError is set, the copy is forced to fail with the error description supplied.
396 | public func putCopyEnd(withError: String? = nil) -> PGResult {
397 | PQputCopyEnd(self.conn, withError)
398 | let result = PGResult(PQgetResult(self.conn))
399 | return result
400 | }
401 |
402 | // !FIX! does not handle binary data
403 | /// Submits a command to the server and waits for the result, with the ability to pass parameters separately from the SQL command text.
404 | public func exec(statement: String, params: [Any?]) -> PGResult {
405 | let count = params.count
406 | let values = UnsafeMutablePointer?>.allocate(capacity: count)
407 | let types = UnsafeMutablePointer.allocate(capacity: count)
408 | let lengths = UnsafeMutablePointer.allocate(capacity: count)
409 | let formats = UnsafeMutablePointer.allocate(capacity: count)
410 | defer {
411 | values.deinitialize(count: count) ; values.deallocate()
412 | types.deinitialize(count: count) ; types.deallocate()
413 | lengths.deinitialize(count: count) ; lengths.deallocate()
414 | formats.deinitialize(count: count) ; formats.deallocate()
415 | }
416 | var asStrings = [String]()
417 | var temps = [[UInt8]]()
418 | for idx in 0..(OpaquePointer(temps.last!))
425 | types[idx] = 0
426 | lengths[idx] = 0
427 | formats[idx] = 0
428 | case let a as [UInt8]:
429 | let length = Int32(a.count)
430 | values[idx] = UnsafePointer(OpaquePointer(a))
431 | types[idx] = 17
432 | lengths[idx] = length
433 | formats[idx] = 1
434 | case let a as [Int8]:
435 | let length = Int32(a.count)
436 | values[idx] = UnsafePointer(OpaquePointer(a))
437 | types[idx] = 17
438 | lengths[idx] = length
439 | formats[idx] = 1
440 | case let d as Data:
441 | let a = d.map { $0 }
442 | let length = Int32(a.count)
443 | temps.append(a)
444 | values[idx] = UnsafePointer(OpaquePointer(temps.last!))
445 | types[idx] = 17
446 | lengths[idx] = length
447 | formats[idx] = 1
448 | default:
449 | if let pm = params[idx] {
450 | asStrings.append("\(pm)")
451 | var aa = [UInt8](asStrings.last!.utf8)
452 | aa.append(0)
453 | temps.append(aa)
454 | values[idx] = UnsafePointer(OpaquePointer(temps.last!))
455 | } else {
456 | values[idx] = nil
457 | }//end if
458 | types[idx] = 0
459 | lengths[idx] = 0
460 | formats[idx] = 0
461 | }
462 | }
463 | let r = PQexecParams(conn, statement, Int32(count), nil, values, lengths, formats, Int32(0))
464 | return PGResult(r)
465 | }
466 |
467 | /// Assert that the connection status is OK
468 | ///
469 | /// - throws: If the connection status is bad
470 | public func ensureStatusIsOk() throws {
471 | switch status() {
472 | case .ok:
473 | // connection status is good
474 | return
475 | case .bad:
476 | throw PostgreSQLError.error("Connection status is bad")
477 | }
478 | }
479 |
480 | /// The binding values
481 | public typealias ExecuteParameterArray = [Any?]
482 |
483 | /// Execute the given statement.
484 | ///
485 | /// - parameter statement: String statement to be executed
486 | /// - parameter params: ExecuteParameterArray? optional bindings
487 | /// - throws: If the status is an error
488 | @discardableResult
489 | public func execute(statement: String, params: ExecuteParameterArray? = nil) throws -> PGResult {
490 | try ensureStatusIsOk()
491 | let res: PGResult
492 | if let params = params {
493 | res = exec(statement: statement, params: params)
494 | } else {
495 | res = exec(statement: statement)
496 | }
497 | let status: PGResult.StatusType = res.status()
498 | switch status {
499 | case .emptyQuery, .commandOK, .tuplesOK:
500 | return res
501 | case .badResponse, .nonFatalError, .fatalError, .singleTuple, .unknown:
502 | throw PostgreSQLError.error("Failed to execute statement. status: \(status)")
503 | }
504 | }
505 |
506 | /// Executes a BEGIN, calls the provided closure and executes a ROLLBACK if an exception occurs or a COMMIT if no exception occurs.
507 | ///
508 | /// - parameter closure: Block to be executed inside transaction
509 | /// - throws: If the provided closure fails
510 | /// - returns: If successful then the return value from the `closure`
511 | public func doWithTransaction(closure: () throws -> Result) throws -> Result {
512 | try ensureStatusIsOk()
513 | try execute(statement: "BEGIN")
514 | do {
515 | let result: Result = try closure()
516 | try execute(statement: "COMMIT")
517 | return result
518 | } catch {
519 | try execute(statement: "ROLLBACK")
520 | throw error
521 | }
522 | }
523 |
524 | /// Handler for receiving a PGResult
525 | public typealias ReceiverProc = (PGResult) -> Void
526 |
527 | /// Handler for processing a text message
528 | public typealias ProcessorProc = (String) -> Void
529 |
530 | /// internal callback for notice receiving
531 | internal var receiver: ReceiverProc = { _ in }
532 |
533 | /// internal callback for notice processing
534 | internal var processor: ProcessorProc = { _ in }
535 |
536 | /// Set a new notice receiver
537 | /// - parameter handler: a closure to handle the incoming notice
538 | /// - returns: a C convention function pointer; would be nil if failed to set.
539 | public func setReceiver(_ handler: @escaping ReceiverProc) -> PQnoticeReceiver? {
540 | guard let cn = self.conn else {
541 | return nil
542 | }
543 | self.receiver = handler
544 | let me = Unmanaged.passUnretained(self).toOpaque()
545 | return PQsetNoticeReceiver(cn, { arg, result in
546 | guard let pointer = arg, let res = result else {
547 | return
548 | }
549 | let this = Unmanaged.fromOpaque(pointer).takeUnretainedValue()
550 | let pgresult = PGResult(res, isBorrowed: true)
551 | this.receiver(pgresult)
552 | }, me)
553 | }
554 |
555 | /// Set a new notice processor
556 | /// - parameter handler: a closure to handle the incoming notice
557 | /// - returns: a C convention function pointer; would be nil if failed to set.
558 | public func setProcessor(_ handler: @escaping ProcessorProc) -> PQnoticeProcessor?{
559 | guard let cn = self.conn else {
560 | return nil
561 | }
562 | self.processor = handler
563 | let me = Unmanaged.passUnretained(self).toOpaque()
564 | return PQsetNoticeProcessor(cn, {arg, msg in
565 | guard let pointer = arg, let message = msg else {
566 | return
567 | }
568 | let this = Unmanaged.fromOpaque(pointer).takeUnretainedValue()
569 | let strmsg = String(cString: message)
570 | this.processor(strmsg)
571 | }, me)
572 | }
573 | }
574 |
575 | #if !swift(>=4.1)
576 | // Added for Swift 4.0/4.1 compat
577 | extension UnsafeMutableRawBufferPointer {
578 | static func allocate(byteCount: Int, alignment: Int) -> UnsafeMutableRawBufferPointer {
579 | return allocate(count: byteCount)
580 | }
581 | }
582 | extension UnsafeMutablePointer {
583 | func deallocate() {
584 | deallocate(capacity: 0)
585 | }
586 | }
587 | extension Collection {
588 | func compactMap(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] {
589 | return try flatMap(transform)
590 | }
591 | }
592 | #endif
593 |
--------------------------------------------------------------------------------
/Sources/PerfectPostgreSQL/PostgresCRUD.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostgresCRUD.swift
3 | // PerfectCRUD
4 | //
5 | // Created by Kyle Jessup on 2017-12-04.
6 | //
7 |
8 | import Foundation
9 | import PerfectCRUD
10 |
11 | public struct PostgresCRUDError: Error, CustomStringConvertible {
12 | public let description: String
13 | public init(_ msg: String) {
14 | description = msg
15 | CRUDLogging.log(.error, msg)
16 | }
17 | }
18 |
19 | extension PGResult {
20 | public func getFieldBlobUInt8(tupleIndex: Int, fieldIndex: Int) -> [UInt8]? {
21 | guard let s = getFieldString(tupleIndex: tupleIndex, fieldIndex: fieldIndex) else {
22 | return nil
23 | }
24 | let sc = s.utf8
25 | guard sc.count % 2 == 0, sc.count >= 2, s[s.startIndex] == "\\", s[s.index(after: s.startIndex)] == "x" else {
26 | return nil
27 | }
28 | var ret = [UInt8]()
29 | var index = sc.index(sc.startIndex, offsetBy: 2)
30 | while index != sc.endIndex {
31 | let c1 = UInt8(sc[index])
32 | index = sc.index(after: index)
33 | let c2 = UInt8(sc[index])
34 | guard let byte = byteFromHexDigits(one: c1, two: c2) else {
35 | return nil
36 | }
37 | ret.append(byte)
38 | index = sc.index(after: index)
39 | }
40 | return ret
41 | }
42 |
43 | private func byteFromHexDigits(one c1v: UInt8, two c2v: UInt8) -> UInt8? {
44 | let capA: UInt8 = 65
45 | let capF: UInt8 = 70
46 | let lowA: UInt8 = 97
47 | let lowF: UInt8 = 102
48 | let zero: UInt8 = 48
49 | let nine: UInt8 = 57
50 | var newChar = UInt8(0)
51 | if c1v >= capA && c1v <= capF {
52 | newChar = c1v - capA + 10
53 | } else if c1v >= lowA && c1v <= lowF {
54 | newChar = c1v - lowA + 10
55 | } else if c1v >= zero && c1v <= nine {
56 | newChar = c1v - zero
57 | } else {
58 | return nil
59 | }
60 | newChar *= 16
61 | if c2v >= capA && c2v <= capF {
62 | newChar += c2v - capA + 10
63 | } else if c2v >= lowA && c2v <= lowF {
64 | newChar += c2v - lowA + 10
65 | } else if c2v >= zero && c2v <= nine {
66 | newChar += c2v - zero
67 | } else {
68 | return nil
69 | }
70 | return newChar
71 | }
72 | }
73 |
74 | class PostgresCRUDRowReader: KeyedDecodingContainerProtocol {
75 | typealias Key = K
76 | var codingPath: [CodingKey] = []
77 | var allKeys: [Key] = []
78 | let results: PGResult
79 | let tupleIndex: Int
80 | let fieldNames: [String:Int]
81 | init(results r: PGResult, tupleIndex ti: Int, fieldNames fn: [String:Int]) {
82 | results = r
83 | tupleIndex = ti
84 | fieldNames = fn
85 | }
86 | func ensureIndex(forKey key: Key) throws -> Int {
87 | guard let index = fieldNames[key.stringValue.lowercased()] else {
88 | throw PostgresCRUDError("No index for column \(key.stringValue)")
89 | }
90 | return index
91 | }
92 | func contains(_ key: Key) -> Bool {
93 | return fieldNames[key.stringValue.lowercased()] != nil
94 | }
95 | func decodeNil(forKey key: Key) throws -> Bool {
96 | return results.fieldIsNull(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key))
97 | }
98 | func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
99 | return results.getFieldBool(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? false
100 | }
101 | func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
102 | return results.getFieldInt(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? 0
103 | }
104 | func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
105 | return results.getFieldInt8(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? 0
106 | }
107 | func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
108 | return results.getFieldInt16(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? 0
109 | }
110 | func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
111 | return results.getFieldInt32(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? 0
112 | }
113 | func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
114 | return results.getFieldInt64(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? 0
115 | }
116 | func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
117 | return results.getFieldUInt(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? 0
118 | }
119 | func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
120 | return results.getFieldUInt8(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? 0
121 | }
122 | func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
123 | return results.getFieldUInt16(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? 0
124 | }
125 | func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
126 | return results.getFieldUInt32(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? 0
127 | }
128 | func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
129 | return results.getFieldUInt64(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? 0
130 | }
131 | func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
132 | return results.getFieldFloat(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? 0
133 | }
134 | func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
135 | return results.getFieldDouble(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? 0
136 | }
137 | func decode(_ type: String.Type, forKey key: Key) throws -> String {
138 | return results.getFieldString(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) ?? ""
139 | }
140 | func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
141 | let index = try ensureIndex(forKey: key)
142 | guard let special = SpecialType(type) else {
143 | throw CRUDDecoderError("Unsupported type: \(type) for key: \(key.stringValue)")
144 | }
145 | switch special {
146 | case .uint8Array:
147 | let ret: [UInt8] = results.getFieldBlobUInt8(tupleIndex: tupleIndex, fieldIndex: index) ?? []
148 | return ret as! T
149 | case .int8Array:
150 | let ret: [Int8] = results.getFieldBlob(tupleIndex: tupleIndex, fieldIndex: index) ?? []
151 | return ret as! T
152 | case .data:
153 | let bytes: [UInt8] = results.getFieldBlobUInt8(tupleIndex: tupleIndex, fieldIndex: index) ?? []
154 | return Data(bytes) as! T
155 | case .uuid:
156 | let str = results.getFieldString(tupleIndex: tupleIndex, fieldIndex: index) ?? ""
157 | guard let ret = UUID(uuidString: str) else {
158 | throw CRUDDecoderError("Invalid UUID string \(str).")
159 | }
160 | return ret as! T
161 | case .date:
162 | let str = results.getFieldString(tupleIndex: tupleIndex, fieldIndex: index) ?? ""
163 | guard let date = Date(fromISO8601: str) else {
164 | throw CRUDDecoderError("Invalid Date string \(str).")
165 | }
166 | return date as! T
167 | case .url:
168 | let str = results.getFieldString(tupleIndex: tupleIndex, fieldIndex: index) ?? ""
169 | guard let url = URL(string: str) else {
170 | throw CRUDDecoderError("Invalid URL string \(str).")
171 | }
172 | return url as! T
173 | case .codable:
174 | guard let data0 = results.getFieldString(tupleIndex: tupleIndex, fieldIndex: try ensureIndex(forKey: key)) else {
175 | throw CRUDDecoderError("Unsupported type: \(type) for key: \(key.stringValue)")
176 | }
177 | if type == String.self {
178 | return data0 as! T
179 | }
180 | let container = data0.count >= 1 && (data0[data0.startIndex] == "[" || data0[data0.startIndex] == "{")
181 | guard let data = data0.data(using: .utf8) else {
182 | throw CRUDDecoderError("Invalid data for type: \(type) for key: \(key.stringValue)")
183 | }
184 | if container {
185 | return try JSONDecoder().decode(type, from: data)
186 | }
187 | guard let obj = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? T else {
188 | throw CRUDDecoderError("Invalid data for type: \(type) for key: \(key.stringValue)")
189 | }
190 | return obj
191 | case .wrapped:
192 | let decoder = CRUDColumnValueDecoder(source: KeyedDecodingContainer(self), key: key)
193 | return try T(from: decoder)
194 | }
195 | }
196 | func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey {
197 | throw CRUDDecoderError("Unimplimented nestedContainer")
198 | }
199 | func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
200 | throw CRUDDecoderError("Unimplimented nestedUnkeyedContainer")
201 | }
202 | func superDecoder() throws -> Decoder {
203 | throw CRUDDecoderError("Unimplimented superDecoder")
204 | }
205 | func superDecoder(forKey key: Key) throws -> Decoder {
206 | throw CRUDDecoderError("Unimplimented superDecoder")
207 | }
208 | }
209 |
210 | struct PostgresColumnInfo: Codable {
211 | let column_name: String
212 | let data_type: String
213 | }
214 |
215 | class PostgresGenDelegate: SQLGenDelegate {
216 | let connection: PGConnection
217 | var parentTableStack: [TableStructure] = []
218 | var bindings: Bindings = []
219 |
220 | init(connection c: PGConnection) {
221 | connection = c
222 | }
223 | func getBinding(for expr: Expression) throws -> String {
224 | let id = "$\(bindings.count+1)"
225 | bindings.append((id, expr))
226 | return id
227 | }
228 | func quote(identifier: String) throws -> String {
229 | return "\"\(identifier.lowercased())\""
230 | }
231 | func getCreateTableSQL(forTable: TableStructure, policy: TableCreatePolicy) throws -> [String] {
232 | parentTableStack.append(forTable)
233 | defer {
234 | parentTableStack.removeLast()
235 | }
236 | var sub: [String] = []
237 | if policy.contains(.dropTable) {
238 | sub += ["DROP TABLE IF EXISTS \(try quote(identifier: forTable.tableName)) CASCADE"]
239 | }
240 | if !policy.contains(.dropTable),
241 | policy.contains(.reconcileTable),
242 | let existingColumns = getExistingColumnData(forTable: forTable.tableName) {
243 | let existingColumnMap: [String:PostgresColumnInfo] = .init(uniqueKeysWithValues: existingColumns.map { ($0.column_name, $0) })
244 | let newColumnMap: [String:TableStructure.Column] = .init(uniqueKeysWithValues: forTable.columns.map { ($0.name.lowercased(), $0) })
245 |
246 | let addColumns = newColumnMap.keys.filter { existingColumnMap[$0] == nil }
247 | let removeColumns: [String] = existingColumnMap.keys.filter { newColumnMap[$0] == nil }
248 |
249 | sub += try removeColumns.map {
250 | return """
251 | ALTER TABLE \(try quote(identifier: forTable.tableName)) DROP COLUMN \(try quote(identifier: $0))
252 | """
253 | }
254 | sub += try addColumns.compactMap { newColumnMap[$0] }.map {
255 | let nameType = try getColumnDefinition($0)
256 | return """
257 | ALTER TABLE \(try quote(identifier: forTable.tableName)) ADD COLUMN \(nameType)
258 | """
259 | }
260 | return sub
261 | } else {
262 | sub += [
263 | """
264 | CREATE TABLE IF NOT EXISTS \(try quote(identifier: forTable.tableName)) (
265 | \(try forTable.columns.map { try getColumnDefinition($0) }.joined(separator: ",\n\t"))
266 | )
267 | """]
268 | }
269 | if !policy.contains(.shallow) {
270 | sub += try forTable.subTables.flatMap {
271 | try getCreateTableSQL(forTable: $0, policy: policy)
272 | }
273 | }
274 |
275 | return sub
276 | }
277 | func getExistingColumnData(forTable: String) -> [PostgresColumnInfo]? {
278 | do {
279 | let statement =
280 | """
281 | SELECT column_name, data_type
282 | FROM INFORMATION_SCHEMA.COLUMNS
283 | WHERE table_name = $1
284 | """
285 | let exeDelegate = PostgresExeDelegate(connection: connection, sql: statement)
286 | exeDelegate.nextBindings = [("$1", .string(forTable.lowercased()))]
287 | var ret: [PostgresColumnInfo] = []
288 | while try exeDelegate.hasNext() {
289 | let rowDecoder: CRUDRowDecoder = CRUDRowDecoder(delegate: exeDelegate)
290 | ret.append(try PostgresColumnInfo(from: rowDecoder))
291 | }
292 | guard !ret.isEmpty else {
293 | return nil
294 | }
295 | return ret
296 | } catch {
297 | return nil
298 | }
299 | }
300 | private func getTypeName(_ type: Any.Type) throws -> String {
301 | let typeName: String
302 | switch type {
303 | case is Int.Type:
304 | typeName = "bigint"
305 | case is Int8.Type:
306 | typeName = "smallint"
307 | case is Int16.Type:
308 | typeName = "smallint"
309 | case is Int32.Type:
310 | typeName = "integer"
311 | case is Int64.Type:
312 | typeName = "bigint"
313 | case is UInt.Type:
314 | typeName = "bigint"
315 | case is UInt8.Type:
316 | typeName = "smallint"
317 | case is UInt16.Type:
318 | typeName = "integer"
319 | case is UInt32.Type:
320 | typeName = "bigint"
321 | case is UInt64.Type:
322 | typeName = "bigint"
323 | case is Double.Type:
324 | typeName = "double precision"
325 | case is Float.Type:
326 | typeName = "real"
327 | case is Bool.Type:
328 | typeName = "boolean"
329 | case is String.Type:
330 | typeName = "text"
331 | default:
332 | guard let special = SpecialType(type) else {
333 | throw PostgresCRUDError("Unsupported SQL column type \(type)")
334 | }
335 | switch special {
336 | case .uint8Array:
337 | typeName = "bytea"
338 | case .int8Array:
339 | typeName = "bytea"
340 | case .data:
341 | typeName = "bytea"
342 | case .uuid:
343 | typeName = "uuid"
344 | case .date:
345 | typeName = "timestamp with time zone"
346 | case .url:
347 | typeName = "text"
348 | case .codable:
349 | typeName = "jsonb"
350 | case .wrapped:
351 | guard let w = type as? WrappedCodableProvider.Type else {
352 | throw PostgresCRUDError("Unsupported SQL column type \(type)")
353 | }
354 | return try getTypeName(w)
355 | }
356 | }
357 | return typeName
358 | }
359 | func getColumnDefinition(_ column: TableStructure.Column) throws -> String {
360 | let name = column.name
361 | let type = column.type
362 | let typeName = try getTypeName(type)
363 | var addendum = ""
364 | if !column.properties.contains(.primaryKey) && !column.optional {
365 | addendum += " NOT NULL"
366 | }
367 | for prop in column.properties {
368 | switch prop {
369 | case .primaryKey:
370 | addendum += " PRIMARY KEY"
371 | case .foreignKey(let table, let column, let onDelete, let onUpdate):
372 | addendum += " REFERENCES \(try quote(identifier: table))(\(try quote(identifier: column)))"
373 | let scenarios = [(" ON DELETE ", onDelete), (" ON UPDATE ", onUpdate)]
374 | for (scenario, action) in scenarios {
375 | addendum += scenario
376 | switch action {
377 | case .ignore:
378 | addendum += "NO ACTION"
379 | case .restrict:
380 | addendum += "RESTRICT"
381 | case .setNull:
382 | addendum += "SET NULL"
383 | case .setDefault:
384 | addendum += "SET DEFAULT"
385 | case .cascade:
386 | addendum += "CASCADE"
387 | }
388 | }
389 | }
390 | }
391 | return "\(try quote(identifier: name)) \(typeName)\(addendum)"
392 | }
393 | func getCreateIndexSQL(forTable name: String, on columns: [String], unique: Bool) throws -> [String] {
394 | let stat =
395 | """
396 | CREATE \(unique ? "UNIQUE " : "")INDEX IF NOT EXISTS \(try quote(identifier: "index_\(columns.joined(separator: "_"))"))
397 | ON \(try quote(identifier: name)) (\(try columns.map{try quote(identifier: $0)}.joined(separator: ",")))
398 | """
399 | return [stat]
400 | }
401 | }
402 |
403 | class PostgresExeDelegate: SQLExeDelegate {
404 | var nextBindings: Bindings = []
405 | let connection: PGConnection
406 | let sql: String
407 | var results: PGResult?
408 | var tupleIndex = -1
409 | var numTuples = 0
410 | var fieldNames: [String:Int] = [:]
411 | init(connection c: PGConnection, sql s: String) {
412 | connection = c
413 | sql = s
414 | }
415 | func bind(_ bindings: Bindings, skip: Int) throws {
416 | results = nil
417 | if skip == 0 {
418 | nextBindings = bindings
419 | } else {
420 | nextBindings = nextBindings[0.. Bool {
431 | tupleIndex += 1
432 | if nil == results {
433 | let r = try connection.exec(statement: sql,
434 | params: nextBindings.map {
435 | try bindOne(expr: $0.1) })
436 | results = r
437 | guard r.isValid() else {
438 | switch connection.status() {
439 | case .ok:
440 | throw CRUDSQLExeError("Fatal error on SQL execution")
441 | case .bad:
442 | throw CRUDSQLExeError(connection.errorMessage())
443 | }
444 | }
445 | let status = r.status()
446 | switch status {
447 | case .emptyQuery:
448 | return false
449 | case .commandOK, .tuplesOK, .singleTuple:
450 | numTuples = r.numTuples()
451 | for i in 0..() throws -> KeyedDecodingContainer? where A : CodingKey {
469 | guard let results = self.results else {
470 | return nil
471 | }
472 | let ret = KeyedDecodingContainer(PostgresCRUDRowReader(results: results,
473 | tupleIndex: tupleIndex,
474 | fieldNames: fieldNames))
475 | return ret
476 | }
477 |
478 | private func bindOne(expr: CRUDExpression) throws -> Any? {
479 | switch expr {
480 | case .lazy(let e):
481 | return try bindOne(expr: e())
482 | case .integer(let i):
483 | return i
484 | case .uinteger(let i):
485 | return i
486 | case .integer64(let i):
487 | return i
488 | case .uinteger64(let i):
489 | return i
490 | case .integer32(let i):
491 | return i
492 | case .uinteger32(let i):
493 | return i
494 | case .integer16(let i):
495 | return i
496 | case .uinteger16(let i):
497 | return i
498 | case .integer8(let i):
499 | return i
500 | case .uinteger8(let i):
501 | return i
502 | case .decimal(let d):
503 | return d
504 | case .float(let f):
505 | return f
506 | case .string(let s):
507 | return s
508 | case .blob(let b):
509 | return b
510 | case .sblob(let b):
511 | return b
512 | case .bool(let b):
513 | return b
514 | case .date(let d):
515 | return d.iso8601()
516 | case .url(let u):
517 | return u.absoluteString
518 | case .uuid(let u):
519 | return u.uuidString
520 | case .null:
521 | return nil as String?
522 | case .column(_), .and(_, _), .or(_, _),
523 | .equality(_, _), .inequality(_, _),
524 | .not(_), .lessThan(_, _), .lessThanEqual(_, _),
525 | .greaterThan(_, _), .greaterThanEqual(_, _),
526 | .keyPath(_), .in(_, _), .like(_, _, _, _):
527 | throw PostgresCRUDError("Asked to bind unsupported expression type: \(expr)")
528 | }
529 | }
530 | }
531 |
532 | public struct PostgresDatabaseConfiguration: DatabaseConfigurationProtocol {
533 | let connection: PGConnection
534 |
535 | public init(url: String?,
536 | name: String?,
537 | host: String?,
538 | port: Int?,
539 | user: String?,
540 | pass: String?) throws {
541 | if let connectionInfo = url {
542 | try self.init(connectionInfo)
543 | } else {
544 | guard let database = name, let host = host else {
545 | throw PostgresCRUDError("Database name and host must be provided.")
546 | }
547 | try self.init(database: database, host: host, port: port, username: user, password: pass)
548 | }
549 | }
550 |
551 | public init(database: String, host: String, port: Int? = nil, username: String? = nil, password: String? = nil) throws {
552 | var s = "host=\(host) dbname=\(database)"
553 | if let p = port {
554 | s += " port=\(p)"
555 | }
556 | if let u = username {
557 | s += " user=\(u)"
558 | }
559 | if let p = password {
560 | s += " password=\(p)"
561 | }
562 | try self.init(s)
563 | }
564 | public init(_ connectionInfo: String) throws {
565 | let con = PGConnection()
566 | guard case .ok = con.connectdb(connectionInfo) else {
567 | throw PostgresCRUDError("Could not connect. \(con.errorMessage())")
568 | }
569 | connection = con
570 | }
571 | public var sqlGenDelegate: SQLGenDelegate {
572 | return PostgresGenDelegate(connection: connection)
573 | }
574 | public func sqlExeDelegate(forSQL: String) throws -> SQLExeDelegate {
575 | return PostgresExeDelegate(connection: connection, sql: forSQL)
576 | }
577 | }
578 |
--------------------------------------------------------------------------------
/Sources/PerfectPostgreSQL/PostgresCRUDInsert.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostgresCRUDInsert.swift
3 | // PerfectPostgreSQL
4 | //
5 | // Created by Kyle Jessup on 2019-01-29.
6 | //
7 |
8 | import Foundation
9 | import PerfectCRUD
10 |
11 | // Promises that instances.count == returnValue.count on exit
12 | // otherwise it will throw
13 | private func _insert(fromTable ft: FromTableType,
14 | instances: [OverAllForm],
15 | includeKeys: [PartialKeyPath],
16 | excludeKeys: [PartialKeyPath]) throws -> [OverAllForm] {
17 | typealias OAF = OverAllForm
18 | let delegate = ft.databaseConfiguration.sqlGenDelegate
19 | var state = SQLGenState(delegate: delegate)
20 | state.command = .insert
21 | try ft.setState(state: &state)
22 | let td = state.tableData[0]
23 | let kpDecoder = td.keyPathDecoder
24 | guard let kpInstance = td.modelInstance else {
25 | throw CRUDSQLGenError("Could not get model instance for key path decoder \(OAF.self)")
26 | }
27 | guard let databaseConfiguration = ft.databaseConfiguration as? PostgresDatabaseConfiguration else {
28 | throw CRUDSQLGenError("This is for Postgres only.")
29 | }
30 | let includeNames: [String]
31 | if includeKeys.isEmpty {
32 | let columnDecoder = CRUDColumnNameDecoder()
33 | _ = try OverAllForm.init(from: columnDecoder)
34 | includeNames = columnDecoder.collectedKeys.map { $0.name }
35 | } else {
36 | includeNames = try includeKeys.map {
37 | guard let n = try kpDecoder.getKeyPathName(kpInstance, keyPath: $0) else {
38 | throw CRUDSQLGenError("Could not get key path name for \(OAF.self) \($0)")
39 | }
40 | return n
41 | }
42 | }
43 | let excludeNames: [String] = try excludeKeys.map {
44 | guard let n = try kpDecoder.getKeyPathName(kpInstance, keyPath: $0) else {
45 | throw CRUDSQLGenError("Could not get key path name for \(OAF.self) \($0)")
46 | }
47 | return n
48 | }
49 |
50 | let encoder = try CRUDBindingsEncoder(delegate: delegate)
51 | try instances[0].encode(to: encoder)
52 |
53 | let bindings = try encoder.completedBindings(allKeys: includeNames, ignoreKeys: Set(excludeNames))
54 | let columnNames = try bindings.map { try delegate.quote(identifier: $0.column) }
55 | let bindIdentifiers = bindings.map { $0.identifier }
56 |
57 | let nameQ = try delegate.quote(identifier: "\(OAF.CRUDTableName)")
58 | let sqlStr = """
59 | INSERT INTO \(nameQ) (\(columnNames.joined(separator: ", ")))
60 | VALUES (\(bindIdentifiers.joined(separator: ", ")))
61 | RETURNING *
62 | """
63 | CRUDLogging.log(.query, sqlStr)
64 | let exeDelegate = PostgresExeDelegate(connection: databaseConfiguration.connection, sql: sqlStr)
65 | try exeDelegate.bind(delegate.bindings)
66 | guard try exeDelegate.hasNext() else {
67 | throw CRUDSQLGenError("Did not get return value from statement \(sqlStr).")
68 | }
69 | var ret: [OverAllForm] = []
70 | ret.append(try OverAllForm(from: CRUDRowDecoder(delegate: exeDelegate)))
71 | for instance in instances[1...] {
72 | exeDelegate.resetResults()
73 | let delegate = databaseConfiguration.sqlGenDelegate
74 | let encoder = try CRUDBindingsEncoder(delegate: delegate)
75 | try instance.encode(to: encoder)
76 | _ = try encoder.completedBindings(allKeys: includeNames, ignoreKeys: Set(excludeNames))
77 | try exeDelegate.bind(delegate.bindings)
78 | guard try exeDelegate.hasNext() else {
79 | throw CRUDSQLGenError("Did not get return value from statement \(sqlStr).")
80 | }
81 | ret.append(try OverAllForm(from: CRUDRowDecoder(delegate: exeDelegate)))
82 | }
83 | return ret
84 | }
85 |
86 | // Promises that instances.count == returnValue.count on exit
87 | // otherwise it will throw
88 | private func _insert(fromTable ft: FromTableType,
89 | instances: [OverAllForm],
90 | returning: KeyPath,
91 | includeKeys: [PartialKeyPath],
92 | excludeKeys: [PartialKeyPath]) throws -> [R] {
93 | typealias OAF = OverAllForm
94 | let delegate = ft.databaseConfiguration.sqlGenDelegate
95 | var state = SQLGenState(delegate: delegate)
96 | state.command = .insert
97 | try ft.setState(state: &state)
98 | let td = state.tableData[0]
99 | let kpDecoder = td.keyPathDecoder
100 | guard let kpInstance = td.modelInstance else {
101 | throw CRUDSQLGenError("Could not get model instance for key path decoder \(OAF.self)")
102 | }
103 | guard let returningName = try kpDecoder.getKeyPathName(kpInstance, keyPath: returning) else {
104 | throw CRUDSQLGenError("Could not get column name for `returning` key path \(returning).")
105 | }
106 | guard let databaseConfiguration = ft.databaseConfiguration as? PostgresDatabaseConfiguration else {
107 | throw CRUDSQLGenError("This is for Postgres only.")
108 | }
109 | let includeNames: [String]
110 | if includeKeys.isEmpty {
111 | let columnDecoder = CRUDColumnNameDecoder()
112 | _ = try OverAllForm.init(from: columnDecoder)
113 | includeNames = columnDecoder.collectedKeys.map { $0.name }
114 | } else {
115 | includeNames = try includeKeys.map {
116 | guard let n = try kpDecoder.getKeyPathName(kpInstance, keyPath: $0) else {
117 | throw CRUDSQLGenError("Could not get key path name for \(OAF.self) \($0)")
118 | }
119 | return n
120 | }
121 | }
122 | let excludeNames: [String] = try excludeKeys.map {
123 | guard let n = try kpDecoder.getKeyPathName(kpInstance, keyPath: $0) else {
124 | throw CRUDSQLGenError("Could not get key path name for \(OAF.self) \($0)")
125 | }
126 | return n
127 | }
128 |
129 | let encoder = try CRUDBindingsEncoder(delegate: delegate)
130 | try instances[0].encode(to: encoder)
131 |
132 | let bindings = try encoder.completedBindings(allKeys: includeNames, ignoreKeys: Set(excludeNames))
133 | let columnNames = try bindings.map { try delegate.quote(identifier: $0.column) }
134 | let bindIdentifiers = bindings.map { $0.identifier }
135 |
136 | let nameQ = try delegate.quote(identifier: "\(OAF.CRUDTableName)")
137 | let sqlStr: String
138 | if columnNames.isEmpty {
139 | sqlStr = "INSERT INTO \(nameQ) DEFAULT VALUES RETURNING \(nameQ).\(try delegate.quote(identifier: returningName))"
140 | } else {
141 | sqlStr = """
142 | INSERT INTO \(nameQ) (\(columnNames.joined(separator: ", ")))
143 | VALUES (\(bindIdentifiers.joined(separator: ", ")))
144 | RETURNING \(nameQ).\(try delegate.quote(identifier: returningName))
145 | """
146 | }
147 | CRUDLogging.log(.query, sqlStr)
148 | let exeDelegate = PostgresExeDelegate(connection: databaseConfiguration.connection, sql: sqlStr)
149 | try exeDelegate.bind(delegate.bindings)
150 | guard try exeDelegate.hasNext(), let next: KeyedDecodingContainer = try exeDelegate.next() else {
151 | throw CRUDSQLGenError("Did not get return value from statement \(sqlStr).")
152 | }
153 | var ret: [R] = []
154 | let value = try next.decode(R.self, forKey: ColumnKey(stringValue: returningName)!)
155 | ret.append(value)
156 | for instance in instances[1...] {
157 | exeDelegate.resetResults()
158 | let delegate = databaseConfiguration.sqlGenDelegate
159 | let encoder = try CRUDBindingsEncoder(delegate: delegate)
160 | try instance.encode(to: encoder)
161 | _ = try encoder.completedBindings(allKeys: includeNames, ignoreKeys: Set(excludeNames))
162 | try exeDelegate.bind(delegate.bindings)
163 | guard try exeDelegate.hasNext(), let next: KeyedDecodingContainer = try exeDelegate.next() else {
164 | throw CRUDSQLGenError("Did not get return value from statement \(sqlStr).")
165 | }
166 | let value = try next.decode(R.self, forKey: ColumnKey(stringValue: returningName)!)
167 | ret.append(value)
168 | }
169 | return ret
170 | }
171 |
172 | public extension Table where C.Configuration == PostgresDatabaseConfiguration {
173 | /// Insert the instance and return the new column value.
174 | func returning(_ returning: KeyPath, insert instance: Form) throws -> R {
175 | return try _insert(fromTable: self, instances: [instance], returning: returning, includeKeys: [], excludeKeys: []).first!
176 | }
177 | /// Insert the instance and return the new column value.
178 | func returning(_ returning: KeyPath, insert instance: Form,
179 | setKeys: KeyPath, _ rest: PartialKeyPath...) throws -> R {
180 | return try _insert(fromTable: self, instances: [instance], returning: returning, includeKeys: [setKeys] + rest, excludeKeys: []).first!
181 | }
182 | /// Insert the instance and return the new column value.
183 | func returning(_ returning: KeyPath, insert instance: Form,
184 | ignoreKeys: KeyPath, _ rest: PartialKeyPath...) throws -> R {
185 | return try _insert(fromTable: self, instances: [instance], returning: returning, includeKeys: [], excludeKeys: [ignoreKeys] + rest).first!
186 | }
187 | /// Insert the instances and return the new column values.
188 | /// Guarantees that insert.count == returnValue.count.
189 | func returning(_ returning: KeyPath, insert instances: [Form]) throws -> [R] {
190 | return try _insert(fromTable: self, instances: instances, returning: returning, includeKeys: [], excludeKeys: [])
191 | }
192 | /// Insert the instances and return the new column values.
193 | /// Guarantees that insert.count == returnValue.count.
194 | func returning(_ returning: KeyPath, insert instances: [Form],
195 | setKeys: KeyPath, _ rest: PartialKeyPath...) throws -> [R] {
196 | return try _insert(fromTable: self, instances: instances, returning: returning, includeKeys: [setKeys] + rest, excludeKeys: [])
197 | }
198 | /// Insert the instances and return the new column values.
199 | /// Guarantees that insert.count == returnValue.count.
200 | func returning(_ returning: KeyPath, insert instances: [Form],
201 | ignoreKeys: KeyPath, _ rest: PartialKeyPath...) throws -> [R] {
202 | return try _insert(fromTable: self, instances: instances, returning: returning, includeKeys: [], excludeKeys: [ignoreKeys] + rest)
203 | }
204 | }
205 |
206 | public extension Table where C.Configuration == PostgresDatabaseConfiguration {
207 | /// Insert the instance and return the new object value.
208 | func returning(insert instance: Form) throws -> OverAllForm {
209 | return try _insert(fromTable: self, instances: [instance], includeKeys: [], excludeKeys: []).first!
210 | }
211 | /// Insert the instance and return the new object value.
212 | func returning(insert instance: Form,
213 | setKeys: KeyPath, _ rest: PartialKeyPath...) throws -> OverAllForm {
214 | return try _insert(fromTable: self, instances: [instance], includeKeys: [setKeys] + rest, excludeKeys: []).first!
215 | }
216 | /// Insert the instance and return the new object value.
217 | func returning(insert instance: Form,
218 | ignoreKeys: KeyPath, _ rest: PartialKeyPath...) throws -> OverAllForm {
219 | return try _insert(fromTable: self, instances: [instance], includeKeys: [], excludeKeys: [ignoreKeys] + rest).first!
220 | }
221 | /// Insert the instances and return the new object values.
222 | /// Guarantees that insert.count == returnValue.count.
223 | func returning(insert instances: [Form]) throws -> [OverAllForm] {
224 | return try _insert(fromTable: self, instances: instances, includeKeys: [], excludeKeys: [])
225 | }
226 | /// Insert the instances and return the new object values.
227 | /// Guarantees that insert.count == returnValue.count.
228 | func returning(insert instances: [Form],
229 | setKeys: KeyPath, _ rest: PartialKeyPath...) throws -> [OverAllForm] {
230 | return try _insert(fromTable: self, instances: instances, includeKeys: [setKeys] + rest, excludeKeys: [])
231 | }
232 | /// Insert the instances and return the new object values.
233 | /// Guarantees that insert.count == returnValue.count.
234 | func returning(insert instances: [Form],
235 | ignoreKeys: KeyPath, _ rest: PartialKeyPath...) throws -> [OverAllForm] {
236 | return try _insert(fromTable: self, instances: instances, includeKeys: [], excludeKeys: [ignoreKeys] + rest)
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/Sources/PerfectPostgreSQL/PostgresCRUDUpdate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostgresCRUDUpdate.swift
3 | // PerfectPostgreSQL
4 | //
5 | // Created by Kyle Jessup on 2019-01-30.
6 | //
7 |
8 | import Foundation
9 | import PerfectCRUD
10 |
11 | // Promises that instances.count == returnValue.count on exit
12 | // otherwise it will throw
13 | private func _update(fromTable ft: FromTableType,
14 | instance: OverAllForm,
15 | includeKeys: [PartialKeyPath],
16 | excludeKeys: [PartialKeyPath]) throws -> [OverAllForm] {
17 | typealias OAF = OverAllForm
18 | let delegate = ft.databaseConfiguration.sqlGenDelegate
19 | var state = SQLGenState(delegate: delegate)
20 | state.command = .update
21 | try ft.setState(state: &state)
22 | let td = state.tableData[0]
23 | let kpDecoder = td.keyPathDecoder
24 | guard let kpInstance = td.modelInstance else {
25 | throw CRUDSQLGenError("Could not get model instance for key path decoder \(OAF.self)")
26 | }
27 | guard let databaseConfiguration = ft.databaseConfiguration as? PostgresDatabaseConfiguration else {
28 | throw CRUDSQLGenError("This is for Postgres only.")
29 | }
30 | let includeNames: [String]
31 | if includeKeys.isEmpty {
32 | let columnDecoder = CRUDColumnNameDecoder()
33 | _ = try OverAllForm.init(from: columnDecoder)
34 | includeNames = columnDecoder.collectedKeys.map { $0.name }
35 | } else {
36 | includeNames = try includeKeys.map {
37 | guard let n = try kpDecoder.getKeyPathName(kpInstance, keyPath: $0) else {
38 | throw CRUDSQLGenError("Could not get key path name for \(OAF.self) \($0)")
39 | }
40 | return n
41 | }
42 | }
43 | let excludeNames: [String] = try excludeKeys.map {
44 | guard let n = try kpDecoder.getKeyPathName(kpInstance, keyPath: $0) else {
45 | throw CRUDSQLGenError("Could not get key path name for \(OAF.self) \($0)")
46 | }
47 | return n
48 | }
49 |
50 | let encoder = try CRUDBindingsEncoder(delegate: delegate)
51 | try instance.encode(to: encoder)
52 | state.bindingsEncoder = encoder
53 | state.columnFilters = (include: includeNames, exclude: excludeNames)
54 | try ft.setSQL(state: &state)
55 | var ret: [OverAllForm] = []
56 | if let stat = state.statements.first { // multi statements?!
57 | let sql = stat.sql + " RETURNING *"
58 | let exeDelegate = try databaseConfiguration.sqlExeDelegate(forSQL: sql)
59 | try exeDelegate.bind(stat.bindings)
60 | while try exeDelegate.hasNext() {
61 | ret.append(try OverAllForm(from: CRUDRowDecoder(delegate: exeDelegate)))
62 | }
63 | }
64 | return ret
65 | }
66 |
67 | // Promises that instances.count == returnValue.count on exit
68 | // otherwise it will throw
69 | private func _update(fromTable ft: FromTableType,
70 | instance: OverAllForm,
71 | returning: KeyPath,
72 | includeKeys: [PartialKeyPath],
73 | excludeKeys: [PartialKeyPath]) throws -> [R] {
74 | typealias OAF = OverAllForm
75 | let delegate = ft.databaseConfiguration.sqlGenDelegate
76 | var state = SQLGenState(delegate: delegate)
77 | state.command = .update
78 | try ft.setState(state: &state)
79 | let td = state.tableData[0]
80 | let kpDecoder = td.keyPathDecoder
81 | guard let kpInstance = td.modelInstance else {
82 | throw CRUDSQLGenError("Could not get model instance for key path decoder \(OAF.self)")
83 | }
84 | guard let returningName = try kpDecoder.getKeyPathName(kpInstance, keyPath: returning) else {
85 | throw CRUDSQLGenError("Could not get column name for `returning` key path \(returning).")
86 | }
87 | guard let databaseConfiguration = ft.databaseConfiguration as? PostgresDatabaseConfiguration else {
88 | throw CRUDSQLGenError("This is for Postgres only.")
89 | }
90 | let includeNames: [String]
91 | if includeKeys.isEmpty {
92 | let columnDecoder = CRUDColumnNameDecoder()
93 | _ = try OverAllForm.init(from: columnDecoder)
94 | includeNames = columnDecoder.collectedKeys.map { $0.name }
95 | } else {
96 | includeNames = try includeKeys.map {
97 | guard let n = try kpDecoder.getKeyPathName(kpInstance, keyPath: $0) else {
98 | throw CRUDSQLGenError("Could not get key path name for \(OAF.self) \($0)")
99 | }
100 | return n
101 | }
102 | }
103 | let excludeNames: [String] = try excludeKeys.map {
104 | guard let n = try kpDecoder.getKeyPathName(kpInstance, keyPath: $0) else {
105 | throw CRUDSQLGenError("Could not get key path name for \(OAF.self) \($0)")
106 | }
107 | return n
108 | }
109 |
110 | let encoder = try CRUDBindingsEncoder(delegate: delegate)
111 | try instance.encode(to: encoder)
112 | state.bindingsEncoder = encoder
113 | state.columnFilters = (include: includeNames, exclude: excludeNames)
114 | try ft.setSQL(state: &state)
115 | var ret: [R] = []
116 | if let stat = state.statements.first { // multi statements?!
117 | let nameQ = try delegate.quote(identifier: "\(OAF.CRUDTableName)")
118 | let sql = stat.sql + " RETURNING \(nameQ).\(try delegate.quote(identifier: returningName))"
119 | let exeDelegate = try databaseConfiguration.sqlExeDelegate(forSQL: sql)
120 | try exeDelegate.bind(stat.bindings)
121 | while try exeDelegate.hasNext(), let next: KeyedDecodingContainer = try exeDelegate.next() {
122 | let value = try next.decode(R.self, forKey: ColumnKey(stringValue: returningName)!)
123 | ret.append(value)
124 | }
125 | }
126 | return ret
127 | }
128 |
129 | public extension Table where C.Configuration == PostgresDatabaseConfiguration {
130 | /// Update the instance and return the new column value.
131 | func returning(_ returning: KeyPath, update instance: Form) throws -> [R] {
132 | return try _update(fromTable: self, instance: instance, returning: returning, includeKeys: [], excludeKeys: [])
133 | }
134 | /// Update the instance and return the new column value.
135 | func returning(_ returning: KeyPath, update instance: Form,
136 | setKeys: KeyPath, _ rest: PartialKeyPath...) throws -> [R] {
137 | return try _update(fromTable: self, instance: instance, returning: returning, includeKeys: [setKeys] + rest, excludeKeys: [])
138 | }
139 | /// Update the instance and return the new column value.
140 | func returning(_ returning: KeyPath, update instance: Form,
141 | ignoreKeys: KeyPath, _ rest: PartialKeyPath...) throws -> [R] {
142 | return try _update(fromTable: self, instance: instance, returning: returning, includeKeys: [], excludeKeys: [ignoreKeys] + rest)
143 | }
144 | /// Update the instance and return the new object value.
145 | func returning(update instance: Form) throws -> [OverAllForm] {
146 | return try _update(fromTable: self, instance: instance, includeKeys: [], excludeKeys: [])
147 | }
148 | /// Update the instance and return the new object value.
149 | func returning(update instance: Form,
150 | setKeys: KeyPath, _ rest: PartialKeyPath...) throws -> [OverAllForm] {
151 | return try _update(fromTable: self, instance: instance, includeKeys: [setKeys] + rest, excludeKeys: [])
152 | }
153 | /// Update the instance and return the new object value.
154 | func returning(update instance: Form,
155 | ignoreKeys: KeyPath, _ rest: PartialKeyPath...) throws -> [OverAllForm] {
156 | return try _update(fromTable: self, instance: instance, includeKeys: [], excludeKeys: [ignoreKeys] + rest)
157 | }
158 | }
159 |
160 | public extension Where where OverAllForm == FromTableType.Form {
161 | /// Update the instance and return the new column value.
162 | func returning(_ returning: KeyPath, update instance: Form) throws -> [R] {
163 | return try _update(fromTable: self, instance: instance, returning: returning, includeKeys: [], excludeKeys: [])
164 | }
165 | /// Update the instance and return the new column value.
166 | func returning(_ returning: KeyPath, update instance: Form,
167 | setKeys: KeyPath, _ rest: PartialKeyPath...) throws -> [R] {
168 | return try _update(fromTable: self, instance: instance, returning: returning, includeKeys: [setKeys] + rest, excludeKeys: [])
169 | }
170 | /// Update the instance and return the new column value.
171 | func returning(_ returning: KeyPath, update instance: Form,
172 | ignoreKeys: KeyPath, _ rest: PartialKeyPath...) throws -> [R] {
173 | return try _update(fromTable: self, instance: instance, returning: returning, includeKeys: [], excludeKeys: [ignoreKeys] + rest)
174 | }
175 | /// Update the instance and return the new object value.
176 | func returning(update instance: Form) throws -> [OverAllForm] {
177 | return try _update(fromTable: self, instance: instance, includeKeys: [], excludeKeys: [])
178 | }
179 | /// Update the instance and return the new object value.
180 | func returning(update instance: Form,
181 | setKeys: KeyPath, _ rest: PartialKeyPath...) throws -> [OverAllForm] {
182 | return try _update(fromTable: self, instance: instance, includeKeys: [setKeys] + rest, excludeKeys: [])
183 | }
184 | /// Update the instance and return the new object value.
185 | func returning(update instance: Form,
186 | ignoreKeys: KeyPath, _ rest: PartialKeyPath...) throws -> [OverAllForm] {
187 | return try _update(fromTable: self, instance: instance, includeKeys: [], excludeKeys: [ignoreKeys] + rest)
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import PerfectPostgreSQLTests
3 |
4 | XCTMain([
5 | testCase(PerfectPostgreSQLTests.allTests),
6 | ])
7 |
--------------------------------------------------------------------------------
/Tests/PerfectPostgreSQLTests/PerfectPostgreSQLTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PostgreSQLTests.swift
3 | // PostgreSQLTests
4 | //
5 | // Created by Kyle Jessup on 2015-10-19.
6 | // Copyright © 2015 PerfectlySoft. All rights reserved.
7 | //
8 | //===----------------------------------------------------------------------===//
9 | //
10 | // This source file is part of the Perfect.org open source project
11 | //
12 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors
13 | // Licensed under Apache License v2.0
14 | //
15 | // See http://perfect.org/licensing.html for license information
16 | //
17 | //===----------------------------------------------------------------------===//
18 | //
19 |
20 | import Foundation
21 | import XCTest
22 | import PerfectCRUD
23 | @testable import PerfectPostgreSQL
24 |
25 | let testDBRowCount = 5
26 | let postgresTestDBName = "testing123"
27 | let postgresInitConnInfo = "host=localhost dbname=postgres"
28 | let postgresTestConnInfo = "host=localhost dbname=testing123"
29 | typealias DBConfiguration = PostgresDatabaseConfiguration
30 | func getDB(reset: Bool = true) throws -> Database {
31 | if reset {
32 | let db = Database(configuration: try DBConfiguration(postgresInitConnInfo))
33 | try? db.sql("DROP DATABASE \(postgresTestDBName)")
34 | try db.sql("CREATE DATABASE \(postgresTestDBName)")
35 | }
36 | return Database(configuration: try DBConfiguration(postgresTestConnInfo))
37 | }
38 |
39 | class PerfectPostgreSQLTests: XCTestCase {
40 | // copy + paste from here into other CRUD driver projects
41 | struct TestTable1: Codable, TableNameProvider {
42 | enum CodingKeys: String, CodingKey {
43 | case id, name, integer = "int", double = "doub", blob, subTables
44 | }
45 | static let tableName = "test_table_1"
46 |
47 | @PrimaryKey var id: Int
48 | let name: String?
49 | let integer: Int?
50 | let double: Double?
51 | let blob: [UInt8]?
52 | let subTables: [TestTable2]?
53 | init(id: Int,
54 | name: String? = nil,
55 | integer: Int? = nil,
56 | double: Double? = nil,
57 | blob: [UInt8]? = nil,
58 | subTables: [TestTable2]? = nil) {
59 | self.id = id
60 | self.name = name
61 | self.integer = integer
62 | self.double = double
63 | self.blob = blob
64 | self.subTables = subTables
65 | }
66 | }
67 |
68 | struct TestTable2: Codable {
69 | @PrimaryKey var id: UUID
70 | @ForeignKey(TestTable1.self, onDelete: cascade, onUpdate: cascade) var parentId: Int
71 | let date: Date
72 | let name: String?
73 | let int: Int?
74 | let doub: Double?
75 | let blob: [UInt8]?
76 | init(id: UUID,
77 | parentId: Int,
78 | date: Date,
79 | name: String? = nil,
80 | int: Int? = nil,
81 | doub: Double? = nil,
82 | blob: [UInt8]? = nil) {
83 | self.id = id
84 | self.date = date
85 | self.name = name
86 | self.int = int
87 | self.doub = doub
88 | self.blob = blob
89 | self.parentId = parentId
90 | }
91 | }
92 |
93 | override func setUp() {
94 | super.setUp()
95 | CRUDClearTableStructureCache()
96 | }
97 | override func tearDown() {
98 | CRUDLogging.flush()
99 | super.tearDown()
100 | }
101 |
102 | func testCreate1() {
103 | do {
104 | let db = try getDB()
105 | try db.create(TestTable1.self, policy: .dropTable)
106 | do {
107 | let t2 = db.table(TestTable2.self)
108 | try t2.index(\.parentId)
109 | }
110 | let t1 = db.table(TestTable1.self)
111 | let t2 = db.table(TestTable2.self)
112 | let subId = UUID()
113 | try db.transaction {
114 | let newOne = TestTable1(id: 2000, name: "New One", integer: 40)
115 | try t1.insert(newOne)
116 | let newSub1 = TestTable2(id: subId, parentId: 2000, date: Date(), name: "Me")
117 | let newSub2 = TestTable2(id: UUID(), parentId: 2000, date: Date(), name: "Not Me")
118 | try t2.insert([newSub1, newSub2])
119 | }
120 | let j21 = try t1.join(\.subTables, on: \.id, equals: \.parentId)
121 | let j2 = j21.where(\TestTable1.id == 2000 && \TestTable2.name == "Me")
122 | let j3 = j21.where(\TestTable1.id > 20 &&
123 | !(\TestTable1.name == "Me" || \TestTable1.name == "You"))
124 | XCTAssertEqual(try j3.count(), 1)
125 | try db.transaction {
126 | let j2a = try j2.select().map { $0 }
127 | XCTAssertEqual(try j2.count(), 1)
128 | XCTAssertEqual(j2a.count, 1)
129 | guard j2a.count == 1 else {
130 | return
131 | }
132 | let obj = j2a[0]
133 | XCTAssertEqual(obj.id, 2000)
134 | XCTAssertNotNil(obj.subTables)
135 | let subTables = obj.subTables!
136 | XCTAssertEqual(subTables.count, 1)
137 | let obj2 = subTables[0]
138 | XCTAssertEqual(obj2.id, subId)
139 | }
140 | try db.create(TestTable1.self)
141 | do {
142 | let j2a = try j2.select().map { $0 }
143 | XCTAssertEqual(try j2.count(), 1)
144 | XCTAssertEqual(j2a[0].id, 2000)
145 | }
146 | try db.create(TestTable1.self, policy: .dropTable)
147 | do {
148 | let j2b = try j2.select().map { $0 }
149 | XCTAssertEqual(j2b.count, 0)
150 | }
151 | } catch {
152 | XCTFail("\(error)")
153 | }
154 | }
155 |
156 | func testCreate2() {
157 | do {
158 | let db = try getTestDB()
159 | try db.create(TestTable1.self, primaryKey: \.id, policy: .dropTable)
160 | do {
161 | let t2 = db.table(TestTable2.self)
162 | try t2.index(\.parentId, \.date)
163 | }
164 | let t1 = db.table(TestTable1.self)
165 | do {
166 | let newOne = TestTable1(id: 2000, name: "New One", integer: 40)
167 | try t1.insert(newOne)
168 | }
169 | let j2 = try t1.where(\TestTable1.id == 2000).select()
170 | do {
171 | let j2a = j2.map { $0 }
172 | XCTAssertEqual(j2a.count, 1)
173 | XCTAssertEqual(j2a[0].id, 2000)
174 | }
175 | try db.create(TestTable1.self)
176 | do {
177 | let j2a = j2.map { $0 }
178 | XCTAssertEqual(j2a.count, 1)
179 | XCTAssertEqual(j2a[0].id, 2000)
180 | }
181 | try db.create(TestTable1.self, policy: .dropTable)
182 | do {
183 | let j2b = j2.map { $0 }
184 | XCTAssertEqual(j2b.count, 0)
185 | }
186 | } catch {
187 | XCTFail("\(error)")
188 | }
189 | }
190 |
191 | func testCreate3() {
192 | struct FakeTestTable1: Codable, TableNameProvider {
193 | enum CodingKeys: String, CodingKey {
194 | case id, name, double = "doub", double2 = "doub2", blob, subTables
195 | }
196 | static let tableName = "test_table_1"
197 | let id: Int
198 | let name: String?
199 | let double2: Double?
200 | let double: Double?
201 | let blob: [UInt8]?
202 | let subTables: [TestTable2]?
203 | }
204 | do {
205 | let db = try getTestDB()
206 | try db.create(TestTable1.self, policy: [.dropTable, .shallow])
207 |
208 | do {
209 | let t1 = db.table(TestTable1.self)
210 | let newOne = TestTable1(id: 2000, name: "New One", integer: 40)
211 | try t1.insert(newOne)
212 | }
213 | do {
214 | try db.create(FakeTestTable1.self, policy: [.reconcileTable, .shallow])
215 | let t1 = db.table(FakeTestTable1.self)
216 | let j2 = try t1.where(\FakeTestTable1.id == 2000).select()
217 | do {
218 | let j2a = j2.map { $0 }
219 | XCTAssertEqual(j2a.count, 1)
220 | XCTAssertEqual(j2a[0].id, 2000)
221 | }
222 | }
223 | } catch {
224 | XCTFail("\(error)")
225 | }
226 | }
227 |
228 | func getTestDB() throws -> Database {
229 | do {
230 | let db = try getDB()
231 | try db.create(TestTable1.self, policy: .dropTable)
232 | try db.transaction {
233 | () -> () in
234 | try db.table(TestTable1.self)
235 | .insert((1...testDBRowCount).map {
236 | num -> TestTable1 in
237 | let n = UInt8(num)
238 | let blob: [UInt8]? = (num % 2 != 0) ? nil : [UInt8](arrayLiteral: n+1, n+2, n+3, n+4, n+5)
239 | return TestTable1(id: num,
240 | name: "This is name bind \(num)",
241 | integer: num,
242 | double: Double(num),
243 | blob: blob)
244 | })
245 | }
246 | try db.transaction {
247 | () -> () in
248 | try db.table(TestTable2.self)
249 | .insert((1...testDBRowCount).flatMap {
250 | parentId -> [TestTable2] in
251 | return (1...testDBRowCount).map {
252 | num -> TestTable2 in
253 | let n = UInt8(num)
254 | let blob: [UInt8]? = [UInt8](arrayLiteral: n+1, n+2, n+3, n+4, n+5)
255 | return TestTable2(id: UUID(),
256 | parentId: parentId,
257 | date: Date(),
258 | name: num % 2 == 0 ? "This is name bind \(num)" : "me",
259 | int: num,
260 | doub: Double(num),
261 | blob: blob)
262 | }
263 | })
264 | }
265 | } catch {
266 | XCTFail("\(error)")
267 | }
268 | return try getDB(reset: false)
269 | }
270 |
271 | func testSelectAll() {
272 | do {
273 | let db = try getTestDB()
274 | let j2 = db.table(TestTable1.self)
275 | for row in try j2.select() {
276 | XCTAssertNil(row.subTables)
277 | }
278 | } catch {
279 | XCTFail("\(error)")
280 | }
281 | }
282 |
283 | func testSelectIn() {
284 | do {
285 | let db = try getTestDB()
286 | let table = db.table(TestTable1.self)
287 | XCTAssertEqual(2, try table.where(\TestTable1.id ~ [2, 4]).count())
288 | XCTAssertEqual(3, try table.where(\TestTable1.id !~ [2, 4]).count())
289 | } catch {
290 | XCTFail("\(error)")
291 | }
292 | }
293 |
294 | func testSelectLikeString() {
295 | do {
296 | let db = try getTestDB()
297 | let table = db.table(TestTable2.self)
298 | XCTAssertEqual(25, try table.where(\TestTable2.name %=% "me").count())
299 | XCTAssertEqual(15, try table.where(\TestTable2.name =% "me").count())
300 | XCTAssertEqual(15, try table.where(\TestTable2.name %= "me").count())
301 | XCTAssertEqual( 0, try table.where(\TestTable2.name %!=% "me").count())
302 | XCTAssertEqual(10, try table.where(\TestTable2.name !=% "me").count())
303 | XCTAssertEqual(10, try table.where(\TestTable2.name %!= "me").count())
304 | } catch {
305 | XCTFail("\(error)")
306 | }
307 | }
308 |
309 | func testSelectJoin() {
310 | do {
311 | let db = try getTestDB()
312 | let j2 = try db.table(TestTable1.self)
313 | .order(by: \TestTable1.name)
314 | .join(\.subTables, on: \.id, equals: \.parentId)
315 | .order(by: \.id)
316 | .where(\TestTable2.name == "me")
317 |
318 | let j2c = try j2.count()
319 | let j2a = try j2.select().map{$0}
320 | let j2ac = j2a.count
321 | XCTAssertNotEqual(j2c, 0)
322 | XCTAssertEqual(j2c, j2ac)
323 | j2a.forEach { row in
324 | XCTAssertFalse(row.subTables?.isEmpty ?? true)
325 | }
326 | } catch {
327 | XCTFail("\(error)")
328 | }
329 | }
330 |
331 | func testInsert1() {
332 | do {
333 | let db = try getTestDB()
334 | let t1 = db.table(TestTable1.self)
335 | let newOne = TestTable1(id: 2000, name: "New One", integer: 40)
336 | try t1.insert(newOne)
337 | let j1 = t1.where(\TestTable1.id == newOne.id)
338 | let j2 = try j1.select().map {$0}
339 | XCTAssertEqual(try j1.count(), 1)
340 | XCTAssertEqual(j2[0].id, 2000)
341 | } catch {
342 | XCTFail("\(error)")
343 | }
344 | }
345 |
346 | func testInsert2() {
347 | do {
348 | let db = try getTestDB()
349 | let t1 = db.table(TestTable1.self)
350 | let newOne = TestTable1(id: 2000, name: "New One", integer: 40)
351 | try t1.insert(newOne, ignoreKeys: \TestTable1.integer)
352 | let j1 = t1.where(\TestTable1.id == newOne.id)
353 | let j2 = try j1.select().map {$0}
354 | XCTAssertEqual(try j1.count(), 1)
355 | XCTAssertEqual(j2[0].id, 2000)
356 | XCTAssertNil(j2[0].integer)
357 | } catch {
358 | XCTFail("\(error)")
359 | }
360 | }
361 |
362 | func testInsert3() {
363 | do {
364 | let db = try getTestDB()
365 | let t1 = db.table(TestTable1.self)
366 | let newOne = TestTable1(id: 2000, name: "New One", integer: 40)
367 | let newTwo = TestTable1(id: 2001, name: "New One", integer: 40)
368 | try t1.insert([newOne, newTwo], setKeys: \TestTable1.id, \TestTable1.integer)
369 | let j1 = t1.where(\TestTable1.id == newOne.id)
370 | let j2 = try j1.select().map {$0}
371 | XCTAssertEqual(try j1.count(), 1)
372 | XCTAssertEqual(j2[0].id, 2000)
373 | XCTAssertEqual(j2[0].integer, 40)
374 | XCTAssertNil(j2[0].name)
375 | } catch {
376 | XCTFail("\(error)")
377 | }
378 | }
379 |
380 | func testUpdate() {
381 | do {
382 | let db = try getTestDB()
383 | let newOne = TestTable1(id: 2000, name: "New One", integer: 40)
384 | let newId: Int = try db.transaction {
385 | try db.table(TestTable1.self).insert(newOne)
386 | let newOne2 = TestTable1(id: 2000, name: "New👻One Updated", integer: 41)
387 | try db.table(TestTable1.self)
388 | .where(\TestTable1.id == newOne.id)
389 | .update(newOne2, setKeys: \.name)
390 | return newOne2.id
391 | }
392 | let j2 = try db.table(TestTable1.self)
393 | .where(\TestTable1.id == newId)
394 | .select().map { $0 }
395 | XCTAssertEqual(1, j2.count)
396 | XCTAssertEqual(2000, j2[0].id)
397 | XCTAssertEqual("New👻One Updated", j2[0].name)
398 | XCTAssertEqual(40, j2[0].integer)
399 | } catch {
400 | XCTFail("\(error)")
401 | }
402 | }
403 |
404 | func testDelete() {
405 | do {
406 | let db = try getTestDB()
407 | let t1 = db.table(TestTable1.self)
408 | let newOne = TestTable1(id: 2000, name: "New One", integer: 40)
409 | try t1.insert(newOne)
410 | let query = t1.where(\TestTable1.id == newOne.id)
411 | let j1 = try query.select().map { $0 }
412 | XCTAssertEqual(j1.count, 1)
413 | try query.delete()
414 | let j2 = try query.select().map { $0 }
415 | XCTAssertEqual(j2.count, 0)
416 | } catch {
417 | XCTFail("\(error)")
418 | }
419 | }
420 |
421 | func testSelectLimit() {
422 | do {
423 | let db = try getTestDB()
424 | do {
425 | let j2 = db.table(TestTable1.self).limit(2, skip: 2)
426 | XCTAssertEqual(try j2.select().map{$0}.count, 2)
427 | }
428 | do {
429 | let j2 = db.table(TestTable1.self).limit(2...3)
430 | XCTAssertEqual(try j2.select().map{$0}.count, 2)
431 | }
432 | do {
433 | let j2 = db.table(TestTable1.self).limit(2..<4)
434 | XCTAssertEqual(try j2.select().map{$0}.count, 2)
435 | }
436 | do {
437 | let j2 = db.table(TestTable1.self).limit(...1)
438 | XCTAssertEqual(try j2.select().map{$0}.count, 2)
439 | }
440 | do {
441 | let j2 = db.table(TestTable1.self).limit(..<2)
442 | XCTAssertEqual(try j2.select().map{$0}.count, 2)
443 | }
444 | do {
445 | let j2 = db.table(TestTable1.self).limit(3...)
446 | XCTAssertEqual(try j2.select().map{$0}.count, 2)
447 | }
448 | do {
449 | let j2 = db.table(TestTable1.self).limit(2, skip: 2)
450 | XCTAssertEqual(try j2.count(), 2)
451 | }
452 | do {
453 | let j2 = db.table(TestTable1.self).limit(2...3)
454 | XCTAssertEqual(try j2.count(), 2)
455 | }
456 | do {
457 | let j2 = db.table(TestTable1.self).limit(2..<4)
458 | XCTAssertEqual(try j2.count(), 2)
459 | }
460 | do {
461 | let j2 = db.table(TestTable1.self).limit(...1)
462 | XCTAssertEqual(try j2.count(), 2)
463 | }
464 | do {
465 | let j2 = db.table(TestTable1.self).limit(..<2)
466 | XCTAssertEqual(try j2.count(), 2)
467 | }
468 | do {
469 | let j2 = db.table(TestTable1.self).limit(3...)
470 | XCTAssertEqual(try j2.count(), 2)
471 | }
472 | } catch {
473 | XCTFail("\(error)")
474 | }
475 | }
476 |
477 | func testSelectLimitWhere() {
478 | do {
479 | let db = try getTestDB()
480 | let j2 = db.table(TestTable1.self).limit(3).where(\TestTable1.id > 3)
481 | XCTAssertEqual(try j2.count(), 2)
482 | XCTAssertEqual(try j2.select().map{$0}.count, 2)
483 | } catch {
484 | XCTFail("\(error)")
485 | }
486 | }
487 |
488 | func testSelectOrderLimitWhere() {
489 | do {
490 | let db = try getTestDB()
491 | let j2 = db.table(TestTable1.self).order(by: \TestTable1.id).limit(3).where(\TestTable1.id > 3)
492 | XCTAssertEqual(try j2.count(), 2)
493 | XCTAssertEqual(try j2.select().map{$0}.count, 2)
494 | } catch {
495 | XCTFail("\(error)")
496 | }
497 | }
498 |
499 | func testSelectWhereNULL() {
500 | do {
501 | let db = try getTestDB()
502 | let t1 = db.table(TestTable1.self)
503 | let j1 = t1.where(\TestTable1.blob == nil)
504 | XCTAssert(try j1.count() > 0)
505 | let j2 = t1.where(\TestTable1.blob != nil)
506 | XCTAssert(try j2.count() > 0)
507 | CRUDLogging.flush()
508 | } catch {
509 | XCTFail("\(error)")
510 | }
511 | }
512 |
513 | // this is the general-overview example used in the readme
514 | func testPersonThing() {
515 | do {
516 | // CRUD can work with most Codable types.
517 | struct PhoneNumber: Codable {
518 | let personId: UUID
519 | let planetCode: Int
520 | let number: String
521 | }
522 | struct Person: Codable {
523 | let id: UUID
524 | let firstName: String
525 | let lastName: String
526 | let phoneNumbers: [PhoneNumber]?
527 | }
528 |
529 | // CRUD usage begins by creating a database connection.
530 | // The inputs for connecting to a database will differ depending on your client library.
531 | // Create a `Database` object by providing a configuration.
532 | // All code would be identical regardless of the datasource type.
533 | let db = try getTestDB()
534 |
535 | // Create the table if it hasn't been done already.
536 | // Table creates are recursive by default, so "PhoneNumber" is also created here.
537 | try db.create(Person.self, policy: .reconcileTable)
538 |
539 | // Get a reference to the tables we will be inserting data into.
540 | let personTable = db.table(Person.self)
541 | let numbersTable = db.table(PhoneNumber.self)
542 |
543 | // Add an index for personId, if it does not already exist.
544 | try numbersTable.index(\.personId)
545 |
546 | // Insert some sample data.
547 | do {
548 | // Insert some sample data.
549 | let owen = Person(id: UUID(), firstName: "Owen", lastName: "Lars", phoneNumbers: nil)
550 | let beru = Person(id: UUID(), firstName: "Beru", lastName: "Lars", phoneNumbers: nil)
551 |
552 | // Insert the people
553 | try personTable.insert([owen, beru])
554 |
555 | // Give them some phone numbers
556 | try numbersTable.insert([
557 | PhoneNumber(personId: owen.id, planetCode: 12, number: "555-555-1212"),
558 | PhoneNumber(personId: owen.id, planetCode: 15, number: "555-555-2222"),
559 | PhoneNumber(personId: beru.id, planetCode: 12, number: "555-555-1212")])
560 | }
561 |
562 | // Perform a query.
563 | // Let's find all people with the last name of Lars which have a phone number on planet 12.
564 | let query = try personTable
565 | .order(by: \.lastName, \.firstName)
566 | .join(\.phoneNumbers, on: \.id, equals: \.personId)
567 | .order(descending: \.planetCode)
568 | .where(\Person.lastName == "Lars" && \PhoneNumber.planetCode == 12)
569 | .select()
570 |
571 | // Loop through the results and print the names.
572 | for user in query {
573 | // We joined PhoneNumbers, so we should have values here.
574 | guard let numbers = user.phoneNumbers else {
575 | continue
576 | }
577 | for number in numbers {
578 | print(number.number)
579 | }
580 | }
581 | CRUDLogging.flush()
582 | } catch {
583 | XCTFail("\(error)")
584 | }
585 | }
586 |
587 | func testStandardJoin() {
588 | do {
589 | let db = try getTestDB()
590 | struct Parent: Codable {
591 | let id: Int
592 | let children: [Child]?
593 | init(id i: Int) {
594 | id = i
595 | children = nil
596 | }
597 | }
598 | struct Child: Codable {
599 | let id: Int
600 | let parentId: Int
601 | }
602 | try db.transaction {
603 | try db.create(Parent.self, policy: [.shallow, .dropTable]).insert(
604 | Parent(id: 1))
605 | try db.create(Child.self, policy: [.shallow, .dropTable]).insert(
606 | [Child(id: 1, parentId: 1),
607 | Child(id: 2, parentId: 1),
608 | Child(id: 3, parentId: 1)])
609 | }
610 | let join = try db.table(Parent.self)
611 | .join(\.children,
612 | on: \.id,
613 | equals: \.parentId)
614 | .where(\Parent.id == 1)
615 |
616 | guard let parent = try join.first() else {
617 | return XCTFail("Failed to find parent id: 1")
618 | }
619 | guard let children = parent.children else {
620 | return XCTFail("Parent had no children")
621 | }
622 | XCTAssertEqual(3, children.count)
623 | for child in children {
624 | XCTAssertEqual(child.parentId, parent.id)
625 | }
626 | CRUDLogging.flush()
627 | } catch {
628 | XCTFail("\(error)")
629 | }
630 | }
631 |
632 | func testJunctionJoin() {
633 | do {
634 | struct Student: Codable {
635 | let id: Int
636 | let classes: [Class]?
637 | init(id i: Int) {
638 | id = i
639 | classes = nil
640 | }
641 | }
642 | struct Class: Codable {
643 | let id: Int
644 | let students: [Student]?
645 | init(id i: Int) {
646 | id = i
647 | students = nil
648 | }
649 | }
650 | struct StudentClasses: Codable {
651 | let studentId: Int
652 | let classId: Int
653 | }
654 | let db = try getTestDB()
655 | try db.transaction {
656 | try db.create(Student.self, policy: [.dropTable, .shallow]).insert(
657 | Student(id: 1))
658 | try db.create(Class.self, policy: [.dropTable, .shallow]).insert([
659 | Class(id: 1),
660 | Class(id: 2),
661 | Class(id: 3)])
662 | try db.create(StudentClasses.self, policy: [.dropTable, .shallow]).insert([
663 | StudentClasses(studentId: 1, classId: 1),
664 | StudentClasses(studentId: 1, classId: 2),
665 | StudentClasses(studentId: 1, classId: 3)])
666 | }
667 | let join = try db.table(Student.self)
668 | .join(\.classes,
669 | with: StudentClasses.self,
670 | on: \.id,
671 | equals: \.studentId,
672 | and: \.id,
673 | is: \.classId)
674 | .where(\Student.id == 1)
675 | guard let student = try join.first() else {
676 | return XCTFail("Failed to find student id: 1")
677 | }
678 | guard let classes = student.classes else {
679 | return XCTFail("Student had no classes")
680 | }
681 | XCTAssertEqual(3, classes.count)
682 | for aClass in classes {
683 | let join = try db.table(Class.self)
684 | .join(\.students,
685 | with: StudentClasses.self,
686 | on: \.id,
687 | equals: \.classId,
688 | and: \.id,
689 | is: \.studentId)
690 | .where(\Class.id == aClass.id)
691 | guard let found = try join.first() else {
692 | XCTFail("Class with no students")
693 | continue
694 | }
695 | guard nil != found.students?.first(where: { $0.id == student.id }) else {
696 | XCTFail("Student not found in class")
697 | continue
698 | }
699 | }
700 | CRUDLogging.flush()
701 | } catch {
702 | XCTFail("\(error)")
703 | }
704 | }
705 |
706 | func testSelfJoin() {
707 | do {
708 | struct Me: Codable {
709 | let id: Int
710 | let parentId: Int
711 | let mes: [Me]?
712 | init(id i: Int, parentId p: Int) {
713 | id = i
714 | parentId = p
715 | mes = nil
716 | }
717 | }
718 | let db = try getTestDB()
719 | try db.transaction {
720 | () -> () in
721 | try db.create(Me.self, policy: .dropTable).insert([
722 | Me(id: 1, parentId: 0),
723 | Me(id: 2, parentId: 1),
724 | Me(id: 3, parentId: 1),
725 | Me(id: 4, parentId: 1),
726 | Me(id: 5, parentId: 1)
727 | ])
728 | }
729 | let join = try db.table(Me.self)
730 | .join(\.mes, on: \.id, equals: \.parentId)
731 | .where(\Me.id == 1)
732 | guard let me = try join.first() else {
733 | return XCTFail("Unable to find me.")
734 | }
735 | guard let mes = me.mes else {
736 | return XCTFail("Unable to find meesa.")
737 | }
738 | XCTAssertEqual(mes.count, 4)
739 | } catch {
740 | XCTFail("\(error)")
741 | }
742 | }
743 |
744 | func testSelfJunctionJoin() {
745 | do {
746 | struct Me: Codable {
747 | let id: Int
748 | let us: [Me]?
749 | init(id i: Int) {
750 | id = i
751 | us = nil
752 | }
753 | }
754 | struct Us: Codable {
755 | let you: Int
756 | let them: Int
757 | }
758 | let db = try getTestDB()
759 | try db.transaction {
760 | () -> () in
761 | try db.create(Me.self, policy: .dropTable)
762 | .insert((1...5).map { .init(id: $0) })
763 | try db.create(Us.self, policy: .dropTable)
764 | .insert((2...5).map { .init(you: 1, them: $0) })
765 | }
766 | let join = try db.table(Me.self)
767 | .join(\.us,
768 | with: Us.self,
769 | on: \.id,
770 | equals: \.you,
771 | and: \.id,
772 | is: \.them)
773 | .where(\Me.id == 1)
774 | guard let me = try join.first() else {
775 | return XCTFail("Unable to find me.")
776 | }
777 | guard let us = me.us else {
778 | return XCTFail("Unable to find us.")
779 | }
780 | XCTAssertEqual(us.count, 4)
781 | } catch {
782 | XCTFail("\(error)")
783 | }
784 | }
785 |
786 | func testCodableProperty() {
787 | do {
788 | struct Sub: Codable {
789 | let id: Int
790 | }
791 | struct Top: Codable {
792 | let id: Int
793 | let sub: Sub?
794 | }
795 | let db = try getTestDB()
796 | try db.create(Sub.self)
797 | try db.create(Top.self)
798 | let t1 = Top(id: 1, sub: Sub(id: 1))
799 | try db.table(Top.self).insert(t1)
800 | guard let top = try db.table(Top.self).where(\Top.id == 1).first() else {
801 | return XCTFail("Unable to find top.")
802 | }
803 | XCTAssertEqual(top.sub?.id, t1.sub?.id)
804 | } catch {
805 | XCTFail("\(error)")
806 | }
807 | }
808 |
809 | func testBadDecoding() {
810 | do {
811 | struct Top: Codable, TableNameProvider {
812 | static var tableName = "Top"
813 | let id: Int
814 | }
815 | struct NTop: Codable, TableNameProvider {
816 | static var tableName = "Top"
817 | let nid: Int
818 | }
819 | let db = try getTestDB()
820 | try db.create(Top.self, policy: .dropTable)
821 | let t1 = Top(id: 1)
822 | try db.table(Top.self).insert(t1)
823 | _ = try db.table(NTop.self).first()
824 | XCTFail("Should not have a valid object.")
825 | } catch {}
826 | }
827 |
828 | func testAllPrimTypes1() {
829 | struct AllTypes: Codable {
830 | let int: Int
831 | let uint: UInt
832 | let int64: Int64
833 | let uint64: UInt64
834 | let int32: Int32?
835 | let uint32: UInt32?
836 | let int16: Int16
837 | let uint16: UInt16
838 | let int8: Int8?
839 | let uint8: UInt8?
840 | let double: Double
841 | let float: Float
842 | let string: String
843 | let bytes: [Int8]
844 | let ubytes: [UInt8]?
845 | let b: Bool
846 | }
847 | do {
848 | let db = try getTestDB()
849 | try db.create(AllTypes.self, policy: .dropTable)
850 | let model = AllTypes(int: 1, uint: 2, int64: 3, uint64: 4, int32: 5, uint32: 6, int16: 7, uint16: 8, int8: 9, uint8: 10, double: 11, float: 12, string: "13", bytes: [1, 4], ubytes: [1, 4], b: true)
851 | try db.table(AllTypes.self).insert(model)
852 |
853 | guard let f = try db.table(AllTypes.self).where(\AllTypes.int == 1).first() else {
854 | return XCTFail("Nil result.")
855 | }
856 | XCTAssertEqual(model.int, f.int)
857 | XCTAssertEqual(model.uint, f.uint)
858 | XCTAssertEqual(model.int64, f.int64)
859 | XCTAssertEqual(model.uint64, f.uint64)
860 | XCTAssertEqual(model.int32, f.int32)
861 | XCTAssertEqual(model.uint32, f.uint32)
862 | XCTAssertEqual(model.int16, f.int16)
863 | XCTAssertEqual(model.uint16, f.uint16)
864 | XCTAssertEqual(model.int8, f.int8)
865 | XCTAssertEqual(model.uint8, f.uint8)
866 | XCTAssertEqual(model.double, f.double)
867 | XCTAssertEqual(model.float, f.float)
868 | XCTAssertEqual(model.string, f.string)
869 | XCTAssertEqual(model.bytes, f.bytes)
870 | XCTAssertEqual(model.ubytes!, f.ubytes!)
871 | XCTAssertEqual(model.b, f.b)
872 | } catch {
873 | XCTFail("\(error)")
874 | }
875 | do {
876 | let db = try getTestDB()
877 | try db.create(AllTypes.self, policy: .dropTable)
878 | let model = AllTypes(int: 1, uint: 2, int64: -3, uint64: 4, int32: nil, uint32: nil, int16: -7, uint16: 8, int8: nil, uint8: nil, double: -11, float: -12, string: "13", bytes: [1, 4], ubytes: nil, b: true)
879 | try db.table(AllTypes.self).insert(model)
880 |
881 | guard let f = try db.table(AllTypes.self)
882 | .where(\AllTypes.int == 1).first() else {
883 | return XCTFail("Nil result.")
884 | }
885 | XCTAssertEqual(model.int, f.int)
886 | XCTAssertEqual(model.uint, f.uint)
887 | XCTAssertEqual(model.int64, f.int64)
888 | XCTAssertEqual(model.uint64, f.uint64)
889 | XCTAssertEqual(model.int32, f.int32)
890 | XCTAssertEqual(model.uint32, f.uint32)
891 | XCTAssertEqual(model.int16, f.int16)
892 | XCTAssertEqual(model.uint16, f.uint16)
893 | XCTAssertEqual(model.int8, f.int8)
894 | XCTAssertEqual(model.uint8, f.uint8)
895 | XCTAssertEqual(model.double, f.double)
896 | XCTAssertEqual(model.float, f.float)
897 | XCTAssertEqual(model.string, f.string)
898 | XCTAssertEqual(model.bytes, f.bytes)
899 | XCTAssertNil(f.ubytes)
900 | XCTAssertEqual(model.b, f.b)
901 | } catch {
902 | XCTFail("\(error)")
903 | }
904 | }
905 |
906 | func testAllPrimTypes2() {
907 | struct AllTypes2: Codable {
908 | func equals(rhs: AllTypes2) -> Bool {
909 | guard int == rhs.int && uint == rhs.uint &&
910 | int64 == rhs.int64 && uint64 == rhs.uint64 &&
911 | int32 == rhs.int32 && uint32 == rhs.uint32 &&
912 | int16 == rhs.int16 && uint16 == rhs.uint16 &&
913 | int8 == rhs.int8 && uint8 == rhs.uint8 else {
914 | return false
915 | }
916 | guard double == rhs.double && float == rhs.float &&
917 | string == rhs.string &&
918 | b == rhs.b else {
919 | return false
920 | }
921 | guard (bytes == nil && rhs.bytes == nil) || (bytes != nil && rhs.bytes != nil) else {
922 | return false
923 | }
924 | guard (ubytes == nil && rhs.ubytes == nil) || (ubytes != nil && rhs.ubytes != nil) else {
925 | return false
926 | }
927 | if let lhsb = bytes {
928 | guard lhsb == rhs.bytes! else {
929 | return false
930 | }
931 | }
932 | if let lhsb = ubytes {
933 | guard lhsb == rhs.ubytes! else {
934 | return false
935 | }
936 | }
937 | return true
938 | }
939 | let int: Int?
940 | let uint: UInt?
941 | let int64: Int64?
942 | let uint64: UInt64?
943 | let int32: Int32?
944 | let uint32: UInt32?
945 | let int16: Int16?
946 | let uint16: UInt16?
947 | let int8: Int8?
948 | let uint8: UInt8?
949 | let double: Double?
950 | let float: Float?
951 | let string: String?
952 | let bytes: [Int8]?
953 | let ubytes: [UInt8]?
954 | let b: Bool?
955 | }
956 |
957 | do {
958 | let db = try getTestDB()
959 | try db.create(AllTypes2.self, policy: .dropTable)
960 | let model = AllTypes2(int: 1, uint: 2, int64: -3, uint64: 4, int32: 5, uint32: 6,
961 | int16: 7, uint16: 8, int8: 9, uint8: 10,
962 | double: 11.2, float: 12.3, string: "13",
963 | bytes: [1, 4], ubytes: [1, 4], b: true)
964 | try db.table(AllTypes2.self).insert(model)
965 | do {
966 | guard let f = try db.table(AllTypes2.self)
967 | .where(\AllTypes2.int == 1 &&
968 | \AllTypes2.uint == 2 &&
969 | \AllTypes2.int64 == -3).first() else {
970 | return XCTFail("Nil result.")
971 | }
972 | XCTAssert(model.equals(rhs: f), "\(model) != \(f)")
973 | XCTAssertEqual(try db.table(AllTypes2.self)
974 | .where(\AllTypes2.int != 1 &&
975 | \AllTypes2.uint != 2 &&
976 | \AllTypes2.int64 != -3).count(), 0)
977 | }
978 | do {
979 | guard let f = try db.table(AllTypes2.self)
980 | .where(\AllTypes2.uint64 == 4 &&
981 | \AllTypes2.int32 == 5 &&
982 | \AllTypes2.uint32 == 6).first() else {
983 | return XCTFail("Nil result.")
984 | }
985 | XCTAssert(model.equals(rhs: f), "\(model) != \(f)")
986 | XCTAssertEqual(try db.table(AllTypes2.self)
987 | .where(\AllTypes2.uint64 != 4 &&
988 | \AllTypes2.int32 != 5 &&
989 | \AllTypes2.uint32 != 6).count(), 0)
990 | }
991 | do {
992 | guard let f = try db.table(AllTypes2.self)
993 | .where(\AllTypes2.int16 == 7 &&
994 | \AllTypes2.uint16 == 8 &&
995 | \AllTypes2.int8 == 9 &&
996 | \AllTypes2.uint8 == 10).first() else {
997 | return XCTFail("Nil result.")
998 | }
999 | XCTAssert(model.equals(rhs: f), "\(model) != \(f)")
1000 | XCTAssertEqual(try db.table(AllTypes2.self)
1001 | .where(\AllTypes2.int16 != 7 &&
1002 | \AllTypes2.uint16 != 8 &&
1003 | \AllTypes2.int8 != 9 &&
1004 | \AllTypes2.uint8 != 10).count(), 0)
1005 | }
1006 | do {
1007 | guard let f = try db.table(AllTypes2.self)
1008 | .where(\AllTypes2.double == 11.2 &&
1009 | \AllTypes2.float == Float(12.3) &&
1010 | \AllTypes2.string == "13").first() else {
1011 | return XCTFail("Nil result.")
1012 | }
1013 | XCTAssert(model.equals(rhs: f), "\(model) != \(f)")
1014 | XCTAssertEqual(try db.table(AllTypes2.self)
1015 | .where(\AllTypes2.double != 11.2 &&
1016 | \AllTypes2.float != Float(12.3) &&
1017 | \AllTypes2.string != "13").count(), 0)
1018 | }
1019 | do {
1020 | guard let f = try db.table(AllTypes2.self)
1021 | .where(\AllTypes2.bytes == [1, 4] as [Int8] &&
1022 | \AllTypes2.ubytes == [1, 4] as [UInt8] &&
1023 | \AllTypes2.b == true).first() else {
1024 | return XCTFail("Nil result.")
1025 | }
1026 | XCTAssert(model.equals(rhs: f), "\(model) != \(f)")
1027 | XCTAssertEqual(try db.table(AllTypes2.self)
1028 | .where(\AllTypes2.bytes != [1, 4] as [Int8] &&
1029 | \AllTypes2.ubytes != [1, 4] as [UInt8] &&
1030 | \AllTypes2.b != true).count(), 0)
1031 | }
1032 | } catch {
1033 | XCTFail("\(error)")
1034 | }
1035 | }
1036 |
1037 | func testBespokeSQL() {
1038 | do {
1039 | let db = try getTestDB()
1040 | do {
1041 | let r = try db.sql("SELECT * FROM \(TestTable1.CRUDTableName) WHERE id = 2", TestTable1.self)
1042 | XCTAssertEqual(r.count, 1)
1043 | }
1044 | do {
1045 | let r = try db.sql("SELECT * FROM \(TestTable1.CRUDTableName)", TestTable1.self)
1046 | XCTAssertEqual(r.count, 5)
1047 | }
1048 | } catch {
1049 | XCTFail("\(error)")
1050 | }
1051 | }
1052 |
1053 | func testModelClasses() {
1054 | class BaseClass: Codable {
1055 | let id: Int
1056 | let name: String
1057 | private enum CodingKeys: String, CodingKey {
1058 | case id, name
1059 | }
1060 | init(id: Int, name: String) {
1061 | self.id = id
1062 | self.name = name
1063 | }
1064 | required init(from decoder: Decoder) throws {
1065 | let container = try decoder.container(keyedBy: CodingKeys.self)
1066 | id = try container.decode(Int.self, forKey: .id)
1067 | name = try container.decode(String.self, forKey: .name)
1068 | }
1069 | func encode(to encoder: Encoder) throws {
1070 | var container = encoder.container(keyedBy: CodingKeys.self)
1071 | try container.encode(id, forKey: .id)
1072 | try container.encode(name, forKey: .name)
1073 | }
1074 | }
1075 |
1076 | class SubClass: BaseClass {
1077 | let another: String
1078 | private enum CodingKeys: String, CodingKey {
1079 | case another
1080 | }
1081 | init(id: Int, name: String, another: String) {
1082 | self.another = another
1083 | super.init(id: id, name: name)
1084 | }
1085 | required init(from decoder: Decoder) throws {
1086 | let container = try decoder.container(keyedBy: CodingKeys.self)
1087 | another = try container.decode(String.self, forKey: .another)
1088 | try super.init(from: decoder)
1089 | }
1090 | override func encode(to encoder: Encoder) throws {
1091 | var container = encoder.container(keyedBy: CodingKeys.self)
1092 | try container.encode(another, forKey: .another)
1093 | try super.encode(to: encoder)
1094 | }
1095 | }
1096 |
1097 | do {
1098 | let db = try getTestDB()
1099 | try db.create(SubClass.self)
1100 | let table = db.table(SubClass.self)
1101 | let obj = SubClass(id: 1, name: "The name", another: "And another thing")
1102 | try table.insert(obj)
1103 |
1104 | guard let found = try table.where(\SubClass.id == 1).first() else {
1105 | return XCTFail("Did not find SubClass")
1106 | }
1107 | XCTAssertEqual(found.another, obj.another)
1108 | XCTAssertEqual(found.name, obj.name)
1109 | } catch {
1110 | XCTFail("\(error)")
1111 | }
1112 | }
1113 |
1114 | func testURL() {
1115 | do {
1116 | let db = try getTestDB()
1117 | struct TableWithURL: Codable {
1118 | let id: Int
1119 | let url: URL
1120 | }
1121 | try db.create(TableWithURL.self)
1122 | let t1 = db.table(TableWithURL.self)
1123 | let newOne = TableWithURL(id: 2000, url: URL(string: "http://localhost/")!)
1124 | try t1.insert(newOne)
1125 | let j1 = t1.where(\TableWithURL.id == newOne.id)
1126 | let j2 = try j1.select().map {$0}
1127 | XCTAssertEqual(try j1.count(), 1)
1128 | XCTAssertEqual(j2[0].id, 2000)
1129 | XCTAssertEqual(j2[0].url.absoluteString, "http://localhost/")
1130 | } catch {
1131 | XCTFail("\(error)")
1132 | }
1133 | }
1134 |
1135 | func testManyJoins() {
1136 | do {
1137 | let db = try getTestDB()
1138 | struct Person2 : Codable{
1139 | var id : UUID
1140 | var name : String
1141 | let cars : [Car]?
1142 | let boats : [Boat]?
1143 | let houses : [House]?
1144 | }
1145 | struct Car : Codable{
1146 | var id : UUID
1147 | var owner : UUID
1148 | }
1149 | struct Boat : Codable{
1150 | var id : UUID
1151 | var owner : UUID
1152 | }
1153 | struct House : Codable{
1154 | var id : UUID
1155 | var owner : UUID
1156 | }
1157 | try db.create(Person2.self)
1158 | try db.table(Car.self).index(\.owner)
1159 | try db.table(Boat.self).index(\.owner)
1160 | try db.table(House.self).index(\.owner)
1161 |
1162 | let t1 = db.table(Person2.self)
1163 | let parentId = UUID()
1164 | let person = Person2(id: parentId, name: "The Person", cars: nil, boats: nil, houses: nil)
1165 | try t1.insert(person)
1166 |
1167 | for _ in 0..<5 {
1168 | try db.table(Car.self).insert(.init(id: UUID(), owner: parentId))
1169 | try db.table(Boat.self).insert(.init(id: UUID(), owner: parentId))
1170 | try db.table(House.self).insert(.init(id: UUID(), owner: parentId))
1171 | }
1172 |
1173 | let j1 = try t1.join(\.cars, on: \.id, equals: \.owner)
1174 | .join(\.boats, on: \.id, equals: \.owner)
1175 | .join(\.houses, on: \.id, equals: \.owner)
1176 | .where(\Person2.id == parentId)
1177 | guard let j2 = try j1.first() else {
1178 | return XCTFail()
1179 | }
1180 | XCTAssertEqual(5, j2.cars?.count)
1181 | XCTAssertEqual(5, j2.boats?.count)
1182 | XCTAssertEqual(5, j2.houses?.count)
1183 | } catch {
1184 | XCTFail("\(error)")
1185 | }
1186 | }
1187 |
1188 | func testDateFormat() {
1189 | let fmt = Date(fromISO8601: "2018-08-18 08:10:51-04")
1190 | XCTAssertNotNil(fmt)
1191 | let fmt1 = Date(fromISO8601: "2018-08-18 08:10:51.32-04:00")
1192 | XCTAssertNotNil(fmt1)
1193 | let fmt2 = Date(fromISO8601: "2018-08-18 08:10:51Z")
1194 | XCTAssertNotNil(fmt2)
1195 | let fmt3 = Date(fromISO8601: "2018-08-18 08:10:51.43Z")
1196 | XCTAssertNotNil(fmt3)
1197 | }
1198 |
1199 | func testAssets() {
1200 | struct Asset: Codable {
1201 | let id: UUID
1202 | let name: String?
1203 | let assetLog: [AssetLog]?
1204 | init(id i: UUID,
1205 | name n: String? = nil,
1206 | assetLog log: [AssetLog]? = nil) {
1207 | id = i
1208 | name = n
1209 | assetLog = log
1210 | }
1211 | }
1212 |
1213 | struct AssetLog: Codable {
1214 | let assetId: UUID
1215 | let userId: UUID
1216 | let taken: Double
1217 | let returned: Double?
1218 | init(assetId: UUID, userId: UUID, taken: Double, returned: Double? = nil) {
1219 | self.assetId = assetId
1220 | self.userId = userId
1221 | self.taken = taken
1222 | self.returned = returned
1223 | }
1224 | }
1225 |
1226 | do {
1227 | let db = try getTestDB()
1228 | try db.create(Asset.self, policy: .dropTable)
1229 | let id = UUID()
1230 | let userId = UUID()
1231 | do {
1232 | let asset = Asset(id: id, name: "name")
1233 | try db.table(Asset.self).insert(asset)
1234 | let assetLogs = [AssetLog(assetId: id, userId: userId, taken: 1.0),
1235 | AssetLog(assetId: id, userId: userId, taken: 2.0)]
1236 | try db.table(AssetLog.self).insert(assetLogs)
1237 | }
1238 | let assetTable = db.table(Asset.self)
1239 | let asset = try assetTable.join(\.assetLog, on: \.id, equals: \.assetId)
1240 | .where(\AssetLog.userId == userId && \AssetLog.returned == nil).first()
1241 | XCTAssertNotNil(asset?.assetLog)
1242 | XCTAssertEqual(asset?.id, id)
1243 | XCTAssertEqual(asset?.assetLog?.count, 2)
1244 | } catch {
1245 | XCTFail("\(error)")
1246 | }
1247 |
1248 | }
1249 |
1250 | func testReturningInsert() {
1251 | do {
1252 | let db = try getTestDB()
1253 | struct ReturningItem: Codable, Equatable {
1254 | let id: UUID
1255 | let def: Int?
1256 | init(id: UUID, def: Int? = nil) {
1257 | self.id = id
1258 | self.def = def
1259 | }
1260 | }
1261 | try db.sql("DROP TABLE IF EXISTS \(ReturningItem.CRUDTableName)")
1262 | try db.sql("CREATE TABLE \(ReturningItem.CRUDTableName) (id UUID PRIMARY KEY, def int DEFAULT 42)")
1263 | let table = db.table(ReturningItem.self)
1264 | do {
1265 | let item = ReturningItem(id: UUID())
1266 | let def = try table.returning(\.def, insert: item, ignoreKeys: \.def)
1267 | XCTAssertEqual(def, 42)
1268 | }
1269 | do {
1270 | let items = [ReturningItem(id: UUID()),
1271 | ReturningItem(id: UUID()),
1272 | ReturningItem(id: UUID())]
1273 | let defs = try table.returning(\.def, insert: items, ignoreKeys: \.def)
1274 | XCTAssertEqual(defs, [42, 42, 42])
1275 | }
1276 | do {
1277 | let id = UUID()
1278 | let item = ReturningItem(id: id, def: 42)
1279 | let id0 = try table.returning(\.id, insert: item)
1280 | XCTAssertEqual(id0, id)
1281 | }
1282 | do {
1283 | let items = [ReturningItem(id: UUID()),
1284 | ReturningItem(id: UUID()),
1285 | ReturningItem(id: UUID())]
1286 | let defs = try table.returning(insert: items, ignoreKeys: \.def)
1287 | XCTAssertEqual(defs.map{$0.id}, items.map{$0.id})
1288 | XCTAssertEqual(defs.compactMap{$0.def}.count, defs.count)
1289 | }
1290 | } catch {
1291 | XCTFail("\(error)")
1292 | }
1293 | }
1294 |
1295 | func testReturningUpdate() {
1296 | do {
1297 | let db = try getTestDB()
1298 | struct ReturningItem: Codable, Equatable {
1299 | let id: UUID
1300 | var def: Int?
1301 | init(id: UUID, def: Int? = nil) {
1302 | self.id = id
1303 | self.def = def
1304 | }
1305 | }
1306 | try db.sql("DROP TABLE IF EXISTS \(ReturningItem.CRUDTableName)")
1307 | try db.sql("CREATE TABLE \(ReturningItem.CRUDTableName) (id UUID PRIMARY KEY, def int DEFAULT 42)")
1308 | let table = db.table(ReturningItem.self)
1309 | let id = UUID()
1310 | var item = ReturningItem(id: id)
1311 | try table.insert(item, ignoreKeys: \.def)
1312 | item.def = 300
1313 | let item0 = try table
1314 | .where(\ReturningItem.id == id)
1315 | .returning(\.def, update: item, ignoreKeys: \.id)
1316 | XCTAssertEqual(item0.count, 1)
1317 | XCTAssertEqual(item.def, item0.first)
1318 | } catch {
1319 | XCTFail("\(error)")
1320 | }
1321 | }
1322 |
1323 | func testEmptyInsert() {
1324 | do {
1325 | let db = try getTestDB()
1326 | struct ReturningItem: Codable, Equatable {
1327 | let id: Int?
1328 | var def: Int?
1329 | init(id: Int, def: Int? = nil) {
1330 | self.id = id
1331 | self.def = def
1332 | }
1333 | }
1334 | try db.sql("DROP TABLE IF EXISTS \(ReturningItem.CRUDTableName)")
1335 | try db.sql("CREATE TABLE \(ReturningItem.CRUDTableName) (id SERIAL PRIMARY KEY, def INT DEFAULT 42)")
1336 | let table = db.table(ReturningItem.self)
1337 |
1338 | try table
1339 | .insert(ReturningItem(id: 0, def: 0),
1340 | ignoreKeys: \ReturningItem.id, \ReturningItem.def)
1341 | _ = try table
1342 | .returning(\.def, insert: ReturningItem(id: 0, def: 0),
1343 | ignoreKeys: \ReturningItem.id, \ReturningItem.def)
1344 | } catch {
1345 | XCTFail("\(error)")
1346 | }
1347 | }
1348 |
1349 | static var allTests = [
1350 | ("testCreate1", testCreate1),
1351 | ("testCreate2", testCreate2),
1352 | ("testCreate3", testCreate3),
1353 | ("testSelectAll", testSelectAll),
1354 | ("testSelectIn", testSelectIn),
1355 | ("testSelectLikeString", testSelectLikeString),
1356 | ("testSelectJoin", testSelectJoin),
1357 | ("testInsert1", testInsert1),
1358 | ("testInsert2", testInsert2),
1359 | ("testInsert3", testInsert3),
1360 | ("testUpdate", testUpdate),
1361 | ("testDelete", testDelete),
1362 | ("testSelectLimit", testSelectLimit),
1363 | ("testSelectLimitWhere", testSelectLimitWhere),
1364 | ("testSelectOrderLimitWhere", testSelectOrderLimitWhere),
1365 | ("testSelectWhereNULL", testSelectWhereNULL),
1366 | ("testPersonThing", testPersonThing),
1367 | ("testStandardJoin", testStandardJoin),
1368 | ("testJunctionJoin", testJunctionJoin),
1369 | ("testSelfJoin", testSelfJoin),
1370 | ("testSelfJunctionJoin", testSelfJunctionJoin),
1371 | ("testCodableProperty", testCodableProperty),
1372 | ("testBadDecoding", testBadDecoding),
1373 | ("testAllPrimTypes1", testAllPrimTypes1),
1374 | ("testAllPrimTypes2", testAllPrimTypes2),
1375 | ("testBespokeSQL", testBespokeSQL),
1376 | ("testModelClasses", testModelClasses),
1377 | ("testURL", testURL),
1378 | ("testManyJoins", testManyJoins),
1379 | ("testDateFormat", testDateFormat),
1380 | ("testAssets", testAssets),
1381 | ("testReturningInsert", testReturningInsert),
1382 | ("testReturningUpdate", testReturningUpdate),
1383 | ("testEmptyInsert", testEmptyInsert)
1384 | ]
1385 | }
1386 |
1387 |
--------------------------------------------------------------------------------
/Tests/PerfectPostgreSQLTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XCTestManifests.swift
3 | //
4 | // Created by Kyle Jessup on 2015-10-19.
5 | // Copyright © 2015 PerfectlySoft. All rights reserved.
6 | //
7 | //===----------------------------------------------------------------------===//
8 | //
9 | // This source file is part of the Perfect.org open source project
10 | //
11 | // Copyright (c) 2015 - 2016 PerfectlySoft Inc. and the Perfect project authors
12 | // Licensed under Apache License v2.0
13 | //
14 | // See http://perfect.org/licensing.html for license information
15 | //
16 | //===----------------------------------------------------------------------===//
17 | //
18 |
19 | import XCTest
20 |
21 | #if !os(OSX)
22 | public func allTests() -> [XCTestCaseEntry] {
23 | return [
24 | testCase(PerfectPostgreSQLTests.allTests)
25 | ]
26 | }
27 | #endif
28 |
--------------------------------------------------------------------------------