UNPKG

@google-cloud/spanner

Version:
1,302 lines (1,301 loc) 76.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 is = require("is"); const stream_1 = require("stream"); const codec_1 = require("./codec"); const partial_result_stream_1 = require("./partial-result-stream"); const instrument_1 = require("./instrument"); const protos_1 = require("../protos/protos"); const common_1 = require("./common"); const protos_2 = require("../protos/protos"); var IsolationLevel = protos_2.google.spanner.v1.TransactionOptions.IsolationLevel; var ReadLockMode = protos_2.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; const instrument_2 = 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'; /** * @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; id; ended; metadata; readTimestamp; readTimestampProto; request; requestStream; session; queryOptions; commonHeaders_; requestOptions; _observabilityOptions; _traceConfig; _dbName; /** * 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); 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_ = (0, common_1.getCommonHeaders)(this._dbName, this._observabilityOptions?.enableEndToEndTracing); this._traceConfig = { opts: this._observabilityOptions, dbName: this._dbName, }; } 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; const reqOpts = { session, options, }; // 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_2.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_2.setSpanError)(span, err); } else { this._update(resp); } 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; } 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_2.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 => { if (response.metadata && response.metadata.transaction && !this.id) { this._update(response.metadata.transaction); } }) .on('error', err => { (0, instrument_2.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_2.setSpanError)(span, err); } span.end(); }); if (resultStream instanceof stream_1.Stream) { (0, stream_1.finished)(resultStream, err => { if (err) { (0, instrument_2.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')); } 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_2.startTrace)('Snapshot.read', { tableName: table, ...this._traceConfig, }, span => { this.createReadStream(table, request) .on('error', err => { const e = err; (0, instrument_2.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_2.startTrace)('Snapshot.run', { ...query, ...this._traceConfig, }, span => { return this.runStream(query) .on('error', (err, rows, stats, metadata) => { (0, instrument_2.setSpanError)(span, err); span.end(); callback(err, rows, stats, metadata); }) .on('response', response => { if (response.metadata) { metadata = response.metadata; if (metadata.transaction && !this.id) { this._update(metadata.transaction); } } }) .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; } 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_2.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_2.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 => { if (response.metadata && response.metadata.transaction && !this.id) { this._update(response.metadata.transaction); } }) .on('error', err => { (0, instrument_2.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_2.setSpanError)(span, err); } span.end(); }); if (resultStream instanceof stream_1.Stream) { (0, stream_1.finished)(resultStream, err => { if (err) { (0, instrument_2.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 (is.empty(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 (is.empty(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 (!is.empty(typeMap)) { Object.keys(typeMap).forEach(param => { const type = typeMap[param]; 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) { const { id, readTimestamp } = resp; this.id = id; this.metadata = resp; const span = (0, instrument_1.getActiveOrNoopSpan)(); 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_2.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_2.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; /** * 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; } 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; } 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_2.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_2.setSpanError)(span, batchUpdateError); span.end(); callback(batchUpdateError, rowCounts, resp); return; } const { resultSets, status } = resp; for (const resultSet of resultSets) { if (!this.id && resultSet.metadata?.transaction) { this._update(resultSet.metadata.transaction); } } 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_2.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; const gaxOpts = 'gaxOptions' in options ? options.gaxOptions : options; const mutations = this._queuedMutations; const session = this.session.formattedName_;