UNPKG

@google-cloud/spanner

Version:
2,314 lines 87.8 kB
"use strict";
/*!
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.PartitionedDml = exports.MutationGroup = exports.MutationSet = exports.Transaction = exports.Dml = exports.Snapshot = void 0;
const precise_date_1 = require("@google-cloud/precise-date");
const promisify_1 = require("@google-cloud/promisify");
const helper_1 = require("./helper");
const events_1 = require("events");
const google_gax_1 = require("google-gax");
const stream_1 = require("stream");
const codec_1 = require("./codec");
const partial_result_stream_1 = require("./partial-result-stream");
const common_1 = require("./common");
const spanner_api_1 = require("@google-cloud/spanner-api");
var spannerClient = spanner_api_1.protos.google;
var google = spanner_api_1.protos.google;
var IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel;
var ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
const instrument_1 = require("./instrument");
const request_id_header_1 = require("./request_id_header");
const RETRY_INFO_TYPE = 'type.googleapis.com/google.rpc.retryinfo';
const RETRY_INFO_BIN = 'google.rpc.retryinfo-bin';
let nextAffinityId = 0;
/**
 * Injects a key-value pair into the gaxOpts.otherArgs.options object
 * without mutating the original.
 */
function injectGaxOpt(existingOpts, key, value) {
    return Object.assign({}, existingOpts, {
        otherArgs: Object.assign({}, existingOpts?.otherArgs, {
            options: Object.assign({}, existingOpts?.otherArgs?.options, {
                [key]: value,
            }),
        }),
    });
}
/**
 * @typedef {object} TimestampBounds
 * @property {boolean} [strong=true] Read at a timestamp where all previously
 *     committed transactions are visible.
 * @property {external:PreciseDate|google.protobuf.Timestamp} [minReadTimestamp]
 *     Executes all reads at a `timestamp >= minReadTimestamp`.
 * @property {number|google.protobuf.Timestamp} [maxStaleness] Read data at a
 *     `timestamp >= NOW - maxStaleness` (milliseconds).
 * @property {external:PreciseDate|google.protobuf.Timestamp} [readTimestamp]
 *     Executes all reads at the given timestamp.
 * @property {number|google.protobuf.Timestamp} [exactStaleness] Executes all
 *     reads at a timestamp that is `exactStaleness` (milliseconds) old.
 * @property {boolean} [returnReadTimestamp=true] When true,
 *     {@link Snapshot#readTimestamp} will be populated after
 *     {@link Snapshot#begin} is called.
 */
/**
 * This transaction type provides guaranteed consistency across several reads,
 * but does not allow writes. Snapshot read-only transactions can be configured
 * to read at timestamps in the past.
 *
 * When finished with the Snapshot, call {@link Snapshot#end} to
 * release the underlying {@link Session}. Failure to do so can result in a
 * Session leak.
 *
 * **This object is created and returned from {@link Database#getSnapshot}.**
 *
 * @class
 * @hideconstructor
 *
 * @see [Timestamp Bounds API Documentation](https://cloud.google.com/spanner/docs/timestamp-bounds)
 *
 * @example
 * ```
 * const {Spanner} = require('@google-cloud/spanner');
 * const spanner = new Spanner();
 *
 * const instance = spanner.instance('my-instance');
 * const database = instance.database('my-database');
 *
 * const timestampBounds = {
 *   strong: true
 * };
 *
 * database.getSnapshot(timestampBounds, (err, transaction) => {
 *   if (err) {
 *     // Error handling omitted.
 *   }
 *
 *   // It should be called when the snapshot finishes.
 *   transaction.end();
 * });
 * ```
 */
