Skip to content
Merged
182 changes: 160 additions & 22 deletions Sources/MongoSwift/BSON/BSONValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,75 @@ extension BSONValue where Self: Equatable {
}
}

/// A protocol that numeric `BSONValue`s should conform to. It provides functionality for converting to BSON's native
/// number types.
public protocol BSONNumber: BSONValue {
/// Create an `Int` from this `BSONNumber`.
/// This will return nil if the conversion cannot result in an exact representation.
var intValue: Int? { get }

/// Create an `Int32` from this `BSONNumber`.
/// This will return nil if the conversion cannot result in an exact representation.
var int32Value: Int32? { get }

/// Create an `Int64` from this `BSONNumber`.
/// This will return nil if the conversion cannot result in an exact representation.
var int64Value: Int64? { get }

/// Create a `Double` from this `BSONNumber`.
/// This will return nil if the conversion cannot result in an exact representation.
var doubleValue: Double? { get }

/// Create a `Decimal128` from this `BSONNumber`.
/// This will return nil if the conversion cannot result in an exact representation.
var decimal128Value: Decimal128? { get }
}

/// Default conformance to `BSONNumber` for `BinaryInteger`s.
extension BSONNumber where Self: BinaryInteger {
/// Create an `Int` from this `BinaryInteger`.
/// This will return nil if the conversion cannot result in an exact representation.
public var intValue: Int? { return Int(exactly: self) }

/// Create an `Int32` from this `BinaryInteger`.
/// This will return nil if the conversion cannot result in an exact representation.
public var int32Value: Int32? { return Int32(exactly: self) }

/// Create an `Int64` from this `BinaryInteger`.
/// This will return nil if the conversion cannot result in an exact representation.
public var int64Value: Int64? { return Int64(exactly: self) }

/// Create a `Double` from this `BinaryInteger`.
/// This will return nil if the conversion cannot result in an exact representation.
public var doubleValue: Double? { return Double(exactly: self) }
}

/// Default conformance to `BSONNumber` for `BinaryFloatingPoint`s.
extension BSONNumber where Self: BinaryFloatingPoint {
/// Create an `Int` from this `BinaryFloatingPoint`.
/// This will return nil if the conversion cannot result in an exact representation.
public var intValue: Int? { return Int(exactly: self) }

/// Create an `Int32` from this `BinaryFloatingPoint`.
/// This will return nil if the conversion cannot result in an exact representation.
public var int32Value: Int32? { return Int32(exactly: self) }

/// Create an `Int64` from this `BinaryFloatingPoint`.
/// This will return nil if the conversion cannot result in an exact representation.
public var int64Value: Int64? { return Int64(exactly: self) }

/// Create a `Double` from this `BinaryFloatingPoint`.
/// This will return nil if the conversion cannot result in an exact representation.
public var doubleValue: Double? { return Double(self) }
}

/// Default implementation of `Decimal128` conversions for all `Numeric`s.
extension BSONNumber where Self: Numeric {
/// Create a `Decimal128` from this `Numeric`.
/// This will return nil if the conversion cannot result in an exact representation.
public var decimal128Value: Decimal128? { return Decimal128(String(describing: self)) }
}

