From 2e1332189e712d5ebcc96e3805d4725fa2f4f86c Mon Sep 17 00:00:00 2001 From: taylorswift Date: Tue, 26 Mar 2024 05:45:37 +0000 Subject: [PATCH 1/7] support ChangeStream --- Sources/BSONABI/BSON.AnyType.swift | 10 +- Sources/BSONABI/BSON.AnyValue.swift | 23 ++- .../BSONABI/Fields/BSON.FieldEncoder.swift | 4 +- Sources/BSONABI/IO/BSON.Input.swift | 4 +- Sources/BSONABI/IO/BSON.Output.swift | 4 +- .../BSONABI/Primitives/BSON.Timestamp.swift | 41 +++++ .../Extensions/BSON.Timestamp (ext).swift | 21 +++ .../Main.DecodeNumeric.swift | 2 +- .../Encodability/BSONEncodable.swift | 4 +- .../Extensions/BSON.Timestamp (ext).swift | 9 + .../BSONReflection/BSON.AnyValue (ext).swift | 8 +- Sources/BSONTests/Main.ValidBSON.swift | 6 +- .../Pipelines/Mongo.ChangeEvent.swift | 0 .../Mongo.ChangeEventIdentifier.swift | 17 ++ .../Pipelines/Mongo.ChangeStreamEncoder.swift | 160 ++++++++++++++++++ .../Mongo.Pipeline.ChangeStream.swift | 8 - .../Pipelines/Mongo.PipelineEncoder.swift | 25 ++- .../CausalConsistency/CausalConsistency.swift | 11 +- .../Transactions/Transactions.swift | 3 +- .../Commands/BSON.Timestamp (ext).swift | 11 ++ .../MongoDriver/Commands/Mongo.Reply.swift | 8 +- .../Commands/Mongo.Timestamp.swift | 74 -------- .../Deployments/Mongo.ClusterTime.swift | 6 +- .../Sessions/Mongo.ReadConcern.Options.swift | 4 +- .../Sessions/Mongo.ReadConcern.Ordering.swift | 6 +- .../MongoDriver/Sessions/Mongo.Session.swift | 5 +- .../Sessions/Mongo.SnapshotSession.swift | 6 +- .../Transactions/Mongo.Transaction.swift | 3 +- .../MongoDriverTests/TestSessionPool.swift | 3 +- 29 files changed, 352 insertions(+), 134 deletions(-) create mode 100644 Sources/BSONABI/Primitives/BSON.Timestamp.swift create mode 100644 Sources/BSONDecoding/Extensions/BSON.Timestamp (ext).swift create mode 100644 Sources/BSONEncoding/Extensions/BSON.Timestamp (ext).swift create mode 100644 Sources/MongoBuiltins/Pipelines/Mongo.ChangeEvent.swift create mode 100644 Sources/MongoBuiltins/Pipelines/Mongo.ChangeEventIdentifier.swift create mode 100644 Sources/MongoBuiltins/Pipelines/Mongo.ChangeStreamEncoder.swift delete mode 100644 Sources/MongoBuiltins/Pipelines/Mongo.Pipeline.ChangeStream.swift create mode 100644 Sources/MongoDriver/Commands/BSON.Timestamp (ext).swift delete mode 100644 Sources/MongoDriver/Commands/Mongo.Timestamp.swift diff --git a/Sources/BSONABI/BSON.AnyType.swift b/Sources/BSONABI/BSON.AnyType.swift index 7282c4ef..d5bc4cf7 100644 --- a/Sources/BSONABI/BSON.AnyType.swift +++ b/Sources/BSONABI/BSON.AnyType.swift @@ -21,7 +21,7 @@ extension BSON case javascriptScope = 0x0F case int32 = 0x10 - case uint64 = 0x11 + case timestamp = 0x11 case int64 = 0x12 case decimal128 = 0x13 @@ -30,6 +30,12 @@ extension BSON } } extension BSON.AnyType +{ + @available(*, deprecated, renamed: "timestamp") + @inlinable public static + var uint64:BSON.AnyType { .timestamp } +} +extension BSON.AnyType { /// Calls ``init(rawValue:)``, but throws a ``TypeError`` instead of returning /// nil. @@ -70,7 +76,7 @@ extension BSON.AnyType case 0x0E: self = .string case 0x0F: self = .javascriptScope case 0x10: self = .int32 - case 0x11: self = .uint64 + case 0x11: self = .timestamp case 0x12: self = .int64 case 0x13: self = .decimal128 case 0xFF: self = .min diff --git a/Sources/BSONABI/BSON.AnyValue.swift b/Sources/BSONABI/BSON.AnyValue.swift index 3844b82c..3911300f 100644 --- a/Sources/BSONABI/BSON.AnyValue.swift +++ b/Sources/BSONABI/BSON.AnyValue.swift @@ -57,7 +57,16 @@ extension BSON /// behavior is not part of the BSON specification, and does not /// affect roundtrippability of BSON documents that are not stored /// in a Mongo database. - case uint64(UInt64) + case timestamp(BSON.Timestamp) + } +} +extension BSON.AnyValue +{ + @available(*, deprecated, renamed: "timestamp(_:)") + @inlinable public static + func uint64(_ value:UInt64) -> Self + { + .timestamp(.init(value)) } } extension BSON.AnyValue:Equatable @@ -89,7 +98,7 @@ extension BSON.AnyValue case .pointer: .pointer case .regex: .regex case .string: .string - case .uint64: .uint64 + case .timestamp: .timestamp } } /// The size of this variant value when encoded. @@ -134,7 +143,7 @@ extension BSON.AnyValue regex.size case .string(let string): string.size - case .uint64: + case .timestamp: 8 } } @@ -182,7 +191,7 @@ extension BSON.AnyValue /// - Returns: /// An integer derived from the payload of this variant /// if it matches one of ``int32(_:)``, ``int64(_:)``, or - /// ``uint64(_:)``, and it can be represented exactly by `T`; + /// ``timestamp(_:)``, and it can be represented exactly by `T`; /// nil otherwise. /// /// The ``decimal128(_:)``, ``double(_:)``, and ``millisecond(_:)`` @@ -215,14 +224,14 @@ extension BSON.AnyValue { throw BSON.IntegerOverflowError.int64(int64) } - case .uint64(let uint64): - if let integer:Integer = .init(exactly: uint64) + case .timestamp(let timestamp): + if let integer:Integer = .init(exactly: timestamp.value) { return integer } else { - throw BSON.IntegerOverflowError.uint64(uint64) + throw BSON.IntegerOverflowError.uint64(timestamp.value) } default: return nil diff --git a/Sources/BSONABI/Fields/BSON.FieldEncoder.swift b/Sources/BSONABI/Fields/BSON.FieldEncoder.swift index 7f76c112..f823ed23 100644 --- a/Sources/BSONABI/Fields/BSON.FieldEncoder.swift +++ b/Sources/BSONABI/Fields/BSON.FieldEncoder.swift @@ -59,9 +59,9 @@ extension BSON.FieldEncoder self[.int32].serialize(integer: int32) } @inlinable public mutating - func encode(uint64:UInt64) + func encode(timestamp:BSON.Timestamp) { - self[.uint64].serialize(integer: uint64) + self[.timestamp].serialize(integer: timestamp.value) } @inlinable public mutating func encode(int64:Int64) diff --git a/Sources/BSONABI/IO/BSON.Input.swift b/Sources/BSONABI/IO/BSON.Input.swift index 85abce20..814eb528 100644 --- a/Sources/BSONABI/IO/BSON.Input.swift +++ b/Sources/BSONABI/IO/BSON.Input.swift @@ -266,8 +266,8 @@ extension BSON.Input case .int32: return .int32(try self.parse(as: Int32.self)) - case .uint64: - return .uint64(try self.parse(as: UInt64.self)) + case .timestamp: + return .timestamp(.init(try self.parse(as: UInt64.self))) case .int64: return .int64(try self.parse(as: Int64.self)) diff --git a/Sources/BSONABI/IO/BSON.Output.swift b/Sources/BSONABI/IO/BSON.Output.swift index 3b9fd32f..a78becdb 100644 --- a/Sources/BSONABI/IO/BSON.Output.swift +++ b/Sources/BSONABI/IO/BSON.Output.swift @@ -115,8 +115,8 @@ extension BSON.Output case .int32(let int32): self.serialize(integer: int32) - case .uint64(let uint64): - self.serialize(integer: uint64) + case .timestamp(let timestamp): + self.serialize(integer: timestamp.value) case .int64(let int64): self.serialize(integer: int64) diff --git a/Sources/BSONABI/Primitives/BSON.Timestamp.swift b/Sources/BSONABI/Primitives/BSON.Timestamp.swift new file mode 100644 index 00000000..657aba3c --- /dev/null +++ b/Sources/BSONABI/Primitives/BSON.Timestamp.swift @@ -0,0 +1,41 @@ +extension BSON +{ + @frozen public + struct Timestamp:Equatable, Hashable, Sendable + { + public + var value:UInt64 + + @inlinable public + init(_ value:UInt64) + { + self.value = value + } + } +} +extension BSON.Timestamp +{ + @inlinable public static + var max:Self { .init(.max) } + + @inlinable public static + var min:Self { .init(.min) } +} +extension BSON.Timestamp:Comparable +{ + @inlinable public static + func < (a:Self, b:Self) -> Bool { a.value < b.value } +} +extension BSON.Timestamp:ExpressibleByIntegerLiteral +{ + @inlinable public + init(integerLiteral:UInt64) { self.init(integerLiteral) } +} +extension BSON.Timestamp:CustomStringConvertible +{ + public + var description:String + { + "\(self.value >> 32)+\(self.value & 0x0000_0000_ffff_ffff)" + } +} diff --git a/Sources/BSONDecoding/Extensions/BSON.Timestamp (ext).swift b/Sources/BSONDecoding/Extensions/BSON.Timestamp (ext).swift new file mode 100644 index 00000000..4a083db0 --- /dev/null +++ b/Sources/BSONDecoding/Extensions/BSON.Timestamp (ext).swift @@ -0,0 +1,21 @@ +extension BSON.Timestamp:BSONDecodable +{ + /// Attempts to cast a BSON variant backed by some storage type to a + /// MongoDB timestamp. The conversion is not a integer case, and will + /// succeed if and only if the variant has type ``BSON.AnyType/uint64``. + @inlinable public + init(bson:BSON.AnyValue) throws + { + self = try bson.cast + { + if case .timestamp(let value) = $0 + { + value + } + else + { + nil + } + } + } +} diff --git a/Sources/BSONDecodingTests/Main.DecodeNumeric.swift b/Sources/BSONDecodingTests/Main.DecodeNumeric.swift index 015230dc..21d0d680 100644 --- a/Sources/BSONDecodingTests/Main.DecodeNumeric.swift +++ b/Sources/BSONDecodingTests/Main.DecodeNumeric.swift @@ -16,7 +16,7 @@ extension Main.DecodeNumeric:TestBattery [ "int32": .int32(0x7fff_ffff), "int64": .int64(0x7fff_ffff_ffff_ffff), - "uint64": .uint64(0x7fff_ffff_ffff_ffff), + "uint64": .timestamp(0x7fff_ffff_ffff_ffff), ] Self.run(tests / "int32-to-uint8", bson: bson, diff --git a/Sources/BSONEncoding/Encodability/BSONEncodable.swift b/Sources/BSONEncoding/Encodability/BSONEncodable.swift index 60493d7f..193f7dd5 100644 --- a/Sources/BSONEncoding/Encodability/BSONEncodable.swift +++ b/Sources/BSONEncoding/Encodability/BSONEncodable.swift @@ -111,7 +111,7 @@ extension UInt64:BSONEncodable @inlinable public func encode(to field:inout BSON.FieldEncoder) { - field.encode(uint64: self) + field.encode(timestamp: .init(self)) } } @available(*, deprecated, @@ -122,6 +122,6 @@ extension UInt:BSONEncodable @inlinable public func encode(to field:inout BSON.FieldEncoder) { - field.encode(uint64: .init(self)) + field.encode(timestamp: .init(UInt64.init(self))) } } diff --git a/Sources/BSONEncoding/Extensions/BSON.Timestamp (ext).swift b/Sources/BSONEncoding/Extensions/BSON.Timestamp (ext).swift new file mode 100644 index 00000000..4882468b --- /dev/null +++ b/Sources/BSONEncoding/Extensions/BSON.Timestamp (ext).swift @@ -0,0 +1,9 @@ +extension BSON.Timestamp:BSONEncodable +{ + /// Encodes this timestamp as a ``BSON.AnyValue/uint64(_:)``. + @inlinable public + func encode(to field:inout BSON.FieldEncoder) + { + field.encode(timestamp: self) + } +} diff --git a/Sources/BSONReflection/BSON.AnyValue (ext).swift b/Sources/BSONReflection/BSON.AnyValue (ext).swift index 81b8bcf8..126d1bdb 100644 --- a/Sources/BSONReflection/BSON.AnyValue (ext).swift +++ b/Sources/BSONReflection/BSON.AnyValue (ext).swift @@ -42,8 +42,8 @@ extension BSON.AnyValue "\(regex)" case .string(let utf8): "\"\(utf8)\"" - case .uint64(let uint64): - "\(uint64) as UInt64" + case .timestamp(let timestamp): + "\(timestamp)" } } } @@ -117,7 +117,7 @@ extension BSON.AnyValue lhs == rhs case (.string (let lhs), .string (let rhs)): lhs == rhs - case (.uint64 (let lhs), .uint64 (let rhs)): + case (.timestamp (let lhs), .timestamp (let rhs)): lhs == rhs default: @@ -156,7 +156,7 @@ extension BSON.AnyValue .pointer, .regex, .string, - .uint64: + .timestamp: self } } diff --git a/Sources/BSONTests/Main.ValidBSON.swift b/Sources/BSONTests/Main.ValidBSON.swift index 4a102d1b..7fe1bab1 100644 --- a/Sources/BSONTests/Main.ValidBSON.swift +++ b/Sources/BSONTests/Main.ValidBSON.swift @@ -113,15 +113,15 @@ extension Main.ValidBSON:TestBattery { Self.run(tests / "(123456789, 42)", canonical: "100000001161002A00000015CD5B0700", - expected: ["a": .uint64(123456789 << 32 | 42)]) + expected: ["a": .timestamp(.init(123456789 << 32 | 42))]) Self.run(tests / "ones", canonical: "10000000116100FFFFFFFFFFFFFFFF00", - expected: ["a": .uint64(.max)]) + expected: ["a": .timestamp(.max)]) Self.run(tests / "(4000000000, 4000000000)", canonical: "1000000011610000286BEE00286BEE00", - expected: ["a": .uint64(4000000000 << 32 | 4000000000)]) + expected: ["a": .timestamp(.init(4000000000 << 32 | 4000000000))]) } // https://github.com/mongodb/specifications/blob/master/source/bson-corpus/tests/top.json diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEvent.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEvent.swift new file mode 100644 index 00000000..e69de29b diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEventIdentifier.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEventIdentifier.swift new file mode 100644 index 00000000..3c1a8efb --- /dev/null +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEventIdentifier.swift @@ -0,0 +1,17 @@ +import BSON + +extension Mongo +{ + @frozen public + struct ChangeEventIdentifier:RawRepresentable, BSONDecodable, BSONEncodable, Sendable + { + public + var rawValue:BSON.Document + + @inlinable public + init(rawValue:BSON.Document) + { + self.rawValue = rawValue + } + } +} diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeStreamEncoder.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeStreamEncoder.swift new file mode 100644 index 00000000..57e708fb --- /dev/null +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeStreamEncoder.swift @@ -0,0 +1,160 @@ +import BSON + +extension Mongo +{ + @frozen public + struct ChangeStreamEncoder:Sendable + { + @usableFromInline + var bson:BSON.DocumentEncoder + + @inlinable internal + init(bson:BSON.DocumentEncoder) + { + self.bson = bson + } + } +} +extension Mongo.ChangeStreamEncoder:BSON.Encoder +{ + @inlinable public + init(_ output:consuming BSON.Output) + { + self.init(bson: .init(output)) + } + + @inlinable public consuming + func move() -> BSON.Output { self.bson.move() } + + @inlinable public static + var type:BSON.AnyType { .document } +} + +extension Mongo.ChangeStreamEncoder +{ + @frozen public + enum Flag:String, Sendable + { + case allChangesForCluster + case showExpandedEvents + } + + @inlinable public + subscript(key:Flag) -> Bool? + { + get + { + nil + } + set(value) + { + value?.encode(to: &self.bson[with: key]) + } + } +} + +extension Mongo.ChangeStreamEncoder +{ + @frozen public + enum FullDocument:String, Sendable + { + case fullDocument + + @frozen public + enum Option:String, BSONDecodable, BSONEncodable, Sendable + { + case `default` + case required + case updateLookup + case whenAvailable + } + } + + @inlinable public + subscript(key:FullDocument) -> FullDocument.Option? + { + get + { + nil + } + set(value) + { + value?.encode(to: &self.bson[with: key]) + } + } +} + +extension Mongo.ChangeStreamEncoder +{ + @frozen public + enum FullDocumentBeforeChange:String, Sendable + { + case fullDocumentBeforeChange + + @frozen public + enum Option:String, BSONDecodable, BSONEncodable, Sendable + { + case off + case whenAvailable + case required + } + } + + @inlinable public + subscript(key:FullDocumentBeforeChange) -> FullDocumentBeforeChange.Option? + { + get + { + nil + } + set(value) + { + value?.encode(to: &self.bson[with: key]) + } + } +} + +extension Mongo.ChangeStreamEncoder +{ + @frozen public + enum ResumeToken:String, Sendable + { + case resumeAfter + case startAfter + } + + @inlinable public + subscript(key:ResumeToken) -> Mongo.ChangeEventIdentifier? + { + get + { + nil + } + set(value) + { + value?.encode(to: &self.bson[with: key]) + } + } +} + +extension Mongo.ChangeStreamEncoder +{ + @frozen public + enum StartAtOperationTime:String, Sendable + { + case startAtOperationTime + } + + @inlinable public + subscript(key:StartAtOperationTime) -> BSON.Timestamp? + { + get + { + nil + } + set(value) + { + value?.encode(to: &self.bson[with: key]) + } + } +} diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.Pipeline.ChangeStream.swift b/Sources/MongoBuiltins/Pipelines/Mongo.Pipeline.ChangeStream.swift deleted file mode 100644 index d532e10f..00000000 --- a/Sources/MongoBuiltins/Pipelines/Mongo.Pipeline.ChangeStream.swift +++ /dev/null @@ -1,8 +0,0 @@ -extension Mongo.Pipeline -{ - @frozen public - enum ChangeStream:String, Hashable, Sendable - { - case changeStream = "$changeStream" - } -} diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.PipelineEncoder.swift b/Sources/MongoBuiltins/Pipelines/Mongo.PipelineEncoder.swift index 25514ee9..d52e6a94 100644 --- a/Sources/MongoBuiltins/Pipelines/Mongo.PipelineEncoder.swift +++ b/Sources/MongoBuiltins/Pipelines/Mongo.PipelineEncoder.swift @@ -215,15 +215,32 @@ extension Mongo.PipelineEncoder } } } +} - @available(*, unavailable, message: "unimplemented") - @inlinable public - subscript(stage key:Mongo.Pipeline.ChangeStream) -> Never? +extension Mongo.PipelineEncoder +{ + @frozen public + enum ChangeStream:String, Sendable { - nil + case changeStream = "$changeStream" } + @inlinable public + subscript(stage key:ChangeStream, + yield:(inout Mongo.ChangeStreamEncoder) -> ()) -> Void + { + mutating get + { + self.list(using: ChangeStream.self) + { + yield(&$0[with: key][as: Mongo.ChangeStreamEncoder.self]) + } + } + } +} +extension Mongo.PipelineEncoder +{ @available(*, deprecated, renamed: "subscript(stage:)") @inlinable public subscript(key:Mongo.Pipeline.CollectionStats) -> Mongo.CollectionStatsDocument? diff --git a/Sources/MongoDBTests/CausalConsistency/CausalConsistency.swift b/Sources/MongoDBTests/CausalConsistency/CausalConsistency.swift index 9b61b3be..9b2e2ad2 100644 --- a/Sources/MongoDBTests/CausalConsistency/CausalConsistency.swift +++ b/Sources/MongoDBTests/CausalConsistency/CausalConsistency.swift @@ -1,3 +1,4 @@ +import BSON import MongoDB import MongoTesting @@ -44,7 +45,7 @@ struct CausalConsistency:MongoTestBattery // We should be able to observe a precondition time after performing the // initialization. - guard var head:Mongo.Timestamp = tests.expect(value: session.preconditionTime) + guard var head:BSON.Timestamp = tests.expect(value: session.preconditionTime) else { return @@ -110,7 +111,7 @@ struct CausalConsistency:MongoTestBattery // We should still be able to observe a precondition time, and the // value of that time should be greater than it was before we inserted // the letter `b` into the collection. - if let time:Mongo.Timestamp = tests.expect(value: session.preconditionTime) + if let time:BSON.Timestamp = tests.expect(value: session.preconditionTime) { tests.expect(true: head < time) head = time @@ -132,7 +133,7 @@ struct CausalConsistency:MongoTestBattery tests.expect(response ==? .init(inserted: 1)) } - if let time:Mongo.Timestamp = tests.expect(value: session.preconditionTime) + if let time:BSON.Timestamp = tests.expect(value: session.preconditionTime) { tests.expect(true: head < time) head = time @@ -155,7 +156,7 @@ struct CausalConsistency:MongoTestBattery tests.expect(response ==? .init(inserted: 1)) } - if let time:Mongo.Timestamp = tests.expect(value: session.preconditionTime) + if let time:BSON.Timestamp = tests.expect(value: session.preconditionTime) { tests.expect(true: head < time) head = time @@ -250,7 +251,7 @@ struct CausalConsistency:MongoTestBattery against: database, on: secondary) - guard let time:Mongo.Timestamp = tests.expect(value: other.preconditionTime) + guard let time:BSON.Timestamp = tests.expect(value: other.preconditionTime) else { return diff --git a/Sources/MongoDBTests/Transactions/Transactions.swift b/Sources/MongoDBTests/Transactions/Transactions.swift index d238c57c..427052ec 100644 --- a/Sources/MongoDBTests/Transactions/Transactions.swift +++ b/Sources/MongoDBTests/Transactions/Transactions.swift @@ -1,3 +1,4 @@ +import BSON import MongoDB import MongoTesting @@ -68,7 +69,7 @@ struct Transactions:MongoTestBattery where Configuration:MongoTes // We should be able to observe a precondition time associated with // this transaction, because we have used its underlying session // before. - let _:Mongo.Timestamp? = tests.expect(value: transaction.preconditionTime) + let _:BSON.Timestamp? = tests.expect(value: transaction.preconditionTime) // We should be able to start a transaction with a write command, // even though it also has a non-nil precondition time. await (tests ! "insert").do diff --git a/Sources/MongoDriver/Commands/BSON.Timestamp (ext).swift b/Sources/MongoDriver/Commands/BSON.Timestamp (ext).swift new file mode 100644 index 00000000..d7911fc8 --- /dev/null +++ b/Sources/MongoDriver/Commands/BSON.Timestamp (ext).swift @@ -0,0 +1,11 @@ +import BSON + +extension Mongo +{ + @available(*, deprecated, renamed: "BSON.Timestamp") + public + typealias Timestamp = BSON.Timestamp +} +extension BSON.Timestamp:Mongo.Instant +{ +} diff --git a/Sources/MongoDriver/Commands/Mongo.Reply.swift b/Sources/MongoDriver/Commands/Mongo.Reply.swift index 398b2958..511d1614 100644 --- a/Sources/MongoDriver/Commands/Mongo.Reply.swift +++ b/Sources/MongoDriver/Commands/Mongo.Reply.swift @@ -10,12 +10,12 @@ extension Mongo let result:Result, any Error> @usableFromInline internal - let operationTime:Timestamp? + let operationTime:BSON.Timestamp? @usableFromInline internal let clusterTime:ClusterTime? init(result:Result, any Error>, - operationTime:Timestamp?, + operationTime:BSON.Timestamp?, clusterTime:ClusterTime?) { self.result = result @@ -51,8 +51,8 @@ extension Mongo.Reply let status:Status = try bson["ok"].decode( to: Status.self) - let operationTime:Mongo.Timestamp? = try bson["operationTime"]?.decode( - to: Mongo.Timestamp.self) + let operationTime:BSON.Timestamp? = try bson["operationTime"]?.decode( + to: BSON.Timestamp.self) let clusterTime:Mongo.ClusterTime? = try bson["$clusterTime"]?.decode( to: Mongo.ClusterTime.self) diff --git a/Sources/MongoDriver/Commands/Mongo.Timestamp.swift b/Sources/MongoDriver/Commands/Mongo.Timestamp.swift deleted file mode 100644 index 7271be7d..00000000 --- a/Sources/MongoDriver/Commands/Mongo.Timestamp.swift +++ /dev/null @@ -1,74 +0,0 @@ -import BSON - -extension Mongo -{ - /// A typed wrapper around a BSON ``UInt64`` value. Unlike - /// ``UInt64`` itself, this type’s ``BSONDecodable`` and - /// ``BSONEncodable`` implementations only use the - /// ``BSON.AnyType/uint64`` data type, and will fail on all other - /// BSON integer types. - /// - /// Despite its name, this is not a true ``InstantProtocol``, - /// because it does not support measuring or advancing by a - /// duration. MongoDB timestamps can only be compared for - /// ordering or equality. - @frozen public - struct Timestamp:Hashable, Sendable - { - /// The raw BSON timestamp value. - public - let value:UInt64 - - @inlinable public - init(_ value:UInt64) - { - self.value = value - } - } -} -extension Mongo.Timestamp:Mongo.Instant, Comparable -{ - @inlinable public static - func < (lhs:Self, rhs:Self) -> Bool - { - lhs.value < rhs.value - } -} -extension Mongo.Timestamp:BSONDecodable -{ - /// Attempts to cast a BSON variant backed by some storage type to a - /// MongoDB timestamp. The conversion is not a integer case, and will - /// succeed if and only if the variant has type ``BSON.AnyType/uint64``. - @inlinable public - init(bson:BSON.AnyValue) throws - { - self.init(try bson.cast - { - if case .uint64(let value) = $0 - { - value - } - else - { - nil - } - }) - } -} -extension Mongo.Timestamp:BSONEncodable -{ - /// Encodes this timestamp as a ``BSON.AnyValue/uint64(_:)``. - @inlinable public - func encode(to field:inout BSON.FieldEncoder) - { - field.encode(uint64: self.value) - } -} -extension Mongo.Timestamp:CustomStringConvertible -{ - public - var description:String - { - "\(self.value >> 32)+\(self.value & 0x0000_0000_ffff_ffff)" - } -} diff --git a/Sources/MongoDriver/Deployments/Mongo.ClusterTime.swift b/Sources/MongoDriver/Deployments/Mongo.ClusterTime.swift index 3d03b7e9..8111b38e 100644 --- a/Sources/MongoDriver/Deployments/Mongo.ClusterTime.swift +++ b/Sources/MongoDriver/Deployments/Mongo.ClusterTime.swift @@ -6,11 +6,11 @@ extension Mongo struct ClusterTime:Sendable { public - let timestamp:Timestamp + let timestamp:BSON.Timestamp let signature:BSON.Document @usableFromInline - init(timestamp:Timestamp, signature:BSON.Document) + init(timestamp:BSON.Timestamp, signature:BSON.Document) { self.signature = signature self.timestamp = timestamp @@ -48,7 +48,7 @@ extension Mongo.ClusterTime:BSONDecodable, BSONDocumentDecodable @inlinable public init(bson:BSON.DocumentDecoder) throws { - self.init(timestamp: try bson[.clusterTime].decode(to: Mongo.Timestamp.self), + self.init(timestamp: try bson[.clusterTime].decode(to: BSON.Timestamp.self), signature: try bson[.signature].decode(to: BSON.Document.self)) } } diff --git a/Sources/MongoDriver/Sessions/Mongo.ReadConcern.Options.swift b/Sources/MongoDriver/Sessions/Mongo.ReadConcern.Options.swift index e915c017..470e04cf 100644 --- a/Sources/MongoDriver/Sessions/Mongo.ReadConcern.Options.swift +++ b/Sources/MongoDriver/Sessions/Mongo.ReadConcern.Options.swift @@ -17,12 +17,12 @@ extension Mongo.ReadConcern } extension Mongo.ReadConcern.Options { - init(level:Mongo.ReadConcern.Level?, after clusterTime:Mongo.Timestamp?) + init(level:Mongo.ReadConcern.Level?, after clusterTime:BSON.Timestamp?) { self.init(ordering: clusterTime.map(Mongo.ReadConcern.Ordering.after(_:)), level: level) } - init(level:Mongo.ReadConcern.Level?, at clusterTime:Mongo.Timestamp?) + init(level:Mongo.ReadConcern.Level?, at clusterTime:BSON.Timestamp?) { self.init(ordering: clusterTime.map(Mongo.ReadConcern.Ordering.at(_:)), level: level) diff --git a/Sources/MongoDriver/Sessions/Mongo.ReadConcern.Ordering.swift b/Sources/MongoDriver/Sessions/Mongo.ReadConcern.Ordering.swift index 3a72195c..40d6eb94 100644 --- a/Sources/MongoDriver/Sessions/Mongo.ReadConcern.Ordering.swift +++ b/Sources/MongoDriver/Sessions/Mongo.ReadConcern.Ordering.swift @@ -1,8 +1,10 @@ +import BSON + extension Mongo.ReadConcern { enum Ordering:Sendable { - case after(Mongo.Timestamp) - case at(Mongo.Timestamp) + case after(BSON.Timestamp) + case at(BSON.Timestamp) } } diff --git a/Sources/MongoDriver/Sessions/Mongo.Session.swift b/Sources/MongoDriver/Sessions/Mongo.Session.swift index 1a034f19..948b90f8 100644 --- a/Sources/MongoDriver/Sessions/Mongo.Session.swift +++ b/Sources/MongoDriver/Sessions/Mongo.Session.swift @@ -1,3 +1,4 @@ +import BSON import Durations import MongoCommands @@ -38,7 +39,7 @@ extension Mongo /// This is simply a long-winded way of saying that time never moves /// backward when running commands on a session. public private(set) - var preconditionTime:Timestamp? + var preconditionTime:BSON.Timestamp? var notarizedTime:ClusterTime? /// The current transaction state of this session. @@ -587,7 +588,7 @@ extension Mongo.Session /// of this session, and is used by the driver to estimate its /// freshness. @usableFromInline internal - func combine(operationTime:Mongo.Timestamp?, + func combine(operationTime:BSON.Timestamp?, clusterTime:Mongo.ClusterTime?, reuse:Bool, sent:ContinuousClock.Instant) diff --git a/Sources/MongoDriver/Sessions/Mongo.SnapshotSession.swift b/Sources/MongoDriver/Sessions/Mongo.SnapshotSession.swift index e4a301aa..59f4f9dc 100644 --- a/Sources/MongoDriver/Sessions/Mongo.SnapshotSession.swift +++ b/Sources/MongoDriver/Sessions/Mongo.SnapshotSession.swift @@ -1,10 +1,12 @@ +import BSON + extension Mongo { public final class SnapshotSession:Identifiable { public private(set) - var snapshotTime:Timestamp + var snapshotTime:BSON.Timestamp public private(set) var transaction:TransactionState private @@ -21,7 +23,7 @@ extension Mongo let pool:SessionPool private - init(snapshotTime:Timestamp, allocation:SessionPool.Allocation, pool:SessionPool) + init(snapshotTime:BSON.Timestamp, allocation:SessionPool.Allocation, pool:SessionPool) { self.snapshotTime = snapshotTime self.transaction = allocation.transaction diff --git a/Sources/MongoDriver/Transactions/Mongo.Transaction.swift b/Sources/MongoDriver/Transactions/Mongo.Transaction.swift index 3bce9868..3e3c0dd7 100644 --- a/Sources/MongoDriver/Transactions/Mongo.Transaction.swift +++ b/Sources/MongoDriver/Transactions/Mongo.Transaction.swift @@ -1,3 +1,4 @@ +import BSON import MongoCommands extension Mongo @@ -35,7 +36,7 @@ extension Mongo.Transaction extension Mongo.Transaction { @inlinable public - var preconditionTime:Mongo.Timestamp? + var preconditionTime:BSON.Timestamp? { self.session.preconditionTime } diff --git a/Sources/MongoDriverTests/TestSessionPool.swift b/Sources/MongoDriverTests/TestSessionPool.swift index b08e69ba..e46df5ea 100644 --- a/Sources/MongoDriverTests/TestSessionPool.swift +++ b/Sources/MongoDriverTests/TestSessionPool.swift @@ -1,3 +1,4 @@ +import BSON import MongoDriver import NIOPosix import Testing @@ -125,7 +126,7 @@ func TestSessionPool(_ tests:TestGroup, bootstrap:Mongo.DriverBootstrap, if !single { - let _:Mongo.Timestamp? = tests.expect(value: b.preconditionTime) + let _:BSON.Timestamp? = tests.expect(value: b.preconditionTime) } try await a.refresh() From c9bb812f428e82b5828e2339636fc88bd293d99b Mon Sep 17 00:00:00 2001 From: taylorswift Date: Tue, 26 Mar 2024 19:38:45 +0000 Subject: [PATCH 2/7] lint deprecated APIs --- .../Pipelines/Mongo.PipelineEncoder.swift | 387 +++++------------- 1 file changed, 97 insertions(+), 290 deletions(-) diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.PipelineEncoder.swift b/Sources/MongoBuiltins/Pipelines/Mongo.PipelineEncoder.swift index d52e6a94..bdefe393 100644 --- a/Sources/MongoBuiltins/Pipelines/Mongo.PipelineEncoder.swift +++ b/Sources/MongoBuiltins/Pipelines/Mongo.PipelineEncoder.swift @@ -32,24 +32,6 @@ extension Mongo.PipelineEncoder:BSON.Encoder } extension Mongo.PipelineEncoder { - /// This is a legacy source compatibility aid that does nothing and will be deprecated soon. - @available(*, deprecated, message: "use subscripts instead") - @inlinable public mutating - func stage(_ populate:(inout Self) throws -> ()) rethrows - { - try populate(&self) - } -} -extension Mongo.PipelineEncoder -{ - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Out) -> Mongo.Collection? - { - get { nil } - set (value) { self[stage: key] = value } - } - @inlinable public subscript(stage key:Mongo.Pipeline.Out) -> Mongo.Collection? { @@ -70,15 +52,6 @@ extension Mongo.PipelineEncoder } } - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Out) -> Mongo.Namespaced? - { - get { nil } - set (value) { self[stage: key] = value } - } - @inlinable public subscript(stage key:Mongo.Pipeline.Out) -> Mongo.Namespaced? { @@ -98,16 +71,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Merge) -> Mongo.MergeDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Merge) -> Mongo.MergeDocument? { @@ -128,16 +95,9 @@ extension Mongo.PipelineEncoder } } } + extension Mongo.PipelineEncoder { - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Bucket) -> Mongo.BucketDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - @inlinable public subscript(stage key:Mongo.Pipeline.Bucket) -> Mongo.BucketDocument? { @@ -157,16 +117,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.BucketAuto) -> Mongo.BucketAutoDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.BucketAuto) -> Mongo.BucketAutoDocument? { @@ -186,16 +140,9 @@ extension Mongo.PipelineEncoder } } } - - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Count) -> Mongo.AnyKeyPath? - { - get { nil } - set (value) { self[stage: key] = value } - } - +} +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Count) -> Mongo.AnyKeyPath? { @@ -241,14 +188,6 @@ extension Mongo.PipelineEncoder extension Mongo.PipelineEncoder { - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.CollectionStats) -> Mongo.CollectionStatsDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - @inlinable public subscript(stage key:Mongo.Pipeline.CollectionStats) -> Mongo.CollectionStatsDocument? { @@ -268,16 +207,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.CurrentOperation) -> Mongo.CurrentOperationDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.CurrentOperation) -> Mongo.CurrentOperationDocument? { @@ -297,23 +230,20 @@ extension Mongo.PipelineEncoder } } } +} +extension Mongo.PipelineEncoder +{ @available(*, unavailable, message: "unimplemented") @inlinable public subscript(stage key:Mongo.Pipeline.Densify) -> Never? { nil } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Documents) -> Array? where Array:BSONEncodable - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Documents) -> Array? where Array:BSONEncodable { @@ -333,16 +263,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Facet) -> Mongo.FacetDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Facet) -> Mongo.FacetDocument? { @@ -362,37 +286,37 @@ extension Mongo.PipelineEncoder } } } - +} +extension Mongo.PipelineEncoder +{ @available(*, unavailable, message: "unimplemented") @inlinable public subscript(stage key:Mongo.Pipeline.Fill) -> Never? { nil } - +} +extension Mongo.PipelineEncoder +{ @available(*, unavailable, message: "unimplemented") @inlinable public subscript(stage key:Mongo.Pipeline.GeoNear) -> Never? { nil } - +} +extension Mongo.PipelineEncoder +{ @available(*, unavailable, message: "unimplemented") @inlinable public subscript(stage key:Mongo.Pipeline.GraphLookup) -> Never? { nil } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Group) -> Mongo.GroupDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Group) -> Mongo.GroupDocument? { @@ -412,16 +336,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.IndexStats) -> [String: Never]? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.IndexStats) -> [String: Never]? { @@ -441,16 +359,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Limit) -> Int? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Limit) -> Int? { @@ -470,29 +382,28 @@ extension Mongo.PipelineEncoder } } } - +} +extension Mongo.PipelineEncoder +{ @available(*, unavailable, message: "unimplemented") @inlinable public subscript(stage key:Mongo.Pipeline.ListLocalSessions) -> Never? { nil } +} +extension Mongo.PipelineEncoder +{ @available(*, unavailable, message: "unimplemented") @inlinable public subscript(stage key:Mongo.Pipeline.ListSessions) -> Never? { nil } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Lookup) -> Mongo.LookupDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Lookup) -> Mongo.LookupDocument? { @@ -512,16 +423,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Match) -> Mongo.PredicateDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Match) -> Mongo.PredicateDocument? { @@ -541,16 +446,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.PlanCacheStats) -> [String: Never]? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.PlanCacheStats) -> [String: Never]? { @@ -570,16 +469,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Project) -> Mongo.ProjectionDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Project) -> Mongo.ProjectionDocument? { @@ -599,17 +492,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Redact) -> RedactMode? - where RedactMode:BSONEncodable - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Redact) -> RedactMode? where RedactMode:BSONEncodable @@ -630,17 +516,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.ReplaceWith) -> Document? - where Document:BSONEncodable - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.ReplaceWith) -> Document? where Document:BSONEncodable @@ -662,14 +541,6 @@ extension Mongo.PipelineEncoder } } - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.ReplaceWith) -> Mongo.SetDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - @inlinable public subscript(stage key:Mongo.Pipeline.ReplaceWith) -> Mongo.SetDocument? { @@ -689,16 +560,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Sample) -> Mongo.SampleDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Sample) -> Mongo.SampleDocument? { @@ -718,16 +583,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Set) -> Mongo.SetDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Set) -> Mongo.SetDocument? { @@ -747,15 +606,20 @@ extension Mongo.PipelineEncoder } } } +} +extension Mongo.PipelineEncoder +{ @available(*, unavailable, message: "unimplemented") @inlinable public subscript(stage key:Mongo.Pipeline.SetWindowFields) -> Never? { nil } +} - +extension Mongo.PipelineEncoder +{ @available(*, deprecated, renamed: "subscript(stage:)") @inlinable public subscript(key:Mongo.Pipeline.ShardedDataDistribution) -> [String: Never]? @@ -783,16 +647,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Skip) -> Int? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Skip) -> Int? { @@ -812,16 +670,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Sort) -> Mongo.SortDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Sort) -> Mongo.SortDocument? { @@ -841,17 +693,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.SortByCount) -> GroupKey? - where GroupKey:BSONEncodable - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.SortByCount) -> GroupKey? where GroupKey:BSONEncodable @@ -872,16 +717,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.UnionWith) -> Mongo.Collection? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.UnionWith) -> Mongo.Collection? { @@ -901,15 +740,10 @@ extension Mongo.PipelineEncoder } } } +} - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.UnionWith) -> Mongo.UnionWithDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.UnionWith) -> Mongo.UnionWithDocument? { @@ -929,16 +763,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Unset) -> Mongo.AnyKeyPath? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Unset) -> Mongo.AnyKeyPath? { @@ -959,13 +787,6 @@ extension Mongo.PipelineEncoder } } - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Unset) -> [Mongo.AnyKeyPath] - { - get { [ ] } - set (value) { self[stage: key] = value } - } /// Does nothing if the assigned array is empty. @inlinable public subscript(stage key:Mongo.Pipeline.Unset) -> [Mongo.AnyKeyPath] @@ -990,16 +811,10 @@ extension Mongo.PipelineEncoder } } } +} - - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Unwind) -> Mongo.AnyKeyPath? - { - get { nil } - set (value) { self[stage: key] = value } - } - +extension Mongo.PipelineEncoder +{ @inlinable public subscript(stage key:Mongo.Pipeline.Unwind) -> Mongo.AnyKeyPath? { @@ -1021,14 +836,6 @@ extension Mongo.PipelineEncoder } } - @available(*, deprecated, renamed: "subscript(stage:)") - @inlinable public - subscript(key:Mongo.Pipeline.Unwind) -> Mongo.UnwindDocument? - { - get { nil } - set (value) { self[stage: key] = value } - } - @inlinable public subscript(stage key:Mongo.Pipeline.Unwind) -> Mongo.UnwindDocument? { From 31428c8da436259705ad7d4f03822bf8772f80ce Mon Sep 17 00:00:00 2001 From: taylorswift Date: Wed, 27 Mar 2024 00:11:52 +0000 Subject: [PATCH 3/7] check in another pointless, wide-reaching refactor to work around difficulties with Swift DSLs and typechecking --- Package.swift | 2 + Snippets/BSONNever.swift | 2 +- Snippets/ExampleBucketDocuments.swift | 2 +- Snippets/ExampleExpressions.swift | 22 ++-- .../Encodability/BSONEncodable.swift | 6 - .../Encoding/BSON.DocumentEncoder.swift | 26 ++-- .../Encoding/BSON.FieldEncoder (ext).swift | 10 ++ .../Encoding/BSON.ListEncoder.swift | 35 +----- .../Fields/Main.FieldDuplication.swift | 2 +- .../Fields/Main.FieldElision.swift | 6 +- .../Inference/Main.LiteralInference.swift | 16 +-- .../Inference/Main.TypeInference.swift | 8 +- .../Main.EncodeDocument.swift | 6 +- .../BSONEncodingTests/Main.EncodeList.swift | 2 +- .../BSONEncodingTests/Main.EncodeString.swift | 2 +- .../Main.EnumeratedCodingKeys.swift | 2 +- .../BSONReflectionTests/Main.Documents.swift | 30 ++--- .../Filter/Mongo.FilterDocument.swift | 2 +- .../Documents/Map/Mongo.MapDocument.swift | 4 +- .../Mongo.Namespaced (ext).swift | 2 +- .../Pipelines/Mongo.Pipeline.swift | 2 +- .../Pipelines/Mongo.PipelineEncoder.swift | 66 +++++----- .../Concerns/Mongo.MaxTime.swift | 12 +- .../Concerns/Mongo.WriteConcernError.swift | 2 +- Sources/MongoCommands/Mongo.Command.swift | 13 +- Sources/MongoCommands/Mongo.CommandType.swift | 2 +- .../Commands/Mongo.Aggregate (ext).swift | 6 +- .../MongoDB/Commands/Mongo.Cursor (ext).swift | 2 + Sources/MongoDB/Mongo.Session (ext).swift | 31 +++-- .../MongoDBTests/Aggregate/Aggregate.swift | 44 ++++--- .../ChangeStreams/ChangeStreams.swift | 32 +++++ Sources/MongoDBTests/Main.swift | 1 + .../Mongo.ConfigureFailpoint.swift | 4 +- .../Commands/GetMore/Mongo.GetMore.swift | 6 +- .../Hello/Mongo.Hello.ClientMetadata.swift | 6 +- .../Commands/Mongo.Command (ext).swift | 15 ++- .../Commands/SASL/Mongo.SASLStart.swift | 2 +- .../Connections/Mongo.Connection.swift | 9 +- .../Mongo.ConnectionPool.Allocations.swift | 9 +- .../Mongo.ConnectionPool.Phase.swift | 2 +- .../Connections/Mongo.ConnectionPool.swift | 45 ++++--- .../Connections/Mongo.Connector.swift | 9 +- .../Connections/Mongo.ConnectorFactory.swift | 4 +- .../MongoDriver/Cursors/Mongo.Cursor.swift | 15 +-- .../Cursors/Mongo.CursorIterator.swift | 16 +-- .../Cursors/Mongo.CursorLifecycle.swift | 8 +- .../Deployments/Mongo.Deployment.swift | 10 +- .../Deployments/Mongo.Server.swift | 5 +- Sources/MongoDriver/MongoExecutor (ext).swift | 16 +-- .../Monitoring/Mongo.MonitorPool.swift | 8 +- .../Monitoring/Mongo.Sampler.swift | 10 +- .../Mongo.DeadlineAdjustments.swift | 23 ++++ .../Mongo.DriverTimeoutError.swift | 3 +- ...meout.swift => Mongo.NetworkTimeout.swift} | 18 +-- .../MongoDriver/Sessions/Mongo.Session.swift | 8 +- .../Commands/Aggregate/Mongo.Aggregate.swift | 114 +++++++++--------- .../MongoQL/Commands/Find/Mongo.Find.swift | 2 +- .../Mongo.ListCollections.swift | 24 ++-- .../ListIndexes/Mongo.ListIndexes.swift | 13 +- .../Commands/Mongo.CursorOptions.swift | 31 +++++ 60 files changed, 453 insertions(+), 382 deletions(-) create mode 100644 Sources/BSONEncoding/Encoding/BSON.FieldEncoder (ext).swift create mode 100644 Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift create mode 100644 Sources/MongoDriver/Retryability/Mongo.DeadlineAdjustments.swift rename Sources/MongoDriver/Retryability/{Mongo.Timeout.swift => Mongo.NetworkTimeout.swift} (72%) create mode 100644 Sources/MongoQL/Commands/Mongo.CursorOptions.swift diff --git a/Package.swift b/Package.swift index bce4c8de..442af9f4 100644 --- a/Package.swift +++ b/Package.swift @@ -318,6 +318,8 @@ let package:Package = .init(name: "swift-mongodb", .executableTarget(name: "MongoDBTests", dependencies: [ + .target(name: "MongoDB"), + .target(name: "MongoQL"), .target(name: "MongoTesting"), ]), diff --git a/Snippets/BSONNever.swift b/Snippets/BSONNever.swift index e04f0383..2bffb017 100644 --- a/Snippets/BSONNever.swift +++ b/Snippets/BSONNever.swift @@ -12,7 +12,7 @@ enum InnerCodingKeys:String let bson:BSON.Document = .init(OuterCodingKeys.self) { - $0[.metadata, InnerCodingKeys.self] + $0[.metadata](InnerCodingKeys.self) { $0[.cLanguageStandard] = 1 } diff --git a/Snippets/ExampleBucketDocuments.swift b/Snippets/ExampleBucketDocuments.swift index 721d359c..e654c98b 100644 --- a/Snippets/ExampleBucketDocuments.swift +++ b/Snippets/ExampleBucketDocuments.swift @@ -28,7 +28,7 @@ func ExampleBucketDocuments() $0[.boundaries] = .init { $0.append("$field") - $0 + $0(BSON.Key.self) { $0["x"] = "y" } diff --git a/Snippets/ExampleExpressions.swift b/Snippets/ExampleExpressions.swift index 96c56337..4ccf0756 100644 --- a/Snippets/ExampleExpressions.swift +++ b/Snippets/ExampleExpressions.swift @@ -3,22 +3,22 @@ import MongoQL func ExampleExpressions() { - let _:BSON.Document = .init + let _:BSON.Document = .init(BSON.Key.self) { $0["_"] = [] as [Never] $0["_"] = [:] - $0["_"] + $0["_"](BSON.Key.self) { - $0["_"] + $0["_"](BSON.Key.self) { $0["_"] = [:] } } } - let _:BSON.Document = .init + let _:BSON.Document = .init(BSON.Key.self) { $0["foo"] = "$field" - $0["bar"] + $0["bar"](BSON.Key.self) { $0["ccc"] = 56 } @@ -27,7 +27,7 @@ func ExampleExpressions() $0.append(56) } } - let _:BSON.Document = .init + let _:BSON.Document = .init(BSON.Key.self) { $0["_"] = nil as Never?? $0["_"] = (nil as Never?) as Never?? @@ -39,7 +39,7 @@ func ExampleExpressions() { $0.append(0) } - $0["_"] + $0["_"](BSON.Key.self) { $0["_"] = 0 } @@ -65,7 +65,7 @@ func ExampleExpressions() $0[.abs] = "$field" } } - let _:BSON.Document = .init + let _:BSON.Document = .init(BSON.Key.self) { $0["_"] = .expr { @@ -163,7 +163,7 @@ func ExampleExpressions() $0[.trunc] = ("$field", places: 2) } } - let _:BSON.Document = .init + let _:BSON.Document = .init(BSON.Key.self) { $0["_"] = .expr { @@ -194,7 +194,7 @@ func ExampleExpressions() $0[.ne] = ("$x", 0) } } - let _:BSON.Document = .init + let _:BSON.Document = .init(BSON.Key.self) { $0["_"] = .expr { @@ -238,7 +238,7 @@ func ExampleExpressions() $0[.ne] = ("$x", 0) } } - let _:BSON.Document = .init + let _:BSON.Document = .init(BSON.Key.self) { $0["_"] = .expr { diff --git a/Sources/BSONEncoding/Encodability/BSONEncodable.swift b/Sources/BSONEncoding/Encodability/BSONEncodable.swift index 193f7dd5..2884e433 100644 --- a/Sources/BSONEncoding/Encodability/BSONEncodable.swift +++ b/Sources/BSONEncoding/Encodability/BSONEncodable.swift @@ -73,12 +73,6 @@ extension BSONEncodable where Self == BSON.Document self.init() try populate(&self.output[as: BSON.DocumentEncoder.self]) } - @inlinable public - init(with populate:(inout BSON.DocumentEncoder) throws -> ()) rethrows - { - self.init() - try populate(&self.output[as: BSON.DocumentEncoder.self]) - } } extension BSONEncodable where Self == BSON.List { diff --git a/Sources/BSONEncoding/Encoding/BSON.DocumentEncoder.swift b/Sources/BSONEncoding/Encoding/BSON.DocumentEncoder.swift index b4bac973..159e9c1e 100644 --- a/Sources/BSONEncoding/Encoding/BSON.DocumentEncoder.swift +++ b/Sources/BSONEncoding/Encoding/BSON.DocumentEncoder.swift @@ -23,17 +23,17 @@ extension BSON.DocumentEncoder:BSON.Encoder } extension BSON.DocumentEncoder { + @inlinable public + subscript(key:CodingKey) -> BSON.FieldEncoder + { + _read { yield self.output[with: .init(key)] } + _modify { yield &self.output[with: .init(key)] } + } @inlinable public subscript(with key:some RawRepresentable) -> BSON.FieldEncoder { - _read - { - yield self.output[with: .init(key)] - } - _modify - { - yield &self.output[with: .init(key)] - } + _read { yield self.output[with: .init(key)] } + _modify { yield &self.output[with: .init(key)] } } } extension BSON.DocumentEncoder @@ -46,17 +46,9 @@ extension BSON.DocumentEncoder yield(&self[with: key][as: BSON.ListEncoder.self]) } } - @inlinable public - subscript(key:CodingKey, yield:(inout BSON.DocumentEncoder) -> ()) -> Void - { - mutating get - { - yield(&self[with: key][as: BSON.DocumentEncoder.self]) - } - } + @inlinable public subscript(key:CodingKey, - _:NestedKey.Type = NestedKey.self, yield:(inout BSON.DocumentEncoder) -> ()) -> Void { mutating get diff --git a/Sources/BSONEncoding/Encoding/BSON.FieldEncoder (ext).swift b/Sources/BSONEncoding/Encoding/BSON.FieldEncoder (ext).swift new file mode 100644 index 00000000..46300dc5 --- /dev/null +++ b/Sources/BSONEncoding/Encoding/BSON.FieldEncoder (ext).swift @@ -0,0 +1,10 @@ +extension BSON.FieldEncoder +{ + /// A shorthand for binding this field encoder to a ``DocumentEncoder``. + @inlinable public mutating + func callAsFunction(_:CodingKey.Type, + yield:(inout BSON.DocumentEncoder) -> ()) -> Void + { + yield(&self[as: BSON.DocumentEncoder.self]) + } +} diff --git a/Sources/BSONEncoding/Encoding/BSON.ListEncoder.swift b/Sources/BSONEncoding/Encoding/BSON.ListEncoder.swift index b1df4861..dee3a3e4 100644 --- a/Sources/BSONEncoding/Encoding/BSON.ListEncoder.swift +++ b/Sources/BSONEncoding/Encoding/BSON.ListEncoder.swift @@ -65,39 +65,14 @@ extension BSON.ListEncoder extension BSON.ListEncoder { @inlinable public mutating - func callAsFunction(with encode:(inout BSON.ListEncoder) -> ()) + func callAsFunction(_ yield:(inout BSON.ListEncoder) -> ()) { - self.append { encode(&$0[as: BSON.ListEncoder.self]) } + self.append { yield(&$0[as: BSON.ListEncoder.self]) } } @inlinable public mutating - func callAsFunction(with encode:(inout BSON.DocumentEncoder) -> ()) + func callAsFunction(_:CodingKey.Type = CodingKey.self, + _ yield:(inout BSON.DocumentEncoder) -> ()) { - self.append { encode(&$0[as: BSON.DocumentEncoder.self]) } - } - @inlinable public mutating - func callAsFunction(using _:CodingKey.Type = CodingKey.self, - with encode:(inout BSON.DocumentEncoder) -> ()) - { - self.append { encode(&$0[as: BSON.DocumentEncoder.self]) } - } - - @available(*, deprecated, renamed: "callAsFunction(with:)") - @inlinable public mutating - func append(with encode:(inout BSON.ListEncoder) -> ()) - { - self(with: encode) - } - @available(*, deprecated, renamed: "callAsFunction(with:)") - @inlinable public mutating - func append(with encode:(inout BSON.DocumentEncoder) -> ()) - { - self(with: encode) - } - @available(*, deprecated, renamed: "callAsFunction(with:)") - @inlinable public mutating - func append(using _:CodingKey.Type = CodingKey.self, - with encode:(inout BSON.DocumentEncoder) -> ()) - { - self(with: encode) + self.append { yield(&$0[as: BSON.DocumentEncoder.self]) } } } diff --git a/Sources/BSONEncodingTests/Fields/Main.FieldDuplication.swift b/Sources/BSONEncodingTests/Fields/Main.FieldDuplication.swift index 0dcd7ffe..9c115c49 100644 --- a/Sources/BSONEncodingTests/Fields/Main.FieldDuplication.swift +++ b/Sources/BSONEncodingTests/Fields/Main.FieldDuplication.swift @@ -13,7 +13,7 @@ extension Main.FieldDuplication:TestBattery func run(tests:TestGroup) { Self.run(tests / "integer", - encoded: .init + encoded: .init(BSON.Key.self) { $0["inhabited"] = 5 $0["uninhabited"] = nil as Never?? diff --git a/Sources/BSONEncodingTests/Fields/Main.FieldElision.swift b/Sources/BSONEncodingTests/Fields/Main.FieldElision.swift index 2e7906a2..e839fe9d 100644 --- a/Sources/BSONEncodingTests/Fields/Main.FieldElision.swift +++ b/Sources/BSONEncodingTests/Fields/Main.FieldElision.swift @@ -15,7 +15,7 @@ extension Main.FieldElision:TestBattery let _:BSON.Document = [:] Self.run(tests / "null", - encoded: .init + encoded: .init(BSON.Key.self) { $0["elided"] = nil as Never?? $0["inhabited"] = BSON.Null.init() @@ -26,7 +26,7 @@ extension Main.FieldElision:TestBattery ]) Self.run(tests / "integer", - encoded: .init + encoded: .init(BSON.Key.self) { $0["elided"] = nil as Int? $0["inhabited"] = 5 @@ -37,7 +37,7 @@ extension Main.FieldElision:TestBattery ]) Self.run(tests / "optional", - encoded: .init + encoded: .init(BSON.Key.self) { $0["elided"] = nil as Int?? $0["inhabited"] = (5 as Int?) as Int?? diff --git a/Sources/BSONEncodingTests/Inference/Main.LiteralInference.swift b/Sources/BSONEncodingTests/Inference/Main.LiteralInference.swift index 58086bb6..2c49b1bd 100644 --- a/Sources/BSONEncodingTests/Inference/Main.LiteralInference.swift +++ b/Sources/BSONEncodingTests/Inference/Main.LiteralInference.swift @@ -13,7 +13,7 @@ extension Main.LiteralInference:TestBattery func run(tests:TestGroup) { Self.run(tests / "integer", - encoded: .init + encoded: .init(BSON.Key.self) { $0["default"] = 1 $0["default-long"] = 0x7fff_ffff_ffff_ffff @@ -33,7 +33,7 @@ extension Main.LiteralInference:TestBattery ]) Self.run(tests / "floating-point", - encoded: .init + encoded: .init(BSON.Key.self) { $0["default"] = 1.0 $0["a"] = 1.0 as Double @@ -45,7 +45,7 @@ extension Main.LiteralInference:TestBattery ]) Self.run(tests / "string", - encoded: .init + encoded: .init(BSON.Key.self) { $0["a"] = "string" $0["b"] = "string" @@ -61,7 +61,7 @@ extension Main.LiteralInference:TestBattery ]) Self.run(tests / "optionals", - encoded: .init + encoded: .init(BSON.Key.self) { $0["a"] = [1, nil, 3] $0["b"] = [1, nil, 3] @@ -77,7 +77,7 @@ extension Main.LiteralInference:TestBattery ]) Self.run(tests / "list", - encoded: .init + encoded: .init(BSON.Key.self) { $0["a"] = [1, 2, 3] $0["b"] = [1, 2, 3] @@ -93,15 +93,15 @@ extension Main.LiteralInference:TestBattery ]) Self.run(tests / "document", - encoded: .init + encoded: .init(BSON.Key.self) { - $0["a"] + $0["a"](BSON.Key.self) { $0["a"] = 1 $0["b"] = 2 $0["c"] = 3 } - $0["b"] + $0["b"](BSON.Key.self) { $0["a"] = 1 $0["b"] = 2 diff --git a/Sources/BSONEncodingTests/Inference/Main.TypeInference.swift b/Sources/BSONEncodingTests/Inference/Main.TypeInference.swift index a38f855a..b02edde6 100644 --- a/Sources/BSONEncodingTests/Inference/Main.TypeInference.swift +++ b/Sources/BSONEncodingTests/Inference/Main.TypeInference.swift @@ -13,7 +13,7 @@ extension Main.TypeInference:TestBattery func run(tests:TestGroup) { Self.run(tests / "binary", - encoded: .init + encoded: .init(BSON.Key.self) { $0["a"] = BSON.BinaryView<[UInt8]>.init(subtype: .generic, bytes: [0xff, 0xff, 0xff]) @@ -25,7 +25,7 @@ extension Main.TypeInference:TestBattery ]) Self.run(tests / "max", - encoded: .init + encoded: .init(BSON.Key.self) { $0["max"] = BSON.Max.init() }, @@ -35,7 +35,7 @@ extension Main.TypeInference:TestBattery ]) Self.run(tests / "min", - encoded: .init + encoded: .init(BSON.Key.self) { $0["min"] = BSON.Min.init() }, @@ -45,7 +45,7 @@ extension Main.TypeInference:TestBattery ]) Self.run(tests / "null", - encoded: .init + encoded: .init(BSON.Key.self) { $0["null"] = (nil as Never?) as Never?? }, diff --git a/Sources/BSONEncodingTests/Main.EncodeDocument.swift b/Sources/BSONEncodingTests/Main.EncodeDocument.swift index 7133f060..1e62bd2f 100644 --- a/Sources/BSONEncodingTests/Main.EncodeDocument.swift +++ b/Sources/BSONEncodingTests/Main.EncodeDocument.swift @@ -13,14 +13,14 @@ extension Main.EncodeDocument:TestBattery func run(tests:TestGroup) { Self.run(tests, - encoded: .init + encoded: .init(BSON.Key.self) { $0["a"] = [:] - $0["b"] + $0["b"](BSON.Key.self) { $0["x"] = 1 } - $0["c"] + $0["c"](BSON.Key.self) { $0["x"] = 1 $0["y"] = 2 diff --git a/Sources/BSONEncodingTests/Main.EncodeList.swift b/Sources/BSONEncodingTests/Main.EncodeList.swift index 35a45f47..75f2857e 100644 --- a/Sources/BSONEncodingTests/Main.EncodeList.swift +++ b/Sources/BSONEncodingTests/Main.EncodeList.swift @@ -13,7 +13,7 @@ extension Main.EncodeList:TestBattery func run(tests:TestGroup) { Self.run(tests, - encoded: .init + encoded: .init(BSON.Key.self) { $0["a"] = [] as [Never] $0["b"] = [1] diff --git a/Sources/BSONEncodingTests/Main.EncodeString.swift b/Sources/BSONEncodingTests/Main.EncodeString.swift index c467b3e5..803fe19e 100644 --- a/Sources/BSONEncodingTests/Main.EncodeString.swift +++ b/Sources/BSONEncodingTests/Main.EncodeString.swift @@ -13,7 +13,7 @@ extension Main.EncodeString:TestBattery func run(tests:TestGroup) { Self.run(tests, - encoded: .init + encoded: .init(BSON.Key.self) { $0["a"] = "" $0["b"] = "foo" diff --git a/Sources/BSONIntegrationTests/Main.EnumeratedCodingKeys.swift b/Sources/BSONIntegrationTests/Main.EnumeratedCodingKeys.swift index 52e8229f..318b79dd 100644 --- a/Sources/BSONIntegrationTests/Main.EnumeratedCodingKeys.swift +++ b/Sources/BSONIntegrationTests/Main.EnumeratedCodingKeys.swift @@ -50,7 +50,7 @@ extension Main.EnumeratedCodingKeys:TestBattery } let expected:Codable = .init(a: 5, b: [5, 6], c: [[5, 6, 7], [8]]) - let bson:BSON.Document = .init + let bson:BSON.Document = .init(BSON.Key.self) { $0["a"] = 5 $0["b"] = [5, 6] diff --git a/Sources/BSONReflectionTests/Main.Documents.swift b/Sources/BSONReflectionTests/Main.Documents.swift index fad4adaf..6f43bb6f 100644 --- a/Sources/BSONReflectionTests/Main.Documents.swift +++ b/Sources/BSONReflectionTests/Main.Documents.swift @@ -13,7 +13,7 @@ extension Main.Documents:TestBattery static func run(tests:TestGroup) { - let document:BSON.Document = .init + let document:BSON.Document = .init(BSON.Key.self) { $0["_id"] = 0x1111_2222_3333_4444_5555_6666 as BSON.Identifier $0["facility"] = "Recreation and Activities Center" @@ -29,95 +29,95 @@ extension Main.Documents:TestBattery $0["campaigns"] = [:] $0["complaints"] { - $0 + $0(BSON.Key.self) { $0["_id"] = 0x4455_6677_8899_AABB $0["type"] = "property damage" $0["supervisor"] = "Raquelle" $0["status"] = "open" - $0["date"] + $0["date"](BSON.Key.self) { $0["Y"] = 2022 $0["M"] = 12 $0["D"] = 31 } } - $0 + $0(BSON.Key.self) { $0["_id"] = 0x4455_6677_8899_AABC $0["type"] = "sexual assault" $0["supervisor"] = "Midge" $0["status"] = "open" $0["rpi"] = true - $0["date"] + $0["date"](BSON.Key.self) { $0["Y"] = 2023 $0["M"] = 1 $0["D"] = 1 } } - $0 + $0(BSON.Key.self) { $0["_id"] = 0x4455_6677_8899_AABD $0["type"] = "property theft" $0["supervisor"] = "Barbie" $0["status"] = "closed" - $0["date"] + $0["date"](BSON.Key.self) { $0["Y"] = 2023 $0["M"] = 1 $0["D"] = 4 } } - $0 + $0(BSON.Key.self) { $0["_id"] = 0x4455_6677_8899_AABE $0["type"] = "property damage" $0["supervisor"] = "Midge" $0["status"] = "open" - $0["date"] + $0["date"](BSON.Key.self) { $0["Y"] = 2023 $0["M"] = 1 $0["D"] = 16 } } - $0 + $0(BSON.Key.self) { $0["_id"] = 0x4455_6677_8899_AABF $0["type"] = "assault" $0["supervisor"] = "Raquelle" $0["status"] = "closed" $0["rpi"] = false - $0["date"] + $0["date"](BSON.Key.self) { $0["Y"] = 2023 $0["M"] = 1 $0["D"] = 22 } } - $0 + $0(BSON.Key.self) { $0["_id"] = 0x4455_6677_8899_AAC0 $0["type"] = "guest expulsion" $0["supervisor"] = "Barbie" $0["status"] = "closed" $0["rpi"] = true - $0["date"] + $0["date"](BSON.Key.self) { $0["Y"] = 2023 $0["M"] = 2 $0["D"] = 14 } } - $0 + $0(BSON.Key.self) { $0["_id"] = 0x4455_6677_8899_AAC1 $0["type"] = "sexual assault" $0["supervisor"] = "Barbie" $0["status"] = "open" $0["rpi"] = false - $0["date"] + $0["date"](BSON.Key.self) { $0["Y"] = 2023 $0["M"] = 2 diff --git a/Sources/MongoBuiltins/Documents/Filter/Mongo.FilterDocument.swift b/Sources/MongoBuiltins/Documents/Filter/Mongo.FilterDocument.swift index 93c5bce7..04103a73 100644 --- a/Sources/MongoBuiltins/Documents/Filter/Mongo.FilterDocument.swift +++ b/Sources/MongoBuiltins/Documents/Filter/Mongo.FilterDocument.swift @@ -36,7 +36,7 @@ extension Mongo.FilterDocument func `let`(_ variable:BSON.Key, with populate:(inout Self) throws -> ()) rethrows -> Self { - var document:Self = .init(.init { $0["as"] = variable }) + var document:Self = .init(.init(BSON.Key.self) { $0["as"] = variable }) try populate(&document) return document } diff --git a/Sources/MongoBuiltins/Documents/Map/Mongo.MapDocument.swift b/Sources/MongoBuiltins/Documents/Map/Mongo.MapDocument.swift index 11aa4730..2327ebb4 100644 --- a/Sources/MongoBuiltins/Documents/Map/Mongo.MapDocument.swift +++ b/Sources/MongoBuiltins/Documents/Map/Mongo.MapDocument.swift @@ -31,11 +31,11 @@ extension Mongo.MapDocument try .let(variable.name, with: populate) } - @inlinable internal static + @inlinable static func `let`(_ variable:BSON.Key, with populate:(inout Self) throws -> ()) rethrows -> Self { - var document:Self = .init(.init { $0["as"] = variable }) + var document:Self = .init(.init(BSON.Key.self) { $0["as"] = variable }) try populate(&document) return document } diff --git a/Sources/MongoBuiltins/Mongo.Namespaced (ext).swift b/Sources/MongoBuiltins/Mongo.Namespaced (ext).swift index c9f533a9..f72a2e9f 100644 --- a/Sources/MongoBuiltins/Mongo.Namespaced (ext).swift +++ b/Sources/MongoBuiltins/Mongo.Namespaced (ext).swift @@ -6,7 +6,7 @@ extension Mongo.Namespaced public var document:BSON.Document { - .init + .init(BSON.Key.self) { $0["db"] = self.database $0["coll"] = self.collection diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.Pipeline.swift b/Sources/MongoBuiltins/Pipelines/Mongo.Pipeline.swift index de3eceb2..46cdf2d0 100644 --- a/Sources/MongoBuiltins/Pipelines/Mongo.Pipeline.swift +++ b/Sources/MongoBuiltins/Pipelines/Mongo.Pipeline.swift @@ -9,7 +9,7 @@ extension Mongo @usableFromInline internal var bson:BSON.List - @inlinable public + @inlinable init(stages bson:BSON.List) { self.bson = bson diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.PipelineEncoder.swift b/Sources/MongoBuiltins/Pipelines/Mongo.PipelineEncoder.swift index bdefe393..b7c759f4 100644 --- a/Sources/MongoBuiltins/Pipelines/Mongo.PipelineEncoder.swift +++ b/Sources/MongoBuiltins/Pipelines/Mongo.PipelineEncoder.swift @@ -45,7 +45,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Out.self) + self.list(Mongo.Pipeline.Out.self) { $0[.out] = value } @@ -65,7 +65,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Out.self) + self.list(Mongo.Pipeline.Out.self) { $0[.out] = value } @@ -88,7 +88,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Merge.self) + self.list(Mongo.Pipeline.Merge.self) { $0[.merge] = value } @@ -111,7 +111,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Bucket.self) + self.list(Mongo.Pipeline.Bucket.self) { $0[.bucket] = value } @@ -134,7 +134,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.BucketAuto.self) + self.list(Mongo.Pipeline.BucketAuto.self) { $0[.bucketAuto] = value } @@ -156,7 +156,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Count.self) + self.list(Mongo.Pipeline.Count.self) { $0[.count] = value.stem } @@ -178,7 +178,7 @@ extension Mongo.PipelineEncoder { mutating get { - self.list(using: ChangeStream.self) + self.list(ChangeStream.self) { yield(&$0[with: key][as: Mongo.ChangeStreamEncoder.self]) } @@ -201,7 +201,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.CollectionStats.self) + self.list(Mongo.Pipeline.CollectionStats.self) { $0[.collectionStats] = value } @@ -224,7 +224,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.CurrentOperation.self) + self.list(Mongo.Pipeline.CurrentOperation.self) { $0[.currentOperation] = value } @@ -257,7 +257,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Documents.self) + self.list(Mongo.Pipeline.Documents.self) { $0[.documents] = value } @@ -280,7 +280,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Facet.self) + self.list(Mongo.Pipeline.Facet.self) { $0[.facet] = value } @@ -330,7 +330,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Group.self) + self.list(Mongo.Pipeline.Group.self) { $0[.group] = value } @@ -353,7 +353,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.IndexStats.self) + self.list(Mongo.Pipeline.IndexStats.self) { $0[.indexStats] = value } @@ -376,7 +376,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Limit.self) + self.list(Mongo.Pipeline.Limit.self) { $0[.limit] = value } @@ -417,7 +417,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Lookup.self) + self.list(Mongo.Pipeline.Lookup.self) { $0[.lookup] = value } @@ -440,7 +440,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Match.self) + self.list(Mongo.Pipeline.Match.self) { $0[.match] = value } @@ -463,7 +463,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.PlanCacheStats.self) + self.list(Mongo.Pipeline.PlanCacheStats.self) { $0[.planCacheStats] = value } @@ -486,7 +486,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Project.self) + self.list(Mongo.Pipeline.Project.self) { $0[.project] = value } @@ -510,7 +510,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Redact.self) + self.list(Mongo.Pipeline.Redact.self) { $0[.redact] = value } @@ -534,7 +534,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.ReplaceWith.self) + self.list(Mongo.Pipeline.ReplaceWith.self) { $0[.replaceWith] = value } @@ -554,7 +554,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.ReplaceWith.self) + self.list(Mongo.Pipeline.ReplaceWith.self) { $0[.replaceWith] = value } @@ -577,7 +577,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Sample.self) + self.list(Mongo.Pipeline.Sample.self) { $0[.sample] = value } @@ -600,7 +600,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Set.self) + self.list(Mongo.Pipeline.Set.self) { $0[.set] = value } @@ -641,7 +641,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.ShardedDataDistribution.self) + self.list(Mongo.Pipeline.ShardedDataDistribution.self) { $0[.shardedDataDistribution] = value } @@ -664,7 +664,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Skip.self) + self.list(Mongo.Pipeline.Skip.self) { $0[.skip] = value } @@ -687,7 +687,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Sort.self) + self.list(Mongo.Pipeline.Sort.self) { $0[.sort] = value } @@ -711,7 +711,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.SortByCount.self) + self.list(Mongo.Pipeline.SortByCount.self) { $0[.sortByCount] = value } @@ -734,7 +734,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.UnionWith.self) + self.list(Mongo.Pipeline.UnionWith.self) { $0[.unionWith] = value } @@ -757,7 +757,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.UnionWith.self) + self.list(Mongo.Pipeline.UnionWith.self) { $0[.unionWith] = value } @@ -780,7 +780,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Unset.self) + self.list(Mongo.Pipeline.Unset.self) { $0[.unset] = value.stem } @@ -799,7 +799,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Unset.self) + self.list(Mongo.Pipeline.Unset.self) { $0[.unset] { @@ -828,7 +828,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Unwind.self) + self.list(Mongo.Pipeline.Unwind.self) { // includes the `$` prefix! $0[.unwind] = value @@ -849,7 +849,7 @@ extension Mongo.PipelineEncoder return } - self.list(using: Mongo.Pipeline.Unwind.self) + self.list(Mongo.Pipeline.Unwind.self) { $0[.unwind] = value } diff --git a/Sources/MongoCommands/Concerns/Mongo.MaxTime.swift b/Sources/MongoCommands/Concerns/Mongo.MaxTime.swift index 3f1f5f25..acf1ca9e 100644 --- a/Sources/MongoCommands/Concerns/Mongo.MaxTime.swift +++ b/Sources/MongoCommands/Concerns/Mongo.MaxTime.swift @@ -1,10 +1,14 @@ extension Mongo { @frozen public - enum MaxTime:Hashable, Sendable + enum MaxTime:Equatable, Hashable, Sendable { - /// The driver will populate the relevant command’s `maxTimeMS` - /// field automatically. - case auto + /// The driver will compute and populate the relevant command’s `maxTimeMS` field + /// automatically, based on user-specified timeout arguments. + case computed + /// The driver will omit the relevant command’s `maxTimeMS` field. This is used by + /// ``GetMore`` commands, to avoid repeating the `maxTimeMS` field from the original + /// cursor-returning command. + case omitted } } diff --git a/Sources/MongoCommands/Concerns/Mongo.WriteConcernError.swift b/Sources/MongoCommands/Concerns/Mongo.WriteConcernError.swift index bc19481f..d4d50669 100644 --- a/Sources/MongoCommands/Concerns/Mongo.WriteConcernError.swift +++ b/Sources/MongoCommands/Concerns/Mongo.WriteConcernError.swift @@ -81,7 +81,7 @@ extension Mongo.WriteConcernError:BSONEncodable, BSONDocumentEncodable return } - bson[.errorDetails] + bson[.errorDetails](BSON.Key.self) { $0["writeConcern"] = details } diff --git a/Sources/MongoCommands/Mongo.Command.swift b/Sources/MongoCommands/Mongo.Command.swift index dab29eb6..55d744c7 100644 --- a/Sources/MongoCommands/Mongo.Command.swift +++ b/Sources/MongoCommands/Mongo.Command.swift @@ -32,7 +32,7 @@ extension Mongo /// The payload of this command. var outline:OutlineVector? { get } - var timeout:MaxTime? { get } + var timeout:MaxTime { get } /// The opaque fields of this command. Not all conforming types will encode /// all of their fields to this property; some may have fields (such as @@ -61,15 +61,10 @@ extension Mongo.Command { /// Returns nil. @inlinable public - var outline:Mongo.OutlineVector? - { - nil - } + var outline:Mongo.OutlineVector? { nil } + /// Returns ``Mongo.MaxTime/computed``. @inlinable public - var timeout:Mongo.MaxTime? - { - .auto - } + var timeout:Mongo.MaxTime { .computed } } extension Mongo.Command where ReadConcern == Mongo.ReadConcern { diff --git a/Sources/MongoCommands/Mongo.CommandType.swift b/Sources/MongoCommands/Mongo.CommandType.swift index f60e5d13..8cd8ade1 100644 --- a/Sources/MongoCommands/Mongo.CommandType.swift +++ b/Sources/MongoCommands/Mongo.CommandType.swift @@ -47,7 +47,7 @@ extension Mongo.CommandType func callAsFunction(_ first:some BSONEncodable, then encode:(inout BSON.DocumentEncoder) -> () = { _ in }) -> BSON.Document { - .init + .init(BSON.Key.self) { $0[.init(rawValue: self.rawValue)] = first encode(&$0) diff --git a/Sources/MongoDB/Commands/Mongo.Aggregate (ext).swift b/Sources/MongoDB/Commands/Mongo.Aggregate (ext).swift index 39ed8e27..98ae803f 100644 --- a/Sources/MongoDB/Commands/Mongo.Aggregate (ext).swift +++ b/Sources/MongoDB/Commands/Mongo.Aggregate (ext).swift @@ -2,12 +2,10 @@ extension Mongo.Aggregate:Mongo.ImplicitSessionCommand, Mongo.TransactableComman { } extension Mongo.Aggregate:Mongo.IterableCommand - where Effect.Stride == Int, + where Effect.Tailing == Mongo.Tailing, + Effect.Stride == Int, Effect.Batch == Mongo.CursorBatch { public typealias Element = Effect.BatchElement - - @inlinable public - var tailing:Mongo.Tailing? { nil } } diff --git a/Sources/MongoDB/Commands/Mongo.Cursor (ext).swift b/Sources/MongoDB/Commands/Mongo.Cursor (ext).swift index ee003944..5f9205fd 100644 --- a/Sources/MongoDB/Commands/Mongo.Cursor (ext).swift +++ b/Sources/MongoDB/Commands/Mongo.Cursor (ext).swift @@ -4,4 +4,6 @@ extension Mongo.Cursor:Mongo.ReadEffect { public typealias Stride = Int + public + typealias Batch = Mongo.CursorBatch } diff --git a/Sources/MongoDB/Mongo.Session (ext).swift b/Sources/MongoDB/Mongo.Session (ext).swift index e24eeca5..b98cd035 100644 --- a/Sources/MongoDB/Mongo.Session (ext).swift +++ b/Sources/MongoDB/Mongo.Session (ext).swift @@ -6,26 +6,25 @@ extension Mongo.Session { let command:Mongo.Aggregate> = .init( collection.name, - pipeline: .init + stride: nil) + { + $0[stage: .collectionStats] = .init { - $0[stage: .collectionStats] = .init - { - $0[.storageStats] = [:] - } + $0[.storageStats] = [:] + } - // A typical collection stats output document contains a huge amount of - // data, most of which is redundant. - $0[stage: .project] = .init + // A typical collection stats output document contains a huge amount of + // data, most of which is redundant. + $0[stage: .project] = .init + { + for key:Mongo.CollectionStats.Storage.CodingKey + in Mongo.CollectionStats.Storage.CodingKey.allCases { - for key:Mongo.CollectionStats.Storage.CodingKey - in Mongo.CollectionStats.Storage.CodingKey.allCases - { - $0[Mongo.CollectionStats[.storage] / - Mongo.CollectionStats.Storage[key]] = true - } + $0[Mongo.CollectionStats[.storage] / + Mongo.CollectionStats.Storage[key]] = true } - }, - stride: nil) + } + } return try await self.run(command: command, against: collection.database) } diff --git a/Sources/MongoDBTests/Aggregate/Aggregate.swift b/Sources/MongoDBTests/Aggregate/Aggregate.swift index 5b774abd..38f3bdb3 100644 --- a/Sources/MongoDBTests/Aggregate/Aggregate.swift +++ b/Sources/MongoDBTests/Aggregate/Aggregate.swift @@ -59,18 +59,17 @@ struct Aggregate:MongoTestBattery where Configuration:MongoTestCo command: Mongo.Aggregate>.init(collection, writeConcern: .majority, readConcern: .majority, - pipeline: .init + stride: 10) + { + $0[stage: .project] = .init { $0[Article[.tags]] = 1 } + $0[stage: .unwind] = Article[.tags] + $0[stage: .group] = .init { - $0[stage: .project] = .init { $0[Article[.tags]] = 1 } - $0[stage: .unwind] = Article[.tags] - $0[stage: .group] = .init - { - $0[.id] = Article[.tags] + $0[.id] = Article[.tags] - $0[TagStats[.count]] = .init { $0[.sum] = 1 } - } - }, - stride: 10), + $0[TagStats[.count]] = .init { $0[.sum] = 1 } + } + }, against: database) { try await $0.reduce(into: []) { $0 += $1 } @@ -90,21 +89,20 @@ struct Aggregate:MongoTestBattery where Configuration:MongoTestCo command: Mongo.Aggregate>.init(collection, writeConcern: .majority, readConcern: .majority, - pipeline: .init + stride: 10) + { + $0[stage: .project] = .init { - $0[stage: .project] = .init - { - $0[Article[.author]] = 1 - $0[Article[.views]] = 1 - } - $0[stage: .group] = .init - { - $0[.id] = Article[.author] + $0[Article[.author]] = 1 + $0[Article[.views]] = 1 + } + $0[stage: .group] = .init + { + $0[.id] = Article[.author] - $0[Article[.views]] = .init { $0[.sum] = Article[.views] } - } - }, - stride: 10), + $0[Article[.views]] = .init { $0[.sum] = Article[.views] } + } + }, against: database) { try await $0.reduce(into: []) { $0 += $1 } diff --git a/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift new file mode 100644 index 00000000..258ca3b7 --- /dev/null +++ b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift @@ -0,0 +1,32 @@ +import BSON +import MongoDB +import MongoTesting + +struct ChangeStreams:MongoTestBattery where Configuration:MongoTestConfiguration +{ + static + func run(tests:TestGroup, pool:Mongo.SessionPool, database:Mongo.Database) async throws + { + let collection:Mongo.Collection = "watchable" + await tests.do + { + let session:Mongo.Session = try await .init(from: pool) + try await session.run( + command: Mongo.Aggregate>.init(collection, + writeConcern: .majority, + readConcern: .majority, + stride: 10) + { + $0[stage: .changeStream] { _ in } + }, + against: database, + by: .now.advanced(by: .seconds(15))) + { + for try await _:[BSON.Document] in $0 + { + return + } + } + } + } +} diff --git a/Sources/MongoDBTests/Main.swift b/Sources/MongoDBTests/Main.swift index 51f73bdb..f27985e9 100644 --- a/Sources/MongoDBTests/Main.swift +++ b/Sources/MongoDBTests/Main.swift @@ -9,6 +9,7 @@ enum Main:TestMain let all:[any TestBattery.Type] = [ Aggregate .self, + ChangeStreams .self, Collections .self, Cursors .self, Databases .self, diff --git a/Sources/MongoDriver/Commands/ConfigureFailPoint/Mongo.ConfigureFailpoint.swift b/Sources/MongoDriver/Commands/ConfigureFailPoint/Mongo.ConfigureFailpoint.swift index 2ce8cf8e..945fca38 100644 --- a/Sources/MongoDriver/Commands/ConfigureFailPoint/Mongo.ConfigureFailpoint.swift +++ b/Sources/MongoDriver/Commands/ConfigureFailPoint/Mongo.ConfigureFailpoint.swift @@ -47,14 +47,14 @@ extension Mongo.ConfigureFailpoint:Mongo.ImplicitSessionCommand, Mongo.Command case .random(let options, probability: let probability): $0["data"] = options - $0["mode"] + $0["mode"](BSON.Key.self) { $0["activationProbability"] = probability } case .times(let options, count: let count): $0["data"] = options - $0["mode"] + $0["mode"](BSON.Key.self) { $0["times"] = count } diff --git a/Sources/MongoDriver/Commands/GetMore/Mongo.GetMore.swift b/Sources/MongoDriver/Commands/GetMore/Mongo.GetMore.swift index ac1024c7..3e8831a0 100644 --- a/Sources/MongoDriver/Commands/GetMore/Mongo.GetMore.swift +++ b/Sources/MongoDriver/Commands/GetMore/Mongo.GetMore.swift @@ -13,14 +13,12 @@ extension Mongo public let collection:Collection public - let timeout:MaxTime? + let timeout:MaxTime public let count:Int? @inlinable public - init(cursor:CursorIdentifier, collection:Collection, - timeout:MaxTime?, - count:Int?) + init(cursor:CursorIdentifier, collection:Collection, timeout:MaxTime, count:Int?) { self.cursor = cursor self.collection = collection diff --git a/Sources/MongoDriver/Commands/Hello/Mongo.Hello.ClientMetadata.swift b/Sources/MongoDriver/Commands/Hello/Mongo.Hello.ClientMetadata.swift index cd047dc9..95f403fa 100644 --- a/Sources/MongoDriver/Commands/Hello/Mongo.Hello.ClientMetadata.swift +++ b/Sources/MongoDriver/Commands/Hello/Mongo.Hello.ClientMetadata.swift @@ -34,17 +34,17 @@ extension Mongo.Hello.ClientMetadata:BSONEncodable, BSONDocumentEncodable { if let appname:String = self.appname { - bson["application"] + bson["application"](BSON.Key.self) { $0["name"] = appname } } - bson["driver"] + bson["driver"](BSON.Key.self) { $0["name"] = "swift-mongodb" $0["version"] = "0" } - bson["os"] + bson["os"](BSON.Key.self) { $0["type"] = Self.os } diff --git a/Sources/MongoDriver/Commands/Mongo.Command (ext).swift b/Sources/MongoDriver/Commands/Mongo.Command (ext).swift index a8eb47ae..0b069346 100644 --- a/Sources/MongoDriver/Commands/Mongo.Command (ext).swift +++ b/Sources/MongoDriver/Commands/Mongo.Command (ext).swift @@ -16,8 +16,8 @@ extension Mongo.Command } extension Mongo.Command { - /// Encodes this command to a BSON document, adding the given database - /// as a field with the key [`"$db"`](). + /// Encodes this command to a BSON document, adding the given database as a field with the + /// key `"$db"`. @usableFromInline __consuming func encode(database:Database, labels:Mongo.SessionLabels?, @@ -37,13 +37,12 @@ extension Mongo.Command return nil } - let timeout:Milliseconds? = self.timeout.map + let timeout:Milliseconds? + + switch self.timeout { - switch $0 - { - case .auto: - .init(truncating: now.duration(to: deadline)) - } + case .computed: timeout = .init(truncating: now.duration(to: deadline)) + case .omitted: timeout = nil } let body:BSON.Document = self.body(database: database, diff --git a/Sources/MongoDriver/Commands/SASL/Mongo.SASLStart.swift b/Sources/MongoDriver/Commands/SASL/Mongo.SASLStart.swift index b5478586..d1ebb30a 100644 --- a/Sources/MongoDriver/Commands/SASL/Mongo.SASLStart.swift +++ b/Sources/MongoDriver/Commands/SASL/Mongo.SASLStart.swift @@ -30,7 +30,7 @@ extension Mongo.SASLStart:BSONDocumentEncodable bson[.init(Self.type)] = true bson["mechanism"] = self.mechanism bson["payload"] = self.scram.message.base64 - bson["options"] + bson["options"](BSON.Key.self) { $0["skipEmptyExchange"] = true } diff --git a/Sources/MongoDriver/Connections/Mongo.Connection.swift b/Sources/MongoDriver/Connections/Mongo.Connection.swift index 47c5fc50..a5d80c2f 100644 --- a/Sources/MongoDriver/Connections/Mongo.Connection.swift +++ b/Sources/MongoDriver/Connections/Mongo.Connection.swift @@ -95,12 +95,13 @@ extension Mongo.Connection by deadline:ContinuousClock.Instant) async throws -> Mongo.Reply where Command:Mongo.Command { - let deadline:ContinuousClock.Instant = self.pool.adjust(deadline: deadline) + let deadline:Mongo.DeadlineAdjustments = self.pool.adjust(deadline: deadline) + guard let command:Mongo.WireMessage.Sections = command.encode( database: database, labels: labels, - by: deadline) + by: deadline.logical) else { throw Mongo.DriverTimeoutError.init() @@ -110,7 +111,9 @@ extension Mongo.Connection do { - message = try await self.allocation.request(sections: command, deadline: deadline) + message = try await self.allocation.request( + sections: command, + deadline: deadline.network) } catch let error { diff --git a/Sources/MongoDriver/Connections/Mongo.ConnectionPool.Allocations.swift b/Sources/MongoDriver/Connections/Mongo.ConnectionPool.Allocations.swift index afd59e69..e018d313 100644 --- a/Sources/MongoDriver/Connections/Mongo.ConnectionPool.Allocations.swift +++ b/Sources/MongoDriver/Connections/Mongo.ConnectionPool.Allocations.swift @@ -5,7 +5,7 @@ extension Mongo.ConnectionPool { /// Categorizes and tracks channels by their observed health and /// allocation status. - struct Allocations + struct Allocations:Sendable { /// Connections that are currently free to be allocated, /// and are believed to be healthy. @@ -21,9 +21,8 @@ extension Mongo.ConnectionPool private var perished:Set - /// Additional channels that have no other way of being - /// represented in this structure. Contributes to the total - /// connection ``count``. + /// Additional channels that have no other way of being represented in this structure. + /// Contributes to the total connection ``count``. private var pending:Int @@ -31,7 +30,7 @@ extension Mongo.ConnectionPool var observers:Mongo.Observers /// All requests currently awaiting connections, identified by `UInt`. private - var requests:[UInt: CheckedContinuation] + var requests:[UInt: CheckedContinuation] /// The current stage of the pool’s lifecycle. Pools start out in the /// “connecting” state, and eventually transition to the “draining” state, /// which is terminal. There are no “in-between” states. diff --git a/Sources/MongoDriver/Connections/Mongo.ConnectionPool.Phase.swift b/Sources/MongoDriver/Connections/Mongo.ConnectionPool.Phase.swift index 83e9e639..7215214b 100644 --- a/Sources/MongoDriver/Connections/Mongo.ConnectionPool.Phase.swift +++ b/Sources/MongoDriver/Connections/Mongo.ConnectionPool.Phase.swift @@ -1,7 +1,7 @@ extension Mongo.ConnectionPool { /// The current stage of a connection pool’s lifecycle. - enum Phase + enum Phase:Sendable { /// The connection pool is active and can create new connections. case connecting(Mongo.Connector) diff --git a/Sources/MongoDriver/Connections/Mongo.ConnectionPool.swift b/Sources/MongoDriver/Connections/Mongo.ConnectionPool.swift index 7bf47c2f..8ed5791b 100644 --- a/Sources/MongoDriver/Connections/Mongo.ConnectionPool.swift +++ b/Sources/MongoDriver/Connections/Mongo.ConnectionPool.swift @@ -48,23 +48,19 @@ extension Mongo let releasing:UnsafeAtomic private nonisolated - let _latency:UnsafeAtomic - - nonisolated - var latency:UnsafeAtomic - { - _read { yield self._latency } - } + let networkLatency:UnsafeAtomic + private nonisolated + let networkTimeout:NetworkTimeout /// Avoid setting the maximum pool size to a very large number, because /// the pool makes no linting guarantees. init(alongside monitor:MonitorDelegate, - connectionTimeout:Milliseconds, connectorFactory:__shared ConnectorFactory, + connectorTimeout:NetworkTimeout, + initialLatency:Nanoseconds, authenticator:Authenticator, generation:UInt, settings:ConnectionPoolSettings, - latency:Nanoseconds, logger:Logger?, host:Host) { @@ -76,23 +72,35 @@ extension Mongo self.logger = logger self.allocations = .init(connector: connectorFactory(authenticator: authenticator, - timeout: connectionTimeout, + timeout: connectorTimeout, host: host)) self.releasing = .create(0) - self._latency = .create(latency) - + self.networkLatency = .create(initialLatency) + self.networkTimeout = connectorTimeout } deinit { + self.networkLatency.destroy() self.allocations.destroy() - self.releasing.destroy() - self._latency.destroy() } } } extension Mongo.ConnectionPool +{ + nonisolated + func updateLatency(with nanoseconds:Nanoseconds) + { + self.networkLatency.store(nanoseconds, ordering: .relaxed) + } + nonisolated + func recentLatency() -> Nanoseconds + { + self.networkLatency.load(ordering: .relaxed) + } +} +extension Mongo.ConnectionPool { func start() async { @@ -146,10 +154,13 @@ extension Mongo.ConnectionPool { /// Adjusts the given deadline to account for round-trip latency, as /// tracked by this pool. - public nonisolated - func adjust(deadline:ContinuousClock.Instant) -> ContinuousClock.Instant + @usableFromInline nonisolated + func adjust(deadline:ContinuousClock.Instant) -> Mongo.DeadlineAdjustments { - deadline - .nanoseconds(self._latency.load(ordering: .relaxed)) + let logical:ContinuousClock.Instant = deadline - .nanoseconds(self.recentLatency()) + let network:ContinuousClock.Instant = logical.advanced( + by: .milliseconds(self.networkTimeout.milliseconds)) + return .init(logical: logical, network: network) } } extension Mongo.ConnectionPool diff --git a/Sources/MongoDriver/Connections/Mongo.Connector.swift b/Sources/MongoDriver/Connections/Mongo.Connector.swift index 3ac19e2c..ea49de39 100644 --- a/Sources/MongoDriver/Connections/Mongo.Connector.swift +++ b/Sources/MongoDriver/Connections/Mongo.Connector.swift @@ -11,7 +11,7 @@ extension Mongo private let bootstrap:ClientBootstrap private - let timeout:Milliseconds + let timeout:NetworkTimeout private let appname:String? @@ -20,7 +20,7 @@ extension Mongo init(authenticator:Authenticator, bootstrap:ClientBootstrap, - timeout:Milliseconds, + timeout:NetworkTimeout, appname:String?, host:Host) { @@ -55,7 +55,8 @@ extension Mongo.Connector // include SCRAM mechanism negotiation (i.e. saslSupportedMechs), as // doing so would make monitoring checks more expensive for the server. // ''' - let deadline:ContinuousClock.Instant = .now.advanced(by: .milliseconds(self.timeout)) + let deadline:ContinuousClock.Instant = .now.advanced( + by: .milliseconds(self.timeout.milliseconds)) let hello:Mongo.Hello = .init(client: self.client, user: nil) let listener:Mongo.Listener.Connection = .init( @@ -99,7 +100,7 @@ extension Mongo.Connector func connect(id:UInt) async throws -> Mongo.ConnectionPool.Allocation { let deadline:ContinuousClock.Instant = .now.advanced( - by: .milliseconds(self.timeout)) + by: .milliseconds(self.timeout.milliseconds)) let connection:Mongo.ConnectionPool.Allocation = .init( channel: try await self.channel(), id: id) diff --git a/Sources/MongoDriver/Connections/Mongo.ConnectorFactory.swift b/Sources/MongoDriver/Connections/Mongo.ConnectorFactory.swift index d12c232b..da3acee3 100644 --- a/Sources/MongoDriver/Connections/Mongo.ConnectorFactory.swift +++ b/Sources/MongoDriver/Connections/Mongo.ConnectorFactory.swift @@ -27,11 +27,11 @@ extension Mongo extension Mongo.ConnectorFactory { func callAsFunction(authenticator:Authenticator, - timeout:Milliseconds, + timeout:Mongo.NetworkTimeout, host:Mongo.Host) -> Mongo.Connector { .init(authenticator: authenticator, - bootstrap: self.bootstrap(timeout: timeout, host: host), + bootstrap: self.bootstrap(timeout: timeout.milliseconds, host: host), timeout: timeout, appname: self.appname, host: host) diff --git a/Sources/MongoDriver/Cursors/Mongo.Cursor.swift b/Sources/MongoDriver/Cursors/Mongo.Cursor.swift index a7652cb5..0e2fc640 100644 --- a/Sources/MongoDriver/Cursors/Mongo.Cursor.swift +++ b/Sources/MongoDriver/Cursors/Mongo.Cursor.swift @@ -2,20 +2,15 @@ import BSON import Durations import MongoCommands -extension Mongo.Cursor -{ - @available(*, deprecated, renamed: "Mongo.CursorBatch") - public - typealias Batch = Mongo.CursorBatch -} extension Mongo { public struct Cursor where BatchElement:BSONDecodable & Sendable { - @usableFromInline internal + @usableFromInline let iterator:AsyncIterator + private init(iterator:AsyncIterator) { self.iterator = iterator @@ -32,10 +27,10 @@ extension Mongo.Cursor } extension Mongo.Cursor { - @usableFromInline internal static + @usableFromInline static func create(preference:Mongo.ReadPreference, lifecycle:Mongo.CursorLifecycle, - timeout:Milliseconds, + timeout:Mongo.NetworkTimeout, initial:Mongo.CursorBatch, stride:Int?, pinned: @@ -63,7 +58,7 @@ extension Mongo.Cursor } return .init(iterator: iterator) } - @usableFromInline internal + @usableFromInline func destroy() async { if let cursor:Mongo.CursorIterator = self.iterator.cursor diff --git a/Sources/MongoDriver/Cursors/Mongo.CursorIterator.swift b/Sources/MongoDriver/Cursors/Mongo.CursorIterator.swift index 9553d14a..8ed2d6d6 100644 --- a/Sources/MongoDriver/Cursors/Mongo.CursorIterator.swift +++ b/Sources/MongoDriver/Cursors/Mongo.CursorIterator.swift @@ -20,16 +20,16 @@ extension Mongo /// The database and collection this cursor iterates over. public let namespace:Namespaced - /// The lifecycle mode of this cursor. This is - /// ``case CursorLifecycle.iterable(_:)`` for tailable cursors, and - /// ``case CursorLifecycle.expires(_:)`` for non-tailable cursors. + /// The lifecycle mode of this cursor. This is ``CursorLifecycle.iterable(_:) [case]`` + /// for tailable cursors, and ``CursorLifecycle.expires(_:) [case]`` for non-tailable + /// cursors. @usableFromInline let lifecycle:CursorLifecycle /// The operation timeout used for ``KillCursors``, and the default /// operation timeout used for ``GetMore`` for tailable cursors without /// an explicit timeout set. @usableFromInline - let timeout:Milliseconds + let timeout:NetworkTimeout /// The maximum size of each batch retrieved by this batch sequence. public let stride:Int? @@ -47,7 +47,7 @@ extension Mongo preference:ReadPreference, namespace:Namespaced, lifecycle:CursorLifecycle, - timeout:Milliseconds, + timeout:NetworkTimeout, stride:Int?, pinned: ( @@ -73,10 +73,10 @@ extension Mongo.CursorIterator switch self.lifecycle { case .iterable(let timeout): - .now.advanced(by: .milliseconds(timeout ?? self.timeout)) + return .now.advanced(by: .milliseconds(timeout ?? self.timeout.milliseconds)) case .expires(let deadline): - deadline + return deadline } } } @@ -125,6 +125,6 @@ extension Mongo.CursorIterator over: self.pinned.connection, on: self.preference, // ``KillCursors`` always refreshes the timeout - by: .now.advanced(by: .milliseconds(self.timeout))) + by: .now.advanced(by: .milliseconds(self.timeout.milliseconds))) } } diff --git a/Sources/MongoDriver/Cursors/Mongo.CursorLifecycle.swift b/Sources/MongoDriver/Cursors/Mongo.CursorLifecycle.swift index ceee8c2b..535a19fa 100644 --- a/Sources/MongoDriver/Cursors/Mongo.CursorLifecycle.swift +++ b/Sources/MongoDriver/Cursors/Mongo.CursorLifecycle.swift @@ -13,15 +13,15 @@ extension Mongo } extension Mongo.CursorLifecycle { - @usableFromInline - var timeout:Mongo.MaxTime? + @inlinable + var timeout:Mongo.MaxTime { switch self { // maxTimeMS can only be sent for tailable // (iteration-based lifecycle) cursors. - case .iterable: .auto - case .expires: nil + case .iterable: .computed + case .expires: .omitted } } } diff --git a/Sources/MongoDriver/Deployments/Mongo.Deployment.swift b/Sources/MongoDriver/Deployments/Mongo.Deployment.swift index a113080b..07857e80 100644 --- a/Sources/MongoDriver/Deployments/Mongo.Deployment.swift +++ b/Sources/MongoDriver/Deployments/Mongo.Deployment.swift @@ -25,9 +25,13 @@ extension Mongo public final actor Deployment { - /// The default timeout for driver operations. + /// The default timeout for driver operations. This is generally understood to represent + /// a global network timeout, and is therefore constant across an application. + /// + /// Some operations (change streams, tailable cursors, etc.) might expect you to supply + /// a separate timeout for the operation itself. @usableFromInline nonisolated - let timeout:Timeout + let timeout:NetworkTimeout // Right now, we don’t do anything with this from this type. But other // types use it through their deployment pointers. internal nonisolated @@ -50,7 +54,7 @@ extension Mongo init(connectionTimeout:Milliseconds, logger:Logger?) { - self.timeout = .init(default: connectionTimeout) + self.timeout = .init(milliseconds: connectionTimeout) self.logger = logger self._clusterTime = .create(nil) diff --git a/Sources/MongoDriver/Deployments/Mongo.Server.swift b/Sources/MongoDriver/Deployments/Mongo.Server.swift index 97f0c420..157ad992 100644 --- a/Sources/MongoDriver/Deployments/Mongo.Server.swift +++ b/Sources/MongoDriver/Deployments/Mongo.Server.swift @@ -42,7 +42,7 @@ extension Mongo.Server func primary(from self:Mongo.Server) -> Self { .init(metadata: .init(staleness: .zero, - latency: self.pool.latency.load(ordering: .relaxed), + latency: self.pool.recentLatency(), tags: self.metadata.tags), pool: self.pool) } @@ -54,10 +54,9 @@ extension Mongo.Server freshest:some Mongo.ReplicaTimingBaseline) -> Self { let staleness:Milliseconds = freshest - self.metadata.timings + heartbeatInterval - let latency:Nanoseconds = self.pool.latency.load(ordering: .relaxed) return .init(metadata: .init( staleness: staleness, - latency: latency, + latency: self.pool.recentLatency(), tags: self.metadata.tags), pool: self.pool) } diff --git a/Sources/MongoDriver/MongoExecutor (ext).swift b/Sources/MongoDriver/MongoExecutor (ext).swift index 3c124c71..c047142e 100644 --- a/Sources/MongoDriver/MongoExecutor (ext).swift +++ b/Sources/MongoDriver/MongoExecutor (ext).swift @@ -4,16 +4,16 @@ import MongoWire extension MongoExecutor { - /// Encodes the given command to a document, adding the given database - /// as a field with the key `"$db"`, sends it over this channel, and - /// awaits its reply. + /// Encodes the given command to a document, adding the given database as a field with the + /// key `"$db"`, sends it over this channel, and awaits its reply. /// - /// If the deadline passes before this function can start executing, this - /// method will not close the channel or send anything over it. + /// If the deadline passes before this function can start executing, this method will not + /// close the channel or send anything over it. As this method is intended for use by the + /// driver itself, **it performs no deadline adjustments** to account for network + /// conditions. /// - /// If the task the caller is running on gets cancelled before this - /// function can start executing, this method will not close the channel - /// or send anything over it. + /// If the task the caller is running on gets cancelled before this function can start + /// executing, this method will not close the channel or send anything over it. /// /// In all other scenarios, the channel will be closed on failure. func run(command:__owned Command, diff --git a/Sources/MongoDriver/Monitoring/Mongo.MonitorPool.swift b/Sources/MongoDriver/Monitoring/Mongo.MonitorPool.swift index 4a3a8c6e..26668368 100644 --- a/Sources/MongoDriver/Monitoring/Mongo.MonitorPool.swift +++ b/Sources/MongoDriver/Monitoring/Mongo.MonitorPool.swift @@ -147,9 +147,9 @@ extension Mongo.MonitorPool private func monitor(host:Mongo.Host, generation:UInt, interval:Milliseconds) async -> Replacement { - let connectionTimeout:Milliseconds = self.deployment.timeout.default + let connectorTimeout:Mongo.NetworkTimeout = self.deployment.timeout let connector:Mongo.Connector = self.connectorFactory(authenticator: nil, - timeout: connectionTimeout, + timeout: connectorTimeout, host: host) let services:Mongo.MonitorServices @@ -188,12 +188,12 @@ extension Mongo.MonitorPool bufferingPolicy: .bufferingOldest(1)) { let pool:Mongo.ConnectionPool = .init(alongside: .init($0), - connectionTimeout: connectionTimeout, connectorFactory: self.connectorFactory, + connectorTimeout: connectorTimeout, + initialLatency: services.initialLatency, authenticator: self.authenticator, generation: generation, settings: self.connectionPoolSettings, - latency: services.initialLatency, logger: self.deployment.logger, host: host) diff --git a/Sources/MongoDriver/Monitoring/Mongo.Sampler.swift b/Sources/MongoDriver/Monitoring/Mongo.Sampler.swift index e389bbef..0e14bdd2 100644 --- a/Sources/MongoDriver/Monitoring/Mongo.Sampler.swift +++ b/Sources/MongoDriver/Monitoring/Mongo.Sampler.swift @@ -38,17 +38,17 @@ extension Mongo.Sampler let cooldown:Void = Task.sleep(for: interval) let deadline:ContinuousClock.Instant = .now.advanced(by: interval) - let latency:Duration = try await self.connection.sample(by: deadline) + let sampleLatency:Duration = try await self.connection.sample(by: deadline) let sample:Double = - 1e-9 * Double.init(latency.components.attoseconds) + - 1e+9 * Double.init(latency.components.seconds) + 1e-9 * Double.init(sampleLatency.components.attoseconds) + + 1e+9 * Double.init(sampleLatency.components.seconds) metric = alpha * sample + (1 - alpha) * metric let rounded:Nanoseconds = .nanoseconds(Int64.init(metric)) - pool.latency.store(rounded, ordering: .relaxed) - pool.log(event: Event.sampled(latency, metric: rounded)) + pool.updateLatency(with: rounded) + pool.log(event: Event.sampled(sampleLatency, metric: rounded)) try await cooldown } diff --git a/Sources/MongoDriver/Retryability/Mongo.DeadlineAdjustments.swift b/Sources/MongoDriver/Retryability/Mongo.DeadlineAdjustments.swift new file mode 100644 index 00000000..de6af17f --- /dev/null +++ b/Sources/MongoDriver/Retryability/Mongo.DeadlineAdjustments.swift @@ -0,0 +1,23 @@ +extension Mongo +{ + @frozen @usableFromInline + struct DeadlineAdjustments:Sendable + { + /// A **logical deadline** is the deadline the driver communicates to the server. It is + /// computed as the nominal (user-supplied) timeout minus the network latency metric. + @usableFromInline + let logical:ContinuousClock.Instant + /// A **network deadline** is the deadline the driver uses to time out network requests. + /// It is computed as the ``logical`` deadline plus whatever global network timeout the + /// driver is using. Having a separate network deadline makes it far less likely that + /// natural variations in network latency will disrupt things such as tailable cursors. + @usableFromInline + let network:ContinuousClock.Instant + + init(logical:ContinuousClock.Instant, network:ContinuousClock.Instant) + { + self.logical = logical + self.network = network + } + } +} diff --git a/Sources/MongoDriver/Retryability/Mongo.DriverTimeoutError.swift b/Sources/MongoDriver/Retryability/Mongo.DriverTimeoutError.swift index 382fb807..9de09eac 100644 --- a/Sources/MongoDriver/Retryability/Mongo.DriverTimeoutError.swift +++ b/Sources/MongoDriver/Retryability/Mongo.DriverTimeoutError.swift @@ -2,7 +2,8 @@ extension Mongo { /// A command was timed-out by the driver because the its deadline has already passed. /// - /// This error indicates that the command was never sent over the wire to begin with. + /// This error indicates that the command was never sent over the wire to begin with, in + /// contrast to `Mongo.WireTimeoutError`. @frozen public struct DriverTimeoutError:Error, Equatable, Sendable { diff --git a/Sources/MongoDriver/Retryability/Mongo.Timeout.swift b/Sources/MongoDriver/Retryability/Mongo.NetworkTimeout.swift similarity index 72% rename from Sources/MongoDriver/Retryability/Mongo.Timeout.swift rename to Sources/MongoDriver/Retryability/Mongo.NetworkTimeout.swift index e88d9aa5..30ba41e4 100644 --- a/Sources/MongoDriver/Retryability/Mongo.Timeout.swift +++ b/Sources/MongoDriver/Retryability/Mongo.NetworkTimeout.swift @@ -2,25 +2,25 @@ import Durations extension Mongo { - @usableFromInline internal - struct Timeout:Sendable + @frozen @usableFromInline + struct NetworkTimeout:Sendable { - public - let `default`:Milliseconds + @usableFromInline + let milliseconds:Milliseconds - @inlinable public - init(default:Milliseconds) + @inlinable + init(milliseconds:Milliseconds) { - self.default = `default` + self.milliseconds = milliseconds } } } -extension Mongo.Timeout +extension Mongo.NetworkTimeout { @inlinable internal func deadline(from start:ContinuousClock.Instant = .now) -> ContinuousClock.Instant { - start.advanced(by: .milliseconds(self.default)) + start.advanced(by: .milliseconds(self.milliseconds)) } @inlinable internal diff --git a/Sources/MongoDriver/Sessions/Mongo.Session.swift b/Sources/MongoDriver/Sessions/Mongo.Session.swift index 948b90f8..68daf7a6 100644 --- a/Sources/MongoDriver/Sessions/Mongo.Session.swift +++ b/Sources/MongoDriver/Sessions/Mongo.Session.swift @@ -243,7 +243,7 @@ extension Mongo.Session } extension Mongo.Session { - @inlinable internal + @inlinable func begin(query:Query, against database:Query.Database, on preference:Mongo.ReadPreference, @@ -292,7 +292,7 @@ extension Mongo.Session throw Mongo.TransactionInProgressError.init() } } - @inlinable internal + @inlinable func begin(query:Query, against database:Query.Database, over connections:Mongo.ConnectionPool, @@ -306,7 +306,7 @@ extension Mongo.Session return .create(preference: preference, lifecycle: query.tailing.map { .iterable($0.timeout) } ?? .expires(deadlines.operation), - timeout: self.deployment.timeout.default, + timeout: self.deployment.timeout, initial: try await self.run(command: query, against: database, over: connection, @@ -461,7 +461,7 @@ extension Mongo.Session } extension Mongo.Session { - @inlinable internal + @inlinable func run(command:Command, against database:Command.Database, over connection:Mongo.Connection, on preference:Mongo.ReadPreference, diff --git a/Sources/MongoQL/Commands/Aggregate/Mongo.Aggregate.swift b/Sources/MongoQL/Commands/Aggregate/Mongo.Aggregate.swift index 10bd4314..fe0e0c1d 100644 --- a/Sources/MongoQL/Commands/Aggregate/Mongo.Aggregate.swift +++ b/Sources/MongoQL/Commands/Aggregate/Mongo.Aggregate.swift @@ -10,19 +10,23 @@ extension Mongo public let readConcern:ReadConcern? public + let tailing:Effect.Tailing? + public let stride:Effect.Stride? public var fields:BSON.Document - @inlinable internal + @inlinable init(writeConcern:WriteConcern?, readConcern:ReadConcern?, + tailing:Effect.Tailing?, stride:Effect.Stride?, fields:BSON.Document) { self.writeConcern = writeConcern self.readConcern = readConcern + self.tailing = tailing self.stride = stride self.fields = fields } @@ -43,103 +47,95 @@ extension Mongo.Aggregate:Mongo.Command } } extension Mongo.Aggregate +{ + @frozen @usableFromInline + enum BuiltinKey:String, Sendable + { + case pipeline + case explain + + case cursor + } +} +extension Mongo.Aggregate { @inlinable public init(_ collection:Mongo.Collection, writeConcern:WriteConcern? = nil, readConcern:ReadConcern? = nil, - pipeline:Mongo.Pipeline, - stride:Effect.Stride?) + tailing:Effect.Tailing? = nil, + stride:Effect.Stride? = nil, + pipeline:(inout Mongo.PipelineEncoder) throws -> ()) rethrows { self.init( writeConcern: writeConcern, readConcern: readConcern, + tailing: tailing, stride: stride, fields: Self.type(collection)) - ; + try { - $0["pipeline"] = pipeline - $0["cursor"] + if let stride:Effect.Stride = stride { - if let stride:Effect.Stride = stride - { - $0["batchSize"] = stride - } - else - { - $0["batchSize"] = Int.max - } + $0[.cursor] = Mongo.CursorOptions.init(batchSize: stride) } - } (&self.fields[BSON.Key.self]) - } - @inlinable public - init(_ collection:Mongo.Collection, - writeConcern:WriteConcern? = nil, - readConcern:ReadConcern? = nil, - pipeline:Mongo.Pipeline, - stride:Effect.Stride?, - with populate:(inout Self) throws -> ()) rethrows - { - self.init(collection, - writeConcern: writeConcern, - readConcern: readConcern, - pipeline: pipeline, - stride: stride) - try populate(&self) - } -} -extension Mongo.Aggregate where Effect.Stride == Never -{ - @inlinable public - init(_ collection:Mongo.Collection, - writeConcern:WriteConcern? = nil, - readConcern:ReadConcern? = nil, - pipeline:Mongo.Pipeline) - { - self.init(collection, - writeConcern: writeConcern, - readConcern: readConcern, - pipeline: pipeline, - stride: nil) + else + { + $0[.cursor] = Mongo.CursorOptions.init(batchSize: .max) + } + + try pipeline(&$0[.pipeline][as: Mongo.PipelineEncoder.self]) + + } (&self.fields[BuiltinKey.self]) } + @inlinable public init(_ collection:Mongo.Collection, writeConcern:WriteConcern? = nil, readConcern:ReadConcern? = nil, - pipeline:Mongo.Pipeline, - with populate:(inout Self) throws -> ()) rethrows + tailing:Effect.Tailing? = nil, + stride:Effect.Stride? = nil, + pipeline:(inout Mongo.PipelineEncoder) throws -> (), + options configure:(inout Self) throws -> ()) rethrows { try self.init(collection, writeConcern: writeConcern, readConcern: readConcern, - pipeline: pipeline, - stride: nil, - with: populate) + tailing: tailing, + stride: stride, + pipeline: pipeline) + try configure(&self) } } extension Mongo.Aggregate { @inlinable public - init(_ collection:Mongo.Collection, pipeline:Mongo.Pipeline) + init(_ collection:Mongo.Collection, + pipeline:(inout Mongo.PipelineEncoder) throws -> ()) rethrows { self.init( writeConcern: nil, readConcern: nil, + tailing: nil, stride: nil, fields: Self.type(collection)) - ; + + try { - $0["pipeline"] = pipeline - $0["explain"] = true - } (&self.fields[BSON.Key.self]) + try pipeline(&$0[.pipeline][as: Mongo.PipelineEncoder.self]) + + $0[.explain] = true + + } (&self.fields[BuiltinKey.self]) } + @inlinable public init(_ collection:Mongo.Collection, - pipeline:Mongo.Pipeline, - with populate:(inout Self) throws -> ()) rethrows + pipeline:(inout Mongo.PipelineEncoder) throws -> (), + options configure:(inout Self) throws -> ()) rethrows { - self.init(collection, pipeline: pipeline) - try populate(&self) + try self.init(collection, pipeline: pipeline) + try configure(&self) } } diff --git a/Sources/MongoQL/Commands/Find/Mongo.Find.swift b/Sources/MongoQL/Commands/Find/Mongo.Find.swift index ea9d38a3..f898fa89 100644 --- a/Sources/MongoQL/Commands/Find/Mongo.Find.swift +++ b/Sources/MongoQL/Commands/Find/Mongo.Find.swift @@ -15,7 +15,7 @@ extension Mongo public var fields:BSON.Document - @inlinable internal + @inlinable init(readConcern:ReadConcern?, tailing:Effect.Tailing?, stride:Effect.Stride?, diff --git a/Sources/MongoQL/Commands/ListCollections/Mongo.ListCollections.swift b/Sources/MongoQL/Commands/ListCollections/Mongo.ListCollections.swift index 4edee7e5..1e810371 100644 --- a/Sources/MongoQL/Commands/ListCollections/Mongo.ListCollections.swift +++ b/Sources/MongoQL/Commands/ListCollections/Mongo.ListCollections.swift @@ -48,6 +48,15 @@ extension Mongo.ListCollections:Mongo.Command public typealias Response = Mongo.CursorBatch } +extension Mongo.ListCollections +{ + @frozen @usableFromInline + enum BuiltinKey:String, Sendable + { + case nameOnly + case cursor + } +} extension Mongo.ListCollections { public @@ -56,13 +65,11 @@ extension Mongo.ListCollections self.init(stride: stride, fields: Self.type(1)) ; { - $0["cursor"] - { - $0["batchSize"] = stride - } - $0["nameOnly"] = true - } (&self.fields[BSON.Key.self]) + $0[.cursor] = Mongo.CursorOptions.init(batchSize: stride) + $0[.nameOnly] = true + } (&self.fields[BuiltinKey.self]) } + @inlinable public init(stride:Int, with populate:(inout Self) throws -> ()) rethrows { @@ -76,10 +83,7 @@ extension Mongo.ListCollections init(stride:Int) { self.init(stride: stride, fields: Self.type(1)) - self.fields[BSON.Key.self]["cursor"] - { - $0["batchSize"] = stride - } + self.fields[BuiltinKey.self][.cursor] = Mongo.CursorOptions.init(batchSize: stride) } @inlinable public init(stride:Int, with populate:(inout Self) throws -> ()) rethrows diff --git a/Sources/MongoQL/Commands/ListIndexes/Mongo.ListIndexes.swift b/Sources/MongoQL/Commands/ListIndexes/Mongo.ListIndexes.swift index 9e763246..6a1e332e 100644 --- a/Sources/MongoQL/Commands/ListIndexes/Mongo.ListIndexes.swift +++ b/Sources/MongoQL/Commands/ListIndexes/Mongo.ListIndexes.swift @@ -36,18 +36,23 @@ extension Mongo.ListIndexes:Mongo.Command public typealias Response = Mongo.CursorBatch } +extension Mongo.ListIndexes +{ + @frozen @usableFromInline + enum BuiltinKey:String, Sendable + { + case cursor + } +} extension Mongo.ListIndexes { public init(_ collection:Mongo.Collection, stride:Int? = nil) { self.init(stride: stride, fields: Self.type(collection)) - if let stride:Int { - { - $0["cursor"] { $0["batchSize"] = stride } - } (&self.fields[BSON.Key.self]) + self.fields[BuiltinKey.self][.cursor] = Mongo.CursorOptions.init(batchSize: stride) } } @inlinable public diff --git a/Sources/MongoQL/Commands/Mongo.CursorOptions.swift b/Sources/MongoQL/Commands/Mongo.CursorOptions.swift new file mode 100644 index 00000000..50e62edd --- /dev/null +++ b/Sources/MongoQL/Commands/Mongo.CursorOptions.swift @@ -0,0 +1,31 @@ +import BSON + +extension Mongo +{ + @frozen @usableFromInline + struct CursorOptions where Stride:BSONEncodable + { + @usableFromInline + let batchSize:Stride + + @inlinable + init(batchSize:Stride) + { + self.batchSize = batchSize + } + } +} +extension Mongo.CursorOptions:BSONDocumentEncodable +{ + @frozen @usableFromInline + enum CodingKey:String, Sendable + { + case batchSize + } + + @inlinable + func encode(to bson:inout BSON.DocumentEncoder) + { + bson[.batchSize] = self.batchSize + } +} From 8730bea43099abd08a94a56f1d4e741dc5e1b592 Mon Sep 17 00:00:00 2001 From: taylorswift Date: Wed, 27 Mar 2024 00:36:12 +0000 Subject: [PATCH 4/7] more-powerful change streams test --- .../ChangeStreams/ChangeStreams.swift | 78 ++++++++++++++++++- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift index 258ca3b7..7f0cef85 100644 --- a/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift +++ b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift @@ -2,12 +2,51 @@ import BSON import MongoDB import MongoTesting +extension ChangeStreams +{ + struct Ticket + { + let id:Int + let value:String + + init(id:Int, value:String) + { + self.id = id + self.value = value + } + } +} +extension ChangeStreams.Ticket:MongoMasterCodingModel +{ + enum CodingKey:String, Sendable + { + case id = "_id" + case value = "v" + } +} +extension ChangeStreams.Ticket:BSONDocumentEncodable +{ + func encode(to bson:inout BSON.DocumentEncoder) + { + bson[.id] = self.id + bson[.value] = self.value + } +} +extension ChangeStreams.Ticket:BSONDocumentDecodable +{ + init(bson:BSON.DocumentDecoder) throws + { + self.init(id: try bson[.id].decode(), value: try bson[.value].decode()) + } +} + struct ChangeStreams:MongoTestBattery where Configuration:MongoTestConfiguration { static func run(tests:TestGroup, pool:Mongo.SessionPool, database:Mongo.Database) async throws { - let collection:Mongo.Collection = "watchable" + let collection:Mongo.Collection = "tickets" + await tests.do { let session:Mongo.Session = try await .init(from: pool) @@ -15,6 +54,9 @@ struct ChangeStreams:MongoTestBattery where Configuration:MongoTe command: Mongo.Aggregate>.init(collection, writeConcern: .majority, readConcern: .majority, + // This is always needed, otherwise the cursor will die after a fixed + // amount of time. + tailing: .init(timeout: 5_000, awaits: true), stride: 10) { $0[stage: .changeStream] { _ in } @@ -22,9 +64,39 @@ struct ChangeStreams:MongoTestBattery where Configuration:MongoTe against: database, by: .now.advanced(by: .seconds(15))) { - for try await _:[BSON.Document] in $0 + var poll:Int = 0 + for try await batch:[BSON.Document] in $0 { - return + defer + { + poll += 1 + } + if poll == 0 + { + tests.expect(batch.count ==? 0) + } + else if + let document:BSON.Document = batch.first + { + print(document) + return + } + else if poll > 5 + { + // If more than 5 polling intervals have passed and we still haven't + // received any documents, then the test has failed. + tests.expect(true: false) + } + + let _:Task = .init + { + let session:Mongo.Session = try await .init(from: pool) + + let document:Ticket = .init(id: 1, value: "a") + let _:Mongo.InsertResponse = try await session.run( + command: Mongo.Insert.init(collection, encoding: [document]), + against: database) + } } } } From 8365489693b5969bb71f251791f94aec902ba8fe Mon Sep 17 00:00:00 2001 From: taylorswift Date: Wed, 27 Mar 2024 22:15:39 +0000 Subject: [PATCH 5/7] model change stream updates for unsharded collections, and add unit tests --- .../Modeling/Mongo.MasterCodingDelta.swift | 13 ++ .../Modeling/Mongo.MasterCodingModel.swift | 29 ++++ Sources/MongoABI/Mongo.Variable.swift | 2 +- Sources/MongoABI/MongoMasterCodingModel.swift | 19 --- .../Mongo.Collection.swift | 0 .../Mongo.Database.swift | 0 .../Mongo.Namespaced.swift | 0 .../Patterns/Mongo.EmptyDocument.swift | 26 ++++ .../Patterns/Mongo.IdentityDocument.swift | 42 ++++++ .../Pipelines/Mongo.ChangeEvent.swift | 70 ++++++++++ .../Mongo.ChangeEventIdentifier.swift | 5 +- .../Pipelines/Mongo.ChangeOperation.swift | 17 +++ .../Pipelines/Mongo.ChangeOperationType.swift | 24 ++++ .../Mongo.ChangeTruncatedArray.swift | 42 ++++++ .../Pipelines/Mongo.ChangeUpdate.swift | 60 +++++++++ .../Mongo.ChangeUpdateDescription.swift | 53 ++++++++ .../Mongo.ChangeUpdateRepresentation.swift | 14 ++ .../Aggregate/Aggregate.Article.swift | 2 +- .../Aggregate/Aggregate.TagStats.swift | 2 +- .../ChangeStreams/ChangeStreams.Plan.swift | 46 +++++++ .../ChangeStreams.PlanDelta.swift | 30 +++++ .../ChangeStreams/ChangeStreams.swift | 124 ++++++++++-------- .../Mongo.CollectionStats.Storage.swift | 2 +- .../Collections/Mongo.CollectionStats.swift | 2 +- 24 files changed, 543 insertions(+), 81 deletions(-) create mode 100644 Sources/MongoABI/Modeling/Mongo.MasterCodingDelta.swift create mode 100644 Sources/MongoABI/Modeling/Mongo.MasterCodingModel.swift delete mode 100644 Sources/MongoABI/MongoMasterCodingModel.swift rename Sources/MongoABI/{Modeling => Namespacing}/Mongo.Collection.swift (100%) rename Sources/MongoABI/{Modeling => Namespacing}/Mongo.Database.swift (100%) rename Sources/MongoABI/{Modeling => Namespacing}/Mongo.Namespaced.swift (100%) create mode 100644 Sources/MongoBuiltins/Patterns/Mongo.EmptyDocument.swift create mode 100644 Sources/MongoBuiltins/Patterns/Mongo.IdentityDocument.swift create mode 100644 Sources/MongoBuiltins/Pipelines/Mongo.ChangeOperation.swift create mode 100644 Sources/MongoBuiltins/Pipelines/Mongo.ChangeOperationType.swift create mode 100644 Sources/MongoBuiltins/Pipelines/Mongo.ChangeTruncatedArray.swift create mode 100644 Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdate.swift create mode 100644 Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdateDescription.swift create mode 100644 Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdateRepresentation.swift create mode 100644 Sources/MongoDBTests/ChangeStreams/ChangeStreams.Plan.swift create mode 100644 Sources/MongoDBTests/ChangeStreams/ChangeStreams.PlanDelta.swift diff --git a/Sources/MongoABI/Modeling/Mongo.MasterCodingDelta.swift b/Sources/MongoABI/Modeling/Mongo.MasterCodingDelta.swift new file mode 100644 index 00000000..16e04725 --- /dev/null +++ b/Sources/MongoABI/Modeling/Mongo.MasterCodingDelta.swift @@ -0,0 +1,13 @@ +import BSON + +extension Mongo +{ + /// A master coding delta type is like a ``MasterCodingModel``, but it has slightly + /// different implied constraints, because you generally only ever decode a delta you + /// receive from elsewhere. + public + protocol MasterCodingDelta:BSONDecodable + { + associatedtype CodingKey:BSONDecodable + } +} diff --git a/Sources/MongoABI/Modeling/Mongo.MasterCodingModel.swift b/Sources/MongoABI/Modeling/Mongo.MasterCodingModel.swift new file mode 100644 index 00000000..9ce8be51 --- /dev/null +++ b/Sources/MongoABI/Modeling/Mongo.MasterCodingModel.swift @@ -0,0 +1,29 @@ +@available(*, deprecated, renamed: "Mongo.MasterCodingModel") +public +typealias MongoMasterCodingModel = Mongo.MasterCodingModel + +extension Mongo +{ + /// A type that serves as a master coding model for a type of document in a MongoDB + /// collection. + /// + /// In many database applications, a single type of document can have many projected + /// representations. A master coding model is a type that defines the full set of + /// coding keys used to encode the documents. + /// + /// Occasionally, a master coding model shares a ``CodingKey`` type with a + /// ``MasterCodingDelta`` type. + public + protocol MasterCodingModel + { + associatedtype CodingKey:RawRepresentable + } +} +extension Mongo.MasterCodingModel +{ + @inlinable public static + subscript(key:CodingKey) -> Mongo.AnyKeyPath + { + .init(rawValue: key.rawValue) + } +} diff --git a/Sources/MongoABI/Mongo.Variable.swift b/Sources/MongoABI/Mongo.Variable.swift index 80230d09..8d0f3105 100644 --- a/Sources/MongoABI/Mongo.Variable.swift +++ b/Sources/MongoABI/Mongo.Variable.swift @@ -15,7 +15,7 @@ extension Mongo } } } -extension Mongo.Variable where T:MongoMasterCodingModel +extension Mongo.Variable where T:Mongo.MasterCodingModel { @inlinable public subscript(key:T.CodingKey) -> Mongo.AnyKeyPath diff --git a/Sources/MongoABI/MongoMasterCodingModel.swift b/Sources/MongoABI/MongoMasterCodingModel.swift deleted file mode 100644 index 993d19ab..00000000 --- a/Sources/MongoABI/MongoMasterCodingModel.swift +++ /dev/null @@ -1,19 +0,0 @@ -/// A type that serves as a master coding model for a type of document in a MongoDB -/// collection. -/// -/// In many database applications, a single type of document can have many projected -/// representations. A master coding model is a type that defines the full set of -/// coding keys used to encode the documents. -public -protocol MongoMasterCodingModel -{ - associatedtype CodingKey:RawRepresentable -} -extension MongoMasterCodingModel -{ - @inlinable public static - subscript(key:CodingKey) -> Mongo.AnyKeyPath - { - .init(rawValue: key.rawValue) - } -} diff --git a/Sources/MongoABI/Modeling/Mongo.Collection.swift b/Sources/MongoABI/Namespacing/Mongo.Collection.swift similarity index 100% rename from Sources/MongoABI/Modeling/Mongo.Collection.swift rename to Sources/MongoABI/Namespacing/Mongo.Collection.swift diff --git a/Sources/MongoABI/Modeling/Mongo.Database.swift b/Sources/MongoABI/Namespacing/Mongo.Database.swift similarity index 100% rename from Sources/MongoABI/Modeling/Mongo.Database.swift rename to Sources/MongoABI/Namespacing/Mongo.Database.swift diff --git a/Sources/MongoABI/Modeling/Mongo.Namespaced.swift b/Sources/MongoABI/Namespacing/Mongo.Namespaced.swift similarity index 100% rename from Sources/MongoABI/Modeling/Mongo.Namespaced.swift rename to Sources/MongoABI/Namespacing/Mongo.Namespaced.swift diff --git a/Sources/MongoBuiltins/Patterns/Mongo.EmptyDocument.swift b/Sources/MongoBuiltins/Patterns/Mongo.EmptyDocument.swift new file mode 100644 index 00000000..663d8908 --- /dev/null +++ b/Sources/MongoBuiltins/Patterns/Mongo.EmptyDocument.swift @@ -0,0 +1,26 @@ +import BSON + +extension Mongo +{ + /// A type that can be used to expect an empty document. Decoding from a non-empty document + /// (or a value that is not a document at all) will throw an error. + @frozen public + struct EmptyDocument + { + @inlinable + init() + { + } + } +} +extension Mongo.EmptyDocument:BSONDocumentDecodable +{ + @inlinable public + init(bson:BSON.DocumentDecoder) throws + { + for unexpected:BSON.FieldDecoder in bson + { + try unexpected.decode(to: Never.self) + } + } +} diff --git a/Sources/MongoBuiltins/Patterns/Mongo.IdentityDocument.swift b/Sources/MongoBuiltins/Patterns/Mongo.IdentityDocument.swift new file mode 100644 index 00000000..94fd5a1c --- /dev/null +++ b/Sources/MongoBuiltins/Patterns/Mongo.IdentityDocument.swift @@ -0,0 +1,42 @@ +import BSON +import MongoABI + +extension Mongo +{ + /// An generic document model that can be used to extract the `_id` field of a document. + /// This type is suitable for unsharded collections only. + @frozen public + struct IdentityDocument + { + public + let id:ID + + @inlinable public + init(id:ID) + { + self.id = id + } + } +} +extension Mongo.IdentityDocument:Identifiable where ID:Hashable +{ +} +extension Mongo.IdentityDocument:Sendable where ID:Sendable +{ +} +extension Mongo.IdentityDocument:Mongo.MasterCodingModel +{ + @frozen public + enum CodingKey:String, Sendable + { + case id = "_id" + } +} +extension Mongo.IdentityDocument:BSONDecodable, BSONDocumentDecodable where ID:BSONDecodable +{ + @inlinable public + init(bson:BSON.DocumentDecoder) throws + { + self.init(id: try bson[.id].decode()) + } +} diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEvent.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEvent.swift index e69de29b..47970409 100644 --- a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEvent.swift +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEvent.swift @@ -0,0 +1,70 @@ +import BSON + +extension Mongo +{ + @frozen public + struct ChangeEvent:Sendable + where Document:Sendable, DocumentUpdate:Sendable + { + public + let id:ChangeEventIdentifier + public + let clusterTime:BSON.Timestamp + public + let operation:ChangeOperation + + @inlinable public + init(id:ChangeEventIdentifier, + clusterTime:BSON.Timestamp, + operation:ChangeOperation) + { + self.id = id + self.clusterTime = clusterTime + self.operation = operation + } + } +} +extension Mongo.ChangeEvent:Equatable where Document:Equatable, DocumentUpdate:Equatable +{ +} +extension Mongo.ChangeEvent:BSONDocumentDecodable, BSONDecodable where + Document:BSONDecodable, + DocumentUpdate:Mongo.ChangeUpdateRepresentation +{ + @frozen public + enum CodingKey:String, Sendable + { + case id = "_id" + case operationType + case documentKey + case fullDocument + case fullDocumentBeforeChange + case updateDescription + case clusterTime + } + + @inlinable public + init(bson:BSON.DocumentDecoder) throws + { + let operation:Mongo.ChangeOperation + + switch try bson[.operationType].decode(to: Mongo.ChangeOperationType.self) + { + case .insert: + operation = .insert(try bson[.fullDocument].decode()) + + case .update: + operation = .update(.init(try bson[.updateDescription].decode(), + in: try bson[.documentKey].decode()), + before: try bson[.fullDocumentBeforeChange]?.decode(), + after: try bson[.fullDocument]?.decode()) + + default: + operation = ._unimplemented + } + + self.init(id: try bson[.id].decode(), + clusterTime: try bson[.clusterTime].decode(), + operation: operation) + } +} diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEventIdentifier.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEventIdentifier.swift index 3c1a8efb..47152150 100644 --- a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEventIdentifier.swift +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEventIdentifier.swift @@ -3,7 +3,7 @@ import BSON extension Mongo { @frozen public - struct ChangeEventIdentifier:RawRepresentable, BSONDecodable, BSONEncodable, Sendable + struct ChangeEventIdentifier:RawRepresentable, Equatable, Sendable { public var rawValue:BSON.Document @@ -15,3 +15,6 @@ extension Mongo } } } +extension Mongo.ChangeEventIdentifier:BSONDecodable, BSONEncodable +{ +} diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeOperation.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeOperation.swift new file mode 100644 index 00000000..f2ca8e6a --- /dev/null +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeOperation.swift @@ -0,0 +1,17 @@ +extension Mongo +{ + @frozen public + enum ChangeOperation + { + case insert(Document) + case update(DocumentUpdate, before:Document?, after:Document?) + + case _unimplemented + } +} +extension Mongo.ChangeOperation:Sendable where Document:Sendable, DocumentUpdate:Sendable +{ +} +extension Mongo.ChangeOperation:Equatable where Document:Equatable, DocumentUpdate:Equatable +{ +} diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeOperationType.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeOperationType.swift new file mode 100644 index 00000000..3d7ed635 --- /dev/null +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeOperationType.swift @@ -0,0 +1,24 @@ +import BSON + +extension Mongo +{ + @frozen @usableFromInline + enum ChangeOperationType:String, BSONDecodable, Equatable, Sendable + { + case create + case createIndexes + case delete + case drop + case dropDatabase + case dropIndexes + case insert + case invalidate + case modify + case refineCollectionShardKey + case rename + case replace + case reshardCollection + case shardCollection + case update + } +} diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeTruncatedArray.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeTruncatedArray.swift new file mode 100644 index 00000000..a5e232a4 --- /dev/null +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeTruncatedArray.swift @@ -0,0 +1,42 @@ +import BSON + +extension Mongo +{ + @frozen public + struct ChangeTruncatedArray + { + public + let field:Field + public + let newSize:Int + + @inlinable public + init(field:Field, newSize:Int) + { + self.field = field + self.newSize = newSize + } + } +} +extension Mongo.ChangeTruncatedArray:Sendable where Field:Sendable +{ +} +extension Mongo.ChangeTruncatedArray:Equatable where Field:Equatable +{ +} +extension Mongo.ChangeTruncatedArray:BSONDecodable, BSONDocumentDecodable + where Field:BSONDecodable +{ + @frozen public + enum CodingKey:String, Hashable, Sendable + { + case field + case newSize + } + + @inlinable public + init(bson:BSON.DocumentDecoder) throws + { + self.init(field: try bson[.field].decode(), newSize: try bson[.newSize].decode()) + } +} diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdate.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdate.swift new file mode 100644 index 00000000..a1f85990 --- /dev/null +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdate.swift @@ -0,0 +1,60 @@ +import BSON +import MongoABI + +extension Mongo +{ + /// Represents an **unsharded** document update. + @frozen public + struct ChangeUpdate where DocumentDelta:MasterCodingDelta + { + public + var updatedFields:DocumentDelta? + public + var removedFields:[DocumentDelta.CodingKey] + public + var truncatedArrays:[ChangeTruncatedArray] + public + var id:ID + + @inlinable public + init( + updatedFields:DocumentDelta?, + removedFields:[DocumentDelta.CodingKey], + truncatedArrays:[ChangeTruncatedArray], + id:ID) + { + self.updatedFields = updatedFields + self.removedFields = removedFields + self.truncatedArrays = truncatedArrays + self.id = id + } + } +} +extension Mongo.ChangeUpdate:Mongo.ChangeUpdateRepresentation where ID:BSONDecodable +{ + @inlinable public + init(_ updateDescription:Mongo.ChangeUpdateDescription, + in key:Mongo.IdentityDocument) + { + self.init( + updatedFields: updateDescription.updatedFields, + removedFields: updateDescription.removedFields, + truncatedArrays: updateDescription.truncatedArrays, + id: key.id) + } +} +extension Mongo.ChangeUpdate:Sendable + where DocumentDelta:Sendable, DocumentDelta.CodingKey:Sendable, ID:Sendable +{ +} +extension Mongo.ChangeUpdate +{ + @inlinable public + var updateDescription:Mongo.ChangeUpdateDescription + { + .init( + updatedFields: self.updatedFields, + removedFields: self.removedFields, + truncatedArrays: self.truncatedArrays) + } +} diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdateDescription.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdateDescription.swift new file mode 100644 index 00000000..ef7c7a82 --- /dev/null +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdateDescription.swift @@ -0,0 +1,53 @@ +import BSON +import MongoABI + +extension Mongo +{ + /// You probably do not want to use this type directly; use ``ChangeUpdate`` instead. + @frozen public + struct ChangeUpdateDescription where DocumentDelta:MasterCodingDelta + { + public + var updatedFields:DocumentDelta? + public + var removedFields:[DocumentDelta.CodingKey] + public + var truncatedArrays:[ChangeTruncatedArray] + + @inlinable public + init(updatedFields:DocumentDelta?, + removedFields:[DocumentDelta.CodingKey], + truncatedArrays:[ChangeTruncatedArray]) + { + self.updatedFields = updatedFields + self.removedFields = removedFields + self.truncatedArrays = truncatedArrays + } + } +} +extension Mongo.ChangeUpdateDescription:Sendable + where DocumentDelta:Sendable, DocumentDelta.CodingKey:Sendable +{ +} +extension Mongo.ChangeUpdateDescription:BSONDecodable, BSONDocumentDecodable +{ + @frozen public + enum CodingKey:String, Hashable, Sendable + { + case updatedFields + case removedFields + case truncatedArrays + case disambiguatedPaths + } + + @inlinable public + init(bson:BSON.DocumentDecoder) throws + { + let _:Mongo.EmptyDocument? = try bson[.disambiguatedPaths]?.decode() + + self.init( + updatedFields: try bson[.updatedFields]?.decode(), + removedFields: try bson[.removedFields]?.decode() ?? [], + truncatedArrays: try bson[.truncatedArrays]?.decode() ?? []) + } +} diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdateRepresentation.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdateRepresentation.swift new file mode 100644 index 00000000..a7c95edb --- /dev/null +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdateRepresentation.swift @@ -0,0 +1,14 @@ +import BSON +import MongoABI + +extension Mongo +{ + public + protocol ChangeUpdateRepresentation + { + associatedtype DocumentDelta:BSONDecodable, MasterCodingDelta + associatedtype DocumentKey:BSONDecodable + + init(_:ChangeUpdateDescription, in:DocumentKey) + } +} diff --git a/Sources/MongoDBTests/Aggregate/Aggregate.Article.swift b/Sources/MongoDBTests/Aggregate/Aggregate.Article.swift index 1017fb94..2720a0de 100644 --- a/Sources/MongoDBTests/Aggregate/Aggregate.Article.swift +++ b/Sources/MongoDBTests/Aggregate/Aggregate.Article.swift @@ -6,7 +6,7 @@ extension Aggregate struct Article:Equatable, Hashable, BSONDocumentDecodable, BSONDocumentEncodable, - MongoMasterCodingModel + Mongo.MasterCodingModel { let id:BSON.Identifier let author:String diff --git a/Sources/MongoDBTests/Aggregate/Aggregate.TagStats.swift b/Sources/MongoDBTests/Aggregate/Aggregate.TagStats.swift index 87929ebc..4de2fe68 100644 --- a/Sources/MongoDBTests/Aggregate/Aggregate.TagStats.swift +++ b/Sources/MongoDBTests/Aggregate/Aggregate.TagStats.swift @@ -3,7 +3,7 @@ import MongoQL extension Aggregate { - struct TagStats:Equatable, Hashable, BSONDocumentDecodable, MongoMasterCodingModel + struct TagStats:Equatable, Hashable, BSONDocumentDecodable, Mongo.MasterCodingModel { let id:String let count:Int diff --git a/Sources/MongoDBTests/ChangeStreams/ChangeStreams.Plan.swift b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.Plan.swift new file mode 100644 index 00000000..eb4e8d1c --- /dev/null +++ b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.Plan.swift @@ -0,0 +1,46 @@ +import BSON +import MongoQL + +extension ChangeStreams +{ + struct Plan:Equatable, Sendable + { + let id:Int + var owner:String + var level:String + + init(id:Int, owner:String, level:String) + { + self.id = id + self.owner = owner + self.level = level + } + } +} +extension ChangeStreams.Plan:Mongo.MasterCodingModel +{ + enum CodingKey:String, BSONDecodable, Sendable + { + case id = "_id" + case owner = "O" + case level = "L" + } +} +extension ChangeStreams.Plan:BSONDocumentEncodable +{ + func encode(to bson:inout BSON.DocumentEncoder) + { + bson[.id] = self.id + bson[.owner] = self.owner + bson[.level] = self.level + } +} +extension ChangeStreams.Plan:BSONDocumentDecodable +{ + init(bson:BSON.DocumentDecoder) throws + { + self.init(id: try bson[.id].decode(), + owner: try bson[.owner].decode(), + level: try bson[.level].decode()) + } +} diff --git a/Sources/MongoDBTests/ChangeStreams/ChangeStreams.PlanDelta.swift b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.PlanDelta.swift new file mode 100644 index 00000000..dd4b4ad3 --- /dev/null +++ b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.PlanDelta.swift @@ -0,0 +1,30 @@ +import BSON +import MongoQL + +extension ChangeStreams +{ + struct PlanDelta:Equatable, Sendable + { + var owner:String? + var level:String? + + init(owner:String? = nil, level:String? = nil) + { + self.owner = owner + self.level = level + } + } +} +extension ChangeStreams.PlanDelta:Mongo.MasterCodingDelta +{ + typealias CodingKey = ChangeStreams.Plan.CodingKey +} +extension ChangeStreams.PlanDelta:BSONDocumentDecodable +{ + init(bson:BSON.DocumentDecoder) throws + { + self.init( + owner: try bson[.owner]?.decode(), + level: try bson[.level]?.decode()) + } +} diff --git a/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift index 7f0cef85..182df189 100644 --- a/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift +++ b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift @@ -2,70 +2,43 @@ import BSON import MongoDB import MongoTesting -extension ChangeStreams -{ - struct Ticket - { - let id:Int - let value:String - - init(id:Int, value:String) - { - self.id = id - self.value = value - } - } -} -extension ChangeStreams.Ticket:MongoMasterCodingModel -{ - enum CodingKey:String, Sendable - { - case id = "_id" - case value = "v" - } -} -extension ChangeStreams.Ticket:BSONDocumentEncodable -{ - func encode(to bson:inout BSON.DocumentEncoder) - { - bson[.id] = self.id - bson[.value] = self.value - } -} -extension ChangeStreams.Ticket:BSONDocumentDecodable -{ - init(bson:BSON.DocumentDecoder) throws - { - self.init(id: try bson[.id].decode(), value: try bson[.value].decode()) - } -} - struct ChangeStreams:MongoTestBattery where Configuration:MongoTestConfiguration { static func run(tests:TestGroup, pool:Mongo.SessionPool, database:Mongo.Database) async throws { - let collection:Mongo.Collection = "tickets" + let collection:Mongo.Collection = "subscriptions" await tests.do { + typealias ChangeEvent = Mongo.ChangeEvent> + let session:Mongo.Session = try await .init(from: pool) + + let state:(Plan, PlanDelta) = + ( + .init(id: 1, owner: "a", level: "x"), + .init(owner: nil, level: "y") + ) try await session.run( - command: Mongo.Aggregate>.init(collection, + command: Mongo.Aggregate>.init(collection, writeConcern: .majority, readConcern: .majority, // This is always needed, otherwise the cursor will die after a fixed // amount of time. - tailing: .init(timeout: 5_000, awaits: true), + tailing: .init(timeout: 4_000, awaits: true), stride: 10) { $0[stage: .changeStream] { _ in } }, against: database, - by: .now.advanced(by: .seconds(15))) + // We set this to an artificially low value to check that the cursor tailing + // is working properly. + by: .now.advanced(by: .seconds(2))) { var poll:Int = 0 - for try await batch:[BSON.Document] in $0 + var insertSeen:Bool = false + for try await batch:[ChangeEvent] in $0 { defer { @@ -74,29 +47,68 @@ struct ChangeStreams:MongoTestBattery where Configuration:MongoTe if poll == 0 { tests.expect(batch.count ==? 0) + + let _:Task = .init + { + let session:Mongo.Session = try await .init(from: pool) + + + let _:Mongo.InsertResponse = try await session.run( + command: Mongo.Insert.init(collection, encoding: [state.0]), + against: database) + + let _:Mongo.UpdateResponse = try await session.run( + command: Mongo.Update.init(collection) + { + $0 + { + $0[.q] { $0[Plan[.id]] = 1 } + $0[.u] + { + $0[.set] { $0[Plan[.level]] = state.1.level } + } + } + }, + against: database) + } } - else if - let document:BSON.Document = batch.first + else if insertSeen, + let document:ChangeEvent = batch.first { - print(document) + guard + case .update(let update, before: _, after: _) = document.operation + else + { + tests.expect(value: nil as Mongo.ChangeUpdate?) + return + } + + tests.expect(update.id ==? state.0.id) + tests.expect(update.updatedFields ==? state.1) + tests.expect(update.removedFields ==? []) + tests.expect(update.truncatedArrays ==? []) return } + else if + let event:ChangeEvent = batch.first + { + guard + case .insert(let document) = event.operation + else + { + tests.expect(value: nil as Plan?) + return + } + + tests.expect(document ==? state.0) + insertSeen = true + } else if poll > 5 { // If more than 5 polling intervals have passed and we still haven't // received any documents, then the test has failed. tests.expect(true: false) } - - let _:Task = .init - { - let session:Mongo.Session = try await .init(from: pool) - - let document:Ticket = .init(id: 1, value: "a") - let _:Mongo.InsertResponse = try await session.run( - command: Mongo.Insert.init(collection, encoding: [document]), - against: database) - } } } } diff --git a/Sources/MongoQL/Collections/Mongo.CollectionStats.Storage.swift b/Sources/MongoQL/Collections/Mongo.CollectionStats.Storage.swift index 3c2295bb..cbd7d357 100644 --- a/Sources/MongoQL/Collections/Mongo.CollectionStats.Storage.swift +++ b/Sources/MongoQL/Collections/Mongo.CollectionStats.Storage.swift @@ -43,7 +43,7 @@ extension Mongo.CollectionStats } } } -extension Mongo.CollectionStats.Storage:MongoMasterCodingModel +extension Mongo.CollectionStats.Storage:Mongo.MasterCodingModel { @frozen public enum CodingKey:String, Sendable, CaseIterable diff --git a/Sources/MongoQL/Collections/Mongo.CollectionStats.swift b/Sources/MongoQL/Collections/Mongo.CollectionStats.swift index 49f10eab..96345212 100644 --- a/Sources/MongoQL/Collections/Mongo.CollectionStats.swift +++ b/Sources/MongoQL/Collections/Mongo.CollectionStats.swift @@ -15,7 +15,7 @@ extension Mongo } } } -extension Mongo.CollectionStats:MongoMasterCodingModel +extension Mongo.CollectionStats:Mongo.MasterCodingModel { @frozen public enum CodingKey:String, Sendable From f0cc7f87d7a190581c69da63d7aa65b56317394f Mon Sep 17 00:00:00 2001 From: taylorswift Date: Wed, 27 Mar 2024 22:58:10 +0000 Subject: [PATCH 6/7] support replace and delete change events --- .../Pipelines/Mongo.ChangeEvent.swift | 9 +++ .../Pipelines/Mongo.ChangeOperation.swift | 26 ++++++- .../Pipelines/Mongo.ChangeUpdate.swift | 10 ++- .../Mongo.ChangeUpdateDescription.swift | 6 +- .../ChangeStreams/ChangeStreams.swift | 75 ++++++++++++------- 5 files changed, 94 insertions(+), 32 deletions(-) diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEvent.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEvent.swift index 47970409..23f46298 100644 --- a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEvent.swift +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeEvent.swift @@ -53,12 +53,21 @@ extension Mongo.ChangeEvent:BSONDocumentDecodable, BSONDecodable where case .insert: operation = .insert(try bson[.fullDocument].decode()) + case .delete: + operation = .delete(.init(.init(), in: try bson[.documentKey].decode())) + case .update: operation = .update(.init(try bson[.updateDescription].decode(), in: try bson[.documentKey].decode()), before: try bson[.fullDocumentBeforeChange]?.decode(), after: try bson[.fullDocument]?.decode()) + case .replace: + operation = .replace(.init(.init(), + in: try bson[.documentKey].decode()), + before: try bson[.fullDocumentBeforeChange]?.decode(), + after: try bson[.fullDocument].decode()) + default: operation = ._unimplemented } diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeOperation.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeOperation.swift index f2ca8e6a..4baddbe9 100644 --- a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeOperation.swift +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeOperation.swift @@ -3,8 +3,32 @@ extension Mongo @frozen public enum ChangeOperation { - case insert(Document) + /// Replacement is similar to a normal ``update(_:before:after:)``, but the + /// `DocumentUpdate` instance will only include the + /// ``ChangeUpdateRepresentation/DocumentKey`` of the replaced document, and + /// additionally, it + /// [always includes](https://github.com/mongodb/specifications/blob/master/source/change-streams/change-streams.rst#server-specification) + /// the post-image document. + /// + /// The case layout wraps the entire `DocumentUpdate` and not just its `_id` field, for + /// future compatibility with sharded collections. + case replace(DocumentUpdate, before:Document?, after:Document) + /// A document update that was not a full replacement. This type of event includes + /// information about the changed fields. + /// + /// The event might include pre- and post-images, if the collection was configured for + /// that. The document images might include unrelated changes, as they respect + /// majority read concern. case update(DocumentUpdate, before:Document?, after:Document?) + /// Deletion is similar to an ``update(_:before:after:)``, but the `DocumentUpdate` + /// instance will only include the ``ChangeUpdateRepresentation/DocumentKey`` of the + /// deleted document. + /// + /// The case layout wraps the entire `DocumentUpdate` and not just its `_id` field, for + /// future compatibility with sharded collections. + case delete(DocumentUpdate) + /// A document insertion. The payload is the inserted document. + case insert(Document) case _unimplemented } diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdate.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdate.swift index a1f85990..cb1a7f84 100644 --- a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdate.swift +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdate.swift @@ -18,9 +18,9 @@ extension Mongo @inlinable public init( - updatedFields:DocumentDelta?, - removedFields:[DocumentDelta.CodingKey], - truncatedArrays:[ChangeTruncatedArray], + updatedFields:DocumentDelta? = nil, + removedFields:[DocumentDelta.CodingKey] = [], + truncatedArrays:[ChangeTruncatedArray] = [], id:ID) { self.updatedFields = updatedFields @@ -43,6 +43,10 @@ extension Mongo.ChangeUpdate:Mongo.ChangeUpdateRepresentation where ID:BSONDecod id: key.id) } } +extension Mongo.ChangeUpdate:Equatable + where DocumentDelta:Equatable, DocumentDelta.CodingKey:Equatable, ID:Equatable +{ +} extension Mongo.ChangeUpdate:Sendable where DocumentDelta:Sendable, DocumentDelta.CodingKey:Sendable, ID:Sendable { diff --git a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdateDescription.swift b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdateDescription.swift index ef7c7a82..58ce3823 100644 --- a/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdateDescription.swift +++ b/Sources/MongoBuiltins/Pipelines/Mongo.ChangeUpdateDescription.swift @@ -15,9 +15,9 @@ extension Mongo var truncatedArrays:[ChangeTruncatedArray] @inlinable public - init(updatedFields:DocumentDelta?, - removedFields:[DocumentDelta.CodingKey], - truncatedArrays:[ChangeTruncatedArray]) + init(updatedFields:DocumentDelta? = nil, + removedFields:[DocumentDelta.CodingKey] = [], + truncatedArrays:[ChangeTruncatedArray] = []) { self.updatedFields = updatedFields self.removedFields = removedFields diff --git a/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift index 182df189..e7818a71 100644 --- a/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift +++ b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift @@ -12,14 +12,31 @@ struct ChangeStreams:MongoTestBattery where Configuration:MongoTe await tests.do { typealias ChangeEvent = Mongo.ChangeEvent> + typealias Change = Mongo.ChangeOperation> let session:Mongo.Session = try await .init(from: pool) - let state:(Plan, PlanDelta) = + let state:(Plan, Plan, Plan) = ( .init(id: 1, owner: "a", level: "x"), - .init(owner: nil, level: "y") + .init(id: 1, owner: "a", level: "y"), + .init(id: 1, owner: "b", level: "y") ) + var changes:[Change] = [ + .insert(state.0), + .update(.init(updatedFields: .init(owner: nil, level: "y"), + id: state.0.id), + before: nil, + after: nil), + .replace(.init(updatedFields: nil, + id: state.0.id), + before: nil, + after: state.2), + .delete(.init(id: state.0.id)) + ] + + changes.reverse() + try await session.run( command: Mongo.Aggregate>.init(collection, writeConcern: .majority, @@ -37,7 +54,6 @@ struct ChangeStreams:MongoTestBattery where Configuration:MongoTe by: .now.advanced(by: .seconds(2))) { var poll:Int = 0 - var insertSeen:Bool = false for try await batch:[ChangeEvent] in $0 { defer @@ -59,55 +75,64 @@ struct ChangeStreams:MongoTestBattery where Configuration:MongoTe let _:Mongo.UpdateResponse = try await session.run( command: Mongo.Update.init(collection) + { + $0[.ordered] = true + } + updates: { $0 { - $0[.q] { $0[Plan[.id]] = 1 } + $0[.q] { $0[Plan[.id]] = state.0.id } $0[.u] { $0[.set] { $0[Plan[.level]] = state.1.level } } } + + $0 + { + $0[.q] { $0[Plan[.id]] = state.0.id } + $0[.u] = state.2 + } + }, + against: database) + + let _:Mongo.DeleteResponse = try await session.run( + command: Mongo.Delete.init(collection) + { + $0 + { + $0[.q] { $0[Plan[.id]] = state.0.id } + $0[.limit] = .one + } }, against: database) } } - else if insertSeen, + else if let document:ChangeEvent = batch.first { guard - case .update(let update, before: _, after: _) = document.operation + let expected:Change = changes.popLast() else { - tests.expect(value: nil as Mongo.ChangeUpdate?) + tests.expect(nil: document) return } - tests.expect(update.id ==? state.0.id) - tests.expect(update.updatedFields ==? state.1) - tests.expect(update.removedFields ==? []) - tests.expect(update.truncatedArrays ==? []) - return - } - else if - let event:ChangeEvent = batch.first - { - guard - case .insert(let document) = event.operation - else + tests.expect(document.operation ==? expected) + + if changes.isEmpty { - tests.expect(value: nil as Plan?) return } - - tests.expect(document ==? state.0) - insertSeen = true } else if poll > 5 { // If more than 5 polling intervals have passed and we still haven't - // received any documents, then the test has failed. - tests.expect(true: false) + // received the expected changes, then the test has failed. + tests.expect(changes ..? []) + return } } } From ba92a0655369529a93ef9d7d4093d1603c5da424 Mon Sep 17 00:00:00 2001 From: taylorswift Date: Wed, 27 Mar 2024 23:08:41 +0000 Subject: [PATCH 7/7] account for case where mongodb coalesces change events into cursor batches --- .../ChangeStreams/ChangeStreams.swift | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift index e7818a71..3545fe3c 100644 --- a/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift +++ b/Sources/MongoDBTests/ChangeStreams/ChangeStreams.swift @@ -109,31 +109,33 @@ struct ChangeStreams:MongoTestBattery where Configuration:MongoTe against: database) } } - else if - let document:ChangeEvent = batch.first + else if poll > 5 + { + // If more than 5 polling intervals have passed and we still haven't + // received the expected changes, then the test has failed. + tests.expect(changes ..? []) + return + } + + // It is rare, but MongoDB does occasionally coalesce multiple changes into + // one cursor batch. + for event:ChangeEvent in batch { guard let expected:Change = changes.popLast() else { - tests.expect(nil: document) + tests.expect(nil: event) return } - tests.expect(document.operation ==? expected) + tests.expect(event.operation ==? expected) if changes.isEmpty { return } } - else if poll > 5 - { - // If more than 5 polling intervals have passed and we still haven't - // received the expected changes, then the test has failed. - tests.expect(changes ..? []) - return - } } } }