class Snapshot extends events_1.EventEmitter {
    _options;
    _seqno = 1;
    _waitingRequests;
    _inlineBeginStarted;
    _useInRunner = false;
    _latestPreCommitToken;
    id;
    _affinityKey;
    _bindGaxOpts;
    _unbindGaxOpts;
    multiplexedSessionPreviousTransactionId;
    ended;
    metadata;
    readTimestamp;
    readTimestampProto;
    request;
    requestStream;
    session;
    queryOptions;
    commonHeaders_;
    requestOptions;
    _observabilityOptions;
    _traceConfig;
    _dbName;
    _mutationKey;
    /**
     * The transaction ID.
     *
     * @name Snapshot#id
     * @type {?(string|Buffer)}
     */
    /**
     * Whether or not the transaction has ended. If true, make no further
     * requests, and discard the transaction.
     *
     * @name Snapshot#ended
     * @type {boolean}
     */
    /**
     * The raw transaction response object. It is populated after
     * {@link Snapshot#begin} is called.
     *
     * @name Snapshot#metadata
     * @type {?TransactionResponse}
     */
    /**
     * **Snapshot only**
     * The timestamp at which all reads are performed.
     *
     * @name Snapshot#readTimestamp
     * @type {?external:PreciseDate}
     */
    /**
     * **Snapshot only**
     * The protobuf version of {@link Snapshot#readTimestamp}. This is useful if
     * you require microsecond precision.
     *
     * @name Snapshot#readTimestampProto
     * @type {?google.protobuf.Timestamp}
     */
    /**
     * @constructor
     *
     * @param {Session} session The parent Session object.
     * @param {TimestampBounds} [options] Snapshot timestamp bounds.
     * @param {QueryOptions} [queryOptions] Default query options to use when none
     *        are specified for a query.
     */
    constructor(session, options, queryOptions) {
        super();
        this.ended = false;
        this.session = session;
        this.queryOptions = Object.assign({}, queryOptions);
        // If the session is multiplexed, generate a unique affinity key for this
        // specific transaction/snapshot. This allows requests using the same shared
        // multiplexed session to be distributed across different gRPC channels.
        if (session.metadata && session.metadata.multiplexed) {
            this._affinityKey = `mux-affinity-${process.pid}-${nextAffinityId++}`;
            // Pre-construct and cache the bind gax options to avoid creating
            // a new object on every request, which improves performance.
            this._bindGaxOpts = {
                otherArgs: {
                    options: {
                        affinityKey: this._affinityKey,
                    },
                },
            };
            // Pre-construct and cache the unbind gax options. This explicitly signals
            // the channel factory to release the affinity mapping when the transaction ends.
            this._unbindGaxOpts = {
                otherArgs: {
                    options: {
                        affinityKey: this._affinityKey,
                        unbind: true,
                    },
                },
            };
            this.request = (config, callback) => {
                let gaxOpts;
                if (!config.gaxOpts || Object.keys(config.gaxOpts).length === 0) {
                    gaxOpts = this._bindGaxOpts;
                }
                else {
                    gaxOpts = injectGaxOpt(config.gaxOpts, 'affinityKey', this._affinityKey);
                }
                config = Object.assign({}, config, { gaxOpts });
                return session.request(config, callback);
            };
            this.requestStream = (config) => {
                let gaxOpts;
                if (!config.gaxOpts || Object.keys(config.gaxOpts).length === 0) {
                    gaxOpts = this._bindGaxOpts;
                }
                else {
                    gaxOpts = injectGaxOpt(config.gaxOpts, 'affinityKey', this._affinityKey);
                }
                config = Object.assign({}, config, { gaxOpts });
                return session.requestStream(config);
            };
        }
        else {
            this.request = session.request.bind(session);
            this.requestStream = session.requestStream.bind(session);
        }
        const readOnly = Snapshot.encodeTimestampBounds(options || {});
        this._options = { readOnly };
        this._dbName = this.session.parent.formattedName_;
        this._waitingRequests = [];
        this._inlineBeginStarted = false;
        this._observabilityOptions = session._observabilityOptions;
        this.commonHeaders_ = { ...session.commonHeaders_ };
        this._traceConfig = {
            opts: this._observabilityOptions,
            dbName: this._dbName,
        };
        this._latestPreCommitToken = null;
        this._mutationKey = null;
    }
    _updatePrecommitToken(resp) {
        if (this._latestPreCommitToken === null ||
            this._latestPreCommitToken === undefined ||
            (resp.precommitToken &&
                this._latestPreCommitToken.seqNum < resp.precommitToken.seqNum)) {
            this._latestPreCommitToken = resp.precommitToken;
        }
    }
    /**
     * Selects a single representative mutation from a list to be used as the
     * transaction's `mutationKey`.
     *
     * This key is required by Spanner and is sent in the `BeginTransactionRequest`
     * for read-write transactions that only contain mutations. The selection follows
     * a two-tiered heuristic to choose the most significant mutation.
     *
     * The selection heuristic is as follows:
     *
     * 1. Priority of Operation Type: High-priority mutations (`delete`, `update`,
     * `replace`, `insertOrUpdate`) are always chosen over low-priority
     * (`insert`) mutations.
     *
     * 2. Selection Strategy:
     * - If any high-priority mutations exist, one is chosen randomly from
     * that group, ignoring the number of rows.
     * - If only `insert` mutations exist, the one(s) with the largest number
     * of rows are identified, and one is chosen randomly from that subset.
     *
     * @protected
     * @param mutations The list of mutations from which to select the key.
     */
    _setMutationKey(mutations) {
        // return if the list is empty
        if (mutations.length === 0) {
            return;
        }
        // maintain a set of high priority keys
        const HIGH_PRIORITY_KEYS = new Set([
            'delete',
            'update',
            'replace',
            'insertOrUpdate',
        ]);
        // maintain a variable for low priority key
        const LOW_PRIORITY_KEY = 'insert';
        // Partition mutations into high and low priority groups.
        const [highPriority, lowPriority] = mutations.reduce((acc, mutation) => {
            const key = Object.keys(mutation)[0];
            if (HIGH_PRIORITY_KEYS.has(key)) {
                acc[0].push(mutation);
            }
            else if (key === LOW_PRIORITY_KEY) {
                acc[1].push(mutation);
            }
            // return accumulated mutations list
            return acc;
        }, [[], []]);
        // Apply the selection logic based on the rules.
        if (highPriority.length > 0) {
            // RULE 1: If high-priority keys exist, pick one randomly.
            const randomIndex = Math.floor(Math.random() * highPriority.length);
            this._mutationKey = highPriority[randomIndex];
        }
        else if (lowPriority.length > 0) {
            // RULE 2: If only 'insert' key(s) exist, find the one with
            // highest number of values
            const { bestCandidates } = lowPriority.reduce((acc, mutation) => {
                const size = mutation.insert?.values?.length || 0;
                if (size > acc.maxSize) {
                    // New largest size found, start a new list
                    return { maxSize: size, bestCandidates: [mutation] };
                }
                if (size === acc.maxSize) {
                    // Same size as current max, add to list
                    acc.bestCandidates.push(mutation);
                }
                // return accumulated mutations list
                return acc;
            }, {
                maxSize: -1,
                bestCandidates: [],
            });
            // Pick randomly from the largest 'insert' mutation(s).
            const randomIndex = Math.floor(Math.random() * bestCandidates.length);
            this._mutationKey = bestCandidates[randomIndex];
        }
        else {
            // No mutations to select from.
            this._mutationKey = null;
        }
    }
    /**
     * Modifies transaction selector to include the multiplexed session previous
     * transaction id.
     * This is essential for operations that use an `inline begin`.
     * @protected
     * @param transaction The transaction selector object that will be mutated
     * to include the multiplexed session previous transaction id.
     */
    _setPreviousTransactionId(transaction) {
        transaction.begin.readWrite = Object.assign({}, transaction.begin.readWrite || {}, {
            multiplexedSessionPreviousTransactionId: this.multiplexedSessionPreviousTransactionId,
        });
    }
    begin(gaxOptionsOrCallback, cb) {
        const gaxOpts = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
        const callback = typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb;
        const session = this.session.formattedName_;
        const options = this._options;
        if (this.multiplexedSessionPreviousTransactionId &&
            this.session.parent.isMuxEnabledForRW_) {
            options.readWrite.multiplexedSessionPreviousTransactionId =
                this.multiplexedSessionPreviousTransactionId;
        }
        const reqOpts = {
            session,
            options,
        };
        if (this._mutationKey) {
            reqOpts.mutationKey = this._mutationKey;
        }
        // Only hand crafted read-write transactions will be able to set a
        // transaction tag for the BeginTransaction RPC. Also, this.requestOptions
        // is only set in the constructor of Transaction, which is the constructor
        // for read/write transactions.
        if (this.requestOptions) {
            reqOpts.requestOptions = this.requestOptions;
        }
        const headers = this.commonHeaders_;
        if (this._getSpanner().routeToLeaderEnabled &&
            (this._options.readWrite !== undefined ||
                this._options.partitionedDml !== undefined)) {
            (0, common_1.addLeaderAwareRoutingHeader)(headers);
        }
        return (0, instrument_1.startTrace)('Snapshot.begin', {
            transactionTag: this.requestOptions?.transactionTag,
            ...this._traceConfig,
        }, span => {
            span.addEvent('Begin Transaction');
            this.request({
                client: 'SpannerClient',
                method: 'beginTransaction',
                reqOpts,
                gaxOpts,
                headers: (0, request_id_header_1.injectRequestIDIntoHeaders)(headers, this.session),
            }, (err, resp) => {
                if (err) {
                    (0, instrument_1.setSpanError)(span, err);
                }
                else {
                    this._updatePrecommitToken(resp);
                    this._update(resp, span);
                }
                span.end();
                callback(err, resp);
            });
        });
    }
    /**
     * A KeyRange represents a range of rows in a table or index.
     *
     * A range has a start key and an end key. These keys can be open or closed,
     * indicating if the range includes rows with that key.
     *
     * Keys are represented by an array of strings where the nth value in the list
     * corresponds to the nth component of the table or index primary key.
     *
     * @typedef {object} KeyRange
     * @property {string[]} [startClosed] If the start is closed, then the range
     *     includes all rows whose first key columns exactly match.
     * @property {string[]} [startOpen] If the start is open, then the range
     *     excludes rows whose first key columns exactly match.
     * @property {string[]} [endClosed] If the end is closed, then the range
     *     includes all rows whose first key columns exactly match.
     * @property {string[]} [endOpen] If the end is open, then the range excludes
     *     rows whose first key columns exactly match.
     */
    /**
     * Read request options. This includes all standard ReadRequest options as
     * well as several convenience properties.
     *
     * @see [StreamingRead API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.StreamingRead)
     * @see [ReadRequest API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ReadRequest)
     *
     * @typedef {object} ReadRequest
     * @property {string} table The name of the table in the database to be read.
     * @property {string[]} columns The columns of the table to be returned for each
     *     row matching this query.
     * @property {string[]|string[][]} keys The primary or index keys of the rows in this table to be
     *     yielded. If using a composite key, provide an array within this array.
     *     See the example below.
     * @property {KeyRange[]} [ranges] An alternative to the keys property; this can
     *     be used to define a range of keys to be yielded.
     * @property {string} [index] The name of an index on the table if a
     *     different index than the primary key should be used to determine which rows to return.
     * @property {boolean} [json=false] Receive the rows as serialized objects. This
     *     is the equivalent of calling `toJSON()` on each row.
     * @property {JSONOptions} [jsonOptions] Configuration options for the serialized
     *     objects.
     * @property {object} [keySet] Defines a collection of keys and/or key ranges to
     *     read.
     * @property {number} [limit] The number of rows to yield.
     * @property {Buffer} [partitionToken]
     *     If present, results will be restricted to the specified partition
     *     previously created using PartitionRead(). There must be an exact
     *     match for the values of fields common to this message and the
     *     PartitionReadRequest message used to create this partition_token.
     * @property {google.spanner.v1.RequestOptions} [requestOptions]
     *     Common options for this request.
     * @property {google.spanner.v1.IDirectedReadOptions} [directedReadOptions]
     *     Indicates which replicas or regions should be used for non-transactional reads or queries.
     * @property {object} [gaxOptions]
     *     Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions}
     *     for more details.
     */
    /**
     * Create a readable object stream to receive rows from the database using key
     * lookups and scans.
     *
     * Wrapper around {@link v1.SpannerClient#streamingRead}.
     *
     * @see {@link v1.SpannerClient#streamingRead}
     * @see [StreamingRead API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.StreamingRead)
     * @see [ReadRequest API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ReadRequest)
     *
     * @fires PartialResultStream#response
     * @fires PartialResultStream#stats
     *
     * @param {string} table The table to read from.
     * @param {ReadRequest} query Configuration object. See official
     *     [`ReadRequest`](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ReadRequest).
     *     API documentation.
     * @returns {ReadableStream} A readable stream that emits rows.
     *
     * @example
     * ```
     * transaction.createReadStream('Singers', {
     *     keys: ['1'],
     *     columns: ['SingerId', 'name']
     *   })
     *   .on('error', function(err) {})
     *   .on('data', function(row) {
     *     // row = [
     *     //   {
     *     //     name: 'SingerId',
     *     //     value: '1'
     *     //   },
     *     //   {
     *     //     name: 'Name',
     *     //     value: 'Eddie Wilson'
     *     //   }
     *     // ]
     *   })
     *   .on('end', function() {
     *     // All results retrieved.
     *   });
     *
     * ```
     * @example Provide an array for `query.keys` to read with a
     * composite key.
     * ```
     * const query = {
     *   keys: [
     *     [
     *       'Id1',
     *       'Name1'
     *     ],
     *     [
     *       'Id2',
     *       'Name2'
     *     ]
     *   ],
     *   // ...
     * };
     * ```
     *
     * @example Rows are returned as an array of object arrays. Each
     * object has a `name` and `value` property. To get a serialized object, call
     * `toJSON()`.
     * ```
     * transaction.createReadStream('Singers', {
     *     keys: ['1'],
     *     columns: ['SingerId', 'name']
     *   })
     *   .on('error', function(err) {})
     *   .on('data', function(row) {
     *     // row.toJSON() = {
     *     //   SingerId: '1',
     *     //   Name: 'Eddie Wilson'
     *     // }
     *   })
     *   .on('end', function() {
     *     // All results retrieved.
     *   });
     * ```
     *
     * @example Alternatively, set `query.json` to `true`, and this step
     * will perform automatically.
     * ```
     * transaction.createReadStream('Singers', {
     *     keys: ['1'],
     *     columns: ['SingerId', 'name'],
     *     json: true,
     *   })
     *   .on('error', function(err) {})
     *   .on('data', function(row) {
     *     // row = {
     *     //   SingerId: '1',
     *     //   Name: 'Eddie Wilson'
     *     // }
     *   })
     *   .on('end', function() {
     *     // All results retrieved.
     *   });
     * ```
     *
     * @example If you anticipate many results, you can end a stream
     * early to prevent unnecessary processing and API requests.
     * ```
     * transaction.createReadStream('Singers', {
     *     keys: ['1'],
     *     columns: ['SingerId', 'name']
     *   })
     *   .on('data', function(row) {
     *     this.end();
     *   });
     * ```
     */
    createReadStream(table, request = {}) {
        const { gaxOptions, json, jsonOptions, maxResumeRetries, requestOptions, columnsMetadata, } = request;
        const keySet = Snapshot.encodeKeySet(request);
        const transaction = {};
        if (this.id) {
            transaction.id = this.id;
        }
        else if (this._options.readWrite) {
            transaction.begin = this._options;
        }
        else {
            transaction.singleUse = this._options;
        }
        if (!this.id &&
            this._options.readWrite &&
            this.session.parent.isMuxEnabledForRW_) {
            this._setPreviousTransactionId(transaction);
        }
        const directedReadOptions = this._getDirectedReadOptions(request.directedReadOptions);
        request = Object.assign({}, request);
        delete request.gaxOptions;
        delete request.json;
        delete request.jsonOptions;
        delete request.maxResumeRetries;
        delete request.keys;
        delete request.ranges;
        delete request.requestOptions;
        delete request.directedReadOptions;
        delete request.columnsMetadata;
        const reqOpts = Object.assign(request, {
            session: this.session.formattedName_,
            requestOptions: this.configureTagOptions(typeof transaction.singleUse !== 'undefined', this.requestOptions?.transactionTag ?? undefined, requestOptions),
            directedReadOptions: directedReadOptions,
            transaction,
            table,
            keySet,
        });
        const headers = this.commonHeaders_;
        if (this._getSpanner().routeToLeaderEnabled &&
            (this._options.readWrite !== undefined ||
                this._options.partitionedDml !== undefined)) {
            (0, common_1.addLeaderAwareRoutingHeader)(headers);
        }
        const traceConfig = {
            ...this._traceConfig,
            tableName: table,
            transactionTag: this.requestOptions?.transactionTag,
            requestTag: requestOptions?.requestTag,
        };
        return (0, instrument_1.startTrace)('Snapshot.createReadStream', traceConfig, span => {
            let attempt = 0;
            const database = this.session.parent;
            const nthRequest = (0, request_id_header_1.nextNthRequest)(database);
            const makeRequest = (resumeToken) => {
                if (this.id && transaction.begin) {
                    delete transaction.begin;
                    transaction.id = this.id;
                }
                attempt++;
                if (!resumeToken) {
                    if (attempt === 1) {
                        span.addEvent('Starting stream');
                    }
                    else {
                        span.addEvent('Re-attempting start stream', { attempt: attempt });
                    }
                }
                else {
                    span.addEvent('Resuming stream', {
                        resume_token: resumeToken.toString(),
                        attempt: attempt,
                    });
                }
                return this.requestStream({
                    client: 'SpannerClient',
                    method: 'streamingRead',
                    reqOpts: Object.assign({}, reqOpts, { resumeToken }),
                    gaxOpts: gaxOptions,
                    headers: (0, request_id_header_1.injectRequestIDIntoHeaders)(headers, this.session, nthRequest, attempt),
                });
            };
            const resultStream = (0, partial_result_stream_1.partialResultStream)(this._wrapWithIdWaiter(makeRequest), {
                json,
                jsonOptions,
                maxResumeRetries,
                columnsMetadata,
                gaxOptions,
            })
                ?.on('response', response => {
                this._updatePrecommitToken(response);
                if (response.metadata && response.metadata.transaction && !this.id) {
                    this._update(response.metadata.transaction, span);
                }
            })
                .on('error', err => {
                (0, instrument_1.setSpanError)(span, err);
                const wasAborted = isErrorAborted(err);
                if (!this.id && this._useInRunner && !wasAborted) {
                    // TODO: resolve https://github.com/googleapis/nodejs-spanner/issues/2170
                    void this.begin();
                }
                else {
                    if (wasAborted) {
                        span.addEvent('Stream broken. Not safe to retry', {
                            'transaction.id': this.id?.toString(),
                        });
                    }
                }
                span.end();
            })
                .on('end', err => {
                if (err) {
                    (0, instrument_1.setSpanError)(span, err);
                }
                span.end();
            });
            if (resultStream instanceof stream_1.Stream) {
                (0, stream_1.finished)(resultStream, err => {
                    if (err) {
                        (0, instrument_1.setSpanError)(span, err);
                    }
                    span.end();
                });
            }
            return resultStream;
        });
    }
    /**
     * Let the client know you're done with a particular transaction. This should
     * mainly be called for {@link Snapshot} objects, however in certain cases
     * you may want to call them for {@link Transaction} objects as well.
     *
     * @example Calling `end` on a read only snapshot
     * ```
     * database.getSnapshot((err, transaction) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   transaction.run('SELECT * FROM Singers', (err, rows) => {
     *     if (err) {
     *       // Error handling omitted.
     *     }
     *
     *     // End the snapshot.
     *     transaction.end();
     *   });
     * });
     * ```
     *
     * @example Calling `end` on a read/write transaction
     * ```
     * database.runTransaction((err, transaction) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   const query = 'UPDATE Account SET Balance = 1000 WHERE Key = 1';
     *
     *   transaction.runUpdate(query, err => {
     *     if (err) {
     *       // In the event of an error, there would be nothing to rollback,
     * so
     *       // instead of continuing, discard the
     * transaction. transaction.end(); return;
     *     }
     *
     *     transaction.commit(err => {});
     *   });
     * });
     * ```
     */
    end() {
        if (this.ended) {
            return;
        }
        this.ended = true;
        process.nextTick(() => this.emit('end'));
        if (this._affinityKey) {
            const database = this.session?.parent;
            const spanner = database?.parent?.parent;
            const client = spanner?.clients_?.get('SpannerClient');
            if (client?.spannerStub) {
                Promise.resolve(client.spannerStub)
                    .then((stub) => {
                    stub?.getChannel?.()?.unbind?.(this._affinityKey);
                })
                    .catch(() => { });
            }
        }
    }
    read(table, requestOrCallback, cb) {
        const rows = [];
        let request;
        let callback;
        if (typeof requestOrCallback === 'function') {
            request = {};
            callback = requestOrCallback;
        }
        else {
            request = requestOrCallback;
            callback = cb;
        }
        return (0, instrument_1.startTrace)('Snapshot.read', {
            tableName: table,
            ...this._traceConfig,
        }, span => {
            this.createReadStream(table, request)
                .on('error', err => {
                const e = err;
                (0, instrument_1.setSpanError)(span, e);
                span.end();
                callback(e, null);
            })
                .on('data', row => rows.push(row))
                .on('end', () => {
                span.end();
                callback(null, rows);
            });
        });
    }
    run(query, callback) {
        const rows = [];
        let stats;
        let metadata;
        (0, instrument_1.startTrace)('Snapshot.run', {
            ...query,
            ...this._traceConfig,
        }, span => {
            return this.runStream(query)
                .on('error', err => {
                (0, instrument_1.setSpanError)(span, err);
                span.end();
                if (!('code' in err)) {
                    Object.assign(err, {
                        code: google_gax_1.grpc.status.UNKNOWN,
                        details: err.message,
                        metadata: new google_gax_1.grpc.Metadata(),
                    });
                }
                callback(err, rows, stats, metadata);
            })
                .on('response', response => {
                if (response.metadata) {
                    metadata = response.metadata;
                    if (metadata.transaction && !this.id) {
                        this._update(metadata.transaction, span);
                    }
                }
            })
                .on('data', row => rows.push(row))
                .on('stats', _stats => (stats = _stats))
                .on('end', () => {
                span.end();
                callback(null, rows, stats, metadata);
            });
        });
    }
    /**
     * ExecuteSql request options. This includes all standard ExecuteSqlRequest
     * options as well as several convenience properties.
     *
     * @see [Query Syntax](https://cloud.google.com/spanner/docs/query-syntax)
     * @see [ExecuteSql API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.ExecuteSql)
     *
     * @typedef {object} ExecuteSqlRequest
     * @property {string} resumeToken The token used to resume getting results.
     * @property {google.spanner.v1.ExecuteSqlRequest.QueryMode} queryMode Query plan and
     *     execution statistics for the SQL statement that
     *     produced this result set.
     * @property {string} partitionToken The partition token.
     * @property {number} seqno The Sequence number. This option is used internally and will be overridden.
     * @property {string} sql The SQL string.
     * @property {google.spanner.v1.ExecuteSqlRequest.IQueryOptions} [queryOptions]
     *     Default query options to use with the database. These options will be
     *     overridden by any query options set in environment variables or that
     *     are specified on a per-query basis.
     * @property {google.spanner.v1.IRequestOptions} requestOptions The request options to include
     *     with the commit request.
     * @property {Object.<string, *>} [params] A map of parameter names to values.
     * @property {Object.<string, (string|ParamType)>} [types] A map of parameter
     *     names to types. If omitted the client will attempt to guess for all
     *     non-null values.
     * @property {boolean} [json=false] Receive the rows as serialized objects. This
     *     is the equivalent of calling `toJSON()` on each row.
     * @property {JSONOptions} [jsonOptions] Configuration options for the
     *     serialized objects.
     * @property {object} [gaxOptions] Request configuration options,
     *     See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions}
     *     for more details.
     *  @property {number} [maxResumeRetries] The maximum number of times that the
     *     stream will retry to push data downstream, when the downstream indicates
     *     that it is not ready for any more data. Increase this value if you
     *     experience 'Stream is still not ready to receive data' errors as a
     *     result of a slow writer in your receiving stream.
     *  @property {object} [directedReadOptions]
     *     Indicates which replicas or regions should be used for non-transactional reads or queries.
     */
    /**
     * Create a readable object stream to receive resulting rows from a SQL
     * statement.
     *
     * Wrapper around {@link v1.SpannerClient#executeStreamingSql}.
     *
     * @see {@link v1.SpannerClient#executeStreamingSql}
     * @see [ExecuteStreamingSql API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.ExecuteStreamingSql)
     * @see [ExecuteSqlRequest API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ExecuteSqlRequest)
     *
     * @fires PartialResultStream#response
     * @fires PartialResultStream#stats
     *
     * @param {string|ExecuteSqlRequest} query A SQL query or
     *     {@link ExecuteSqlRequest} object.
     * @returns {ReadableStream}
     *
     * @example
     * ```
     * const query = 'SELECT * FROM Singers';
     *
     * transaction.runStream(query)
     *   .on('error', function(err) {})
     *   .on('data', function(row) {
     *     // row = {
     *     //   SingerId: '1',
     *     //   Name: 'Eddie Wilson'
     *     // }
     *   })
     *   .on('end', function() {
     *     // All results retrieved.
     *   });
     *
     * ```
     * @example The SQL query string can contain parameter placeholders.
     * A parameter placeholder consists of '@' followed by the parameter name.
     * ```
     * const query = {
     *   sql: 'SELECT * FROM Singers WHERE name = @name',
     *   params: {
     *     name: 'Eddie Wilson'
     *   }
     * };
     *
     * transaction.runStream(query)
     *   .on('error', function(err) {})
     *   .on('data', function(row) {})
     *   .on('end', function() {});
     * ```
     *
     * @example If you anticipate many results, you can end a stream
     * early to prevent unnecessary processing and API requests.
     * ```
     * transaction.runStream(query)
     *   .on('data', function(row) {
     *     this.end();
     *   });
     * ```
     */
    runStream(query) {
        if (typeof query === 'string') {
            query = { sql: query };
        }
        query = Object.assign({}, query);
        query.queryOptions = Object.assign(Object.assign({}, this.queryOptions), query.queryOptions);
        const { gaxOptions, json, jsonOptions, maxResumeRetries, requestOptions, columnsMetadata, } = query;
        let reqOpts;
        const directedReadOptions = this._getDirectedReadOptions(query.directedReadOptions);
        const sanitizeRequest = () => {
            query = query;
            const { params, paramTypes } = Snapshot.encodeParams(query);
            const transaction = {};
            if (this.id) {
                transaction.id = this.id;
            }
            else if (this._options.readWrite) {
                transaction.begin = this._options;
            }
            else {
                transaction.singleUse = this._options;
            }
            if (!this.id &&
                this._options.readWrite &&
                this.session.parent.isMuxEnabledForRW_) {
                this._setPreviousTransactionId(transaction);
            }
            delete query.gaxOptions;
            delete query.json;
            delete query.jsonOptions;
            delete query.maxResumeRetries;
            delete query.requestOptions;
            delete query.types;
            delete query.directedReadOptions;
            delete query.columnsMetadata;
            reqOpts = Object.assign(query, {
                session: this.session.formattedName_,
                seqno: this._seqno++,
                requestOptions: this.configureTagOptions(typeof transaction.singleUse !== 'undefined', this.requestOptions?.transactionTag ?? undefined, requestOptions),
                directedReadOptions: directedReadOptions,
                transaction,
                params,
                paramTypes,
            });
        };
        const headers = this.commonHeaders_;
        if (this._getSpanner().routeToLeaderEnabled &&
            (this._options.readWrite !== undefined ||
                this._options.partitionedDml !== undefined)) {
            (0, common_1.addLeaderAwareRoutingHeader)(headers);
        }
        const traceConfig = {
            transactionTag: this.requestOptions?.transactionTag,
            requestTag: requestOptions?.requestTag,
            ...query,
            ...this._traceConfig,
        };
        return (0, instrument_1.startTrace)('Snapshot.runStream', traceConfig, span => {
            let attempt = 0;
            const database = this.session.parent;
            const nthRequest = (0, request_id_header_1.nextNthRequest)(database);
            const makeRequest = (resumeToken) => {
                attempt++;
                if (!resumeToken) {
                    if (attempt === 1) {
                        span.addEvent('Starting stream');
                    }
                    else {
                        span.addEvent('Re-attempting start stream', { attempt: attempt });
                    }
                }
                else {
                    span.addEvent('Resuming stream', {
                        resume_token: resumeToken.toString(),
                        attempt: attempt,
                    });
                }
                if (!reqOpts || (this.id && !reqOpts.transaction.id)) {
                    try {
                        sanitizeRequest();
                    }
                    catch (e) {
                        const errorStream = new stream_1.PassThrough();
                        (0, instrument_1.setSpanErrorAndException)(span, e);
                        span.end();
                        setImmediate(() => errorStream.destroy(e));
                        return errorStream;
                    }
                }
                return this.requestStream({
                    client: 'SpannerClient',
                    method: 'executeStreamingSql',
                    reqOpts: Object.assign({}, reqOpts, { resumeToken }),
                    gaxOpts: gaxOptions,
                    headers: (0, request_id_header_1.injectRequestIDIntoHeaders)(headers, this.session, nthRequest, attempt),
                });
            };
            const resultStream = (0, partial_result_stream_1.partialResultStream)(this._wrapWithIdWaiter(makeRequest), {
                json,
                jsonOptions,
                maxResumeRetries,
                columnsMetadata,
                gaxOptions,
            })
                .on('response', response => {
                this._updatePrecommitToken(response);
                if (response.metadata && response.metadata.transaction && !this.id) {
                    this._update(response.metadata.transaction, span);
                }
            })
                .on('error', err => {
                (0, instrument_1.setSpanError)(span, err);
                const wasAborted = isErrorAborted(err);
                if (!this.id && this._useInRunner && !wasAborted) {
                    span.addEvent('Stream broken. Safe to retry');
                    // TODO: resolve https://github.com/googleapis/nodejs-spanner/issues/2170
                    void this.begin();
                }
                else {
                    if (wasAborted) {
                        span.addEvent('Stream broken. Not safe to retry', {
                            'transaction.id': this.id?.toString(),
                        });
                    }
                }
                span.end();
            })
                .on('end', err => {
                if (err) {
                    (0, instrument_1.setSpanError)(span, err);
                }
                span.end();
            });
            if (resultStream instanceof stream_1.Stream) {
                (0, stream_1.finished)(resultStream, err => {
                    if (err) {
                        (0, instrument_1.setSpanError)(span, err);
                    }
                    span.end();
                });
            }
            return resultStream;
        });
    }
    /**
     *
     * @private
     */
    configureTagOptions(singleUse, transactionTag, requestOptions = {}) {
        if (!singleUse && transactionTag) {
            requestOptions.transactionTag = transactionTag;
        }
        return requestOptions;
    }
    /**
     * Transforms convenience options `keys` and `ranges` into a KeySet object.
     *
     * @private
     * @static
     *
     * @param {ReadRequest} request The read request.
     * @returns {object}
     */
    static encodeKeySet(request) {
        const keySet = request.keySet || {};
        if (request.keys) {
            keySet.keys = (0, helper_1.toArray)(request.keys).map(codec_1.codec.convertToListValue);
        }
        if (request.ranges) {
            keySet.ranges = (0, helper_1.toArray)(request.ranges).map(range => {
                const encodedRange = {};
                Object.keys(range).forEach(bound => {
                    encodedRange[bound] = codec_1.codec.convertToListValue(range[bound]);
                });
                return encodedRange;
            });
        }
        if ((0, helper_1.isEmpty)(keySet)) {
            keySet.all = true;
        }
        return keySet;
    }
    /**
     * Formats timestamp options into proto format.
     *
     * @private
     * @static
     *
     * @param {TimestampBounds} options The user supplied options.
     * @returns {object}
     */
    static encodeTimestampBounds(options) {
        const readOnly = {};
        const { returnReadTimestamp = true } = options;
        if (options.minReadTimestamp instanceof precise_date_1.PreciseDate) {
            readOnly.minReadTimestamp = options.minReadTimestamp.toStruct();
        }
        if (options.readTimestamp instanceof precise_date_1.PreciseDate) {
            readOnly.readTimestamp = options.readTimestamp.toStruct();
        }
        if (typeof options.maxStaleness === 'number') {
            readOnly.maxStaleness = codec_1.codec.convertMsToProtoTimestamp(options.maxStaleness);
        }
        if (typeof options.exactStaleness === 'number') {
            readOnly.exactStaleness = codec_1.codec.convertMsToProtoTimestamp(options.exactStaleness);
        }
        // If we didn't detect a convenience format, we'll just assume that
        // they passed in a protobuf timestamp.
        if ((0, helper_1.isEmpty)(readOnly)) {
            Object.assign(readOnly, options);
        }
        readOnly.returnReadTimestamp = returnReadTimestamp;
        return readOnly;
    }
    /**
     * Encodes convenience options `param` and `types` into the proto formatted.
     *
     * @private
     * @static
     *
     * @param {ExecuteSqlRequest} request The SQL request.
     * @returns {object}
     */
    static encodeParams(request) {
        const typeMap = request.types || {};
        const params = { fields: request.params?.fields || {} };
        const paramTypes = request.paramTypes || {};
        if (request.params && !request.params.fields) {
            const fields = {};
            Object.keys(request.params).forEach(param => {
                const value = request.params[param];
                if (!typeMap[param]) {
                    typeMap[param] = codec_1.codec.getType(value);
                }
                fields[param] = codec_1.codec.encode(value);
            });
            params.fields = fields;
        }
        if (!(0, helper_1.isEmpty)(typeMap)) {
            Object.keys(typeMap).forEach(param => {
                const type = typeMap[param];
                if (process.env['SPANNER_ENABLE_UUID_AS_UNTYPED'] === 'true') {
                    const typeObject = codec_1.codec.createTypeObject(type);
                    if ((type.child &&
                        typeObject.code === 'ARRAY' &&
                        typeObject.arrayElementType?.code !== 'TYPE_CODE_UNSPECIFIED') ||
                        (!type.child && typeObject.code !== 'TYPE_CODE_UNSPECIFIED')) {
                        paramTypes[param] = typeObject;
                    }
                }
                else {
                    paramTypes[param] = codec_1.codec.createTypeObject(type);
                }
            });
        }
        return { params, paramTypes };
    }
    /**
     * Get directed read options
     * @private
     * @param {google.spanner.v1.IDirectedReadOptions} directedReadOptions Request directedReadOptions object.
     */
    _getDirectedReadOptions(directedReadOptions) {
        if (!directedReadOptions &&
            this._getSpanner().directedReadOptions &&
            this._options.readOnly) {
            return this._getSpanner().directedReadOptions;
        }
        return directedReadOptions;
    }
    /**
     * Update transaction properties from the response.
     *
     * @private
     *
     * @param {spannerClient.spanner.v1.ITransaction} resp Response object.
     */
    _update(resp, span) {
        const { id, readTimestamp } = resp;
        this.id = id;
        this.metadata = resp;
        span.addEvent('Transaction Creation Done', { id: this.id.toString() });
        if (readTimestamp) {
            this.readTimestampProto = readTimestamp;
            this.readTimestamp = new precise_date_1.PreciseDate(readTimestamp);
        }
        this._releaseWaitingRequests();
    }
    /**
     * Wrap `makeRequest` function with the lock to make sure the inline begin
     * transaction can happen only once.
     *
     * @param makeRequest
     * @private
     */
    _wrapWithIdWaiter(makeRequest) {
        if (this.id || !this._options.readWrite) {
            return makeRequest;
        }
        if (!this._inlineBeginStarted) {
            this._inlineBeginStarted = true;
            return makeRequest;
        }
        // Queue subsequent requests.
        return (resumeToken) => {
            const streamProxy = new stream_1.Readable({
                read() { },
            });
            this._waitingRequests.push(() => {
                makeRequest(resumeToken)
                    .on('data', chunk => streamProxy.emit('data', chunk))
                    .on('error', err => streamProxy.emit('error', err))
                    .on('end', () => streamProxy.emit('end'));
            });
            return streamProxy;
        };
    }
    _releaseWaitingRequests() {
        while (this._waitingRequests.length > 0) {
            const request = this._waitingRequests.shift();
            request?.();
        }
    }
    /**
     * Gets the Spanner object
     *
     * @private
     *
     * @returns {Spanner}
     */
    _getSpanner() {
        return this.session.parent.parent.parent;
    }
}
exports.Snapshot = Snapshot;
/*! Developer Documentation
 *
 * All async methods (except for streams) return a Promise in the event
 * that a callback is omitted.
 */
