/*
 * Copyright 2009-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <mongoc/mongoc-change-stream-private.h>
#include <mongoc/mongoc-client-private.h>
#include <mongoc/mongoc-client-session-private.h>
#include <mongoc/mongoc-cluster-private.h>
#include <mongoc/mongoc-collection-private.h>
#include <mongoc/mongoc-cursor-private.h>
#include <mongoc/mongoc-database-private.h>
#include <mongoc/mongoc-error-private.h>

#include <bson/bson.h>

#define CHANGE_STREAM_ERR(_str) \
   _mongoc_set_error(&stream->err, MONGOC_ERROR_CURSOR, MONGOC_ERROR_BSON, "Could not set " _str)

/* the caller knows either a client or server error has occurred.
 * `reply` contains the server reply or an empty document. */
static bool
_is_resumable_error(mongoc_change_stream_t *stream, const bson_t *reply)
{
   bson_error_t error = {0};

   /* Change Streams Spec resumable criteria: "any error encountered which is
    * not a server error (e.g. a timeout error or network error)" */
   if (bson_empty(reply)) {
      return true;
   }

   if (_mongoc_cmd_check_ok(reply, MONGOC_ERROR_API_VERSION_2, &error)) {
      return true;
   }

   if (error.code == MONGOC_SERVER_ERR_CURSOR_NOT_FOUND) {
      return true;
   }

   if (stream->max_wire_version >= WIRE_VERSION_4_4) {
      return mongoc_error_has_label(reply, "ResumableChangeStreamError");
   }

   switch (error.code) {
   case MONGOC_SERVER_ERR_HOSTUNREACHABLE:
   case MONGOC_SERVER_ERR_HOSTNOTFOUND:
   case MONGOC_SERVER_ERR_NETWORKTIMEOUT:
   case MONGOC_SERVER_ERR_SHUTDOWNINPROGRESS:
   case MONGOC_SERVER_ERR_PRIMARYSTEPPEDDOWN:
   case MONGOC_SERVER_ERR_EXCEEDEDTIMELIMIT:
   case MONGOC_SERVER_ERR_SOCKETEXCEPTION:
   case MONGOC_SERVER_ERR_NOTPRIMARY:
   case MONGOC_SERVER_ERR_INTERRUPTEDATSHUTDOWN:
   case MONGOC_SERVER_ERR_INTERRUPTEDDUETOREPLSTATECHANGE:
   case MONGOC_SERVER_ERR_NOTPRIMARYNOSECONDARYOK:
   case MONGOC_SERVER_ERR_NOTPRIMARYORSECONDARY:
   case MONGOC_SERVER_ERR_STALESHARDVERSION:
   case MONGOC_SERVER_ERR_STALEEPOCH:
   case MONGOC_SERVER_ERR_STALECONFIG:
   case MONGOC_SERVER_ERR_RETRYCHANGESTREAM:
   case MONGOC_SERVER_ERR_FAILEDTOSATISFYREADPREFERENCE:
      return true;
   default:
      return false;
   }
}


static void
_set_resume_token(mongoc_change_stream_t *stream, const bson_t *resume_token)
{
   BSON_ASSERT(stream);
   BSON_ASSERT(resume_token);

   bson_destroy(&stream->resume_token);
   bson_copy_to(resume_token, &stream->resume_token);
}


/* construct the aggregate command in cmd. looks like one of the following:
 * for a collection change stream:
 *   { aggregate: collname, pipeline: [], cursor: { batchSize: x } }
 * for a database change stream:
 *   { aggregate: 1, pipeline: [], cursor: { batchSize: x } }
 * for a client change stream:
 *   { aggregate: 1, pipeline: [{$changeStream: {allChangesForCluster: true}}],
 *     cursor: { batchSize: x } }
 */
