Esempio n. 1
0
        /// <summary>
        /// Applies a document modification to the document tree. Returns the DocumentChange event for
        /// successful modifications, or null if the old and new documents have the same update timestamp.
        /// </summary>
        private DocumentChange ModifyDocument(DocumentSnapshot newDocument)
        {
            var docRef = newDocument.Reference;

            if (!_documentSet.TryGetDocument(docRef, out var oldDocument))
            {
                // TODO: Is this appropriate? Java throws an NPE here...
                throw new InvalidOperationException("Attempt to create a document modification, but document wasn't in set.");
            }
            if (oldDocument.UpdateTime == newDocument.UpdateTime)
            {
                return(null);
            }
            int oldIndex = _documentSet.IndexOf(docRef);

            _documentSet = _documentSet.WithDocumentRemoved(docRef);
            _documentSet = _documentSet.WithDocumentAdded(newDocument);
            int newIndex = _documentSet.IndexOf(docRef);

            return(new DocumentChange(newDocument, DocumentChange.Type.Modified, oldIndex, newIndex));
        }
Esempio n. 2
0
        internal async Task <QuerySnapshot> SnapshotAsync(ByteString transactionId, CancellationToken cancellationToken)
        {
            var       responses = StreamResponsesAsync(transactionId, cancellationToken);
            Timestamp?readTime  = null;
            List <DocumentSnapshot> snapshots = new List <DocumentSnapshot>();
            await responses.ForEachAsync(response =>
            {
                if (response.Document != null)
                {
                    snapshots.Add(DocumentSnapshot.ForDocument(Database, response.Document, Timestamp.FromProto(response.ReadTime)));
                }
                if (readTime == null && response.ReadTime != null)
                {
                    readTime = Timestamp.FromProto(response.ReadTime);
                }
            }, cancellationToken).ConfigureAwait(false);

            GaxPreconditions.CheckState(readTime != null, "The stream returned from RunQuery did not provide a read timestamp.");

            return(new QuerySnapshot(this, snapshots.AsReadOnly(), readTime.Value));
        }
Esempio n. 3
0
        private ChangeSet ExtractChanges(Timestamp readTime)
        {
            ChangeSet changeSet = new ChangeSet();

            foreach (var entry in _changeMap)
            {
                if (entry.Value == null)
                {
                    if (_documentSet.TryGetDocument(entry.Key, out var document))
                    {
                        changeSet.Deletes.Add(document);
                    }
                }
                else
                {
                    DocumentSnapshot snapshot = DocumentSnapshot.ForDocument(_query.Database, entry.Value, readTime);
                    var list = _documentSet.Contains(entry.Key) ? changeSet.Updates : changeSet.Adds;
                    list.Add(snapshot);
                }
            }

            return(changeSet);
        }