(0, promisify_1.promisifyAll)(Snapshot, {
    exclude: ['configureTagOptions', 'end'],
});
/**
 * Never use DML class directly. Instead, it should be extended upon
 * if a class requires DML capabilities.
 *
 * @private
 * @class
 */
class Dml extends Snapshot {
    runUpdate(query, callback) {
        if (typeof query === 'string') {
            query = { sql: query };
        }
        return (0, instrument_1.startTrace)('Dml.runUpdate', {
            ...query,
            ...this._traceConfig,
            transactionTag: this.requestOptions?.transactionTag,
            requestTag: query.requestOptions?.requestTag,
        }, span => {
            this.run(query, (err, rows, stats) => {
                let rowCount = 0;
                if (stats && stats.rowCount) {
                    rowCount = Math.floor(stats[stats.rowCount]);
                }
                if (err) {
                    (0, instrument_1.setSpanError)(span, err);
                }
                span.end();
                callback(err, rowCount);
            });
        });
    }
}
exports.Dml = Dml;
/*! Developer Documentation
 *
 * All async methods (except for streams) return a Promise in the event
 * that a callback is omitted.
 */
(0, promisify_1.promisifyAll)(Dml);
/**
 * This type of transaction is the only way to write data into Cloud Spanner.
 * These transactions rely on pessimistic locking and, if necessary, two-phase
 * commit. Locking read-write transactions may abort, requiring the application
 * to retry.
 *
 * Calling either {@link Transaction#commit} or {@link Transaction#rollback}
 * signals that the transaction is finished and no further requests will be
 * made. If for some reason you decide not to call one of the aformentioned
 * methods, call {@link Transaction#end} to release the underlying
 * {@link Session}.
 *
 * Running a transaction via {@link Database#runTransaction} or
 * {@link Database#runTransactionAsync} automatically re-runs the
 * transaction on `ABORTED` errors.
 *
 * {@link Database#getTransaction} returns a plain {@link Transaction}
 * object, requiring the user to retry manually.
 *
 * @class
 * @extends Snapshot
 *
 * @param {Session} session The parent Session object.
 *
 * @example
 * ```
 * const {Spanner} = require('@google-cloud/spanner');
 * const spanner = new Spanner();
 *
 * const instance = spanner.instance('my-instance');
 * const database = instance.database('my-database');
 *
 * database.runTransaction(function(err, transaction) {
 *   // The `transaction` object is ready for use.
 * });
 *
 * ```
 * @example To manually control retrying the transaction, use the
 * `getTransaction` method.
 * ```
 * database.getTransaction(function(err, transaction) {
 *   // The `transaction` object is ready for use.
 * });
 * ```
 */