static void
_make_command(mongoc_change_stream_t *stream, bson_t *command)
{
   bson_iter_t iter;
   bson_t change_stream_stage; /* { $changeStream: <change_stream_doc> } */
   bson_t change_stream_doc;
   bson_array_builder_t *pipeline;
   bson_t cursor_doc;

   if (stream->change_stream_type == MONGOC_CHANGE_STREAM_COLLECTION) {
      bson_append_utf8(command, "aggregate", 9, stream->coll, (int)strlen(stream->coll));
   } else {
      bson_append_int32(command, "aggregate", 9, 1);
   }

   bson_append_array_builder_begin(command, "pipeline", 8, &pipeline);

   /* append the $changeStream stage. */
   bson_array_builder_append_document_begin(pipeline, &change_stream_stage);
   bson_append_document_begin(&change_stream_stage, "$changeStream", 13, &change_stream_doc);
   if (stream->full_document) {
      bson_concat(&change_stream_doc, stream->full_document);
   }
   if (stream->full_document_before_change) {
      bson_concat(&change_stream_doc, stream->full_document_before_change);
   }
   if (stream->show_expanded_events) {
      BSON_APPEND_BOOL(&change_stream_doc, "showExpandedEvents", stream->show_expanded_events);
   }

   if (stream->resumed) {
      /* Change stream spec: Resume Process */
      /* If there is a cached resumeToken: */
      if (!bson_empty(&stream->resume_token)) {
         /* If the ChangeStream was started with startAfter
            and has yet to return a result document: */
         if (!bson_empty(&stream->opts.startAfter) && !stream->has_returned_results) {
            /* The driver MUST set startAfter to the cached resumeToken */
            BSON_APPEND_DOCUMENT(&change_stream_doc, "startAfter", &stream->resume_token);
         } else {
            /* The driver MUST set resumeAfter to the cached resumeToken */
            BSON_APPEND_DOCUMENT(&change_stream_doc, "resumeAfter", &stream->resume_token);
         }
      } else if (!_mongoc_timestamp_empty(&stream->operation_time)) {
         /* Else if there is no cached resumeToken and the ChangeStream
            has a saved operation time and the max wire version is >= 7,
            the driver MUST set startAtOperationTime */
         _mongoc_timestamp_append(&stream->operation_time, &change_stream_doc, "startAtOperationTime");
      }
   } else {
      /* Change streams spec: "startAtOperationTime, resumeAfter, and startAfter
       * are all mutually exclusive; if any two are set, the server will return
       * an error. Drivers MUST NOT throw a custom error, and MUST defer to the
       * server error." */
      if (!bson_empty(&stream->opts.resumeAfter)) {
         BSON_APPEND_DOCUMENT(&change_stream_doc, "resumeAfter", &stream->opts.resumeAfter);

         /* Update the cached resume token */
         _set_resume_token(stream, &stream->opts.resumeAfter);
      }

      if (!bson_empty(&stream->opts.startAfter)) {
         BSON_APPEND_DOCUMENT(&change_stream_doc, "startAfter", &stream->opts.startAfter);

         /* Update the cached resume token (take precedence over resumeAfter) */
         _set_resume_token(stream, &stream->opts.startAfter);
      }

      if (!_mongoc_timestamp_empty(&stream->operation_time)) {
         _mongoc_timestamp_append(&stream->operation_time, &change_stream_doc, "startAtOperationTime");
      }
   }

   if (stream->change_stream_type == MONGOC_CHANGE_STREAM_CLIENT) {
      bson_append_bool(&change_stream_doc, "allChangesForCluster", 20, true);
   }
   bson_append_document_end(&change_stream_stage, &change_stream_doc);
   bson_array_builder_append_document_end(pipeline, &change_stream_stage);

   /* Append user pipeline if it exists */
   if (bson_iter_init_find(&iter, &stream->pipeline_to_append, "pipeline") && BSON_ITER_HOLDS_ARRAY(&iter)) {
      bson_iter_t child_iter;

      BSON_ASSERT(bson_iter_recurse(&iter, &child_iter));
      while (bson_iter_next(&child_iter)) {
         /* the user pipeline may consist of invalid stages or non-documents.
          * append anyway, and rely on the server error. */
         bson_array_builder_append_value(pipeline, bson_iter_value(&child_iter));
      }
   }

   bson_append_array_builder_end(command, pipeline);

   /* Add batch size if needed */
   bson_append_document_begin(command, "cursor", 6, &cursor_doc);
   if (stream->batch_size >= 0) {
      // `batchSize:0` is supported and applied to `aggregate`. `batchSize:0` requests an immediate cursor. This is
      // useful to avoid a long-running server-side aggregate. Once created, `mongoc_change_stream_destroy` can use
      // `killCursors` to kill the server-side cursor.
      bson_append_int32(&cursor_doc, "batchSize", 9, stream->batch_size);
   }
   bson_append_document_end(command, &cursor_doc);
}

