Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.

Commit f4fbe71

Browse files
feat: add support for UpdateDatabase (#1802)
* feat: add support for drop database protection Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 72efb81 commit f4fbe71

7 files changed

Lines changed: 322 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/nodejs-spanner/tre
110110
| Gets the schema definition of an existing database | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/database-get-ddl.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/database-get-ddl.js,samples/README.md) |
111111
| Gets the default leader option of an existing database | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/database-get-default-leader.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/database-get-default-leader.js,samples/README.md) |
112112
| Updates the default leader of an existing database | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/database-update-default-leader.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/database-update-default-leader.js,samples/README.md) |
113+
| Updates a Cloud Spanner Database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/database-update.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/database-update.js,samples/README.md) |
113114
| Datatypes | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/datatypes.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/datatypes.js,samples/README.md) |
114115
| Delete using DML returning. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml-returning-delete.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/dml-returning-delete.js,samples/README.md) |
115116
| Insert using DML returning. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml-returning-insert.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/dml-returning-insert.js,samples/README.md) |

samples/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ and automatic, synchronous replication for high availability.
3535
* [Gets the schema definition of an existing database](#gets-the-schema-definition-of-an-existing-database)
3636
* [Gets the default leader option of an existing database](#gets-the-default-leader-option-of-an-existing-database)
3737
* [Updates the default leader of an existing database](#updates-the-default-leader-of-an-existing-database)
38+
* [Updates a Cloud Spanner Database.](#updates-a-cloud-spanner-database.)
3839
* [Datatypes](#datatypes)
3940
* [Delete using DML returning.](#delete-using-dml-returning.)
4041
* [Insert using DML returning.](#insert-using-dml-returning.)
@@ -473,6 +474,23 @@ __Usage:__
473474

474475

475476

477+
### Updates a Cloud Spanner Database.
478+
479+
View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/database-update.js).
480+
481+
[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/database-update.js,samples/README.md)
482+
483+
__Usage:__
484+
485+
486+
`node database-update.js <INSTANCE_ID> <DATABASE_ID> <PROJECT_ID>`
487+
488+
489+
-----
490+
491+
492+
493+
476494
### Datatypes
477495

478496
View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/datatypes.js).

samples/database-update.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// sample-metadata:
16+
// title: Updates a Cloud Spanner Database.
17+
// usage: node database-update.js <INSTANCE_ID> <DATABASE_ID> <PROJECT_ID>
18+
19+
'use strict';
20+
21+
function main(
22+
instanceId = 'my-instance',
23+
databaseId = 'my-database',
24+
projectId = 'my-project-id'
25+
) {
26+
// [START spanner_update_database]
27+
/**
28+
* TODO(developer): Uncomment these variables before running the sample.
29+
*/
30+
// const instanceId = 'my-instance';
31+
// const databaseId = 'my-database';
32+
// const projectId = 'my-project-id';
33+
34+
// Imports the Google Cloud Spanner client library
35+
const {Spanner} = require('@google-cloud/spanner');
36+
37+
// Instantiates a client
38+
const spanner = new Spanner({
39+
projectId: projectId,
40+
});
41+
42+
async function updateDatabase() {
43+
// Gets a reference to a Cloud Spanner instance and database
44+
const instance = spanner.instance(instanceId);
45+
const database = instance.database(databaseId);
46+
47+
try {
48+
console.log(`Updating database ${database.id}.`);
49+
const [operation] = await database.setMetadata({
50+
enableDropProtection: true,
51+
});
52+
console.log(
53+
`Waiting for update operation for ${database.id} to complete...`
54+
);
55+
await operation.promise();
56+
console.log(`Updated database ${database.id}.`);
57+
} catch (err) {
58+
console.log('ERROR:', err);
59+
} finally {
60+
// Close the database when finished.
61+
database.close();
62+
}
63+
}
64+
updateDatabase();
65+
// [END spanner_update_database]
66+
}
67+
68+
process.on('unhandledRejection', err => {
69+
console.error(err.message);
70+
process.exitCode = 1;
71+
});
72+
main(...process.argv.slice(2));

samples/system-test/spanner.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,25 @@ describe('Spanner', () => {
314314
);
315315
});
316316

317+
// update_database
318+
it('should set database metadata', async () => {
319+
const output = execSync(
320+
`node database-update.js ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}`
321+
);
322+
assert.match(
323+
output,
324+
new RegExp(
325+
`Waiting for update operation for ${DATABASE_ID} to complete...`
326+
)
327+
);
328+
assert.match(output, new RegExp(`Updated database ${DATABASE_ID}.`));
329+
// cleanup
330+
const [operation] = await instance
331+
.database(DATABASE_ID)
332+
.setMetadata({enableDropProtection: false});
333+
await operation.promise();
334+
});
335+
317336
describe('encrypted database', () => {
318337
after(async () => {
319338
const instance = spanner.instance(INSTANCE_ID);

src/database.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ import {ServiceError} from 'google-gax';
9595
import IPolicy = google.iam.v1.IPolicy;
9696
import Policy = google.iam.v1.Policy;
9797
import FieldMask = google.protobuf.FieldMask;
98+
import IDatabase = google.spanner.admin.database.v1.IDatabase;
99+
import snakeCase = require('lodash.snakecase');
100+
98101
export type GetDatabaseRolesCallback = RequestCallback<
99102
IDatabaseRole,
100103
databaseAdmin.spanner.admin.database.v1.IListDatabaseRolesResponse
@@ -103,6 +106,8 @@ export type GetDatabaseRolesResponse = PagedResponse<
103106
IDatabaseRole,
104107
databaseAdmin.spanner.admin.database.v1.IListDatabaseRolesResponse
105108
>;
109+
type SetDatabaseMetadataCallback = ResourceCallback<GaxOperation, IOperation>;
110+
type SetDatabaseMetadataResponse = [GaxOperation, IOperation];
106111
type IDatabaseRole = databaseAdmin.spanner.admin.database.v1.IDatabaseRole;
107112

108113
type CreateBatchTransactionCallback = ResourceCallback<
@@ -423,6 +428,108 @@ class Database extends common.GrpcServiceObject {
423428
Database.getEnvironmentQueryOptions()
424429
);
425430
}
431+
/**
432+
* @typedef {array} SetDatabaseMetadataResponse
433+
* @property {object} 0 The {@link Database} metadata.
434+
* @property {object} 1 The full API response.
435+
*/
436+
/**
437+
* @callback SetDatabaseMetadataCallback
438+
* @param {?Error} err Request error, if any.
439+
* @param {object} metadata The {@link Database} metadata.
440+
* @param {object} apiResponse The full API response.
441+
*/
442+
/**
443+
* Update the metadata for this database. Note that this method follows PATCH
444+
* semantics, so previously-configured settings will persist.
445+
*
446+
* Wrapper around {@link v1.DatabaseAdminClient#updateDatabase}.
447+
*
448+
* @see {@link v1.DatabaseAdminClient#updateDatabase}
449+
* @see [UpdateDatabase API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabase)
450+
*
451+
* @param {object} metadata The metadata you wish to set.
452+
* @param {object} [gaxOptions] Request configuration options,
453+
* See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions}
454+
* for more details.
455+
* @param {SetDatabaseMetadataCallback} [callback] Callback function.
456+
* @returns {Promise<SetDatabaseMetadataResponse>}
457+
*
458+
* @example
459+
* ```
460+
* const {Spanner} = require('@google-cloud/spanner');
461+
* const spanner = new Spanner();
462+
*
463+
* const instance = spanner.instance('my-instance');
464+
* const database = instance.database('my-database');
465+
*
466+
* const metadata = {
467+
* enableDropProtection: true
468+
* };
469+
*
470+
* database.setMetadata(metadata, function(err, operation, apiResponse) {
471+
* if (err) {
472+
* // Error handling omitted.
473+
* }
474+
*
475+
* operation
476+
* .on('error', function(err) {})
477+
* .on('complete', function() {
478+
* // Metadata updated successfully.
479+
* });
480+
* });
481+
*
482+
* //-
483+
* // If the callback is omitted, we'll return a Promise.
484+
* //-
485+
* database.setMetadata(metadata).then(function(data) {
486+
* const operation = data[0];
487+
* const apiResponse = data[1];
488+
* });
489+
* ```
490+
*/
491+
setMetadata(
492+
metadata: IDatabase,
493+
gaxOptions?: CallOptions
494+
): Promise<SetDatabaseMetadataResponse>;
495+
setMetadata(metadata: IDatabase, callback: SetDatabaseMetadataCallback): void;
496+
setMetadata(
497+
metadata: IDatabase,
498+
gaxOptions: CallOptions,
499+
callback: SetDatabaseMetadataCallback
500+
): void;
501+
setMetadata(
502+
metadata: IDatabase,
503+
optionsOrCallback?: CallOptions | SetDatabaseMetadataCallback,
504+
cb?: SetDatabaseMetadataCallback
505+
): void | Promise<SetDatabaseMetadataResponse> {
506+
const gaxOpts =
507+
typeof optionsOrCallback === 'object' ? optionsOrCallback : {};
508+
const callback =
509+
typeof optionsOrCallback === 'function' ? optionsOrCallback : cb!;
510+
511+
const reqOpts = {
512+
database: extend(
513+
{
514+
name: this.formattedName_,
515+
},
516+
metadata
517+
),
518+
updateMask: {
519+
paths: Object.keys(metadata).map(snakeCase),
520+
},
521+
};
522+
return this.request(
523+
{
524+
client: 'DatabaseAdminClient',
525+
method: 'updateDatabase',
526+
reqOpts,
527+
gaxOpts,
528+
headers: this.resourceHeader_,
529+
},
530+
callback!
531+
);
532+
}
426533

427534
static getEnvironmentQueryOptions() {
428535
const options =
@@ -3332,6 +3439,7 @@ callbackifyAll(Database, {
33323439
'runTransaction',
33333440
'runTransactionAsync',
33343441
'session',
3442+
'setMetadata',
33353443
'table',
33363444
'updateSchema',
33373445
],

system-test/spanner.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ describe('Spanner', () => {
100100
const RESOURCES_TO_CLEAN: Array<Instance | Backup | Database> = [];
101101
const INSTANCE_CONFIGS_TO_CLEAN: Array<InstanceConfig> = [];
102102
const DATABASE = instance.database(generateName('database'), {incStep: 1});
103+
const DATABASE_DROP_PROTECTION = instance.database(generateName('database'), {
104+
incStep: 1,
105+
});
103106
const TABLE_NAME = 'Singers';
104107
const PG_DATABASE = instance.database(generateName('pg-db'), {incStep: 1});
105108

@@ -119,17 +122,28 @@ describe('Spanner', () => {
119122
`Not creating temp instance, using + ${instance.formattedName_}...`
120123
);
121124
}
122-
const [, googleSqlOperation] = await DATABASE.create({
125+
const [, googleSqlOperation1] = await DATABASE.create({
123126
schema: `
124127
CREATE TABLE ${TABLE_NAME} (
125128
SingerId STRING(1024) NOT NULL,
126129
Name STRING(1024),
127130
) PRIMARY KEY(SingerId)`,
128131
gaxOptions: GAX_OPTIONS,
129132
});
130-
await googleSqlOperation.promise();
133+
await googleSqlOperation1.promise();
131134
RESOURCES_TO_CLEAN.push(DATABASE);
132135

136+
const [, googleSqlOperation2] = await DATABASE_DROP_PROTECTION.create({
137+
schema: `
138+
CREATE TABLE ${TABLE_NAME} (
139+
SingerId STRING(1024) NOT NULL,
140+
Name STRING(1024),
141+
) PRIMARY KEY(SingerId)`,
142+
gaxOptions: GAX_OPTIONS,
143+
});
144+
await googleSqlOperation2.promise();
145+
RESOURCES_TO_CLEAN.push(DATABASE_DROP_PROTECTION);
146+
133147
if (!IS_EMULATOR_ENABLED) {
134148
const [pg_database, postgreSqlOperation] = await PG_DATABASE.create({
135149
databaseDialect: Spanner.POSTGRESQL,
@@ -1973,6 +1987,10 @@ describe('Spanner', () => {
19731987
instance.getDatabases((err, databases) => {
19741988
assert.ifError(err);
19751989
assert(databases!.length > 0);
1990+
// check if enableDropProtection is populated for databases.
1991+
databases!.map(db => {
1992+
assert.notStrictEqual(db.metadata.enableDropProtection, null);
1993+
});
19761994
done();
19771995
});
19781996
});
@@ -2139,6 +2157,36 @@ describe('Spanner', () => {
21392157
await listDatabaseOperation(PG_DATABASE);
21402158
});
21412159

2160+
it('enable_drop_protection should be disabled by default', async function () {
2161+
if (IS_EMULATOR_ENABLED) {
2162+
this.skip();
2163+
}
2164+
const [databaseMetadata] = await DATABASE_DROP_PROTECTION.getMetadata();
2165+
assert.strictEqual(databaseMetadata!.enableDropProtection, false);
2166+
});
2167+
2168+
it('enable_drop_protection on database', async function () {
2169+
if (IS_EMULATOR_ENABLED) {
2170+
this.skip();
2171+
}
2172+
const [operation1] = await DATABASE_DROP_PROTECTION.setMetadata({
2173+
enableDropProtection: true,
2174+
});
2175+
await operation1.promise();
2176+
2177+
try {
2178+
await DATABASE_DROP_PROTECTION.delete();
2179+
assert.ok(false);
2180+
} catch (err) {
2181+
assert.ok(true);
2182+
}
2183+
2184+
const [operation2] = await DATABASE_DROP_PROTECTION.setMetadata({
2185+
enableDropProtection: false,
2186+
});
2187+
await operation2.promise();
2188+
});
2189+
21422190
describe('FineGrainedAccessControl', () => {
21432191
before(function () {
21442192
if (SKIP_FGAC_TESTS === 'true') {

0 commit comments

Comments
 (0)