class Transaction extends Dml {
    commitTimestamp;
    commitTimestampProto;
    _queuedMutations;
    _retryCommit;
    /**
     * Timestamp at which the transaction was committed. Will be populated once
     * {@link Transaction#commit} is called.
     *
     * @name Transaction#commitTimestamp
     * @type {?external:PreciseDate}
     */
    /**
     * The protobuf version of {@link Transaction#commitTimestamp}. This is useful
     * if you require microsecond precision.
     *
     * @name Transaction#commitTimestampProto
     * @type {?google.protobuf.Timestamp}
     */
    /**
     * Execute a DML statement and get the affected row count.
     *
     * @name Transaction#runUpdate
     *
     * @see {@link Transaction#run}
     *
     * @param {string|object} query A DML statement or
     *     [`ExecuteSqlRequest`](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ExecuteSqlRequest)
     *     object.
     * @param {object} [query.params] A map of parameter name to values.
     * @param {object} [query.types] A map of parameter types.
     * @param {RunUpdateCallback} [callback] Callback function.
     * @returns {Promise<RunUpdateResponse>}
     *
     * @example
     * ```
     * const query = 'UPDATE Account SET Balance = 1000 WHERE Key = 1';
     *
     * transaction.runUpdate(query, (err, rowCount) => {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     * });
     * ```
     */
    constructor(session, options = {}, queryOptions, requestOptions) {
        super(session, undefined, queryOptions);
        this._queuedMutations = [];
        this._options = { readWrite: options };
        this._options.isolationLevel = IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED;
        this.requestOptions = requestOptions;
        this._retryCommit = false;
    }
    batchUpdate(queries, optionsOrCallback, cb) {
        const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
        const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
        const gaxOpts = 'gaxOptions' in options
            ? options.gaxOptions
            : options;
        if (!Array.isArray(queries) || !queries.length) {
            const rowCounts = [];
            const error = new Error('batchUpdate requires at least 1 DML statement.');
            const batchError = Object.assign(error, {
                code: 3, // invalid argument
                rowCounts,
            });
            callback(batchError, rowCounts);
            return;
        }
        const statements = queries.map(query => {
            if (typeof query === 'string') {
                return { sql: query };
            }
            const { sql } = query;
            const { params, paramTypes } = Snapshot.encodeParams(query);
            return { sql, params, paramTypes };
        });
        const transaction = {};
        if (this.id) {
            transaction.id = this.id;
        }
        else {
            transaction.begin = this._options;
        }
        if (!this.id &&
            this._options.readWrite &&
            this.session.parent.isMuxEnabledForRW_) {
            this._setPreviousTransactionId(transaction);
        }
        const requestOptionsWithTag = this.configureTagOptions(false, this.requestOptions?.transactionTag ?? undefined, options.requestOptions);
        const reqOpts = {
            session: this.session.formattedName_,
            requestOptions: requestOptionsWithTag,
            transaction,
            seqno: this._seqno++,
            statements,
        };
        const database = this.session.parent;
        const headers = (0, request_id_header_1.injectRequestIDIntoHeaders)(this.commonHeaders_, this.session, (0, request_id_header_1.nextNthRequest)(database), 1);
        if (this._getSpanner().routeToLeaderEnabled) {
            (0, common_1.addLeaderAwareRoutingHeader)(headers);
        }
        const traceConfig = {
            ...this._traceConfig,
            transactionTag: requestOptionsWithTag?.transactionTag,
            requestTag: options?.requestOptions?.requestTag,
        };
        return (0, instrument_1.startTrace)('Transaction.batchUpdate', traceConfig, span => {
            this.request({
                client: 'SpannerClient',
                method: 'executeBatchDml',
                reqOpts,
                gaxOpts,
                headers: headers,
            }, (err, resp) => {
                let batchUpdateError;
                if (err) {
                    const rowCounts = [];
                    batchUpdateError = Object.assign(err, { rowCounts });
                    (0, instrument_1.setSpanError)(span, batchUpdateError);
                    span.end();
                    callback(batchUpdateError, rowCounts, resp);
                    return;
                }
                this._updatePrecommitToken(resp);
                const { resultSets, status } = resp;
                for (const resultSet of resultSets) {
                    if (!this.id && resultSet.metadata?.transaction) {
                        this._update(resultSet.metadata.transaction, span);
                    }
                }
                const rowCounts = resultSets.map(({ stats }) => {
                    return ((stats &&
                        Number(stats[stats.rowCount])) ||
                        0);
                });
                if (status && status.code !== 0) {
                    const error = new Error(status.message);
                    batchUpdateError = Object.assign(error, {
                        code: status.code,
                        metadata: Transaction.extractKnownMetadata(status.details),
                        rowCounts,
                    });
                    (0, instrument_1.setSpanError)(span, batchUpdateError);
                }
                span.end();
                callback(batchUpdateError, rowCounts, resp);
            });
        });
    }
    static extractKnownMetadata(details) {
        if (details && typeof details[Symbol.iterator] === 'function') {
            const metadata = new google_gax_1.grpc.Metadata();
            for (const detail of details) {
                if (detail.type_url === RETRY_INFO_TYPE && detail.value) {
                    metadata.add(RETRY_INFO_BIN, detail.value);
                }
            }
            return metadata;
        }
        return undefined;
    }
    /**
     * This method updates the _queuedMutations property of the transaction.
     *
     * @public
     *
     * @param {spannerClient.spanner.v1.Mutation[]} [mutation]
     */
    setQueuedMutations(mutation) {
        this._queuedMutations = mutation;
    }
    commit(optionsOrCallback, cb) {
        const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
        const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
        let gaxOpts = 'gaxOptions' in options ? options.gaxOptions : options;
        const mutations = this._queuedMutations;
        const session = this.session.formattedName_;
        const precommitToken = this._latestPreCommitToken;
        const requestOptions = options.requestOptions;
        const reqOpts = {
            mutations,
            session,
            requestOptions,
            precommitToken,
        };
        return (0, instrument_1.startTrace)('Transaction.commit', {
            transactionTag: this.requestOptions?.transactionTag,
            ...this._traceConfig,
        }, span => {
            if (this.id) {
                reqOpts.transactionId = this.id;
            }
            else if (!this._useInRunner) {
                reqOpts.singleUseTransaction = this._options;
            }
            else {
                if (this.session.parent.isMuxEnabledForRW_) {
                    this._setMutationKey(mutations);
                }
                this.begin().then(() => {
                    this.commit(options, (err, resp) => {
                        if (err) {
                            (0, instrument_1.setSpanError)(span, err);
                        }
                        span.end();
                        callback(err, resp);
                    });
                }, err => {
                    (0, instrument_1.setSpanError)(span, err);
                    span.end();
                    callback(err, null);
                });
                return;
            }
            if ('returnCommitStats' in options &&
                options.returnCommitStats) {
                reqOpts.returnCommitStats = options.returnCommitStats;
            }
            if ('maxCommitDelay' in options &&
                options.maxCommitDelay) {
                reqOpts.maxCommitDelay = options.maxCommitDelay;
            }
            reqOpts.requestOptions = Object.assign(requestOptions || {}, this.requestOptions);
            const headers = this.commonHeaders_;
            if (this._getSpanner().routeToLeaderEnabled) {
                (0, common_1.addLeaderAwareRoutingHeader)(headers);
            }
            span.addEvent('Starting Commit');
            const database = this.session.parent;
            if (this._affinityKey) {
                if (!gaxOpts || Object.keys(gaxOpts).length === 0) {
                    gaxOpts = this._unbindGaxOpts;
                }
                else {
                    gaxOpts = injectGaxOpt(gaxOpts, 'unbind', true);
                }
            }
            this.request({
                client: 'SpannerClient',
                method: 'commit',
                reqOpts,
                gaxOpts,
                headers: (0, request_id_header_1.injectRequestIDIntoHeaders)(headers, this.session, (0, request_id_header_1.nextNthRequest)(database), 1),
            }, (err, resp) => {
                if (resp &&
                    'MultiplexedSessionRetry' in resp &&
                    !this._retryCommit) {
                    this._retryCommit = true;
                    this._updatePrecommitToken(resp);
                    return this.commit(options, callback);
                }
                this.end();
                if (err) {
                    span.addEvent('Commit failed');
                    (0, instrument_1.setSpanError)(span, err);
                }
                else {
                    span.addEvent('Commit Done');
                }
                if (resp && resp.commitTimestamp) {
                    this.commitTimestampProto = resp.commitTimestamp;
                    this.commitTimestamp = new precise_date_1.PreciseDate(resp.commitTimestamp);
                }
                err = Transaction.decorateCommitError(err, mutations);
                span.end();
                callback(err, resp);
            });
        });
    }
    /**
     * Decorates an error returned by a commit with additional information for
     * specific known errors.
     * @param err the error to check and decorate with additional information if possible
     * @param mutations the mutations included in the commit request
     * @private
     */
    static decorateCommitError(err, mutations) {
        if (!err) {
            return err;
        }
        if (err.code === google_gax_1.Status.FAILED_PRECONDITION) {
            const mismatchErr = Transaction.decoratePossibleJsonMismatchError(err, mutations);
            if (mismatchErr) {
                return mismatchErr;
            }
        }
        return err;
    }
    /**
     * Decorates an error returned by a commit with additional information if the
     * error was returned because the application tried to insert an array of
     * objects into a JSON column. An array of objects will by default be encoded
     * as ARRAY<JSON>, but can also be interpreted as JSON. An application must
     * specify a top-level array of objects that should be inserted into a JSON
     * column as a string instead of as an array of objects.
     * @param err the error returned by the commit RPC
     * @param mutations the mutations included in the commit request
     * @private
     */
    static decoratePossibleJsonMismatchError(err, mutations) {
        const errorMessage = /Invalid value for column (?<column>.+) in table (?<table>.+): Expected JSON./;
        const found = err.message && err.message.match(errorMessage);
        if (found && found.groups) {
            const table = found.groups.table;
            const column = found.groups.column;
            for (const mutation of mutations) {
                const write = mutation.insert ||
                    mutation.update ||
                    mutation.insertOrUpdate ||
                    mutation.replace;
                if (write && write.table === table) {
                    const index = write.columns?.indexOf(column);
                    if (index && index > -1 && write.values) {
                        for (const row of write.values) {
                            if (row.values?.length &&
                                row.values.length > index &&
                                row.values[index].listValue) {
                                // If the value is an array, the client library also encoded it as
                                // an array. Inserting an array into a JSON column is not possible,
                                // although if it is encoded as a string containing a top-level JSON
                                // array it will work.
                                const additionalMessage = 'The value is an array. Convert the value to a JSON string containing an array instead in order to insert it into a JSON column. Example: `[{"key": "value 1"}, {"key": "value 2"}]` instead of [{key: "value 1"}, {key: "value 2"}]';
                                return {
                                    code: err.code,
                                    details: `${err.details} ${additionalMessage}`,
                                    message: `${err.message} ${additionalMessage}`,
                                    metadata: err.metadata,
                                    name: err.name,
                                    stack: err.stack,
                                };
                            }
                        }
                    }
                }
            }
        }
        return null;
    }
    /**
     * Delete rows from a table.
     *
     * @see [Commit API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.Commit)
     *
     * @param {string} table The name of the table.
     * @param {array} keys The keys for the rows to delete. If using a
     *     composite key, provide an array within this array. See the example
     * below.
     *
     * @example
     * ```
     * const keys = ['Id1', 'Id2', 'Id3'];
     *
     * database.runTransaction(function(err, transaction) {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   // Queue this mutation until later calling `commit`.
     *   // Note that a callback is not passed to `deleteRows`.
     *   transaction.deleteRows('Singers', keys);
     *
     *   // Commit the transaction.
     *   transaction.commit(function(err) {
     *     if (!err) {
     *       // The rows were deleted successfully.
     *     }
     *   });
     * });
     *
     * ```
     * @example Provide an array for `keys` to delete rows with a
     * composite key.
     * ```
     * const keys = [
     *   [
     *     'Id1',
     *     'Name1'
     *   ],
     *   [
     *     'Id2',
     *     'Name2'
     *   ]
     * ];
     * ```
     */
    deleteRows(table, keys) {
        this._queuedMutations.push(buildDeleteMutation(table, keys));
    }
    /**
     * Insert rows of data into this table.
     *
     * @see [Commit API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.Commit)
     *
     * @param {string} table The name of the table.
     * @param {object|object[]} rows A map of names to values of data to insert
     *     into this table.
     *
     * @example
     * ```
     * const row = {
     *   SingerId: 'Id3',
     *   Name: 'Eddie Wilson'
     * };
     *
     * database.runTransaction(function(err, transaction) {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   // Queue this mutation until later calling `commit`.
     *   // Note that a callback is not passed to `insert`.
     *   transaction.insert('Singers', row);
     *
     *   // Commit the transaction.
     *   transaction.commit(function(err) {
     *     if (!err) {
     *       // The row was inserted successfully.
     *     }
     *   });
     * });
     *
     * ```
     * @example Multiple rows can be inserted at once.
     * ```
     * const row2 = {
     *   SingerId: 'Id3b',
     *   Name: 'Joe West'
     * };
     *
     * database.runTransaction(function(err, transaction) {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   // Queue multiple mutations until later calling `commit`.
     *   // Note that a callback is not passed to `insert`.
     *   transaction.insert('Singers', [
     *     row,
     *     row2
     *   ]);
     *
     *   // Commit the transaction.
     *   transaction.commit(function(err) {
     *     if (!err) {
     *       // The rows were inserted successfully.
     *     }
     *   });
     * });
     * ```
     */
    insert(table, rows) {
        this._mutate('insert', table, rows);
    }
    /**
     * Replace rows of data within a table.
     *
     * @see [Commit API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.Commit)
     *
     * @param {string} table The table to read from.
     * @param {object|object[]} rows A map of names to values of data to insert
     *     into this table.
     *
     * @example
     * ```
     * const row = {
     *   SingerId: 'Id3',
     *   Name: 'Joe West'
     * };
     *
     * database.runTransaction(function(err, transaction) {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   // Queue this mutation until later calling `commit`.
     *   // Note that a callback is not passed to `replace`.
     *   transaction.replace('Singers', row);
     *
     *   // Commit the transaction.
     *   transaction.commit(function(err) {
     *     if (!err) {
     *       // The row was replaced successfully.
     *     }
     *   });
     * });
     * ```
     */
    replace(table, rows) {
        this._mutate('replace', table, rows);
    }
    rollback(gaxOptionsOrCallback, cb) {
        let gaxOpts = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
        const callback = typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb;
        return (0, instrument_1.startTrace)('Transaction.rollback', this._traceConfig, span => {
            if (!this.id) {
                span.addEvent('Transaction ID is unknown, nothing to rollback.');
                span.end();
                callback(null);
                return;
            }
            const session = this.session.formattedName_;
            const transactionId = this.id;
            const reqOpts = {
                session,
                transactionId,
            };
            const headers = this.commonHeaders_;
            if (this._getSpanner().routeToLeaderEnabled) {
                (0, common_1.addLeaderAwareRoutingHeader)(headers);
            }
            if (this._affinityKey) {
                if (!gaxOpts || Object.keys(gaxOpts).length === 0) {
                    gaxOpts = this._unbindGaxOpts;
                }
                else {
                    gaxOpts = injectGaxOpt(gaxOpts, 'unbind', true);
                }
            }
            this.request({
                client: 'SpannerClient',
                method: 'rollback',
                reqOpts,
                gaxOpts,
                headers: headers,
            }, (err) => {
                if (err) {
                    (0, instrument_1.setSpanError)(span, err);
                }
                span.end();
                this.end();
                callback(err);
            });
        });
    }
    /**
     * Update rows of data within a table.
     *
     * @see [Commit API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.Commit)
     *
     * @param {string} table The table to read from.
     * @param {object|object[]} rows A map of names to values of data to insert
     *     into this table.
     *
     * @example
     * ```
     * const row = {
     *   SingerId: 'Id3',
     *   Name: 'Joe West'
     * };
     *
     * database.runTransaction(function(err, transaction) {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   // Queue this mutation until later calling `commit`.
     *   // Note that a callback is not passed to `update`.
     *   transaction.update('Singers', row);
     *
     *   // Commit the transaction.
     *   transaction.commit(function(err) {
     *     if (!err) {
     *       // The row was updated successfully.
     *     }
     *   });
     * });
     * ```
     */
    update(table, rows) {
        this._mutate('update', table, rows);
    }
    /**
     * Insert or update rows of data within a table.
     *
     * @see [Commit API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.Commit)
     *
     * @param {string} table The table to read from.
     * @param {object|object[]} rows A map of names to values of data to insert
     *     into this table.
     *
     * @example
     * ```
     * const row = {
     *   SingerId: 'Id3',
     *   Name: 'Joe West'
     * };
     *
     * database.runTransaction(function(err, transaction) {
     *   if (err) {
     *     // Error handling omitted.
     *   }
     *
     *   // Queue this mutation until later calling `commit`.
     *   // Note that a callback is not passed to `upsert`.
     *   transaction.upsert('Singers', row);
     *
     *   // Commit the transaction.
     *   transaction.commit(function(err) {
     *     if (!err) {
     *       // The row was updated or inserted successfully.
     *     }
     *   });
     * });
     * ```
     */
    upsert(table, rows) {
        this._mutate('insertOrUpdate', table, rows);
    }
    /**
     * Formats the mutations.
     *
     * @see [Commit API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.Commit)
     *
     * @private
     *
     * @param {string} method CRUD method (insert, update, etc.).
     * @param {string} table Table to perform mutations in.
     * @param {object} rows Hash of key value pairs.
     */
    _mutate(method, table, keyVals) {
        this._queuedMutations.push(buildMutation(method, table, keyVals));
    }
    /**
     * Takes a list of rows and returns all unique column names.
     *
     * @private
     *
     * @param {object[]} rows The rows.
     * @returns {string[]}
     */
    static getUniqueKeys(rows) {
        const allKeys = [];
        rows.forEach(row => allKeys.push(...Object.keys(row)));
        const unique = new Set(allKeys);
        return Array.from(unique).sort();
    }
    /**
     * Mark transaction as started from the runner.
     */
    useInRunner() {
        this._useInRunner = true;
    }
    /**
     * Use optimistic concurrency control for the transaction.
     *
     * In this concurrency mode, operations during the execution phase, i.e.,
     * reads and queries, are performed without acquiring locks, and transactional
     * consistency is ensured by running a validation process in the commit phase
     * (when any needed locks are acquired). The validation process succeeds only
     * if there are no conflicting committed transactions (that committed
     * mutations to the read data at a commit timestamp after the read timestamp).
     *
     * @deprecated Set readLockMode through setReadWriteTransactionOptions instead.
     */
    useOptimisticLock() {
        this._options.readWrite.readLockMode = ReadLockMode.OPTIMISTIC;
    }
    /**
     * Use option excludeTxnFromChangeStreams to exclude read/write transactions
     * from being tracked in change streams.
     *
     * Enabling this options to true will effectively disable change stream tracking
     * for a specified transaction, allowing read/write transaction to operate without being
     * included in change streams.
     */
    excludeTxnFromChangeStreams() {
        this._options.excludeTxnFromChangeStreams = true;
    }
    setReadWriteTransactionOptions(options) {
        /**
         * Set option excludeTxnFromChangeStreams=true to exclude read/write transactions
         * from being tracked in change streams.
         */
        if (options?.excludeTxnFromChangeStreams) {
            this._options.excludeTxnFromChangeStreams = true;
        }
        /**
         * Set isolation level.
         */
        this._options.isolationLevel = options?.isolationLevel
            ? options?.isolationLevel
            : this._getSpanner().defaultTransactionOptions.isolationLevel;
        /**
         * Set read lock mode.
         */
        this._options.readWrite.readLockMode = options?.readLockMode
            ? options?.readLockMode
            : this._getSpanner().defaultTransactionOptions.readLockMode;
    }
}
exports.Transaction = Transaction;
/*! Developer Documentation
 *
 * All async methods (except for streams) return a Promise in the event
 * that a callback is omitted.
 */