/*---------------------------------------------------------------------------
 *
 * _make_cursor --
 *
 *       Construct and send the aggregate command and create the resulting
 *       cursor. On error, stream->cursor remains NULL, otherwise it is
 *       created and must be destroyed.
 *
 * Return:
 *       False on error and sets stream->err.
 *
 *--------------------------------------------------------------------------
 */
static bool
_make_cursor(mongoc_change_stream_t *stream)
{
   mongoc_client_session_t *cs = NULL;
   bson_t command_opts;
   bson_t command; /* { aggregate: "coll", pipeline: [], ... } */
   bson_t reply;
   bson_t getmore_opts = BSON_INITIALIZER;
   bson_iter_t iter;
   mongoc_server_stream_t *server_stream;

   BSON_ASSERT(stream);
   BSON_ASSERT(!stream->cursor);
   bson_init(&command);
   bson_copy_to(&(stream->opts.extra), &command_opts);

   if (stream->opts.comment.value_type != BSON_TYPE_EOD) {
      bson_append_value(&command_opts, "comment", 7, &stream->opts.comment);
      bson_append_value(&getmore_opts, "comment", 7, &stream->opts.comment);
   }

   if (bson_iter_init_find(&iter, &command_opts, "sessionId")) {
      if (!_mongoc_client_session_from_iter(stream->client, &iter, &cs, &stream->err)) {
         goto cleanup;
      }
   } else if (stream->implicit_session) {
      /* If an implicit session was created before, and this cursor is now
       * being recreated after resuming, then use the same session as before. */
      cs = stream->implicit_session;
      if (!mongoc_client_session_append(cs, &command_opts, &stream->err)) {
         goto cleanup;
      }
   } else {
      /* Create an implicit session. This session lsid must be the same for the
       * agg command and the subsequent getMores. Thus, this implicit session is
       * passed as if it were an explicit session to
       * mongoc_client_read_command_with_opts and
       * _mongoc_cursor_change_stream_new, but it is still implicit and its
       * lifetime is owned by this change_stream_t. */
      mongoc_session_opt_t *session_opts;
      session_opts = mongoc_session_opts_new();
      mongoc_session_opts_set_causal_consistency(session_opts, false);
      /* returns NULL if sessions aren't supported. ignore errors. */
      cs = mongoc_client_start_session(stream->client, session_opts, NULL);
      stream->implicit_session = cs;
      mongoc_session_opts_destroy(session_opts);
      if (cs && !mongoc_client_session_append(cs, &command_opts, &stream->err)) {
         goto cleanup;
      }
   }

   if (cs && !mongoc_client_session_append(cs, &getmore_opts, &stream->err)) {
      goto cleanup;
   }

   const mongoc_ss_log_context_t ss_log_context = {.operation = "aggregate"};
   server_stream = mongoc_cluster_stream_for_reads(
      &stream->client->cluster, &ss_log_context, stream->read_prefs, cs, NULL, &reply, &stream->err);
   if (!server_stream) {
      bson_destroy(&stream->err_doc);
      bson_copy_to(&reply, &stream->err_doc);
      bson_destroy(&reply);
      goto cleanup;
   }
   bson_append_int32(&command_opts, "serverId", 8, server_stream->sd->id);
   bson_append_int32(&getmore_opts, "serverId", 8, server_stream->sd->id);
   stream->max_wire_version = server_stream->sd->max_wire_version;
   mongoc_server_stream_cleanup(server_stream);

   if (stream->read_concern && !bson_has_field(&command_opts, "readConcern")) {
      mongoc_read_concern_append(stream->read_concern, &command_opts);
   }

   _make_command(stream, &command);

   /* even though serverId has already been set, still pass the read prefs.
    * they are necessary for OP_MSG if sending to a secondary. */
   if (!mongoc_client_read_command_with_opts(
          stream->client, stream->db, &command, stream->read_prefs, &command_opts, &reply, &stream->err)) {
      bson_destroy(&stream->err_doc);
      bson_copy_to(&reply, &stream->err_doc);
      bson_destroy(&reply);
      goto cleanup;
   }

   bson_append_bool(&getmore_opts, MONGOC_CURSOR_TAILABLE, MONGOC_CURSOR_TAILABLE_LEN, true);
   bson_append_bool(&getmore_opts, MONGOC_CURSOR_AWAIT_DATA, MONGOC_CURSOR_AWAIT_DATA_LEN, true);

   /* maxTimeMS is only appended to getMores if these are set in cursor opts. */
   if (stream->max_await_time_ms > 0) {
      bson_append_int64(&getmore_opts,
                        MONGOC_CURSOR_MAX_AWAIT_TIME_MS,
                        MONGOC_CURSOR_MAX_AWAIT_TIME_MS_LEN,
                        stream->max_await_time_ms);
   }

   if (stream->batch_size > 0) {
      bson_append_int32(&getmore_opts, MONGOC_CURSOR_BATCH_SIZE, MONGOC_CURSOR_BATCH_SIZE_LEN, stream->batch_size);
   }

   /* steals reply. */
   stream->cursor = _mongoc_cursor_change_stream_new(stream->client, &reply, &getmore_opts);

   if (mongoc_cursor_error(stream->cursor, NULL)) {
      goto cleanup;
   }

   /* Change stream spec: "When aggregate or getMore returns: If an empty batch
    * was returned and a postBatchResumeToken was included, cache it." */
   if (_mongoc_cursor_change_stream_end_of_batch(stream->cursor) &&
       _mongoc_cursor_change_stream_has_post_batch_resume_token(stream->cursor)) {
      _set_resume_token(stream, _mongoc_cursor_change_stream_get_post_batch_resume_token(stream->cursor));
   }

   /* Change stream spec: startAtOperationTime */
   if (bson_empty(&stream->opts.resumeAfter) && bson_empty(&stream->opts.startAfter) &&
       _mongoc_timestamp_empty(&stream->operation_time) && bson_empty(&stream->resume_token) &&
       bson_iter_init_find(&iter, _mongoc_cursor_change_stream_get_reply(stream->cursor), "operationTime") &&
       BSON_ITER_HOLDS_TIMESTAMP(&iter)) {
      _mongoc_timestamp_set_from_bson(&stream->operation_time, &iter);
   }

cleanup:
   bson_destroy(&command);
   bson_destroy(&command_opts);
   bson_destroy(&getmore_opts);
   return stream->err.code == 0;
}

