@google-cloud/spanner
Version:
Cloud Spanner Client Library for Node.js
1,302 lines (1,301 loc) • 76.8 kB
JavaScript
"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_;