(0, promisify_1.promisifyAll)(Transaction, {
    exclude: ['deleteRows', 'insert', 'replace', 'update', 'upsert'],
});
/**
 * Builds an array of protobuf Mutations from the given row(s).
 *
 * @param {string} method - CRUD method (insert, update, etc.).
 * @param {string} table - Table to perform mutations in.
 * @param {object | object[]} keyVals - Hash of key-value pairs representing the rows.
 * @returns {spannerClient.spanner.v1.Mutation} - The formatted mutation.
 * @throws {GoogleError} - If a row does not contain the correct number of columns.
 */
function buildMutation(method, table, keyVals) {
    const rows = (0, helper_1.toArray)(keyVals);
    const columns = Transaction.getUniqueKeys(rows);
    const values = rows.map((row, index) => {
        const keys = Object.keys(row);
        const missingColumns = columns.filter(column => !keys.includes(column));
        if (missingColumns.length > 0) {
            throw new google_gax_1.GoogleError([
                `Row at index ${index} does not contain the correct number of columns.`,
                `Missing columns: ${JSON.stringify(missingColumns)}`,
            ].join('\n\n'));
        }
        const values = columns.map(column => row[column]);
        return codec_1.codec.convertToListValue(values);
    });
    const mutation = {
        [method]: { table, columns, values },
    };
    return mutation;
}
/**
 * Builds a delete mutation.
 *
 * @param {string} table - The name of the table.
 * @param {Key[]} keys - The keys for the rows to delete.
 * @returns {spannerClient.spanner.v1.Mutation} - The formatted delete mutation.
 */
