@google-cloud/spanner
Version:
Cloud Spanner Client Library for Node.js
1,711 lines • 67.3 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, databaseRole) {
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);
}
},
});
this.formattedName_ = formattedName_;
this.instance = instance;
const poolOpts = typeof poolOptions === 'object' ? poolOptions : null;
this.databaseRole = databaseRole || poolOpts?.databaseRole || null;
this.labels = this._getSpanner().sessionLabels || poolOpts?.labels || null;
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_ = {
...instance.commonHeaders_,
[common_1.CLOUD_RESOURCE_HEADER]: this.formattedName_,
};
// 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: Object.assign({
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 || this.labels || null;
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
? Object.assign({}, 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);
const reqOpts = Object.assign({}, options, {
database: this.formattedName_,
});
delete reqOpts.gaxOptions;
// Copy over pageSize and pageToken values from gaxOptions.
// However values set on options take precedence.
if (gaxOpts) {
const gax = gaxOpts;
if (gax.pageSize !== undefined) {
reqOpts.pageSize ??= gax.pageSize;
delete gax.pageSize;
}
if (gax.pageToken !== undefined) {
reqOpts.pageToken ??= gax.pageToken;
delete gax.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
? Object.assign({}, 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);
const reqOpts = Object.assign({}, options, {
database: this.formattedName_,
});
delete reqOpts.gaxOptions;
// Copy over pageSize and pageToken values from gaxOptions.
// However values set on options take precedence.
if (gaxOpts) {
const gax = gaxOpts;
if (gax.pageSize !== undefined) {
reqOpts.pageSize ??= gax.pageSize;
delete gax.pageSize;
}
if (gax.pageToken !== undefined) {
reqOpts.pageToken ??= gax.pageToken;
delete gax.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;
const reqOpts = {
parent: this.formattedName_,
};
// Copy over pageSize and pageToken values from gaxOptions.
// However, values set on options take precedence.
if (gaxOpts) {
const gax = gaxOpts;
if (gax.pageSize !== undefined) {
reqOpts.pageSize ??= gax.pageSize;
delete gax.pageSize;
}
if (gax.pageToken !== undefined) {
reqOpts.pageToken ??= gax.pageToken;
delete gax.pageToken;
}
}
this.request({
client: 'DatabaseAdminClient',
method: 'listDatabaseRoles',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
}, (err, roles, nextPageRequest, ...args) => {
const nextQuery = nextPageRequest
? Object.assign({}, 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 transaction in Cloud Spanner is a set of reads and writes that execute
* atomically at a single logical point in time across columns, rows, and tables
* in a database.
*
* Note that Cloud Spanner does not support nested transactions. If a new
* transaction is started inside of the run function, it will be an independent
* transaction.
*
* The async function you provide will become the "run function". It
* will be executed with a {@link Transaction}
* object. The Transaction object will let you run queries and queue mutations
* until you are ready to {@link Transaction#commit}.
*
* In the event that an aborted error occurs, we will re-run the `runFn` in its
* entirety. If you prefer to handle aborted errors for yourself please refer to
* {@link Database#getTransaction}.
*
* **NOTE:** In the event that you encounter an error while reading/writing,
* if you decide to forgo calling {@link Transaction#commit} or
* {@link Transaction#rollback}, then you need to call
* {@link Transaction#end} to release the underlying {@link Session} object.
* **Failure to do could result in a Session leak.**
*
* For a more complete listing of functionality available to a Transaction, see
* the {@link Transaction} API documentation. For a general overview of
* transactions within Cloud Spanner, see
* [Transactions](https://cloud.google.com/spanner/docs/transactions) from the
* official Cloud Spanner documentation.
*
* @see [Transactions](https://cloud.google.com/spanner/docs/transactions)
*
* @param {RunTransactionOptions} [options] Transaction runner options.
* @param {AsyncRunTransactionCallback} callback A function to execute in the
* context of a transaction.
* @returns {Promise}
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner();
*
* const instance = spanner.instance('my-instance');
* const database = instance.database('my-database');
*
* const data = await database.runTransactionAsync(async (transaction) => {
* const [rows] = await transaction.run('SELECT * FROM MyTable');
* const data = rows.map(row => row.thing);
*
* await transaction.commit();
* return data;
* });
* ```
*/
async runTransactionAsync(optionsOrRunFn, fn) {
const runFn = typeof optionsOrRunFn === 'function'
? optionsOrRunFn
: fn;
const options = typeof optionsOrRunFn === 'object'
? optionsOrRunFn
: {};
let sessionId = '';
const getSession = this.sessionFactory_.getSessionForReadWrite.bind(this.sessionFactory_);
return (0, instrument_1.startTrace)('Database.runTransactionAsync', {
...this._traceConfig,
transactionTag: options?.requestOptions?.transactionTag,
}, async (span) => {
// Loop to retry 'Session not found' errors.
// (and yes, we like while (true) more than for (;;) here)
// eslint-disable-next-line no-constant-condition
while (true) {
try {
const [session, transaction] = await (0, promisify_1.promisify)(getSession)();
transaction.requestOptions = Object.assign(transaction.requestOptions || {}, options.requestOptions);
transaction.setReadWriteTransactionOptions(options);
sessionId = session?.id;
span.addEvent('Using Session', { 'session.id': sessionId });
const runner = new transaction_runner_1.AsyncTransactionRunner(session, transaction, runFn, options);
try {
return await runner.run();
}
catch (e) {
(0, instrument_1.setSpanErrorAndException)(span, e);
throw e;
}
finally {
span.end();
this.sessionFactory_.release(session);
}
}
catch (e) {
if ((0, session_pool_1.isSessionNotFoundError)(e)) {
span.addEvent('No session available', {
'session.id': sessionId,
});
}
else {
span.end();
throw e;
}
}
}
});
}
/**
* Write a batch of mutations to Spanner.
*
* All mutations in a group are committed atomically. However, mutations across
* groups can be committed non-atomically in an unspecified order and thus, they
* must be independent of each other. Partial failure is possible, i.e., some groups
* may have been committed successfully, while some may have failed. The results of
* individual batches are streamed into the response as the batches are applied.
*
* batchWriteAtLeastOnce requests are not replay protected, meaning that each mutation group may
* be applied more than once. Replays of non-idempotent mutations may have undesirable
* effects. For example, replays of an insert mutation may produce an already exists
* error or if you use generated or commit timestamp-based keys, it may result in additional
* rows being added to the mutation's table. We recommend structuring your mutation groups to
* be idempotent to avoid this issue.
*
* @method Spanner#batchWriteAtLeastOnce
*
* @param {MutationGroup[]} [mutationGroups] The group of mutations to be applied.
* @param {BatchWriteOptions} [options] Options object for batch write request.
*
* @returns {ReadableStream} An object stream which emits
* {@link protos.google.spanner.v1.BatchWriteResponse|BatchWriteResponse}
* on 'data' event.
*
* @example
* ```
* const {Spanner} = require('@google-cloud/spanner');
* const spanner = new Spanner();
*
* const instance = spanner.instance('my-instance');
* const database = instance.database('my-database');
* const mutationGroup = new MutationGroup();
* mutationGroup.insert('Singers', {
* SingerId: '1',
* FirstName: 'Marc',
* LastName: 'Richards',
* });
*
* database.batchWriteAtLeastOnce([mutationGroup])
* .on('error', console.error)
* .on('data', response => {
* console.log('response: ', response);
* })
* .on('end', () => {
* console.log('Request completed successfully');
* });
*
* //-
* // If you anticipate many results, you can end a stream early to prevent
* // unnecessary processing and API requests.
* //-
* database.batchWriteAtLeastOnce()
* .on('data', response => {
* this.end();
* });
* ```
*/
batchWriteAtLeastOnce(mutationGroups, options) {
const proxyStream = through.obj();
return (0, instrument_1.startTrace)('Database.batchWriteAtLeastOnce', {
...this._traceConfig,
transactionTag: options?.requestOptions?.transactionTag,
}, span => {
this.sessionFactory_.getSessionForReadWrite((err, session) => {
if (err) {
proxyStream.destroy(err);
(0, instrument_1.setSpanError)(span, err);
span.end();
return;
}
span.addEvent('Using Session', { 'session.id': session?.id });
const gaxOpts = extend(true, {}, options?.gaxOptions);
const reqOpts = Object.assign({}, {
session: session.formattedName_,
mutationGroups: mutationGroups.map(mg => mg.proto()),
requestOptions: options?.requestOptions,
excludeTxnFromChangeStream: options?.excludeTxnFromChangeStreams,
});
let dataReceived = false;
let dataStream = this.requestStream({
client: 'SpannerClient',
method: 'batchWrite',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
});
dataStream
.once('data', () => (dataReceived = true))
.once('error', err => {
(0, instrument_1.setSpanError)(span, err);
if (!dataReceived &&
(0, session_pool_1.isSessionNotFoundError)(err)) {
// If there's a 'Session not found' error and we have not yet received
// any data, we can safely retry the writes 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.end();
span.end();
// Create a new stream and add it to the end user stream.
dataStream = this.batchWriteAtLeastOnce(mutationGroups, options);
dataStream.pipe(proxyStream);
}
else {
span.end();
proxyStream.destroy(err);
}
})
.once('end', () => {
span.end();
this.sessionFactory_.release(session);
})
.pipe(proxyStream);
});
return proxyStream;
});
}
writeAtLeastOnce(mutations, optionsOrCallback, callback) {
const cb = typeof optionsOrCallback === 'function'
? optionsOrCallback
: callback;
const options = typeof optionsOrCallback === 'object' && optionsOrCallback
? optionsOrCallback
: {};
return (0, instrument_1.startTrace)('Database.writeAtLeastOnce', this._traceConfig, span => {
this.sessionFactory_.getSession((err, session, transaction) => {
if (err &&
(0, session_pool_1.isSessionNotFoundError)(err) &&
!this.sessionFactory_.isMultiplexedEnabled()) {
span.addEvent('No session available', {
'session.id': session?.id,
});
span.end();
this.writeAtLeastOnce(mutations, options, cb);
return;
}
if (err) {
(0, instrument_1.setSpanError)(span, err);
span.end();
cb(err);
return;
}
span.addEvent('Using Session', { 'session.id': session?.id });
this._releaseOnEnd(session, transaction, span);
try {
transaction.setReadWriteTransactionOptions(options);
transaction?.setQueuedMutations(mutations.proto());
return transaction?.commit(options, (err, resp) => {
if (err) {
(0, instrument_1.setSpanError)(span, err);
}
span.end();
cb(err, resp);
});
}
catch (e) {
(0, instrument_1.setSpanErrorAndException)(span, e);
span.end();
throw e;
}
});
});
}
/**
* Create a Session object.
*
* It is unlikely you will need to interact with sessions directly. By default,
* sessions are created and utilized for maximum performance automatically.
*
* @param {string} [name] The name of the session. If not provided, it is
* assumed you are going to create it.
* @returns {Session} A Session object.
*
* @example
* ```
* var session = database.session('session-name');
* ```
*/
session(name) {
return new session_1.Session(this, name);
}
setIamPolicy(policy, optionsOrCallback, cb) {
const options = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
const callback = typeof optionsOrCallback === 'function'
? optionsOrCallback
: cb;
const gaxOpts = options;
const reqOpts = {
resource: this.formattedName_,
policy: policy.policy,
updateMask: policy.updateMask || null,
};
this.request({
client: 'DatabaseAdminClient',
method: 'setIamPolicy',
reqOpts,
gaxOpts: gaxOpts,
headers: this.commonHeaders_,
}, (err, resp) => {
callback(err, resp);
});
}
table(name) {
if (!name) {
throw new google_gax_1.GoogleError('A name is required to access a Table object.');
}
return new table_1.Table(this, name);
}
updateSchema(statements, optionsOrCallback, cb) {
const gaxOpts = typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : cb;
if (typeof statements === 'string' || Array.isArray(statements)) {
statements = {
statements: (0, helper_1.toArray)(statements),
};
}
const reqOpts = Object.assign({
database: this.formattedName_,
}, statements);
return this.request({
client: 'DatabaseAdminClient',
method: 'updateDatabaseDdl',
reqOpts,
gaxOpts,
headers: this.commonHeaders_,
}, callback);
}
/**
* Format the database name to include the instance name.
*
* @private
*
* @param {string} instanceName The formatted instance name.
* @param {string} name The table name.
* @returns {string}
*
* @example
* ```
* Database.formatName_(
* 'projects/grape-spaceship-123/instances/my-instance',
* 'my-database'
* );
* // 'projects/grape-spaceship-123/instances/my-instance/databases/my-database'
* ```
*/
static formatName_(instanceName, name) {
if (name.indexOf('/') > -1) {
return name;
}
const databaseName = name.split('/').pop();
return instanceName + '/databases/' + databaseName;
}
/**
* Gets the Spanner object
*
* @private
*
* @returns {Spanner}
*/
_getSpanner() {
return this.instance.parent;
}
}
exports.Database = Database;
/*! Developer Documentation
*
* All async methods (except for streams) will return a Promise in the event
* that a callback is omitted.
*/
(0, promisify_1.promisifyAll)(Database, {
exclude: [
'batchTransaction',
'batchWriteAtLeastOnce',
'getRestoreInfo',
'getState',
'getDatabaseDialect',
'getOperations',
'runTransaction',
'runTransactionAsync',
'table',
'session',
],
});
/*! Developer Documentation
*
* All async methods (except for streams) will return a Promise in the event
* that a callback is omitted.
*/
(0, promisify_1.callbackifyAll)(Database, {
exclude: [
'create',
'batchCreateSessions',
'batchTransaction',
'batchWriteAtLeastOnce',
'writeAtLeastOnce',
'close',
'createBatchTransaction',
'createSession',
'createTable',
'delete',
'exists',
'get',
'getDatabaseDialect',
'getMetadata',
'getSchema',
'getSessions',
'getSnapshot',
'getTransaction',
'getIamPolicy',
'restore',
'run',
'runPartitionedUpdate',
'runTransaction',
'runTransactionAsync',
'session',
'setMetadata',
'table',
'updateSchema',
],
});
//# sourceMappingURL=database.js.map