@google-cloud/spanner
Version:
Cloud Spanner Client Library for Node.js
1,270 lines • 66.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.Database = void 0;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const common = require('./common-grpc/service-object');
const promisify_1 = require("@google-cloud/promisify");
const extend = require("extend");
const streamEvents = require("stream-events");
const through = require("through2");
const google_gax_1 = require("google-gax");
const backup_1 = require("./backup");
const batch_transaction_1 = require("./batch-transaction");
const session_factory_1 = require("./session-factory");
const session_1 = require("./session");
const session_pool_1 = require("./session-pool");
const table_1 = require("./table");
const transaction_runner_1 = require("./transaction-runner");
const common_1 = require("./common");
const stream_1 = require("stream");
const helper_1 = require("./helper");
const snakeCase = require("lodash.snakecase");
const instrument_1 = require("./instrument");
const request_id_header_1 = require("./request_id_header");
/**
* Create a Database object to interact with a Cloud Spanner database.
*
* @class
*
* @param {string} name Name of the database.
* @param {SessionPoolOptions|SessionPoolInterface} options Session pool
* configuration options or custom pool interface.
* @param {google.spanner.v1.ExecuteSqlRequest.IQueryOptions} queryOptions
* The default query options to use for queries on the database.
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner();
* const instance = spanner.instance('my-instance');
* const database = instance.database('my-database');
* ```
*/
class Database extends common.GrpcServiceObject {
instance;
formattedName_;
pool_;
sessionFactory_;
queryOptions_;
isMuxEnabledForRW_;
commonHeaders_;
request;
databaseRole;
labels;
databaseDialect;
_observabilityOptions; // TODO: exmaine if we can remove it
_traceConfig;
_nthRequest;
_clientId;
constructor(instance, name, poolOptions, queryOptions) {
const methods = {
/**
* Create a database.
*
* @method Database#create
* @param {CreateDatabaseRequest} [options] Configuration object.
* @param {CreateDatabaseCallback} [callback] Callback function.
* @returns {Promise<CreateDatabaseResponse>}
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner();
* const instance = spanner.instance('my-instance');
* const database = instance.database('my-database');
*
* database.create(function(err, database, operation, apiResponse) {
* if (err) {
* // Error handling omitted.
* }
*
* operation
* .on('error', function(err) {})
* .on('complete', function() {
* // Database created successfully.
* });
* });
*
* //-
* // If the callback is omitted, we'll return a Promise.
* //-
* database.create()
* .then(function(data) {
* const operation = data[0];
* const apiResponse = data[1];
*
* return operation.promise();
* })
* .then(function() {
* // Database created successfully.
* });
* ```
*/
create: true,
};
const formattedName_ = Database.formatName_(instance.formattedName_, name);
super({
parent: instance,
id: name,
methods,
createMethod: (_, options, callback) => {
const pool = this.pool_;
if (pool._pending > 0) {
// If there are BatchCreateSessions requests pending, then we should
// wait until these have finished before we try to create the database.
// Otherwise the results of these requests might be propagated to
// client requests that are submitted after the database has been
// created. If the pending requests have not finished within 10 seconds,
// they will be ignored and the database creation will proceed.
let timeout;
const promises = [
new Promise(resolve => (timeout = setTimeout(resolve, 10000))),
new Promise(resolve => {
pool
.on('available', () => {
if (pool._pending === 0) {
clearTimeout(timeout);
resolve();
}
})
.on('createError', () => {
if (pool._pending === 0) {
clearTimeout(timeout);
resolve();
}
});
}),
];
Promise.race(promises)
.then(() => instance.createDatabase(formattedName_, options, callback))
.catch(() => { });
}
else {
return instance.createDatabase(formattedName_, options, callback);
}
},
});
if (typeof poolOptions === 'object') {
this.databaseRole = poolOptions.databaseRole || null;
this.labels = poolOptions.labels || null;
}
this.formattedName_ = formattedName_;
this.instance = instance;
this._observabilityOptions = instance._observabilityOptions;
this._traceConfig = {
opts: this._observabilityOptions,
dbName: this.formattedName_,
};
this.request = instance.request;
this._nthRequest = (0, request_id_header_1.newAtomicCounter)(0);
if (this.parent && this.parent.parent) {
this._clientId = this.parent.parent._nthClientId;
}
else {
this._clientId = instance._nthClientId;
}
this._observabilityOptions = instance._observabilityOptions;
this.commonHeaders_ = (0, common_1.getCommonHeaders)(this.formattedName_, this._observabilityOptions?.enableEndToEndTracing);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.requestStream = instance.requestStream;
this.sessionFactory_ = new session_factory_1.SessionFactory(this, name, poolOptions);
this.pool_ = this.sessionFactory_.getPool();
this.isMuxEnabledForRW_ = this.sessionFactory_.isMultiplexedEnabledForRW();
const sessionPoolInstance = this.pool_;
if (sessionPoolInstance) {
sessionPoolInstance._observabilityOptions =
instance._observabilityOptions;
}
this.queryOptions_ = Object.assign(Object.assign({}, queryOptions), Database.getEnvironmentQueryOptions());
}
_nextNthRequest() {
return this._nthRequest.increment();
}
setMetadata(metadata, optionsOrCallback, cb) {
const gaxOpts = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
const reqOpts = {
database: extend({
name: this.formattedName_,
}, metadata),
updateMask: {
paths: Object.keys(metadata).map(snakeCase),
},
};
return this.request({
client: 'DatabaseAdminClient',
method: 'updateDatabase',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
}, callback);
}
static getEnvironmentQueryOptions() {
const options = {};
if (process.env.SPANNER_OPTIMIZER_VERSION) {
options.optimizerVersion = process.env.SPANNER_OPTIMIZER_VERSION;
}
if (process.env.SPANNER_OPTIMIZER_STATISTICS_PACKAGE) {
options.optimizerStatisticsPackage =
process.env.SPANNER_OPTIMIZER_STATISTICS_PACKAGE;
}
return options;
}
batchCreateSessions(options, callback) {
if (typeof options === 'number') {
options = { count: options };
}
const count = options.count;
const labels = options.labels || {};
const databaseRole = options.databaseRole || this.databaseRole || null;
const reqOpts = {
database: this.formattedName_,
sessionTemplate: { labels: labels, creatorRole: databaseRole },
sessionCount: count,
};
const headers = this.commonHeaders_;
if (this._getSpanner().routeToLeaderEnabled) {
(0, common_1.addLeaderAwareRoutingHeader)(headers);
}
const allHeaders = this._metadataWithRequestId(this._nextNthRequest(), 1, headers);
(0, instrument_1.startTrace)('Database.batchCreateSessions', this._traceConfig, span => {
this.request({
client: 'SpannerClient',
method: 'batchCreateSessions',
reqOpts,
gaxOpts: options.gaxOptions,
headers: allHeaders,
}, (err, resp) => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
span.end();
callback(err, null, resp);
return;
}
const sessions = (resp.session || []).map(metadata => {
const session = this.session(metadata.name);
session._observabilityOptions = this._traceConfig.opts;
session.metadata = metadata;
return session;
});
span.end();
callback(null, sessions, resp);
});
});
}
_metadataWithRequestId(nthRequest, attempt, priorMetadata) {
if (!priorMetadata) {
priorMetadata = {};
}
const withReqId = {
...priorMetadata,
};
withReqId[request_id_header_1.X_GOOG_SPANNER_REQUEST_ID_HEADER] = (0, request_id_header_1.craftRequestId)(this._clientId || 1, 1, // TODO: Properly infer the channelId
nthRequest, attempt);
return withReqId;
}
/**
* Get a reference to a {@link BatchTransaction} object.
*
* @see {@link BatchTransaction#identifier} to generate an identifier.
*
* @param {TransactionIdentifier} identifier The transaction identifier.
* @param {object} [options] [Transaction options](https://cloud.google.com/spanner/docs/timestamp-bounds).
* @returns {BatchTransaction} A batch transaction object.
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner();
*
* const instance = spanner.instance('my-instance');
* const database = instance.database('my-database');
*
* const transaction = database.batchTransaction({
* session: 'my-session',
* transaction: 'my-transaction',
* readTimestamp: 1518464696657
* });
* ```
*/
batchTransaction(identifier, options) {
const session = typeof identifier.session === 'string'
? this.session(identifier.session)
: identifier.session;
const id = identifier.transaction;
const transaction = new batch_transaction_1.BatchTransaction(session, options);
transaction.id = id;
transaction._observabilityOptions = this._traceConfig.opts;
transaction.readTimestamp = identifier.timestamp;
return transaction;
}
close(callback) {
const key = this.id.split('/').pop();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.parent.databases_.delete(key);
this.pool_.close(callback);
}
createBatchTransaction(optionsOrCallback, cb) {
const callback = typeof optionsOrCallback === 'function'
? optionsOrCallback
: cb;
const options = typeof optionsOrCallback === 'object'
? optionsOrCallback
: {};
return (0, instrument_1.startTrace)('Database.createBatchTransaction', this._traceConfig, span => {
this.sessionFactory_.getSession((err, session) => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
span.end();
callback(err, null, undefined);
return;
}
const transaction = this.batchTransaction({ session: session }, options);
this._releaseOnEnd(session, transaction, span);
transaction.begin((err, resp) => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
if ((0, session_pool_1.isSessionNotFoundError)(err)) {
span.addEvent('No session available', {
'session.id': session?.id,
});
}
span.end();
callback(err, null, resp);
return;
}
span.addEvent('Using Session', { 'session.id': session?.id });
span.end();
callback(null, transaction, resp);
});
});
});
}
createSession(optionsOrCallback, cb) {
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
const options = typeof optionsOrCallback === 'object' && optionsOrCallback
? extend({}, optionsOrCallback)
: {};
const reqOpts = {
database: this.formattedName_,
};
reqOpts.session = {};
if (options.multiplexed) {
reqOpts.session.multiplexed = options.multiplexed;
}
reqOpts.session.labels = options.labels || this.labels || null;
reqOpts.session.creatorRole =
options.databaseRole || this.databaseRole || null;
const headers = this._metadataWithRequestId(this._nextNthRequest(), 1, this.commonHeaders_);
if (this._getSpanner().routeToLeaderEnabled) {
(0, common_1.addLeaderAwareRoutingHeader)(headers);
}
(0, instrument_1.startTrace)('Database.createSession', this._traceConfig, span => {
this.request({
client: 'SpannerClient',
method: 'createSession',
reqOpts,
gaxOpts: options.gaxOptions,
headers: headers,
}, (err, resp) => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
span.end();
callback(err, null, resp);
return;
}
const session = this.session(resp.name);
session.metadata = resp;
session._observabilityOptions = this._traceConfig.opts;
span.end();
callback(null, session, resp);
});
});
}
createTable(schema, gaxOptionsOrCallback, cb) {
const gaxOptions = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
const callback = typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb;
this.updateSchema(schema, gaxOptions, (err, operation, resp) => {
if (err) {
callback(err, null, null, resp);
return;
}
const tableName = schema.match(/CREATE TABLE `*([^\s`(]+)/)[1];
const table = this.table(tableName);
table._observabilityOptions = this._traceConfig.opts;
callback(null, table, operation, resp);
});
}
/**
* Decorates transaction so that when end() is called it will return the session
* back into the pool.
*
* @private
*
* @param {Session} session The session to release.
* @param {Transaction} transaction The transaction to observe.
* @returns {Transaction}
*/
_releaseOnEnd(session, transaction, span) {
transaction.once('end', () => {
try {
this.sessionFactory_.release(session);
}
catch (e) {
(0, instrument_1.setSpanErrorAndException)(span, e);
this.emit('error', e);
}
finally {
span.end();
}
});
}
delete(optionsOrCallback, cb) {
const gaxOpts = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
const reqOpts = {
database: this.formattedName_,
};
this.close(() => {
this.request({
client: 'DatabaseAdminClient',
method: 'dropDatabase',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
}, callback);
});
}
exists(gaxOptionsOrCallback, cb) {
const gaxOptions = typeof gaxOptionsOrCallback === 'object' ? gaxOptionsOrCallback : {};
const callback = typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb;
const NOT_FOUND = 5;
this.getMetadata(gaxOptions, err => {
if (err && err.code !== NOT_FOUND) {
callback(err);
return;
}
const exists = !err || err.code !== NOT_FOUND;
callback(null, exists);
});
}
get(optionsOrCallback, cb) {
const options = typeof optionsOrCallback === 'object'
? optionsOrCallback
: {};
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
this.getMetadata(options.gaxOptions, (err, metadata) => {
if (err) {
if (options.autoCreate && err.code === 5) {
this.create(options, (err, database, operation) => {
if (err) {
callback(err);
return;
}
operation
.on('error', callback)
.on('complete', (metadata) => {
this.metadata = metadata;
callback(null, this, metadata);
});
});
return;
}
callback(err);
return;
}
callback(null, this, metadata);
});
}
getMetadata(gaxOptionsOrCallback, cb) {
const callback = typeof gaxOptionsOrCallback === 'function'
? gaxOptionsOrCallback
: cb;
const gaxOpts = typeof gaxOptionsOrCallback === 'object'
? gaxOptionsOrCallback
: {};
const reqOpts = {
name: this.formattedName_,
};
return this.request({
client: 'DatabaseAdminClient',
method: 'getDatabase',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
}, (err, resp) => {
if (resp) {
this.metadata = resp;
}
callback(err, resp);
});
}
async getRestoreInfo(optionsOrCallback) {
const gaxOptions = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
const [metadata] = await this.getMetadata(gaxOptions);
return metadata.restoreInfo ? metadata.restoreInfo : undefined;
}
async getState(optionsOrCallback) {
const gaxOptions = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
const [metadata] = await this.getMetadata(gaxOptions);
return metadata.state || undefined;
}
async getDatabaseDialect(optionsOrCallback, callback) {
const gaxOptions = typeof optionsOrCallback === 'object'
? optionsOrCallback
: {};
const cb = typeof optionsOrCallback === 'function'
? optionsOrCallback
: callback;
try {
if (this.databaseDialect === 'DATABASE_DIALECT_UNSPECIFIED' ||
this.databaseDialect === null ||
this.databaseDialect === undefined) {
const [metadata] = await this.getMetadata(gaxOptions);
this.databaseDialect = metadata.databaseDialect;
}
if (cb) {
cb(null, this.databaseDialect);
return;
}
return this.databaseDialect || undefined;
}
catch (err) {
cb(err);
return;
}
}
getSchema(optionsOrCallback, cb) {
const gaxOpts = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
const reqOpts = {
database: this.formattedName_,
};
this.request({
client: 'DatabaseAdminClient',
method: 'getDatabaseDdl',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(err, statements, ...args) => {
callback(err, statements ? statements.statements : null, ...args);
});
}
getIamPolicy(optionsOrCallback, cb) {
const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
const reqOpts = {
resource: this.formattedName_,
options: {
requestedPolicyVersion: options.requestedPolicyVersion || null,
},
};
this.request({
client: 'DatabaseAdminClient',
method: 'getIamPolicy',
reqOpts,
gaxOpts: options.gaxOptions,
headers: this.commonHeaders_,
}, (err, resp) => {
callback(err, resp);
});
}
getSessions(optionsOrCallback, cb) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
const options = typeof optionsOrCallback === 'object'
? optionsOrCallback
: {};
const gaxOpts = extend(true, {}, options.gaxOptions);
let reqOpts = extend({}, options, {
database: this.formattedName_,
});
delete reqOpts.gaxOptions;
// Copy over pageSize and pageToken values from gaxOptions.
// However values set on options take precedence.
if (gaxOpts) {
reqOpts = extend({}, {
pageSize: gaxOpts.pageSize,
pageToken: gaxOpts.pageToken,
}, reqOpts);
delete gaxOpts.pageSize;
delete gaxOpts.pageToken;
}
const headers = this._metadataWithRequestId(this._nextNthRequest(), 1, this.commonHeaders_);
return (0, instrument_1.startTrace)('Database.getSessions', this._traceConfig, span => {
this.request({
client: 'SpannerClient',
method: 'listSessions',
reqOpts,
gaxOpts,
headers: headers,
}, (err, sessions, nextPageRequest, ...args) => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
}
let sessionInstances = null;
if (sessions) {
sessionInstances = sessions.map(metadata => {
const session = self.session(metadata.name);
session.metadata = metadata;
session._observabilityOptions = this._traceConfig.opts;
return session;
});
}
span.end();
const nextQuery = nextPageRequest
? extend({}, options, nextPageRequest)
: null;
callback(err, sessionInstances, nextQuery, ...args);
});
});
}
/**
* Get a list of sessions as a readable object stream.
*
* Wrapper around {@link v1.SpannerClient#listSessions}
*
* @see {@link v1.SpannerClient#listSessions}
* @see [ListSessions API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.ListSessions)
*
* @method Spanner#getSessionsStream
* @param {GetSessionsOptions} [options] Options object for listing sessions.
* @returns {ReadableStream} A readable stream that emits {@link Session}
* instances.
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner();
*
* const instance = spanner.instance('my-instance');
* const database = instance.database('my-database');
*
* database.getSessionsStream()
* .on('error', console.error)
* .on('data', function(database) {
* // `sessions` is a `Session` object.
* })
* .on('end', function() {
* // All sessions retrieved.
* });
*
* //-
* // If you anticipate many results, you can end a stream early to prevent
* // unnecessary processing and API requests.
* //-
* database.getSessionsStream()
* .on('data', function(session) {
* this.end();
* });
* ```
*/
getSessionsStream(options = {}) {
const gaxOpts = extend(true, {}, options.gaxOptions);
let reqOpts = extend({}, options, {
database: this.formattedName_,
});
delete reqOpts.gaxOptions;
// Copy over pageSize and pageToken values from gaxOptions.
// However values set on options take precedence.
if (gaxOpts) {
reqOpts = extend({}, {
pageSize: gaxOpts.pageSize,
pageToken: gaxOpts.pageToken,
}, reqOpts);
delete gaxOpts.pageSize;
delete gaxOpts.pageToken;
}
return this.requestStream({
client: 'SpannerClient',
method: 'listSessionsStream',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
});
}
getSnapshot(optionsOrCallback, cb) {
const callback = typeof optionsOrCallback === 'function'
? optionsOrCallback
: cb;
const options = typeof optionsOrCallback === 'object'
? optionsOrCallback
: {};
if (('maxStaleness' in options &&
options.maxStaleness !== null &&
options.maxStaleness !== undefined) ||
('minReadTimestamp' in options &&
options.minReadTimestamp !== null &&
options.minReadTimestamp !== undefined)) {
const error = Object.assign(new Error('maxStaleness / minReadTimestamp is not supported for multi-use read-only transactions.'), {
code: 3, // invalid argument
});
callback(error);
return;
}
return (0, instrument_1.startTrace)('Database.getSnapshot', this._traceConfig, span => {
this.sessionFactory_.getSession((err, session) => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
span.end();
callback(err);
return;
}
const snapshot = session.snapshot(options, this.queryOptions_);
snapshot.begin(err => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
if ((0, session_pool_1.isSessionNotFoundError)(err) &&
!this.sessionFactory_.isMultiplexedEnabled()) {
span.addEvent('No session available', {
'session.id': session?.id,
});
session.lastError = err;
this.sessionFactory_.release(session);
span.end();
this.getSnapshot(options, callback);
}
else {
span.addEvent('Using Session', { 'session.id': session?.id });
this.sessionFactory_.release(session);
span.end();
callback(err);
}
return;
}
this._releaseOnEnd(session, snapshot, span);
span.end();
callback(err, snapshot);
});
});
});
}
getTransaction(optionsOrCallback, callback) {
const cb = typeof optionsOrCallback === 'function'
? optionsOrCallback
: callback;
const options = typeof optionsOrCallback === 'object' && optionsOrCallback
? optionsOrCallback
: {};
return (0, instrument_1.startTrace)('Database.getTransaction', {
...this._traceConfig,
transactionTag: options.requestOptions?.transactionTag,
}, span => {
this.sessionFactory_.getSessionForReadWrite((err, session, transaction) => {
if (!err) {
if (options.requestOptions) {
transaction.requestOptions = Object.assign(transaction.requestOptions || {}, options.requestOptions);
}
transaction?.setReadWriteTransactionOptions(options);
span.addEvent('Using Session', { 'session.id': session?.id });
transaction._observabilityOptions = this._observabilityOptions;
this._releaseOnEnd(session, transaction, span);
}
else {
(0, instrument_1.setSpanError)(span, err);
}
span.end();
cb(err, transaction);
});
});
}
async getOperations(optionsOrCallback) {
const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
// Create a query that lists database operations only on this database from
// the instance. Operation name will be prefixed with the database path for
// all operations on this database
let dbSpecificFilter = `name:${this.formattedName_}`;
if (options && options.filter) {
dbSpecificFilter = `(${dbSpecificFilter}) AND (${options.filter})`;
}
const dbSpecificQuery = {
...options,
filter: dbSpecificFilter,
};
return this.instance.getDatabaseOperations(dbSpecificQuery);
}
getDatabaseRoles(optionsOrCallback, cb) {
const gaxOpts = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
let reqOpts = {
parent: this.formattedName_,
};
// Copy over pageSize and pageToken values from gaxOptions.
// However, values set on options take precedence.
if (gaxOpts) {
reqOpts = extend({}, {
pageSize: gaxOpts.pageSize,
pageToken: gaxOpts.pageToken,
}, reqOpts);
delete gaxOpts.pageSize;
delete gaxOpts.pageToken;
}
this.request({
client: 'DatabaseAdminClient',
method: 'listDatabaseRoles',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
}, (err, roles, nextPageRequest, ...args) => {
const nextQuery = nextPageRequest
? extend({}, gaxOpts, nextPageRequest)
: null;
callback(err, roles, nextQuery, ...args);
});
}
makePooledRequest_(config, callback) {
const sessionFactory_ = this.sessionFactory_;
sessionFactory_.getSessionForReadWrite((err, session) => {
if (err) {
callback(err, null);
return;
}
const span = (0, instrument_1.getActiveOrNoopSpan)();
span.addEvent('Using Session', { 'session.id': session?.id });
config.reqOpts.session = session.formattedName_;
this.request(config, (err, ...args) => {
sessionFactory_.release(session);
callback(err, ...args);
});
});
}
/**
* Make an API request as a stream, first assuring an active session is used.
*
* @private
*
* @param {object} config Request config
* @returns {Stream}
*/
makePooledStreamingRequest_(config) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const sessionFactory_ = this.sessionFactory_;
let requestStream;
let session;
const waitForSessionStream = streamEvents(through.obj());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
waitForSessionStream.abort = () => {
releaseSession();
if (requestStream) {
requestStream.cancel();
}
};
function destroyStream(err) {
waitForSessionStream.destroy(err);
}
function releaseSession() {
if (session) {
sessionFactory_.release(session);
session = null;
}
}
waitForSessionStream.on('reading', () => {
sessionFactory_.getSession((err, session_) => {
const span = (0, instrument_1.getActiveOrNoopSpan)();
if (err) {
(0, instrument_1.setSpanError)(span, err);
destroyStream(err);
return;
}
span.addEvent('Using Session', { 'session.id': session_?.id });
session = session_;
config.reqOpts.session = session.formattedName_;
requestStream = self.requestStream(config);
requestStream
.on('error', releaseSession)
.on('error', destroyStream)
.on('end', releaseSession)
.pipe(waitForSessionStream);
});
});
return waitForSessionStream;
}
restore(backupName, optionsOrCallback, cb) {
const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
const callback = typeof optionsOrCallback === 'function'
? optionsOrCallback
: cb;
const gaxOpts = 'gaxOptions' in options
? options.gaxOptions
: options;
const reqOpts = {
parent: this.instance.formattedName_,
databaseId: this.id,
backup: backup_1.Backup.formatName_(this.instance.formattedName_, backupName),
};
if ('encryptionConfig' in options &&
options.encryptionConfig) {
reqOpts.encryptionConfig = options.encryptionConfig;
}
return this.request({
client: 'DatabaseAdminClient',
method: 'restoreDatabase',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
}, (err, operation, resp) => {
if (err) {
callback(err, null, null, resp);
return;
}
callback(null, this, operation, resp);
});
}
run(query, optionsOrCallback, cb) {
let stats;
let metadata;
const rows = [];
const callback = typeof optionsOrCallback === 'function'
? optionsOrCallback
: cb;
const options = typeof optionsOrCallback === 'object'
? optionsOrCallback
: {};
return (0, instrument_1.startTrace)('Database.run', {
...query,
...this._traceConfig,
}, span => {
this.runStream(query, options)
.on('error', err => {
(0, instrument_1.setSpanError)(span, err);
span.end();
callback(err, rows, stats, metadata);
})
.on('response', response => {
if (response.metadata) {
metadata = response.metadata;
}
})
.on('stats', _stats => (stats = _stats))
.on('data', row => {
rows.push(row);
})
.on('end', () => {
span.end();
callback(null, rows, stats, metadata);
});
});
}
runPartitionedUpdate(query, callback) {
return (0, instrument_1.startTrace)('Database.runPartitionedUpdate', {
...query,
...this._traceConfig,
requestTag: query?.requestOptions
?.requestTag,
}, span => {
this.sessionFactory_.getSessionForPartitionedOps((err, session) => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
span.end();
callback(err, 0);
return;
}
void this._runPartitionedUpdate(session, query, (err, count) => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
}
span.end();
callback(err, count);
});
});
});
}
_runPartitionedUpdate(session, query, callback) {
const transaction = session.partitionedDml();
if (typeof query !== 'string' && query.excludeTxnFromChangeStreams) {
transaction.excludeTxnFromChangeStreams();
}
transaction.begin(err => {
if (err) {
this.sessionFactory_.release(session);
callback(err, 0);
return;
}
transaction.runUpdate(query, async (err, updateCount) => {
if (err) {
if (err.code !== google_gax_1.grpc.status.ABORTED) {
this.sessionFactory_.release(session);
callback(err, 0);
return;
}
void this._runPartitionedUpdate(session, query, callback);
}
else {
this.sessionFactory_.release(session);
callback(null, updateCount);
return;
}
});
});
}
/**
* 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 [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)
*
* @fires PartialResultStream#response
*
* @param {string|ExecuteSqlRequest} query A SQL query or
* {@link ExecuteSqlRequest} object.
* @param {TimestampBounds} [options] Snapshot timestamp bounds.
* @returns {PartialResultStream} A readable stream that emits rows.
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner();
*
* const instance = spanner.instance('my-instance');
* const database = instance.database('my-database');
*
* const query = 'SELECT * FROM Singers';
*
* database.runStream(query)
* .on('error', function(err) {})
* .on('data', function(row) {
* // row = [
* // {
* // name: 'SingerId',
* // value: '1'
* // },
* // {
* // name: 'Name',
* // value: 'Eddie Wilson'
* // }
* // ]
* // ]
* })
* .on('end', function() {
* // All results retrieved.
* });
*
* //-
* // Rows are returned as an array of objects. Each object has a `name` and
* // `value` property. To get a serialized object, call `toJSON()`.
* //-
* database.runStream(query)
* .on('error', function(err) {})
* .on('data', function(row) {
* // row.toJSON() = {
* // SingerId: '1',
* // Name: 'Eddie Wilson'
* // }
* })
* .on('end', function() {
* // All results retrieved.
* });
*
* //-
* // Alternatively, set `query.json` to `true`, and this step will be performed
* // automatically.
* //-
* query.json = true;
*
* database.runStream(query)
* .on('error', function(err) {})
* .on('data', function(row) {
* // row = {
* // SingerId: '1',
* // Name: 'Eddie Wilson'
* // }
* })
* .on('end', function() {
* // All results retrieved.
* });
*
* //-
* // 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'
* }
* };
*
* database.runStream(query)
* .on('error', function(err) {})
* .on('data', function(row) {})
* .on('end', function() {});
*
* //-
* // If you need to enforce a specific param type, a types map can be provided.
* // This is typically useful if your param value can be null.
* //-
* const query = {
* sql: 'SELECT * FROM Singers WHERE name = @name',
* params: {
* name: 'Eddie Wilson'
* },
* types: {
* name: 'string'
* }
* };
*
* database.runStream(query)
* .on('error', function(err) {})
* .on('data', function(row) {})
* .on('end', function() {});
*
* //-
* // If you anticipate many results, you can end a stream early to prevent
* // unnecessary processing and API requests.
* //-
* database.runStream(query)
* .on('data', function(row) {
* this.end();
* });
* ```
*/
runStream(query, options) {
const proxyStream = through.obj();
return (0, instrument_1.startTrace)('Database.runStream', {
...query,
...this._traceConfig,
requestTag: query?.requestOptions?.requestTag,
}, span => {
this.sessionFactory_.getSession((err, session) => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
proxyStream.destroy(err);
span.end();
return;
}
span.addEvent('Using Session', { 'session.id': session?.id });
const snapshot = session.snapshot(options, this.queryOptions_);
this._releaseOnEnd(session, snapshot, span);
let dataReceived = false;
let dataStream = snapshot.runStream(query);
const endListener = () => {
snapshot.end();
};
dataStream
.once('data', () => (dataReceived = true))
.once('error', err => {
(0, instrument_1.setSpanError)(span, err);
if (!dataReceived &&
(0, session_pool_1.isSessionNotFoundError)(err) &&
!this.sessionFactory_.isMultiplexedEnabled()) {
// If it is a 'Session not found' error and we have not yet received
// any data, we can safely retry the query on a new session.
// Register the error on the session so the pool can discard it.
if (session) {
session.lastError = err;
}
span.addEvent('No session available', {
'session.id': session?.id,
});
// Remove the current data stream from the end user stream.
dataStream.unpipe(proxyStream);
dataStream.removeListener('end', endListener);
dataStream.end();
snapshot.end();
span.end();
// Create a new data stream and add it to the end user stream.
dataStream = this.runStream(query, options);
dataStream.pipe(proxyStream);
}
else {
proxyStream.destroy(err);
snapshot.end();
}
})
.on('stats', stats => proxyStream.emit('stats', stats))
.on('response', response => proxyStream.emit('response', response))
.once('end', endListener)
.pipe(proxyStream);
});
(0, stream_1.finished)(proxyStream, err => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
}
span.end();
});
return proxyStream;
});
}
runTransaction(optionsOrRunFn, fn) {
const runFn = typeof optionsOrRunFn === 'function'
? optionsOrRunFn
: fn;
const options = typeof optionsOrRunFn === 'object' && optionsOrRunFn
? optionsOrRunFn
: {};
(0, instrument_1.startTrace)('Database.runTransaction', {
...this._traceConfig,
transactionTag: options.requestOptions?.transactionTag,
}, span => {
this.sessionFactory_.getSessionForReadWrite((err, session, transaction) => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
}
if (err && (0, session_pool_1.isSessionNotFoundError)(err)) {
span.addEvent('No session available', {
'session.id': session?.id,
});
span.end();
this.runTransaction(options, runFn);
return;
}
if (err) {
span.end();
runFn(err);
return;
}
transaction._observabilityOptions = this._observabilityOptions;
transaction.requestOptions = Object.assign(transaction.requestOptions || {}, options.requestOptions);
transaction.setReadWriteTransactionOptions(options);
const release = () => {
this.sessionFactory_.release(session);
span.end();
};
const runner = new transaction_runner_1.TransactionRunner(session, transaction, runFn, options);
runner.run().then(release, err => {
(0, instrument_1.setSpanError)(span, err);
if ((0, session_pool_1.isSessionNotFoundError)(err)) {
span.addEvent('No session available', {
'session.id': session?.id,
});
release();
this.runTransaction(options, runFn);
}
else {
setImmediate(runFn, err);
release();
}
});
});
});
}
/**
* @callback AsyncRunTransactionCallback
* @param {Transaction} transaction The transaction object. The transaction has
* already been created, and is ready to be queried and committed against.
*/
/**
* A tr