function buildDeleteMutation(table, keys) {
    const keySet = {
        keys: (0, helper_1.toArray)(keys).map(codec_1.codec.convertToListValue),
    };
    const mutation = {
        delete: { table, keySet },
    };
    return mutation;
}
/**
 * MutationSet represent a set of changes to be applied atomically to a Cloud Spanner
 * database with a {@link Transaction}.
 * Mutations are used to insert, update, upsert(insert or update), replace, or
 * delete rows within tables.
 *
 * Mutations are added to a {@link Transaction} and are not executed until the
 * transaction is committed via {@link Transaction#commit}.
 *
 * If the transaction is rolled back or encounters an error, the mutations are
 * discarded.
 *
 * @example
 * ```
 * const {Spanner, Mutation} = require('@google-cloud/spanner');
 * const spanner = new Spanner();
 *
 * const instance = spanner.instance('my-instance');
 * const database = instance.database('my-database');
 *
 * const mutations = new MutationSet();
 * mutations.insert('Singers', {SingerId: '123', FirstName: 'David'});
 * mutations.update('Singers', {SingerId: '123', FirstName: 'Marc'});
 *
 * try {
 *  database.writeAtLeastOnce(mutations, (err, res) => {
 *    console.log("RESPONSE: ", res);
 *  });
 * } catch(err) {
 *  console.log("ERROR: ", err);
 * }
 * ```
 */