/*---------------------------------------------------------------------------
 *
 * _change_stream_init --
 *
 *       Called after @stream has the collection name, database name, read
 *       preferences, and read concern set. Creates the change streams
 *       cursor.
 *
 *--------------------------------------------------------------------------
 */
void
_change_stream_init(mongoc_change_stream_t *stream, const bson_t *pipeline, const bson_t *opts)
{
   BSON_ASSERT(pipeline);

   stream->max_await_time_ms = -1;
   stream->batch_size = -1;
   bson_init(&stream->pipeline_to_append);
   bson_init(&stream->resume_token);
   bson_init(&stream->err_doc);

   if (!_mongoc_change_stream_opts_parse(stream->client, opts, &stream->opts, &stream->err)) {
      return;
   }

   if (stream->opts.fullDocument) {
      stream->full_document = BCON_NEW("fullDocument", stream->opts.fullDocument);
   }

   if (stream->opts.fullDocumentBeforeChange) {
      stream->full_document_before_change = BCON_NEW("fullDocumentBeforeChange", stream->opts.fullDocumentBeforeChange);
   }

   _mongoc_timestamp_set(&stream->operation_time, &stream->opts.startAtOperationTime);

   stream->batch_size = stream->opts.batchSize; // `stream->opts.batchSize` is -1 if not present in `opts`.
   stream->max_await_time_ms = stream->opts.maxAwaitTimeMS;
   stream->show_expanded_events = stream->opts.showExpandedEvents;

   /* Accept two forms of user pipeline:
    * 1. A document like: { "pipeline": [...] }
    * 2. An array-like document: { "0": {}, "1": {}, ... }
    * If the passed pipeline is invalid, we pass it along and let the server
    * error instead.
    */
   if (!bson_empty(pipeline)) {
      bson_iter_t iter;
      if (bson_iter_init_find(&iter, pipeline, "pipeline") && BSON_ITER_HOLDS_ARRAY(&iter)) {
         if (!BSON_APPEND_VALUE(&stream->pipeline_to_append, "pipeline", bson_iter_value(&iter))) {
            CHANGE_STREAM_ERR("pipeline");
         }
      } else {
         if (!BSON_APPEND_ARRAY(&stream->pipeline_to_append, "pipeline", pipeline)) {
            CHANGE_STREAM_ERR("pipeline");
         }
      }
   }

   if (stream->err.code == 0) {
      (void)_make_cursor(stream);
   }
}

