UNPKG

@google-cloud/spanner

Version:
1,270 lines 66.8 kB
"use strict"; /*! * Copyright 2016 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.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