class MutationSet {
    /**
     * An array to store the mutations.
     */
    _queuedMutations;
    /**
     * Creates a new Mutation object.
     */
    constructor() {
        this._queuedMutations = [];
    }
    /**
     * Adds an insert operation to the mutation set.
     * @param {string} table. The name of the table to insert into.
     * @param {object|object[]} rows. A single row object or an array of row objects to insert.
     */
    insert(table, rows) {
        this._queuedMutations.push(buildMutation('insert', table, rows));
    }
    /**
     * Adds an update operation to the mutation set.
     * @param {string} table. The name of the table to update.
     * @param {object|object[]} rows. A single row object or an array of row objects to update.
     * Each row object must contain the primary key values to indentify the row to update.
     */
    update(table, rows) {
        this._queuedMutations.push(buildMutation('update', table, rows));
    }
    /**
     * Adds an upsert operation to the mutation set.
     * An upsert will insert a new row if it does not exist or update an existing row if it does.
     * @param {string} table. The name of the table to upsert.
     * @param {object|object[]} rows. A single row object or an array of row objects to upsert.
     */
    upsert(table, rows) {
        this._queuedMutations.push(buildMutation('insertOrUpdate', table, rows));
    }
    /**
     * Adds a replace operation to the mutation set.
     * A replace operation deletes the existing row (if it exists) and inserts the new row.
     * @param {string} table. The name of the table to replace.
     * @param {object|object[]} rows. A single row object or an array of row objects to replace.
     */
    replace(table, rows) {
        this._queuedMutations.push(buildMutation('replace', table, rows));
    }
    /**
     * Adds a deleteRows operation to the mutation set.
     * This operation deletes rows from the specified table based on their primary keys.
     * @param {string} table. The name of the table to deleteRows from.
     * @param {key[]} key. An array of key objects, each represeting the primary key of a row to delete.
     */
    deleteRows(table, keys) {
        this._queuedMutations.push(buildDeleteMutation(table, keys));
    }
    /**
     * Returns the internal representation of the queued mutations as a protobuf message.
     * @returns {spannerClient.spanner.v1.Mutation[]}. The protobuf message representing the mutations.
     */
    proto() {
        return this._queuedMutations;
    }
}
exports.MutationSet = MutationSet;
/**
 * A group of mutations to be committed together.
 * Related mutations should be placed in a group.
 *
 * For example, two mutations inserting rows with the same primary
 * key prefix in both parent and child tables are related.
 *
 * This object is created and returned from {@link Database#MutationGroup}.
 *
 * @example
 * ```
 * const {Spanner} = require('@google-cloud/spanner');
 * const spanner = new Spanner();
 *
 * const instance = spanner.instance('my-instance');
 * const database = instance.database('my-database');
 *
 * const mutationGroup = new MutationGroup();
 * mutationGroup.insert('Singers', {SingerId: '123', FirstName: 'David'});
 * mutationGroup.update('Singers', {SingerId: '123', FirstName: 'Marc'});
 *
 * database.batchWriteAtLeastOnce([mutationGroup], {})
 *    .on('error', console.error)
 *    .on('data', response => {
 *          console.log('response: ', response);
 *      })
 *     .on('end', () => {
 *          console.log('Request completed successfully');
 *      });
 * ```
 */
