diff --git a/Documentation/Index.md b/Documentation/Index.md index 7d13b5a7..a1370cd5 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -50,6 +50,7 @@ - [Date-Time Values](#date-time-values) - [Binary Data](#binary-data) - [Custom Type Caveats](#custom-type-caveats) + - [Codable Types](#codable-types) - [Other Operators](#other-operators) - [Core SQLite Functions](#core-sqlite-functions) - [Aggregate SQLite Functions](#aggregate-sqlite-functions) @@ -72,7 +73,6 @@ [Carthage][] is a simple, decentralized dependency manager for Cocoa. To install SQLite.swift with Carthage: - 1. Make sure Carthage is [installed][Carthage Installation]. 2. Update your Cartfile to include the following: @@ -1296,6 +1296,93 @@ extension Row { } ``` +## Codable Types + +Codable types were introduced as a part of Swift 4 to allow serializing and deserializing types. SQLite.swift +supports the insertion, updating, and retrieval of basic Codable types. + +### Inserting Codable Types + +Queries have a method to allow inserting an Encodable type. + +``` swift +try db.run(users.insert(user)) + +``` + +There are two other parameters also available to this method: + +- `userInfo` is a dictionary that is passed to the encoder and made available to encodable types to allow customizing their behavior. +- `otherSetters` allows you to specify additional setters on top of those that are generated from the encodable types themselves. + +### Updating Codable Types + +Queries have a method to allow updating an Encodable type. + +``` swift +try db.run(users.update(user)) + +``` + +There are two other parameters also available to this method: + +- `userInfo` is a dictionary that is passed to the encoder and made available to encodable types to allow customizing their behavior. +- `otherSetters` allows you to specify additional setters on top of those that are generated from the encodable types themselves. + +### Retrieving Codable Types + +Rows have a method to decode a Decodable type. + +``` swift +let loadedUsers: [User] = try db.prepare(users).map { row in + return try row.decode() +} +``` + +You can also create a decoder to use manually yourself. This can be useful for example if you are using +the [Facade pattern](https://en.wikipedia.org/wiki/Facade_pattern) to hide subclasses behind a super class. +For example, you may want to encode an Image type that can be multiple different formats such as PNGImage, JPGImage, +or HEIFIamge. You will need to determine the correct subclass before you know which type to decode. + +``` swift +enum ImageCodingKeys: String, CodingKey { + case kind +} + +enum ImageKind: Int, Codable { + case png, jpg, heif +} + +let loadedImages: [Image] = try db.prepare(images).map { row in + let decoder = row.decoder() + let container = try decoder.container(keyedBy: ImageCodingKeys.self) + switch try container.decode(ImageKind.self, forKey: .kind) { + case .png: + return try PNGImage(from: decoder) + case .jpg: + return try JPGImage(from: decoder) + case .heif: + return try HEIFImage(from: decoder) + } +} +``` + +Both of the above methods also have the following optional parameter: + +- `userInfo` is a dictionary that is passed to the decoder and made available to decodable types to allow customizing their behavior. + +### Restrictions + +There are a few restrictions on using Codable types: + +- The encodable and decodable objects can only use the following types: + - Int, Bool, Float, Double, String + - Nested Codable types that will be encoded as JSON to a single column +- These methods will not handle object relationships for you. You must write your own Codable and Decodable +implementations if you wish to support this. +- The Codable types may not try to access nested containers or nested unkeyed containers +- The Codable types may not access single value containers or unkeyed containers +- The Codable types may not access super decoders or encoders ## Other Operators diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index c033cf3a..3093b11d 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -97,6 +97,10 @@ 3D67B3FB1DB2470600A4F4C6 /* SQLite-Bridging.h in Headers */ = {isa = PBXBuildFile; fileRef = EE91808D1C46E5230038162A /* SQLite-Bridging.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3D67B3FC1DB2471B00A4F4C6 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3D67B3FD1DB2472D00A4F4C6 /* fts3_tokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */; }; + 49EB68C41F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; + 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; + 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; + 49EB68C71F7B3CB400D89D40 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EB68C31F7B3CB400D89D40 /* Coding.swift */; }; EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE247ADE1C3F04ED00AE3E12 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE247AD31C3F04ED00AE3E12 /* SQLite.framework */; }; EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; @@ -213,6 +217,7 @@ 19A17B93B48B5560E6E51791 /* Fixtures.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fixtures.swift; sourceTree = ""; }; 19A17E2695737FAB5D6086E3 /* fixtures */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = folder; path = fixtures; sourceTree = ""; }; 3D67B3E51DB2469200A4F4C6 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS3.0.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; + 49EB68C31F7B3CB400D89D40 /* Coding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = ""; }; A121AC451CA35C79005A31D1 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EE247AD31C3F04ED00AE3E12 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EE247AD61C3F04ED00AE3E12 /* SQLite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SQLite.h; sourceTree = ""; }; @@ -446,6 +451,7 @@ EE247B001C3F06E900AE3E12 /* Query.swift */, EE247B011C3F06E900AE3E12 /* Schema.swift */, EE247B021C3F06E900AE3E12 /* Setter.swift */, + 49EB68C31F7B3CB400D89D40 /* Coding.swift */, ); path = Typed; sourceTree = ""; @@ -779,6 +785,7 @@ buildActionMask = 2147483647; files = ( 03A65E801C6BB2FB0062603F /* CoreFunctions.swift in Sources */, + 49EB68C61F7B3CB400D89D40 /* Coding.swift in Sources */, 03A65E761C6BB2E60062603F /* Blob.swift in Sources */, 03A65E7D1C6BB2F70062603F /* RTree.swift in Sources */, 03A65E791C6BB2EF0062603F /* SQLite-Bridging.m in Sources */, @@ -834,6 +841,7 @@ buildActionMask = 2147483647; files = ( 3D67B3F91DB246E700A4F4C6 /* SQLite-Bridging.m in Sources */, + 49EB68C71F7B3CB400D89D40 /* Coding.swift in Sources */, 3D67B3F71DB246D700A4F4C6 /* Foundation.swift in Sources */, 3D67B3F81DB246D700A4F4C6 /* Helpers.swift in Sources */, 3D67B3E91DB246D100A4F4C6 /* Statement.swift in Sources */, @@ -862,6 +870,7 @@ buildActionMask = 2147483647; files = ( EE247B0F1C3F06E900AE3E12 /* CoreFunctions.swift in Sources */, + 49EB68C41F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B0A1C3F06E900AE3E12 /* RTree.swift in Sources */, EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */, EE247B0B1C3F06E900AE3E12 /* Foundation.swift in Sources */, @@ -917,6 +926,7 @@ buildActionMask = 2147483647; files = ( EE247B6F1C3F3FEC00AE3E12 /* CoreFunctions.swift in Sources */, + 49EB68C51F7B3CB400D89D40 /* Coding.swift in Sources */, EE247B651C3F3FEC00AE3E12 /* Blob.swift in Sources */, EE247B6C1C3F3FEC00AE3E12 /* RTree.swift in Sources */, EE247B681C3F3FEC00AE3E12 /* SQLite-Bridging.m in Sources */, diff --git a/Sources/SQLite/Typed/Coding.swift b/Sources/SQLite/Typed/Coding.swift new file mode 100644 index 00000000..dd6a4ec2 --- /dev/null +++ b/Sources/SQLite/Typed/Coding.swift @@ -0,0 +1,340 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +extension QueryType { + /// Creates an `INSERT` statement by encoding the given object + /// This method converts any custom nested types to JSON data and does not handle any sort + /// of object relationships. If you want to support relationships between objects you will + /// have to provide your own Encodable implementations that encode the correct ids. + /// + /// - Parameters: + /// + /// - encodable: An encodable object to insert + /// + /// - userInfo: User info to be passed to encoder + /// + /// - otherSetters: Any other setters to include in the insert + /// + /// - Returns: An `INSERT` statement fort the encodable object + public func insert(_ encodable: Encodable, userInfo: [CodingUserInfoKey:Any] = [:], otherSetters: [Setter] = []) throws -> Insert { + let encoder = SQLiteEncoder(userInfo: userInfo) + try encodable.encode(to: encoder) + return self.insert(encoder.setters + otherSetters) + } + + /// Creates an `UPDATE` statement by encoding the given object + /// This method converts any custom nested types to JSON data and does not handle any sort + /// of object relationships. If you want to support relationships between objects you will + /// have to provide your own Encodable implementations that encode the correct ids. + /// + /// - Parameters: + /// + /// - encodable: An encodable object to insert + /// + /// - userInfo: User info to be passed to encoder + /// + /// - otherSetters: Any other setters to include in the insert + /// + /// - Returns: An `UPDATE` statement fort the encodable object + public func update(_ encodable: Encodable, userInfo: [CodingUserInfoKey:Any] = [:], otherSetters: [Setter] = []) throws -> Update { + let encoder = SQLiteEncoder(userInfo: userInfo) + try encodable.encode(to: encoder) + return self.update(encoder.setters + otherSetters) + } +} + +extension Row { + /// Decode an object from this row + /// This method expects any custom nested types to be in the form of JSON data and does not handle + /// any sort of object relationships. If you want to support relationships between objects you will + /// have to provide your own Decodable implementations that decodes the correct columns. + /// + /// - Parameter: userInfo + /// + /// - Returns: a decoded object from this row + public func decode(userInfo: [CodingUserInfoKey: Any] = [:]) throws -> V { + return try V(from: self.decoder(userInfo: userInfo)) + } + + public func decoder(userInfo: [CodingUserInfoKey: Any] = [:]) -> Decoder { + return SQLiteDecoder(row: self, userInfo: userInfo) + } +} + +/// Generates a list of settings for an Encodable object +fileprivate class SQLiteEncoder: Encoder { + class SQLiteKeyedEncodingContainer: KeyedEncodingContainerProtocol { + typealias Key = MyKey + + let encoder: SQLiteEncoder + let codingPath: [CodingKey] = [] + + init(encoder: SQLiteEncoder) { + self.encoder = encoder + } + + func superEncoder() -> Swift.Encoder { + fatalError("SQLiteEncoding does not support super encoders") + } + + func superEncoder(forKey key: Key) -> Swift.Encoder { + fatalError("SQLiteEncoding does not support super encoders") + } + + func encodeNil(forKey key: SQLiteEncoder.SQLiteKeyedEncodingContainer.Key) throws { + self.encoder.setters.append(Expression(key.stringValue) <- nil) + } + + func encode(_ value: Int, forKey key: SQLiteEncoder.SQLiteKeyedEncodingContainer.Key) throws { + self.encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encode(_ value: Bool, forKey key: Key) throws { + self.encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encode(_ value: Float, forKey key: Key) throws { + self.encoder.setters.append(Expression(key.stringValue) <- Double(value)) + } + + func encode(_ value: Double, forKey key: Key) throws { + self.encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encode(_ value: String, forKey key: Key) throws { + self.encoder.setters.append(Expression(key.stringValue) <- value) + } + + func encode(_ value: T, forKey key: Key) throws where T : Swift.Encodable { + if let data = value as? Data { + self.encoder.setters.append(Expression(key.stringValue) <- data) + } + else { + let encoded = try JSONEncoder().encode(value) + let string = String(data: encoded, encoding: .utf8) + self.encoder.setters.append(Expression(key.stringValue) <- string) + } + } + + func encode(_ value: Int8, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an Int8 is not supported")) + } + + func encode(_ value: Int16, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an Int16 is not supported")) + } + + func encode(_ value: Int32, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an Int32 is not supported")) + } + + func encode(_ value: Int64, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an Int64 is not supported")) + } + + func encode(_ value: UInt, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an UInt is not supported")) + } + + func encode(_ value: UInt8, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an UInt8 is not supported")) + } + + func encode(_ value: UInt16, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an UInt16 is not supported")) + } + + func encode(_ value: UInt32, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an UInt32 is not supported")) + } + + func encode(_ value: UInt64, forKey key: Key) throws { + throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: self.codingPath, debugDescription: "encoding an UInt64 is not supported")) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { + fatalError("encoding a nested container is not supported") + } + + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + fatalError("encoding nested values is not supported") + } + } + + fileprivate var setters: [SQLite.Setter] = [] + let codingPath: [CodingKey] = [] + let userInfo: [CodingUserInfoKey: Any] + + init(userInfo: [CodingUserInfoKey: Any]) { + self.userInfo = userInfo + } + + func singleValueContainer() -> SingleValueEncodingContainer { + fatalError("not supported") + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + fatalError("not supported") + } + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + return KeyedEncodingContainer(SQLiteKeyedEncodingContainer(encoder: self)) + } +} + +fileprivate class SQLiteDecoder : Decoder { + class SQLiteKeyedDecodingContainer : KeyedDecodingContainerProtocol { + typealias Key = MyKey + + let codingPath: [CodingKey] = [] + let row: Row + + init(row: Row) { + self.row = row + } + + var allKeys: [Key] { + return self.row.columnNames.keys.flatMap({Key(stringValue: $0)}) + } + + func contains(_ key: Key) -> Bool { + return self.row.hasValue(for: key.stringValue) + } + + func decodeNil(forKey key: Key) throws -> Bool { + return !self.contains(key) + } + + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + return try self.row.get(Expression(key.stringValue)) + } + + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { + return try self.row.get(Expression(key.stringValue)) + } + + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an Int8 is not supported")) + } + + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an Int16 is not supported")) + } + + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an Int32 is not supported")) + } + + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt64 is not supported")) + } + + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt is not supported")) + + } + + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt8 is not supported")) + } + + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt16 is not supported")) + } + + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt32 is not supported")) + } + + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an UInt64 is not supported")) + } + + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { + return Float(try self.row.get(Expression(key.stringValue))) + } + + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { + return try self.row.get(Expression(key.stringValue)) + } + + func decode(_ type: String.Type, forKey key: Key) throws -> String { + return try self.row.get(Expression(key.stringValue)) + } + + func decode(_ type: T.Type, forKey key: Key) throws -> T where T: Swift.Decodable { + if type == Data.self { + let data = try self.row.get(Expression(key.stringValue)) + return data as! T + } + guard let JSONString = try self.row.get(Expression(key.stringValue)) else { + throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: self.codingPath, debugDescription: "an unsupported type was found")) + } + guard let data = JSONString.data(using: .utf8) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "invalid utf8 data found")) + } + return try JSONDecoder().decode(type, from: data) + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding nested containers is not supported")) + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding unkeyed containers is not supported")) + } + + func superDecoder() throws -> Swift.Decoder { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding super encoders containers is not supported")) + } + + func superDecoder(forKey key: Key) throws -> Swift.Decoder { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding super decoders is not supported")) + } + } + + let row: Row + let codingPath: [CodingKey] = [] + let userInfo: [CodingUserInfoKey: Any] + + init(row: Row, userInfo: [CodingUserInfoKey: Any]) { + self.row = row + self.userInfo = userInfo + } + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { + return KeyedDecodingContainer(SQLiteKeyedDecodingContainer(row: self.row)) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding an unkeyed container is not supported")) + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "decoding a single value container is not supported")) + } +} + diff --git a/Sources/SQLite/Typed/Query.swift b/Sources/SQLite/Typed/Query.swift index 8fa2c539..5b778431 100644 --- a/Sources/SQLite/Typed/Query.swift +++ b/Sources/SQLite/Typed/Query.swift @@ -22,6 +22,8 @@ // THE SOFTWARE. // +import Foundation + public protocol QueryType : Expressible { var clauses: QueryClauses { get set } @@ -1027,7 +1029,7 @@ extension Connection { public struct Row { - fileprivate let columnNames: [String: Int] + let columnNames: [String: Int] fileprivate let values: [Binding?] @@ -1036,6 +1038,13 @@ public struct Row { self.values = values } + func hasValue(for column: String) -> Bool { + guard let idx = columnNames[column.quote()] else { + return false + } + return values[idx] != nil + } + /// Returns a row’s value for the given column. /// /// - Parameter column: An expression representing a column selected in a Query. @@ -1134,3 +1143,4 @@ public struct QueryClauses { } } + diff --git a/Tests/SQLiteTests/QueryTests.swift b/Tests/SQLiteTests/QueryTests.swift index 7dde0c53..6ba55d16 100644 --- a/Tests/SQLiteTests/QueryTests.swift +++ b/Tests/SQLiteTests/QueryTests.swift @@ -238,6 +238,29 @@ class QueryTests : XCTestCase { ) } + func test_insert_encodable() throws { + let emails = Table("emails") + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) + let insert = try emails.insert(value) + AssertSQL( + "INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\") VALUES (1, '2', 1, 3.0, 4.0)", + insert + ) + } + + func test_insert_encodable_with_nested_encodable() throws { + let emails = Table("emails") + let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: "optional", sub: value1) + let insert = try emails.insert(value) + let encodedJSON = try JSONEncoder().encode(value1) + let encodedJSONString = String(data: encodedJSON, encoding: .utf8)! + AssertSQL( + "INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"optional\", \"sub\") VALUES (1, '2', 1, 3.0, 4.0, 'optional', '\(encodedJSONString)')", + insert + ) + } + func test_update_compilesUpdateExpression() { AssertSQL( "UPDATE \"users\" SET \"age\" = 30, \"admin\" = 1 WHERE (\"id\" = 1)", @@ -252,6 +275,29 @@ class QueryTests : XCTestCase { ) } + func test_update_encodable() throws { + let emails = Table("emails") + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) + let update = try emails.update(value) + AssertSQL( + "UPDATE \"emails\" SET \"int\" = 1, \"string\" = '2', \"bool\" = 1, \"float\" = 3.0, \"double\" = 4.0", + update + ) + } + + func test_update_encodable_with_nested_encodable() throws { + let emails = Table("emails") + let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) + let value = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: value1) + let update = try emails.update(value) + let encodedJSON = try JSONEncoder().encode(value1) + let encodedJSONString = String(data: encodedJSON, encoding: .utf8)! + AssertSQL( + "UPDATE \"emails\" SET \"int\" = 1, \"string\" = '2', \"bool\" = 1, \"float\" = 3.0, \"double\" = 4.0, \"sub\" = '\(encodedJSONString)'", + update + ) + } + func test_delete_compilesDeleteExpression() { AssertSQL( "DELETE FROM \"users\" WHERE (\"id\" = 1)", @@ -356,6 +402,41 @@ class QueryIntegrationTests : SQLiteTestCase { } } + func test_select_codable() throws { + let table = Table("codable") + try db.run(table.create { builder in + builder.column(Expression("int")) + builder.column(Expression("string")) + builder.column(Expression("bool")) + builder.column(Expression("float")) + builder.column(Expression("double")) + builder.column(Expression("optional")) + builder.column(Expression("sub")) + }) + + let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, optional: nil, sub: nil) + let value = TestCodable(int: 5, string: "6", bool: true, float: 7, double: 8, optional: "optional", sub: value1) + + try db.run(table.insert(value)) + + let rows = try db.prepare(table) + let values: [TestCodable] = try rows.map({ try $0.decode() }) + XCTAssertEqual(values.count, 1) + XCTAssertEqual(values[0].int, 5) + XCTAssertEqual(values[0].string, "6") + XCTAssertEqual(values[0].bool, true) + XCTAssertEqual(values[0].float, 7) + XCTAssertEqual(values[0].double, 8) + XCTAssertEqual(values[0].optional, "optional") + XCTAssertEqual(values[0].sub?.int, 1) + XCTAssertEqual(values[0].sub?.string, "2") + XCTAssertEqual(values[0].sub?.bool, true) + XCTAssertEqual(values[0].sub?.float, 3) + XCTAssertEqual(values[0].sub?.double, 4) + XCTAssertNil(values[0].sub?.optional) + XCTAssertNil(values[0].sub?.sub) + } + func test_scalar() { XCTAssertEqual(0, try! db.scalar(users.count)) XCTAssertEqual(false, try! db.scalar(users.exists)) diff --git a/Tests/SQLiteTests/TestHelpers.swift b/Tests/SQLiteTests/TestHelpers.swift index b66144b3..4572f6fe 100644 --- a/Tests/SQLiteTests/TestHelpers.swift +++ b/Tests/SQLiteTests/TestHelpers.swift @@ -113,3 +113,23 @@ let table = Table("table") let qualifiedTable = Table("table", database: "main") let virtualTable = VirtualTable("virtual_table") let _view = View("view") // avoid Mac XCTestCase collision + +class TestCodable: Codable { + let int: Int + let string: String + let bool: Bool + let float: Float + let double: Double + let optional: String? + let sub: TestCodable? + + init(int: Int, string: String, bool: Bool, float: Float, double: Double, optional: String?, sub: TestCodable?) { + self.int = int + self.string = string + self.bool = bool + self.float = float + self.double = double + self.optional = optional + self.sub = sub + } +}