mongoc_change_stream_t *
_mongoc_change_stream_new_from_collection(const mongoc_collection_t *coll, const bson_t *pipeline, const bson_t *opts)
{
   mongoc_change_stream_t *stream;
   BSON_ASSERT(coll);

   stream = BSON_ALIGNED_ALLOC0(mongoc_change_stream_t);
   stream->db = bson_strdup(coll->db);
   stream->coll = bson_strdup(coll->collection);
   stream->read_prefs = mongoc_read_prefs_copy(coll->read_prefs);
   stream->read_concern = mongoc_read_concern_copy(coll->read_concern);
   stream->client = coll->client;
   stream->change_stream_type = MONGOC_CHANGE_STREAM_COLLECTION;
   _change_stream_init(stream, pipeline, opts);
   return stream;
}

mongoc_change_stream_t *
_mongoc_change_stream_new_from_database(const mongoc_database_t *db, const bson_t *pipeline, const bson_t *opts)
{
   mongoc_change_stream_t *stream;
   BSON_ASSERT(db);

   stream = BSON_ALIGNED_ALLOC0(mongoc_change_stream_t);
   stream->db = bson_strdup(db->name);
   stream->coll = NULL;
   stream->read_prefs = mongoc_read_prefs_copy(db->read_prefs);
   stream->read_concern = mongoc_read_concern_copy(db->read_concern);
   stream->client = db->client;
   stream->change_stream_type = MONGOC_CHANGE_STREAM_DATABASE;
   _change_stream_init(stream, pipeline, opts);
   return stream;
}

mongoc_change_stream_t *
_mongoc_change_stream_new_from_client(mongoc_client_t *client, const bson_t *pipeline, const bson_t *opts)
{
   mongoc_change_stream_t *stream;
   BSON_ASSERT(client);

   stream = BSON_ALIGNED_ALLOC0(mongoc_change_stream_t);
   stream->db = bson_strdup("admin");
   stream->coll = NULL;
   stream->read_prefs = mongoc_read_prefs_copy(client->read_prefs);
   stream->read_concern = mongoc_read_concern_copy(client->read_concern);
   stream->client = client;
   stream->change_stream_type = MONGOC_CHANGE_STREAM_CLIENT;
   _change_stream_init(stream, pipeline, opts);
   return stream;
}


const bson_t *
mongoc_change_stream_get_resume_token(mongoc_change_stream_t *stream)
{
   if (!bson_empty(&stream->resume_token)) {
      return &stream->resume_token;
   }

   return NULL;
}