/// An extension of `Array` to represent the BSON array type.
extension Array: BSONValue {
public var bsonType: BSONType { return .array }
Expand Down Expand Up @@ -427,7 +496,7 @@ public struct DBPointer: BSONValue, Codable, Equatable {
}

/// A struct to represent the BSON Decimal128 type.
public struct Decimal128: BSONValue, Equatable, Codable, CustomStringConvertible {
public struct Decimal128: BSONNumber, Equatable, Codable, CustomStringConvertible {
public var bsonType: BSONType { return .decimal128 }

public var description: String {
Expand Down Expand Up @@ -502,8 +571,32 @@ public struct Decimal128: BSONValue, Equatable, Codable, CustomStringConvertible
}
}

/// Extension of `Decimal128` to add `BSONNumber` conformance.
/// TODO: implement the missing converters (SWIFT-367)
extension Decimal128 {
/// Create an `Int` from this `Decimal128`.
/// Note: this function is not implemented yet and will always return nil.
public var intValue: Int? { return nil }

/// Create an `Int32` from this `Decimal128`.
/// Note: this function is not implemented yet and will always return nil.
public var int32Value: Int32? { return nil }

/// Create an `Int64` from this `Decimal128`.
/// Note: this function is not implemented yet and will always return nil.
public var int64Value: Int64? { return nil }

/// Create a `Double` from this `Decimal128`.
/// Note: this function is not implemented yet and will always return nil.
public var doubleValue: Double? { return nil }

/// Returns this `Decimal128`.
/// This is implemented as part of `BSONNumber` conformance.
public var decimal128Value: Decimal128? { return self }
}

/// An extension of `Double` to represent the BSON Double type.
extension Double: BSONValue {
extension Double: BSONNumber {
public var bsonType: BSONType { return .double }

public func encode(to storage: DocumentStorage, forKey key: String) throws {
Expand All @@ -524,39 +617,66 @@ extension Double: BSONValue {
}

/// An extension of `Int` to represent the BSON Int32 or Int64 type.
/// The `Int` will be encoded as an Int32 if possible, or an Int64 if necessary.
extension Int: BSONValue {
public var bsonType: BSONType { return self.int32Value != nil ? .int32 : .int64 }
/// On 64-bit systems, `Int` corresponds to a BSON Int64. On 32-bit systems, it corresponds to a BSON Int32.
extension Int: BSONNumber {
/// `Int` corresponds to a BSON int32 or int64 depending upon whether the compilation system is 32 or 64 bit.
/// Use MemoryLayout instead of Int.bitWidth to avoid a compiler warning.
/// See: https://forums.swift.org/t/how-can-i-condition-on-the-size-of-int/9080/4
internal static var bsonType: BSONType {
return MemoryLayout<Int>.size == 4 ? .int32 : .int64
}

public var bsonType: BSONType { return Int.bsonType }

internal var int32Value: Int32? { return Int32(exactly: self) }
internal var int64Value: Int64? { return Int64(exactly: self) }
// Return this `Int` as an `Int32` on 32-bit systems or an `Int64` on 64-bit systems
internal var typedValue: BSONNumber {
if self.bsonType == .int64 {
return Int64(self)
}
return Int32(self)
}

public func encode(to storage: DocumentStorage, forKey key: String) throws {
if let int32 = self.int32Value {
return try int32.encode(to: storage, forKey: key)
try self.typedValue.encode(to: storage, forKey: key)
}

public func bsonEquals(_ other: BSONValue?) -> Bool {
guard let other = other, other.bsonType == self.bsonType else {
return false
}
if let int64 = self.int64Value {
return try int64.encode(to: storage, forKey: key)

if let otherInt = other as? Int {
return self == otherInt
}

throw RuntimeError.internalError(message: "`Int` value \(self) could not be encoded as `Int32` or `Int64`")
switch (self.typedValue, other) {
case let (self32 as Int32, other32 as Int32):
return self32 == other32
case let (self64 as Int64, other64 as Int64):
return self64 == other64
default:
return false
}
}

public static func from(iterator iter: DocumentIterator) throws -> Int {
return try iter.withBSONIterPointer { iterPtr in
// TODO: handle this more gracefully (SWIFT-221)
switch iter.currentType {
case .int32, .int64:
return self.init(Int(bson_iter_int32(iterPtr)))
default:
throw wrongIterTypeError(iter, expected: Int.self)
}
var val: Int?
if Int.bsonType == .int64 {
val = Int(exactly: try Int64.from(iterator: iter))
} else {
val = Int(exactly: try Int32.from(iterator: iter))
}

guard let out = val else {
// This should not occur
throw RuntimeError.internalError(message: "Couldn't read `Int` from Document")
}
return out
}
}

/// An extension of `Int32` to represent the BSON Int32 type.
extension Int32: BSONValue {
extension Int32: BSONNumber {
public var bsonType: BSONType { return .int32 }

public func encode(to storage: DocumentStorage, forKey key: String) throws {
Expand All @@ -574,10 +694,19 @@ extension Int32: BSONValue {
self.init(bson_iter_int32(iterPtr))
}
}

public func bsonEquals(_ other: BSONValue?) -> Bool {
if let other32 = other as? Int32 {
return self == other32
} else if let otherInt = other as? Int {
return self == otherInt.typedValue as? Int32
}
return false
}
}

/// An extension of `Int64` to represent the BSON Int64 type.
extension Int64: BSONValue {
extension Int64: BSONNumber {
public var bsonType: BSONType { return .int64 }

public func encode(to storage: DocumentStorage, forKey key: String) throws {
Expand All @@ -595,6 +724,15 @@ extension Int64: BSONValue {
self.init(bson_iter_int64(iterPtr))
}
}

public func bsonEquals(_ other: BSONValue?) -> Bool {
if let other64 = other as? Int64 {
return self == other64
} else if let otherInt = other as? Int {
return self == otherInt.typedValue as? Int64
}
return false
}
}

/// A struct to represent the BSON Code and CodeWithScope types.
Expand Down
2 changes: 1 addition & 1 deletion Sources/MongoSwift/BSON/CodableNumber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ extension UInt8: CodableNumber {
extension UInt16: CodableNumber {
internal var bsonValue: BSONValue? {
// UInt16 always fits in an Int32
return Int(exactly: self)
return Int32(exactly: self)
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/MongoSwift/BSON/DocumentIterator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ public class DocumentIterator: IteratorProtocol {
.javascript: CodeWithScope.self,
.symbol: Symbol.self,
.javascriptWithScope: CodeWithScope.self,
.int32: Int.self,
.int32: Int.bsonType == .int32 ? Int.self : Int32.self,
.timestamp: Timestamp.self,
.int64: Int64.self,
.int64: Int.bsonType == .int64 ? Int.self : Int64.self,
.decimal128: Decimal128.self,
.minKey: MinKey.self,
.maxKey: MaxKey.self,
Expand Down
9 changes: 5 additions & 4 deletions Sources/MongoSwift/BSON/Overwritable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ extension Bool: Overwritable {

extension Int: Overwritable {
internal func writeToCurrentPosition(of iter: DocumentIterator) throws {
if let int32 = self.int32Value {
switch self.typedValue {
case let int32 as Int32:
return int32.writeToCurrentPosition(of: iter)
} else if let int64 = self.int64Value {
case let int64 as Int64:
return int64.writeToCurrentPosition(of: iter)
default:
throw RuntimeError.internalError(message: "`Int` value \(self) could not be encoded as `Int32` or `Int64`")
}

throw RuntimeError.internalError(message: "`Int` value \(self) could not be encoded as `Int32` or `Int64`")
}
}

Expand Down
16 changes: 10 additions & 6 deletions Sources/MongoSwift/MongoCollection+BulkWrite.swift
Original file line number Diff line number Diff line change
Expand Up @@ -443,18 +443,22 @@ public struct BulkWriteResult {
* - `RuntimeError.internalError` if an unexpected error occurs reading the reply from the server.
*/
fileprivate init(reply: Document, insertedIds: [Int: BSONValue]) throws {
self.deletedCount = try reply.getValue(for: "nRemoved") as? Int ?? 0
self.insertedCount = try reply.getValue(for: "nInserted") as? Int ?? 0
// These values are converted to Int via BSONNumber because they're returned from libmongoc as BSON int32s,
// which are retrieved from documents as Ints on 32-bit systems and Int32s on 64-bit ones. To retrieve them in a
// cross-platform manner, we must convert them this way. Also, regardless of how they are stored in the
// we want to use them as Ints.
self.deletedCount = (try reply.getValue(for: "nRemoved") as? BSONNumber)?.intValue ?? 0
self.insertedCount = (try reply.getValue(for: "nInserted") as? BSONNumber)?.intValue ?? 0
self.insertedIds = insertedIds
self.matchedCount = try reply.getValue(for: "nMatched") as? Int ?? 0
self.modifiedCount = try reply.getValue(for: "nModified") as? Int ?? 0
self.upsertedCount = try reply.getValue(for: "nUpserted") as? Int ?? 0
self.matchedCount = (try reply.getValue(for: "nMatched") as? BSONNumber)?.intValue ?? 0
self.modifiedCount = (try reply.getValue(for: "nModified") as? BSONNumber)?.intValue ?? 0
self.upsertedCount = (try reply.getValue(for: "nUpserted") as? BSONNumber)?.intValue ?? 0

var upsertedIds = [Int: BSONValue]()

if let upserted = try reply.getValue(for: "upserted") as? [Document] {
for upsert in upserted {
guard let index = try upsert.getValue(for: "index") as? Int else {
guard let index = (try upsert.getValue(for: "index") as? BSONNumber)?.intValue else {
throw RuntimeError.internalError(message: "Could not cast upserted index to `Int`")
}
upsertedIds[index] = upsert["_id"]
Expand Down
2 changes: 1 addition & 1 deletion Sources/MongoSwift/MongoCollection+Indexes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public struct IndexModel: Encodable {

/// Gets the default name for this index.
internal var defaultName: String {
return String(cString: mongoc_collection_keys_to_index_string(self.keys.data))
return self.keys.map { k, v in "\(k)_\(v)" }.joined(separator: "_")
}

// Encode own data as well as nested options data
Expand Down
18 changes: 0 additions & 18 deletions Sources/MongoSwift/MongoCollection+Write.swift
Original file line number Diff line number Diff line change
Expand Up @@ -402,24 +402,6 @@ public struct InsertManyResult {
/// Map of the index of the document in `values` to the value of its ID
public let insertedIds: [Int: BSONValue]

/**
* Create an `InsertManyResult` from a reply and map of inserted IDs.
*
* Note: we forgo using a Decodable initializer because we still need to
* explicitly add `insertedIds`.
*
* - Parameters:
* - reply: A `Document` result from `mongoc_collection_insert_many()`
* - insertedIds: Map of inserted IDs
*
* - Throws:
* - `RuntimeError.internalError` if an unexpected error occurs the reading server reply.
*/
fileprivate init(reply: Document, insertedIds: [Int: BSONValue]) throws {
self.insertedCount = try reply.getValue(for: "insertedCount") as? Int ?? 0
self.insertedIds = insertedIds
}

/// Internal initializer used for converting from a `BulkWriteResult` optional to an `InsertManyResult` optional.
internal init?(from result: BulkWriteResult?) {
guard let result = result else {
Expand Down
8 changes: 4 additions & 4 deletions Sources/MongoSwift/SDAM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,11 @@ public struct ServerDescription {
self.opTime = lastWrite["opTime"] as? ObjectId
}

if let minVersion = isMaster["minWireVersion"] as? Int32 {
if let minVersion = (isMaster["minWireVersion"] as? BSONNumber)?.int32Value {
self.minWireVersion = minVersion
}

if let maxVersion = isMaster["maxWireVersion"] as? Int32 {
if let maxVersion = (isMaster["maxWireVersion"] as? BSONNumber)?.int32Value {
self.maxWireVersion = maxVersion
}

Expand All @@ -156,14 +156,14 @@ public struct ServerDescription {
}

self.setName = isMaster["setName"] as? String
self.setVersion = isMaster["setVersion"] as? Int64
self.setVersion = (isMaster["setVersion"] as? BSONNumber)?.int64Value
self.electionId = isMaster["electionId"] as? ObjectId

if let primary = isMaster["primary"] as? String {
self.primary = ConnectionId(primary)
}

self.logicalSessionTimeoutMinutes = isMaster["logicalSessionTimeoutMinutes"] as? Int64
self.logicalSessionTimeoutMinutes = (isMaster["logicalSessionTimeoutMinutes"] as? BSONNumber)?.int64Value
}

/// An internal initializer to create a `ServerDescription` from an OpaquePointer to a
Expand Down
Loading