class MutationGroup {
    _proto;
    constructor() {
        this._proto =
            new spannerClient.spanner.v1.BatchWriteRequest.MutationGroup();
    }
    insert(table, rows) {
        this._proto.mutations.push(buildMutation('insert', table, rows));
    }
    update(table, rows) {
        this._proto.mutations.push(buildMutation('update', table, rows));
    }
    upsert(table, rows) {
        this._proto.mutations.push(buildMutation('insertOrUpdate', table, rows));
    }
    replace(table, rows) {
        this._proto.mutations.push(buildMutation('replace', table, rows));
    }
    deleteRows(table, keys) {
        this._proto.mutations.push(buildDeleteMutation(table, keys));
    }
    proto() {
        return this._proto;
    }
}
exports.MutationGroup = MutationGroup;
/**
 * This type of transaction is used to execute a single Partitioned DML
 * statement. Partitioned DML partitions the key space and runs the DML
 * statement over each partition in parallel using separate, internal
 * transactions that commit independently.
 *
 * Chances are, you'll never need to create a partitioned DML transaction
 * directly, instead you'll want to use {@link Database#runPartitionedUpdate}.
 *
 * @class
 * @extends Snapshot
 *
 * @see Database#runPartitionedUpdate
 */
class PartitionedDml extends Dml {
    constructor(session, options = {}) {
        super(session);
        this._options = { partitionedDml: options };
    }
    /**
     * Use option excludeTxnFromChangeStreams to exclude partitionedDml
     * queries from being tracked in change streams.
     *
     * Enabling this options to true will effectively disable change stream tracking
     * for a specified partitionedDml query, allowing write queries to operate
     * without being included in change streams.
     */
    excludeTxnFromChangeStreams() {
        this._options.excludeTxnFromChangeStreams = true;
    }
    runUpdate(query, callback) {
        return (0, instrument_1.startTrace)('PartitionedDml.runUpdate', {
            ...query,
            ...this._traceConfig,
        }, span => {
            super.runUpdate(query, (err, count) => {
                if (err) {
                    (0, instrument_1.setSpanError)(span, err);
                }
                this.end();
                span.end();
                callback(err, count);
            });
        });
    }
}
exports.PartitionedDml = PartitionedDml;
function isErrorAborted(err) {
    return (err &&
        typeof err === 'object' &&
        'code' in err &&
        err.code === google_gax_1.grpc.status.ABORTED);
}
/*! Developer Documentation
 *
 * All async methods (except for streams) return a Promise in the event
 * that a callback is omitted.
 */
(0, promisify_1.promisifyAll)(PartitionedDml);
//# sourceMappingURL=transaction.js.map