bool
mongoc_change_stream_next(mongoc_change_stream_t *stream, const bson_t **bson)
{
   bson_iter_t iter;
   bson_t doc_resume_token;
   uint32_t len;
   const uint8_t *data;
   bool ret = false;

   BSON_ASSERT(stream);
   BSON_ASSERT(bson);

   if (stream->err.code != 0) {
      goto end;
   }

   BSON_ASSERT(stream->cursor);
   if (!mongoc_cursor_next(stream->cursor, bson)) {
      const bson_t *err_doc;
      bson_error_t err;
      bool resumable = false;

      if (!mongoc_cursor_error_document(stream->cursor, &err, &err_doc)) {
         /* no error occurred, just no documents left. */
         goto end;
      }

      /* the cursor is closed. */
      if (stream->cursor->cursor_id == 0) {
         _mongoc_set_error(&stream->err,
                           MONGOC_ERROR_CURSOR,
                           MONGOC_ERROR_CURSOR_INVALID_CURSOR,
                           "Cannot advance a closed change stream.");
         goto end;
      }

      resumable = _is_resumable_error(stream, err_doc);
      int iteration_timeout_count = 0;
      while (resumable) {
         /* recreate the cursor. */
         mongoc_cursor_destroy(stream->cursor);
         stream->cursor = NULL;
         stream->resumed = true;
         if (!_make_cursor(stream)) {
            goto end;
         }
         if (mongoc_cursor_next(stream->cursor, bson)) {
            break;
         }
         if (!mongoc_cursor_error_document(stream->cursor, &err, &err_doc)) {
            goto end;
         }
         BSON_ASSERT(err_doc);
         if (stream->cursor->had_stream_timeout) {
            iteration_timeout_count++;
         }
         if (iteration_timeout_count >= 2) {
            // CDRIVER-6182: Do not resume if two iteration timeouts occur. Intended to avoid a possible resume loop
            // when `aggregate` succeeds but `getMore` consistently times out.
            MONGOC_WARNING("Breaking change stream resume loop after two timeouts");
            resumable = false;
         } else {
            resumable = _is_resumable_error(stream, err_doc);
         }
      }

      if (!resumable) {
         stream->err = err;
         bson_destroy(&stream->err_doc);
         bson_copy_to(err_doc, &stream->err_doc);
         goto end;
      }
   }

   /* we have received documents, either from the first call to next or after a
    * resume. */
   stream->has_returned_results = true;

   if (!bson_iter_init_find(&iter, *bson, "_id") || !BSON_ITER_HOLDS_DOCUMENT(&iter)) {
      _mongoc_set_error(&stream->err,
                        MONGOC_ERROR_CURSOR,
                        MONGOC_ERROR_CHANGE_STREAM_NO_RESUME_TOKEN,
                        "Cannot provide resume functionality when the resume "
                        "token is missing");
      goto end;
   }

   /* copy the resume token. */
   bson_iter_document(&iter, &len, &data);
   BSON_ASSERT(bson_init_static(&doc_resume_token, data, len));
   _set_resume_token(stream, &doc_resume_token);

   /* clear out the operation time, since we no longer need it to resume. */
   _mongoc_timestamp_clear(&stream->operation_time);
   ret = true;

end:
   /* Change stream spec: Updating the Cached Resume Token */
   if (stream->cursor && !mongoc_cursor_error(stream->cursor, NULL) &&
       _mongoc_cursor_change_stream_end_of_batch(stream->cursor) &&
       _mongoc_cursor_change_stream_has_post_batch_resume_token(stream->cursor)) {
      _set_resume_token(stream, _mongoc_cursor_change_stream_get_post_batch_resume_token(stream->cursor));
   }


   /* Driver Sessions Spec: "When an implicit session is associated with a
    * cursor for use with getMore operations, the session MUST be returned to
    * the pool immediately following a getMore operation that indicates that the
    * cursor has been exhausted." */
   if (stream->implicit_session) {
      /* if creating the change stream cursor errored, it may be null. */
      if (!stream->cursor || stream->cursor->cursor_id == 0) {
         mongoc_client_session_destroy(stream->implicit_session);
         stream->implicit_session = NULL;
      }
   }
   return ret;
}

bool
mongoc_change_stream_error_document(const mongoc_change_stream_t *stream, bson_error_t *err, const bson_t **bson)
{
   BSON_ASSERT(stream);

   if (stream->err.code != 0) {
      if (err) {
         *err = stream->err;
      }
      if (bson) {
         *bson = &stream->err_doc;
      }
      return true;
   }

   if (bson) {
      *bson = NULL;
   }
   return false;
}

void
mongoc_change_stream_destroy(mongoc_change_stream_t *stream)
{
   if (!stream) {
      return;
   }

   bson_destroy(&stream->pipeline_to_append);
   bson_destroy(&stream->resume_token);
   bson_destroy(stream->full_document);
   bson_destroy(stream->full_document_before_change);
   bson_destroy(&stream->err_doc);
   _mongoc_change_stream_opts_cleanup(&stream->opts);
   mongoc_cursor_destroy(stream->cursor);
   mongoc_client_session_destroy(stream->implicit_session);
   mongoc_read_prefs_destroy(stream->read_prefs);
   mongoc_read_concern_destroy(stream->read_concern);

   bson_free(stream->db);
   bson_free(stream->coll);
   bson_free(stream);
}