Esempio n. 4
0
        /// <summary>
        /// Watch this document for changes.
        /// </summary>
        /// <param name="callback">The callback to invoke each time the document changes. Must not be null.</param>
        /// <param name="cancellationToken">Optional cancellation token which may be used to cancel the listening operation.</param>
        /// <returns>A <see cref="FirestoreChangeListener"/> which may be used to monitor the listening operation and stop it gracefully.</returns>
        public FirestoreChangeListener Listen(Func <DocumentSnapshot, CancellationToken, Task> callback, CancellationToken cancellationToken = default)
        {
            GaxPreconditions.CheckNotNull(callback, nameof(callback));
            var target = WatchStream.CreateTarget(this);
            Func <QuerySnapshot, CancellationToken, Task> queryCallback = async(querySnapshot, localCancellationToken) =>
            {
                foreach (var doc in querySnapshot)
                {
                    if (doc.Reference.Equals(this))
                    {
                        await callback(doc, localCancellationToken).ConfigureAwait(false);

                        return;
                    }
                }
                // TODO: This will mean parsing the path back to a DocumentReference. Maybe we should accept "this".
                var missingDoc = DocumentSnapshot.ForMissingDocument(Database, Path, querySnapshot.ReadTime);
                await callback(missingDoc, cancellationToken).ConfigureAwait(false);
            };
            var stream = new WatchStream(new WatchState(Parent, queryCallback), target, Database, cancellationToken);

            return(FirestoreChangeListener.Start(stream));
        }
        /// <summary>
        /// Fetches document snapshots from the server, based on an optional transaction ID.
        /// </summary>
        /// <param name="documents">The document references to fetch. Must not be null, or contain null references.</param>
        /// <param name="transactionId">A transaction ID, or null to not include any transaction ID.</param>
        /// <param name="fieldMask">The field mask to use to restrict which fields are retrieved. May be null, in which
        /// case no field mask is applied, and the complete documents are retrieved.</param>
        /// <param name="cancellationToken">A cancellation token for the operation.</param>
        /// <returns>The document snapshots, in the order they are provided in the response. (This may not be the order of <paramref name="documents"/>.)</returns>
        internal async Task <IList <DocumentSnapshot> > GetDocumentSnapshotsAsync(IEnumerable <DocumentReference> documents, ByteString transactionId, FieldMask fieldMask, CancellationToken cancellationToken)
        {
            GaxPreconditions.CheckNotNull(documents, nameof(documents));
            var request = new BatchGetDocumentsRequest
            {
                Database  = RootPath,
                Documents = { documents.Select(ExtractPath) },
                Mask      = fieldMask?.ToProto()
            };

            if (transactionId != null)
            {
                request.Transaction = transactionId;
            }

            var clock        = Client.Settings.Clock ?? SystemClock.Instance;
            var scheduler    = Client.Settings.Scheduler ?? SystemScheduler.Instance;
            var callSettings = _batchGetCallSettings.WithCancellationToken(cancellationToken);

            // This is the function that we'll retry. We can't use the built-in retry functionality, because it's not a unary gRPC call.
            // (We could potentially simulate a unary call, but it would be a little odd to do so.)
            // Note that we perform a "whole request" retry. In theory we could collect some documents, then see an error, and only
            // request the remaining documents. Given how rarely we retry anyway in practice, that's probably not worth doing.
            Func <BatchGetDocumentsRequest, CallSettings, Task <List <DocumentSnapshot> > > function = async(req, settings) =>
            {
                var stream = Client.BatchGetDocuments(req, settings);
                using (var responseStream = stream.ResponseStream)
                {
                    List <DocumentSnapshot> snapshots = new List <DocumentSnapshot>();

                    // Note: no need to worry about passing the cancellation token in here, as we've passed it into the overall call.
                    // If the token is cancelled, the call will be aborted.
                    while (await responseStream.MoveNext().ConfigureAwait(false))
                    {
                        var response = responseStream.Current;
                        var readTime = Timestamp.FromProto(response.ReadTime);
                        switch (response.ResultCase)
                        {
                        case BatchGetDocumentsResponse.ResultOneofCase.Found:
                            snapshots.Add(DocumentSnapshot.ForDocument(this, response.Found, readTime));
                            break;

                        case BatchGetDocumentsResponse.ResultOneofCase.Missing:
                            snapshots.Add(DocumentSnapshot.ForMissingDocument(this, response.Missing, readTime));
                            break;

                        default:
                            throw new InvalidOperationException($"Unknown response type: {response.ResultCase}");
                        }
                    }
                    return(snapshots);
                }
            };

            var retryingTask = RetryHelper.Retry(function, request, callSettings, clock, scheduler);

            return(await retryingTask.ConfigureAwait(false));

            string ExtractPath(DocumentReference documentReference)
            {
                GaxPreconditions.CheckArgument(documentReference != null, nameof(documents), "DocumentReference sequence must not contain null elements.");
                return(documentReference.Path);
            }
        }
Esempio n. 6
0
        private Cursor CreateCursorFromSnapshot(DocumentSnapshot snapshot, bool before, out IReadOnlyList <InternalOrdering> newOrderings)
        {
            GaxPreconditions.CheckArgument(Equals(snapshot.Reference.Parent, Collection),
                                           nameof(snapshot), "Snapshot was from incorrect collection");

            GaxPreconditions.CheckNotNull(snapshot, nameof(snapshot));
            var cursor = new Cursor {
                Before = before
            };
            bool hasDocumentId = false;

            // We may or may not need to add some orderings; this is communicated through the out parameter.
            newOrderings = _orderings;
            // Only used when we need to add orderings; set newOrderings to this at the same time.
            List <InternalOrdering> modifiedOrderings = null;

            if (_orderings.Count == 0 && _filters != null)
            {
                // If no explicit ordering is specified, use the first inequality to define an implicit order.
                foreach (var filter in _filters)
                {
                    if (!filter.IsEqualityFilter())
                    {
                        modifiedOrderings = new List <InternalOrdering>(newOrderings)
                        {
                            new InternalOrdering(filter.Field, Direction.Ascending)
                        };
                        newOrderings = modifiedOrderings;
                    }
                }
            }
            else
            {
                hasDocumentId = _orderings.Any(order => Equals(order.Field, FieldPath.DocumentId));
            }

            if (!hasDocumentId)
            {
                // Add implicit sorting by name, using the last specified direction.
                Direction lastDirection = _orderings.Count == 0 ? Direction.Ascending : _orderings.Last().Direction;

                // Clone iff this is the first new ordering.
                if (modifiedOrderings == null)
                {
                    modifiedOrderings = new List <InternalOrdering>(newOrderings);
                    newOrderings      = modifiedOrderings;
                }
                modifiedOrderings.Add(new InternalOrdering(FieldPath.DocumentId, lastDirection));
            }

            foreach (var ordering in newOrderings)
            {
                var field = ordering.Field;
                var value = Equals(field, FieldPath.DocumentId) ? ValueSerializer.Serialize(snapshot.Reference) : snapshot.ExtractValue(field);
                if (value == null)
                {
                    throw new ArgumentException($"Snapshot does not contain field {field}", nameof(snapshot));
                }
                cursor.Values.Add(ValueSerializer.Serialize(value));
            }
            return(cursor);
        }
Esempio n. 7
0
        internal Query EndAtSnapshot(DocumentSnapshot snapshot, bool before)
        {
            var cursor = CreateCursorFromSnapshot(snapshot, before, out var newOrderings);

            return(new Query(Collection, _offset, _limit, newOrderings, _filters, _projections, _startAt, cursor));
        }
Esempio n. 8
0
 internal IAsyncEnumerable <DocumentSnapshot> StreamAsync(ByteString transactionId, CancellationToken cancellationToken) =>
 StreamResponsesAsync(transactionId, cancellationToken)
 .Where(resp => resp.Document != null)
 .Select(resp => DocumentSnapshot.ForDocument(Database, resp.Document, Timestamp.FromProto(resp.ReadTime)));
Esempio n. 9
0
 /// <summary>
 /// Creates and returns a new query that ends at the document snapshot provided fields relative to the order of the
 /// query.
 /// </summary>
 /// <remarks>
 /// This call replaces any previously specified end position in the query.
 /// </remarks>
 /// <param name="snapshot">The snapshot of the document to end at.</param>
 /// <returns>A new query based on the current one, but with the specified end position.</returns>
 public Query EndAt(DocumentSnapshot snapshot) => EndAtSnapshot(snapshot, false);
Esempio n. 10
0
 /// <summary>
 /// Creates and returns a new query that ends before the document snapshot provided fields relative to the order of the
 /// query.
 /// </summary>
 /// <remarks>
 /// This call replaces any previously specified end position in the query.
 /// </remarks>
 /// <param name="snapshot">The snapshot of the document to end before. Must not be null.</param>
 /// <returns>A new query based on the current one, but with the specified end position.</returns>
 public Query EndBefore(DocumentSnapshot snapshot) => EndAtSnapshot(snapshot, true);
Esempio n. 11
0
 /// <summary>
 /// Creates and returns a new query that starts after the document snapshot provided fields relative to the order of the
 /// query.
 /// </summary>
 /// <remarks>
 /// This call replaces any previously specified start position in the query.
 /// </remarks>
 /// <param name="snapshot">The snapshot of the document to start after. Must not be null.</param>
 /// <returns>A new query based on the current one, but with the specified start position.</returns>
 public Query StartAfter(DocumentSnapshot snapshot) => StartAtSnapshot(snapshot, false);
Esempio n. 12
0
 /// <summary>
 /// Creates and returns a new query that starts at the document snapshot provided fields relative to the order of the
 /// query.
 /// </summary>
 /// <remarks>
 /// This call replaces any previously specified start position in the query.
 /// </remarks>
 /// <param name="snapshot">The snapshot of the document to start at. Must not be null.</param>
 /// <returns>A new query based on the current one, but with the specified start position.</returns>
 public Query StartAt(DocumentSnapshot snapshot) => StartAtSnapshot(snapshot, true);
 /// <summary>
 /// Constructs a new context.
 /// </summary>
 /// <param name="snapshot">The document snapshot being deserialized. Must not be null.</param>
 internal DeserializationContext(DocumentSnapshot snapshot)
 {
     Snapshot = GaxPreconditions.CheckNotNull(snapshot, nameof(snapshot));
 }