diff --git a/spanner/crud.js b/spanner/crud.js new file mode 100644 index 0000000000..c14c057bac --- /dev/null +++ b/spanner/crud.js @@ -0,0 +1,395 @@ +// Copyright 2017 Google LLC +// +// 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. + +'use strict'; + +// sample-metadata: +// title: CRUD + +async function updateData(instanceId, databaseId, projectId) { + // [START spanner_update_data] + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + + // Creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner instance and database + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + // Update a row in the Albums table + // Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so they + // must be converted to strings before being inserted as INT64s + const albumsTable = database.table('Albums'); + + try { + await albumsTable.update([ + {SingerId: '1', AlbumId: '1', MarketingBudget: '100000'}, + {SingerId: '2', AlbumId: '2', MarketingBudget: '500000'}, + ]); + console.log('Updated data.'); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + await database.close(); + } + // [END spanner_update_data] +} + +async function insertData(instanceId, databaseId, projectId) { + // [START spanner_insert_data] + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + + // Creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner instance and database + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + // Instantiate Spanner table objects + const singersTable = database.table('Singers'); + const albumsTable = database.table('Albums'); + + // Inserts rows into the Singers table + // Note: Cloud Spanner interprets Node.js numbers as FLOAT64s, so + // they must be converted to strings before being inserted as INT64s + try { + await singersTable.insert([ + {SingerId: '1', FirstName: 'Marc', LastName: 'Richards'}, + {SingerId: '2', FirstName: 'Catalina', LastName: 'Smith'}, + {SingerId: '3', FirstName: 'Alice', LastName: 'Trentor'}, + {SingerId: '4', FirstName: 'Lea', LastName: 'Martin'}, + {SingerId: '5', FirstName: 'David', LastName: 'Lomond'}, + ]); + + await albumsTable.insert([ + {SingerId: '1', AlbumId: '1', AlbumTitle: 'Total Junk'}, + {SingerId: '1', AlbumId: '2', AlbumTitle: 'Go, Go, Go'}, + {SingerId: '2', AlbumId: '1', AlbumTitle: 'Green'}, + {SingerId: '2', AlbumId: '2', AlbumTitle: 'Forever Hold your Peace'}, + {SingerId: '2', AlbumId: '3', AlbumTitle: 'Terrified'}, + ]); + + console.log('Inserted data.'); + } catch (err) { + console.error('ERROR:', err); + } finally { + await database.close(); + } + // [END spanner_insert_data] +} + +async function deleteData(instanceId, databaseId, projectId) { + // [START spanner_delete_data] + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + + // Creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner instance and database + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + // Instantiate Spanner table object + const albumsTable = database.table('Albums'); + + // Deletes individual rows from the Albums table. + try { + const keys = [ + ['2', '1'], + ['2', '3'], + ]; + await albumsTable.deleteRows(keys); + console.log('Deleted individual rows in Albums.'); + } catch (err) { + console.error('ERROR:', err); + } + + // Delete a range of rows where the column key is >=3 and <5 + database.runTransaction(async (err, transaction) => { + if (err) { + console.error(err); + return; + } + try { + const [rowCount] = await transaction.runUpdate({ + sql: 'DELETE FROM Singers WHERE SingerId >= 3 AND SingerId < 5', + }); + console.log(`${rowCount} records deleted from Singers.`); + } catch (err) { + console.error('ERROR:', err); + } + + // Deletes remaining rows from the Singers table and the Albums table, + // because Albums table is defined with ON DELETE CASCADE. + try { + // The WHERE clause is required for DELETE statements to prevent + // accidentally deleting all rows in a table. + // https://cloud.google.com/spanner/docs/dml-syntax#where_clause + const [rowCount] = await transaction.runUpdate({ + sql: 'DELETE FROM Singers WHERE true', + }); + console.log(`${rowCount} records deleted from Singers.`); + await transaction.commit(); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + await database.close(); + } + }); + // [END spanner_delete_data] +} + +async function queryData(instanceId, databaseId, projectId) { + // [START spanner_query_data] + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + + // Creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner instance and database + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + const query = { + sql: 'SELECT SingerId, AlbumId, AlbumTitle FROM Albums', + }; + + // Queries rows from the Albums table + try { + const [rows] = await database.run(query); + + rows.forEach(row => { + const json = row.toJSON(); + console.log( + `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}` + ); + }); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + await database.close(); + } + // [END spanner_query_data] +} + +async function readData(instanceId, databaseId, projectId) { + // [START spanner_read_data] + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + + // Creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner instance and database + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + // Reads rows from the Albums table + const albumsTable = database.table('Albums'); + + const query = { + columns: ['SingerId', 'AlbumId', 'AlbumTitle'], + keySet: { + all: true, + }, + }; + + try { + const [rows] = await albumsTable.read(query); + + rows.forEach(row => { + const json = row.toJSON(); + console.log( + `SingerId: ${json.SingerId}, AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}` + ); + }); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + await database.close(); + } + // [END spanner_read_data] +} + +async function readStaleData(instanceId, databaseId, projectId) { + // [START spanner_read_stale_data] + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + + // Creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner instance and database + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + // Reads rows from the Albums table + const albumsTable = database.table('Albums'); + + const query = { + columns: ['SingerId', 'AlbumId', 'AlbumTitle', 'MarketingBudget'], + keySet: { + all: true, + }, + }; + + const options = { + // Guarantees that all writes committed more than 15000 milliseconds ago are visible + exactStaleness: 15000, + }; + + try { + const [rows] = await albumsTable.read(query, options); + + rows.forEach(row => { + const json = row.toJSON(); + const id = json.SingerId; + const album = json.AlbumId; + const title = json.AlbumTitle; + const budget = json.MarketingBudget ? json.MarketingBudget : ''; + console.log( + `SingerId: ${id}, AlbumId: ${album}, AlbumTitle: ${title}, MarketingBudget: ${budget}` + ); + }); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + await database.close(); + } + // [END spanner_read_stale_data] +} + +const {getCommitStats} = require('./get-commit-stats'); + +require('yargs') + .demand(1) + .command( + 'update ', + 'Modifies existing rows of data in an example Cloud Spanner table.', + {}, + opts => updateData(opts.instanceName, opts.databaseName, opts.projectId) + ) + .command( + 'query ', + 'Executes a read-only SQL query against an example Cloud Spanner table.', + {}, + opts => queryData(opts.instanceName, opts.databaseName, opts.projectId) + ) + .command( + 'insert ', + 'Inserts new rows of data into an example Cloud Spanner table.', + {}, + opts => insertData(opts.instanceName, opts.databaseName, opts.projectId) + ) + .command( + 'delete ', + 'Deletes rows from an example Cloud Spanner table.', + {}, + opts => deleteData(opts.instanceName, opts.databaseName, opts.projectId) + ) + .command( + 'read ', + 'Reads data in an example Cloud Spanner table.', + {}, + opts => readData(opts.instanceName, opts.databaseName, opts.projectId) + ) + .command( + 'read-stale ', + 'Reads stale data in an example Cloud Spanner table.', + {}, + opts => readStaleData(opts.instanceName, opts.databaseName, opts.projectId) + ) + .command( + 'getCommitStats ', + 'Updates rows in example Cloud Spanner table and reads CommitStats.', + {}, + opts => getCommitStats(opts.instanceName, opts.databaseName, opts.projectId) + ) + .example('node $0 update "my-instance" "my-database" "my-project-id"') + .example('node $0 query "my-instance" "my-database" "my-project-id"') + .example('node $0 insert "my-instance" "my-database" "my-project-id"') + .example('node $0 delete "my-instance" "my-database" "my-project-id"') + .example('node $0 read "my-instance" "my-database" "my-project-id"') + .example('node $0 read-stale "my-instance" "my-database" "my-project-id"') + .example('node $0 getCommitStats "my-instance" "my-database" "my-project-id"') + .wrap(120) + .recommendCommands() + .epilogue('For more information, see https://cloud.google.com/spanner/docs') + .strict() + .help().argv; diff --git a/spanner/database-create-with-encryption-key.js b/spanner/database-create-with-encryption-key.js new file mode 100644 index 0000000000..ba0c722a4e --- /dev/null +++ b/spanner/database-create-with-encryption-key.js @@ -0,0 +1,77 @@ +// Copyright 2024 Google LLC +// +// 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. + +'use strict'; + +async function createDatabaseWithEncryptionKey( + instanceId, + databaseId, + projectId, + keyName +) { + // [START spanner_create_database_with_encryption_key] + + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + // const keyName = + // 'projects/my-project-id/my-region/keyRings/my-key-ring/cryptoKeys/my-key'; + + // creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner Database Admin Client object + const databaseAdminClient = spanner.getDatabaseAdminClient(); + try { + // Creates a database + const [operation] = await databaseAdminClient.createDatabase({ + createStatement: 'CREATE DATABASE `' + databaseId + '`', + parent: databaseAdminClient.instancePath(projectId, instanceId), + encryptionConfig: { + kmsKeyName: keyName, + }, + }); + + console.log(`Waiting for operation on ${databaseId} to complete...`); + await operation.promise(); + + console.log(`Created database ${databaseId} on instance ${instanceId}.`); + + // Get encryption key + const [metadata] = await databaseAdminClient.getDatabase({ + name: databaseAdminClient.databasePath(projectId, instanceId, databaseId), + }); + + console.log( + `Database encrypted with key ${metadata.encryptionConfig.kmsKeyName}.` + ); + } catch (error) { + console.error( + 'Failed to create database with encryption key:', + error.message || error + ); + } + // [END spanner_create_database_with_encryption_key] +} + +module.exports.createDatabaseWithEncryptionKey = + createDatabaseWithEncryptionKey; diff --git a/spanner/database-create-with-version-retention-period.js b/spanner/database-create-with-version-retention-period.js new file mode 100644 index 0000000000..4a4a9af004 --- /dev/null +++ b/spanner/database-create-with-version-retention-period.js @@ -0,0 +1,86 @@ +/** + * Copyright 2024 Google LLC + * 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. + */ + +'use strict'; + +async function createDatabaseWithVersionRetentionPeriod( + instanceId, + databaseId, + projectId +) { + // [START spanner_create_database_with_version_retention_period] + + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + + // creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner Database Admin Client object + const databaseAdminClient = spanner.getDatabaseAdminClient(); + + try { + // Create a new database with an extra statement which will alter the + // database after creation to set the version retention period. + console.log( + `Creating database ${databaseAdminClient.instancePath( + projectId, + instanceId + )}.` + ); + const versionRetentionStatement = ` + ALTER DATABASE \`${databaseId}\` + SET OPTIONS (version_retention_period = '1d')`; + + const [operation] = await databaseAdminClient.createDatabase({ + createStatement: 'CREATE DATABASE `' + databaseId + '`', + extraStatements: [versionRetentionStatement], + parent: databaseAdminClient.instancePath(projectId, instanceId), + }); + + console.log(`Waiting for operation on ${databaseId} to complete...`); + await operation.promise(); + console.log(` + Created database ${databaseId} with version retention period.`); + + const [metadata] = await databaseAdminClient.getDatabase({ + name: databaseAdminClient.databasePath(projectId, instanceId, databaseId), + }); + + console.log(`Version retention period: ${metadata.versionRetentionPeriod}`); + const milliseconds = + parseInt(metadata.earliestVersionTime.seconds, 10) * 1000 + + parseInt(metadata.earliestVersionTime.nanos, 10) / 1e6; + const date = new Date(milliseconds); + console.log(`Earliest version time: ${date.toString()}`); + } catch (error) { + console.error( + 'Failed to create database with version retention period:', + error.message || error + ); + } + // [END spanner_create_database_with_version_retention_period] +} + +module.exports.createDatabaseWithVersionRetentionPeriod = + createDatabaseWithVersionRetentionPeriod; diff --git a/spanner/database-update.js b/spanner/database-update.js new file mode 100644 index 0000000000..36cdb6ccd2 --- /dev/null +++ b/spanner/database-update.js @@ -0,0 +1,81 @@ +// Copyright 2024 Google LLC +// +// 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. + +// sample-metadata: +// title: Updates a Cloud Spanner Database. +// usage: node database-update.js + +'use strict'; + +function main( + instanceId = 'my-instance', + databaseId = 'my-database', + projectId = 'my-project-id' +) { + // [START spanner_update_database] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + // const projectId = 'my-project-id'; + + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + // creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + const databaseAdminClient = spanner.getDatabaseAdminClient(); + + async function updateDatabase() { + // Update the database metadata fields + try { + console.log( + `Updating database ${databaseAdminClient.databasePath( + projectId, + instanceId, + databaseId + )}.` + ); + const [operation] = await databaseAdminClient.updateDatabase({ + database: { + name: databaseAdminClient.databasePath( + projectId, + instanceId, + databaseId + ), + enableDropProtection: true, + }, + // updateMask contains the fields to be updated in database + updateMask: { + paths: ['enable_drop_protection'], + }, + }); + console.log( + `Waiting for update operation for ${databaseId} to complete...` + ); + await operation.promise(); + console.log(`Updated database ${databaseId}.`); + } catch (error) { + console.error('Failed to update database:', error.message || error); + } + } + updateDatabase(); + // [END spanner_update_database] +} + +main(...process.argv.slice(2)); diff --git a/spanner/get-commit-stats.js b/spanner/get-commit-stats.js new file mode 100644 index 0000000000..44eadf6725 --- /dev/null +++ b/spanner/get-commit-stats.js @@ -0,0 +1,62 @@ +// Copyright 2021 Google LLC +// +// 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. + +'use strict'; + +async function getCommitStats(instanceId, databaseId, projectId) { + // [START spanner_get_commit_stats] + // Imports the Google Cloud client library. + const {Spanner} = require('@google-cloud/spanner'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + + // Creates a client. + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner instance and database. + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + // Instantiate Spanner table objects. + const albumsTable = database.table('Albums'); + + // Updates rows in the Venues table. + try { + const [response] = await albumsTable.upsert( + [ + {SingerId: '1', AlbumId: '1', MarketingBudget: '200000'}, + {SingerId: '2', AlbumId: '2', MarketingBudget: '400000'}, + ], + {returnCommitStats: true} + ); + console.log( + `Updated data with ${response.commitStats.mutationCount} mutations.` + ); + } catch (error) { + console.error('Failed to get commit stats:', error.message || error); + } finally { + // Close the database when finished. + await database.close(); + } + // [END spanner_get_commit_stats] +} + +module.exports.getCommitStats = getCommitStats; diff --git a/spanner/index-create-storing.js b/spanner/index-create-storing.js new file mode 100644 index 0000000000..1762d12600 --- /dev/null +++ b/spanner/index-create-storing.js @@ -0,0 +1,76 @@ +// Copyright 2024 Google LLC +// +// 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. + +// sample-metadata: +// title: Creates a new value-storing index +// usage: node createStoringIndex + +'use strict'; + +function main( + instanceId = 'my-instance', + databaseId = 'my-database', + projectId = 'my-project-id' +) { + // [START spanner_create_storing_index] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + // const projectId = 'my-project-id'; + + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + // creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + const databaseAdminClient = spanner.getDatabaseAdminClient(); + + async function createStoringIndex() { + const request = [ + 'CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget)', + ]; + + // Creates a new index in the database + try { + const [operation] = await databaseAdminClient.updateDatabaseDdl({ + database: databaseAdminClient.databasePath( + projectId, + instanceId, + databaseId + ), + statements: request, + }); + + console.log('Waiting for operation to complete...'); + await operation.promise(); + + console.log('Added the AlbumsByAlbumTitle2 index.'); + } catch (err) { + console.error('Failed to create storing index:', err.message || err); + } finally { + // Close the spanner client when finished. + // The databaseAdminClient does not require explicit closure. The closure of the Spanner client will automatically close the databaseAdminClient. + spanner.close(); + } + } + createStoringIndex(); + // [END spanner_create_storing_index] +} + +main(...process.argv.slice(2)); diff --git a/spanner/index-create.js b/spanner/index-create.js new file mode 100644 index 0000000000..5486d34502 --- /dev/null +++ b/spanner/index-create.js @@ -0,0 +1,74 @@ +// Copyright 2024 Google LLC +// +// 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. + +// sample-metadata: +// title: Creates a new index +// usage: node createIndex + +'use strict'; + +function main( + instanceId = 'my-instance', + databaseId = 'my-database', + projectId = 'my-project-id' +) { + // [START spanner_create_index] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + // const projectId = 'my-project-id'; + + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + // creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + const databaseAdminClient = spanner.getDatabaseAdminClient(); + + async function createIndex() { + const request = ['CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle)']; + + // Creates a new index in the database + try { + const [operation] = await databaseAdminClient.updateDatabaseDdl({ + database: databaseAdminClient.databasePath( + projectId, + instanceId, + databaseId + ), + statements: request, + }); + + console.log('Waiting for operation to complete...'); + await operation.promise(); + + console.log('Added the AlbumsByAlbumTitle index.'); + } catch (err) { + console.error('Failed to create index:', err.message || err); + } finally { + // Close the spanner client when finished. + // The databaseAdminClient does not require explicit closure. The closure of the Spanner client will automatically close the databaseAdminClient. + spanner.close(); + } + } + createIndex(); + // [END spanner_create_index] +} + +main(...process.argv.slice(2)); diff --git a/spanner/index-query-data.js b/spanner/index-query-data.js new file mode 100644 index 0000000000..88684d3849 --- /dev/null +++ b/spanner/index-query-data.js @@ -0,0 +1,85 @@ +// Copyright 2021 Google LLC +// +// 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. + +// sample-metadata: +// title: Executes a read-only SQL query using an existing index. +// usage: node queryDataWithIndex + +'use strict'; + +function main( + instanceId = 'my-instance', + databaseId = 'my-database', + projectId = 'my-project-id', + startTitle = 'Ardvark', + endTitle = 'Goo' +) { + // [START spanner_query_data_with_index] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + // const projectId = 'my-project-id'; + // const startTitle = 'Ardvark'; + // const endTitle = 'Goo'; + + // Imports the Google Cloud Spanner client library + const {Spanner} = require('@google-cloud/spanner'); + + // Instantiates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + async function queryDataWithIndex() { + // Gets a reference to a Cloud Spanner instance and database + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + const query = { + sql: `SELECT AlbumId, AlbumTitle, MarketingBudget + FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle} + WHERE AlbumTitle >= @startTitle AND AlbumTitle <= @endTitle`, + params: { + startTitle: startTitle, + endTitle: endTitle, + }, + }; + + // Queries rows from the Albums table + try { + const [rows] = await database.run(query); + + rows.forEach(row => { + const json = row.toJSON(); + const marketingBudget = json.MarketingBudget + ? json.MarketingBudget + : null; // This value is nullable + console.log( + `AlbumId: ${json.AlbumId}, AlbumTitle: ${json.AlbumTitle}, MarketingBudget: ${marketingBudget}` + ); + }); + } catch (err) { + console.error('Failed to query data with index:', err.message || err); + } finally { + // Close the database when finished. + await database.close(); + } + } + queryDataWithIndex(); + // [END spanner_query_data_with_index] +} + +main(...process.argv.slice(2)); diff --git a/spanner/package.json b/spanner/package.json new file mode 100644 index 0000000000..4d6524f235 --- /dev/null +++ b/spanner/package.json @@ -0,0 +1,38 @@ +{ + "name": "nodejs-docs-samples-spanner", + "private": true, + "license": "Apache-2.0", + "author": "Google Inc.", + "repository": "googleapis/nodejs-spanner", + "files": [ + "*.js" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "test-with-archived": "mocha system-test --timeout 1600000", + "test": "mocha system-test/spanner.test.js --timeout 1600000" + }, + "dependencies": { + "@google-cloud/kms": "^5.0.0", + "@google-cloud/precise-date": "^5.0.0", + "@google-cloud/spanner": "^8.6.0", + "protobufjs": "^7.0.0", + "yargs": "^17.0.0" + }, + "devDependencies": { + "@google-cloud/opentelemetry-cloud-trace-exporter": "^2.4.1", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation-grpc": "^0.57.0", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/sdk-trace-base": "~1.30.1", + "@opentelemetry/sdk-trace-node": "^1.30.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1", + "chai": "^4.2.0", + "mocha": "^9.0.0", + "p-limit": "^3.0.1" + } +} \ No newline at end of file diff --git a/spanner/schema.js b/spanner/schema.js new file mode 100644 index 0000000000..32a0005eb9 --- /dev/null +++ b/spanner/schema.js @@ -0,0 +1,246 @@ +/** + * Copyright 2024 Google LLC + * 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. + */ + +'use strict'; + +// creates a database using Database Admin Client +async function createDatabase(instanceID, databaseID, projectID) { + // [START spanner_create_database] + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + // creates a client + const spanner = new Spanner({ + projectId: projectID, + }); + + const databaseAdminClient = spanner.getDatabaseAdminClient(); + + const createSingersTableStatement = ` + CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX), + FullName STRING(2048) AS (ARRAY_TO_STRING([FirstName, LastName], " ")) STORED, + ) PRIMARY KEY (SingerId)`; + const createAlbumsTableStatement = ` + CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE`; + + // Creates a new database + try { + const [operation] = await databaseAdminClient.createDatabase({ + createStatement: 'CREATE DATABASE `' + databaseID + '`', + extraStatements: [ + createSingersTableStatement, + createAlbumsTableStatement, + ], + parent: databaseAdminClient.instancePath(projectID, instanceID), + }); + + console.log(`Waiting for creation of ${databaseID} to complete...`); + await operation.promise(); + + console.log(`Created database ${databaseID} on instance ${instanceID}.`); + } catch (err) { + console.error('Failed to create database:', err.message || err); + } + + // [END spanner_create_database] +} + +async function addColumn(instanceId, databaseId, projectId) { + // [START spanner_add_column] + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + // creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + const databaseAdminClient = spanner.getDatabaseAdminClient(); + + // Creates a new index in the database + try { + const [operation] = await databaseAdminClient.updateDatabaseDdl({ + database: databaseAdminClient.databasePath( + projectId, + instanceId, + databaseId + ), + statements: ['ALTER TABLE Albums ADD COLUMN MarketingBudget INT64'], + }); + + console.log('Waiting for operation to complete...'); + await operation.promise(); + + console.log('Added the MarketingBudget column.'); + } catch (err) { + console.error('Failed to add column:', err.message || err); + } finally { + // Close the spanner client when finished. + // The databaseAdminClient does not require explicit closure. The closure of the Spanner client will automatically close the databaseAdminClient. + spanner.close(); + } + + // [END spanner_add_column] +} + +async function queryDataWithNewColumn(instanceId, databaseId, projectId) { + // [START spanner_query_data_with_new_column] + // This sample uses the `MarketingBudget` column. You can add the column + // by running the `add_column` sample or by running this DDL statement against + // your database: + // ALTER TABLE Albums ADD COLUMN MarketingBudget INT64 + + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + + // Creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Gets a reference to a Cloud Spanner instance and database + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + const query = { + sql: 'SELECT SingerId, AlbumId, MarketingBudget FROM Albums', + }; + + // Queries rows from the Albums table + try { + const [rows] = await database.run(query); + + rows.forEach(row => { + const json = row.toJSON(); + + console.log( + `SingerId: ${json.SingerId}, AlbumId: ${ + json.AlbumId + }, MarketingBudget: ${ + json.MarketingBudget ? json.MarketingBudget : null + }` + ); + }); + } catch (err) { + console.error('Failed to query data with new column:', err.message || err); + } finally { + // Close the database when finished. + await database.close(); + } + // [END spanner_query_data_with_new_column] +} + +const { + createDatabaseWithVersionRetentionPeriod, +} = require('./database-create-with-version-retention-period'); + +const { + createDatabaseWithEncryptionKey, +} = require('./database-create-with-encryption-key'); + +require('yargs') + .demand(1) + .command( + 'createDatabase ', + 'Creates an example database with two tables in a Cloud Spanner instance using Database Admin Client.', + {}, + opts => createDatabase(opts.instanceName, opts.databaseName, opts.projectId) + ) + .example('node $0 createDatabase "my-instance" "my-database" "my-project-id"') + .command( + 'addColumn ', + 'Adds an example MarketingBudget column to an example Cloud Spanner table.', + {}, + opts => addColumn(opts.instanceName, opts.databaseName, opts.projectId) + ) + .example('node $0 addColumn "my-instance" "my-database" "my-project-id"') + .command( + 'queryNewColumn ', + 'Executes a read-only SQL query against an example Cloud Spanner table with an additional column (MarketingBudget) added by addColumn.', + {}, + opts => + queryDataWithNewColumn( + opts.instanceName, + opts.databaseName, + opts.projectId + ) + ) + .example('node $0 queryNewColumn "my-instance" "my-database" "my-project-id"') + .command( + 'createDatabaseWithVersionRetentionPeriod ', + 'Creates a database with a version retention period.', + {}, + opts => + createDatabaseWithVersionRetentionPeriod( + opts.instanceName, + opts.databaseId, + opts.projectId + ) + ) + .example( + 'node $0 createDatabaseWithVersionRetentionPeriod "my-instance" "my-database-id" "my-project-id"' + ) + .command( + 'createDatabaseWithEncryptionKey ', + 'Creates an example database using given encryption key in a Cloud Spanner instance.', + {}, + opts => + createDatabaseWithEncryptionKey( + opts.instanceName, + opts.databaseName, + opts.projectId, + opts.keyName + ) + ) + .example( + 'node $0 createDatabaseWithEncryptionKey "my-instance" "my-database" "my-project-id" "key-name"' + ) + .wrap(120) + .recommendCommands() + .epilogue('For more information, see https://cloud.google.com/spanner/docs') + .strict() + .help().argv; diff --git a/spanner/system-test/spanner.test.js b/spanner/system-test/spanner.test.js new file mode 100644 index 0000000000..3c263c0375 --- /dev/null +++ b/spanner/system-test/spanner.test.js @@ -0,0 +1,2677 @@ +// Copyright 2024 Google LLC +// +// 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. + +'use strict'; + +const {Spanner} = require('@google-cloud/spanner'); +const {KeyManagementServiceClient} = require('@google-cloud/kms'); +const {assert} = require('chai'); +const {describe, it, before, after, afterEach} = require('mocha'); +const cp = require('child_process'); +const pLimit = require('p-limit'); + +const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'}); + +const batchCmd = 'node batch.js'; +const crudCmd = 'node crud.js'; +const schemaCmd = 'node schema.js'; +const queryOptionsCmd = 'node queryoptions.js'; +const rpcPriorityRunCommand = 'node rpc-priority-run.js'; +const rpcPriorityReadCommand = 'node rpc-priority-read.js'; +const rpcPriorityBatchDMLCommand = 'node rpc-priority-batch-dml.js'; +const rpcPriorityPartitionedDMLCommand = 'node rpc-priority-partitioned-dml.js'; +const rpcPriorityTransactionCommand = 'node rpc-priority-transaction.js'; +const rpcPriorityQueryPartitionsCommand = + 'node rpc-priority-query-partitions.js'; +const transactionCmd = 'node transaction.js'; +const transactionTagCommand = 'node transaction-tag.js'; +const transactionTimeoutCommand = 'node transaction-timeout.js'; +const statementTimeoutCommand = 'node statement-timeout.js'; +const requestTagCommand = 'node request-tag.js'; +const timestampCmd = 'node timestamp.js'; +const structCmd = 'node struct.js'; +const dmlCmd = 'node dml.js'; +const batchWriteCmd = 'node batch-write.js'; +const datatypesCmd = 'node datatypes.js'; +const backupsCmd = 'node backups.js'; +const instanceCmd = 'node instance.js'; +const createTableWithForeignKeyDeleteCascadeCommand = + 'node table-create-with-foreign-key-delete-cascade.js'; +const createInstanceWithoutDefaultBackupSchedulesCommand = + 'node create-instance-without-default-backup-schedules.js'; +const updateInstanceDefaultBackupScheduleTypeCommand = + 'node update-instance-default-backup-schedule-type.js'; +const alterTableWithForeignKeyDeleteCascadeCommand = + 'node table-alter-with-foreign-key-delete-cascade.js'; +const dropForeignKeyConstraintDeleteCascaseCommand = + 'node table-drop-foreign-key-constraint-delete-cascade.js'; +const traceObservabilityCommand = 'node observability-traces.js'; + +const CURRENT_TIME = Math.round(Date.now() / 1000).toString(); +const PROJECT_ID = process.env.GCLOUD_PROJECT; +const PREFIX = 'test-instance'; +const INSTANCE_ID = + process.env.SPANNERTEST_INSTANCE || `${PREFIX}-${CURRENT_TIME}`; +const MULTI_REGION_INSTANCE_ID = + process.env.SPANNERTEST_MR_INSTANCE || `${PREFIX}-mr-${CURRENT_TIME}`; +const SAMPLE_INSTANCE_ID = `${PREFIX}-my-sample-instance-${CURRENT_TIME}`; +const SAMPLE_INSTANCE_CONFIG_ID = `custom-my-sample-instance-config-${CURRENT_TIME}`; +const BASE_INSTANCE_CONFIG_ID = 'regional-us-central1'; +const INSTANCE_ALREADY_EXISTS = !!process.env.SPANNERTEST_INSTANCE; +const DATABASE_ID = `test-database-${CURRENT_TIME}`; +const PG_DATABASE_ID = `test-pg-database-${CURRENT_TIME}`; +const RESTORE_DATABASE_ID = `test-database-${CURRENT_TIME}-r`; +const ENCRYPTED_RESTORE_DATABASE_ID = `test-database-${CURRENT_TIME}-r-enc`; +const VERSION_RETENTION_DATABASE_ID = `test-database-${CURRENT_TIME}-v`; +const ENCRYPTED_DATABASE_ID = `test-database-${CURRENT_TIME}-enc`; +const DEFAULT_LEADER_DATABASE_ID = `test-database-${CURRENT_TIME}-dl`; +const SEQUENCE_DATABASE_ID = `test-seq-database-${CURRENT_TIME}-r`; +const PROTO_DATABASE_ID = `test-db${CURRENT_TIME}-proto1`; +const BACKUP_ID = `test-backup-${CURRENT_TIME}`; +const COPY_BACKUP_ID = `test-copy-backup-${CURRENT_TIME}`; +const ENCRYPTED_BACKUP_ID = `test-backup-${CURRENT_TIME}-enc`; +const CANCELLED_BACKUP_ID = `test-backup-${CURRENT_TIME}-c`; +const LOCATION_ID = 'regional-us-central1'; +const MULTI_REGION_LOCATION_ID = 'nam3'; +const PG_LOCATION_ID = 'regional-us-west2'; +const KEY_LOCATION_ID1 = 'us-central1'; +const KEY_LOCATION_ID2 = 'us-east1'; +const KEY_LOCATION_ID3 = 'us-east4'; +const KEY_RING_ID = 'test-key-ring-node'; +const KEY_ID = 'test-key'; +const DEFAULT_LEADER = 'us-central1'; +const DEFAULT_LEADER_2 = 'us-east1'; +const SKIP_BACKUPS = process.env.SKIP_BACKUPS; +const KOKORO_JOB_NAME = process.env.KOKORO_JOB_NAME; + +const spanner = new Spanner({ + projectId: PROJECT_ID, +}); +const LABEL = 'node-sample-tests'; +const GAX_OPTIONS = { + retry: { + retryCodes: [4, 8, 14], + backoffSettings: { + initialRetryDelayMillis: 1000, + retryDelayMultiplier: 1.3, + maxRetryDelayMillis: 32000, + initialRpcTimeoutMillis: 60000, + rpcTimeoutMultiplier: 1, + maxRpcTimeoutMillis: 60000, + totalTimeoutMillis: 600000, + }, + }, +}; + +async function cleanupDatabase(instanceId, databaseId) { + const database = spanner.instance(instanceId).database(databaseId); + try { + await database.delete(); + } catch (err) { + // ignore the error in case the database doesn't exists + if (err.code !== 5) { + throw err; + } + } +} + +const delay = async (test, cleanupFn = null) => { + const retries = test.currentRetry(); + // No retry on the first failure. + if (retries === 0) return; + + // run cleanup for database, provided + if (cleanupFn) { + await cleanupFn(); + } + // See: https://cloud.google.com/storage/docs/exponential-backoff + const ms = Math.pow(2, retries) + Math.random() * 1000; + return new Promise(done => { + console.info(`retrying "${test.title}" in ${ms}ms`); + setTimeout(done, ms); + }); +}; + +async function deleteStaleInstances() { + let [instances] = await spanner.getInstances({ + filter: `(labels.${LABEL}:true) OR (labels.cloud_spanner_samples:true)`, + }); + const old = new Date(); + old.setHours(old.getHours() - 4); + + instances = instances.filter(instance => { + return ( + instance.metadata.labels['created'] && + new Date(parseInt(instance.metadata.labels['created']) * 1000) < old + ); + }); + const limit = pLimit(5); + await Promise.all( + instances.map(instance => limit(() => deleteInstance(instance))) + ); +} + +async function deleteInstance(instance) { + const [backups] = await instance.getBackups(); + await Promise.all(backups.map(backup => backup.delete(GAX_OPTIONS))); + try { + const [databases] = await instance.getDatabases(); + await Promise.all( + databases.map(async database => { + try { + const [operation] = await database.setMetadata({ + enableDropProtection: false, + }); + await operation.promise(); + } catch (err) { + console.warn( + `[Cleanup] Could not disable drop protection for ${database.id}:`, + err.message + ); + } + }) + ); + } catch (err) { + console.warn( + `[Cleanup] Could not list databases for instance ${instance.id}:`, + err.message + ); + } + return instance.delete(GAX_OPTIONS); +} + +async function getCryptoKey(key_location) { + const NOT_FOUND = 5; + + // Instantiates a client. + const client = new KeyManagementServiceClient(); + + // Build the parent key ring name. + const keyRingName = client.keyRingPath(PROJECT_ID, key_location, KEY_RING_ID); + + // Get key ring. + try { + await client.getKeyRing({name: keyRingName}); + } catch (err) { + // Create key ring if it doesn't exist. + if (err.code === NOT_FOUND) { + // Build the parent location name. + const locationName = client.locationPath(PROJECT_ID, key_location); + await client.createKeyRing({ + parent: locationName, + keyRingId: KEY_RING_ID, + }); + } else { + throw err; + } + } + + // Get key. + try { + // Build the key name + const keyName = client.cryptoKeyPath( + PROJECT_ID, + key_location, + KEY_RING_ID, + KEY_ID + ); + const [key] = await client.getCryptoKey({ + name: keyName, + }); + return key; + } catch (err) { + // Create key if it doesn't exist. + if (err.code === NOT_FOUND) { + const [key] = await client.createCryptoKey({ + parent: keyRingName, + cryptoKeyId: KEY_ID, + cryptoKey: { + purpose: 'ENCRYPT_DECRYPT', + versionTemplate: { + algorithm: 'GOOGLE_SYMMETRIC_ENCRYPTION', + }, + }, + }); + return key; + } else { + throw err; + } + } +} + +describe('Autogenerated Admin Clients', () => { + const instance = spanner.instance(INSTANCE_ID); + + before(async () => { + await deleteStaleInstances(); + + if (!INSTANCE_ALREADY_EXISTS) { + const [, operation] = await instance.create({ + config: LOCATION_ID, + nodes: 1, + edition: 'ENTERPRISE_PLUS', + labels: { + [LABEL]: 'true', + created: CURRENT_TIME, + }, + gaxOptions: GAX_OPTIONS, + }); + return operation.promise(); + } else { + console.log( + `Not creating temp instance, using + ${instance.formattedName_}...` + ); + } + }); + + after(async () => { + const instance = spanner.instance(INSTANCE_ID); + + const safeDelete = async (deletePromise, resourceName) => { + try { + await deletePromise; + } catch (err) { + if (err.code !== 5) { + console.error( + `[WARNING] Failed to clean up ${resourceName}:`, + err.message + ); + } + } + }; + + if (!INSTANCE_ALREADY_EXISTS) { + await Promise.all([ + safeDelete(instance.backup(BACKUP_ID).delete(GAX_OPTIONS), 'BACKUP_ID'), + safeDelete( + instance.backup(ENCRYPTED_BACKUP_ID).delete(GAX_OPTIONS), + 'ENCRYPTED_BACKUP_ID' + ), + safeDelete( + instance.backup(COPY_BACKUP_ID).delete(GAX_OPTIONS), + 'COPY_BACKUP_ID' + ), + safeDelete( + instance.backup(CANCELLED_BACKUP_ID).delete(GAX_OPTIONS), + 'CANCELLED_BACKUP_ID' + ), + ]); + await safeDelete(instance.delete(GAX_OPTIONS), 'INSTANCE_ID'); + } else { + await Promise.all([ + safeDelete(instance.database(DATABASE_ID).delete(), 'DATABASE_ID'), + safeDelete( + instance.database(PG_DATABASE_ID).delete(), + 'PG_DATABASE_ID' + ), + safeDelete( + instance.database(RESTORE_DATABASE_ID).delete(), + 'RESTORE_DATABASE_ID' + ), + safeDelete( + instance.database(ENCRYPTED_RESTORE_DATABASE_ID).delete(), + 'ENCRYPTED_RESTORE_DATABASE_ID' + ), + safeDelete( + instance.database(VERSION_RETENTION_DATABASE_ID).delete(), + 'VERSION_RETENTION_DATABASE_ID' + ), + safeDelete( + instance.database(ENCRYPTED_DATABASE_ID).delete(), + 'ENCRYPTED_DATABASE_ID' + ), + safeDelete(instance.backup(BACKUP_ID).delete(GAX_OPTIONS), 'BACKUP_ID'), + safeDelete( + instance.backup(COPY_BACKUP_ID).delete(GAX_OPTIONS), + 'COPY_BACKUP_ID' + ), + safeDelete( + instance.backup(ENCRYPTED_BACKUP_ID).delete(GAX_OPTIONS), + 'ENCRYPTED_BACKUP_ID' + ), + safeDelete( + instance.backup(CANCELLED_BACKUP_ID).delete(GAX_OPTIONS), + 'CANCELLED_BACKUP_ID' + ), + ]); + } + await safeDelete( + spanner.instance(SAMPLE_INSTANCE_ID).delete(GAX_OPTIONS), + 'SAMPLE_INSTANCE_ID' + ); + }); + + describe.skip('instance', () => { + afterEach(async () => { + const sample_instance = spanner.instance(SAMPLE_INSTANCE_ID); + await sample_instance.delete(); + }); + + // create_and_update_instance + it('should create and update an example instance with spanner editions', async () => { + const createInstanceOutput = execSync( + `${instanceCmd} createInstance "${SAMPLE_INSTANCE_ID}" ${PROJECT_ID}` + ); + assert.match( + createInstanceOutput, + new RegExp( + `Waiting for operation on ${SAMPLE_INSTANCE_ID} to complete...` + ) + ); + assert.match( + createInstanceOutput, + new RegExp(`Created instance ${SAMPLE_INSTANCE_ID}.`) + ); + + const updateInstanceOutput = execSync( + `node instance-update "${SAMPLE_INSTANCE_ID}" ${PROJECT_ID}` + ); + assert.match( + updateInstanceOutput, + new RegExp( + `Waiting for operation on ${SAMPLE_INSTANCE_ID} to complete...` + ) + ); + assert.match( + updateInstanceOutput, + new RegExp(`Updated instance ${SAMPLE_INSTANCE_ID}.`) + ); + assert.match( + updateInstanceOutput, + new RegExp( + `Instance ${SAMPLE_INSTANCE_ID} has been updated with the ENTERPRISE edition.` + ) + ); + }); + + // create_and_update_instance_default_backup_schedule_type + it('should create an example instance without default backup schedule type and update the instance to have it', async () => { + const createInstanceOutput = execSync( + `${createInstanceWithoutDefaultBackupSchedulesCommand} "${SAMPLE_INSTANCE_ID}" ${PROJECT_ID}` + ); + assert.match( + createInstanceOutput, + new RegExp( + `Created instance ${SAMPLE_INSTANCE_ID} without default backup schedules.` + ) + ); + + const updateInstanceOutput = execSync( + `${updateInstanceDefaultBackupScheduleTypeCommand} "${SAMPLE_INSTANCE_ID}" ${PROJECT_ID}` + ); + assert.match( + updateInstanceOutput, + new RegExp( + `Instance ${SAMPLE_INSTANCE_ID} has been updated with the AUTOMATIC default backup schedule type.` + ) + ); + }); + + // create_instance_with_processing_units + it('should create an example instance with processing units', async () => { + const output = execSync( + `${instanceCmd} createInstanceWithProcessingUnits "${SAMPLE_INSTANCE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Waiting for operation on ${SAMPLE_INSTANCE_ID} to complete...` + ) + ); + assert.match( + output, + new RegExp(`Created instance ${SAMPLE_INSTANCE_ID}.`) + ); + assert.match( + output, + new RegExp(`Instance ${SAMPLE_INSTANCE_ID} has 500 processing units.`) + ); + }); + + // create_instance_with_autoscaling_config + it('should create an example instance with autoscaling config', async () => { + const output = execSync( + `node instance-with-autoscaling-config.js "${SAMPLE_INSTANCE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Waiting for operation on ${SAMPLE_INSTANCE_ID} to complete...` + ) + ); + assert.match( + output, + new RegExp(`Created instance ${SAMPLE_INSTANCE_ID}.`) + ); + assert.match( + output, + new RegExp( + `Autoscaling configurations of ${SAMPLE_INSTANCE_ID} are: ` + + '\n' + + 'Min nodes: 1 ' + + 'nodes.' + + '\n' + + 'Max nodes: 2' + + ' nodes.' + + '\n' + + 'High priority cpu utilization percent: 65.' + + '\n' + + 'Storage utilization percent: 95.' + ) + ); + }); + + // create_instance_with_asymmetric_autoscaling_config + it('should create an example instance with autoscaling config and asymmetric Autoscaling Options', async () => { + const output = execSync( + `node instance-with-asymmetric-autoscaling-config.js "${SAMPLE_INSTANCE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Waiting for operation on ${SAMPLE_INSTANCE_ID} to complete...` + ) + ); + assert.match( + output, + new RegExp(`Created instance ${SAMPLE_INSTANCE_ID}.`) + ); + assert.match( + output, + new RegExp( + `Autoscaling configurations of ${SAMPLE_INSTANCE_ID} are: ` + + '\n' + + 'Min nodes: 1 ' + + 'nodes.' + + '\n' + + 'Max nodes: 2' + + ' nodes.' + + '\n' + + 'High priority cpu utilization percent: 65.' + + '\n' + + 'Storage utilization percent: 95.' + + '\n' + + 'Asymmetric Autoscaling Options: europe-west1, europe-west4, asia-east1' + ) + ); + }); + }); + + // check that base instance was created + it('should have created an instance', async () => { + const [exists] = await instance.exists(); + assert.strictEqual( + exists, + true, + 'The main instance was not created successfully!' + ); + }); + + // create_database + it('should create an example database', async () => { + const output = execSync( + `${schemaCmd} createDatabase "${INSTANCE_ID}" "${DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp(`Waiting for creation of ${DATABASE_ID} to complete...`) + ); + assert.match( + output, + new RegExp(`Created database ${DATABASE_ID} on instance ${INSTANCE_ID}.`) + ); + }); + + // update_database + it('should set database metadata', async () => { + const output = execSync( + `node database-update.js ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Waiting for update operation for ${DATABASE_ID} to complete...` + ) + ); + assert.match(output, new RegExp(`Updated database ${DATABASE_ID}.`)); + // cleanup + const [operation] = await instance + .database(DATABASE_ID) + .setMetadata({enableDropProtection: false}); + await operation.promise(); + }); + + describe.skip('encrypted database', () => { + after(async () => { + const instance = spanner.instance(INSTANCE_ID); + const encrypted_database = instance.database(ENCRYPTED_DATABASE_ID); + await encrypted_database.delete(); + }); + + // create_database_with_encryption_key + it('should create a database with an encryption key', async () => { + const key = await getCryptoKey(KEY_LOCATION_ID1); + + const output = execSync( + `${schemaCmd} createDatabaseWithEncryptionKey "${INSTANCE_ID}" "${ENCRYPTED_DATABASE_ID}" ${PROJECT_ID} "${key.name}"` + ); + assert.match( + output, + new RegExp( + `Waiting for operation on ${ENCRYPTED_DATABASE_ID} to complete...` + ) + ); + assert.match( + output, + new RegExp( + `Created database ${ENCRYPTED_DATABASE_ID} on instance ${INSTANCE_ID}.` + ) + ); + assert.match( + output, + new RegExp(`Database encrypted with key ${key.name}.`) + ); + }); + }); + + describe.skip('quickstart', () => { + // Running the quickstart test in here since there's already a spanner + // instance and database set up at this point. + it('should query a table', async () => { + const output = execSync( + `node quickstart ${PROJECT_ID} ${INSTANCE_ID} ${DATABASE_ID}` + ); + assert.match(output, /Query: \d+ found./); + }); + }); + + // insert_data + it('should insert rows into an example table', async () => { + const output = execSync( + `${crudCmd} insert ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Inserted data\./); + }); + + // delete_data + it('should delete and then insert rows in the example tables', async () => { + let output = execSync( + `${crudCmd} delete ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.include(output, 'Deleted individual rows in Albums.'); + assert.include(output, '2 records deleted from Singers.'); + assert.include(output, '3 records deleted from Singers.'); + output = execSync( + `${crudCmd} insert ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Inserted data\./); + }); + + // query_data + it('should query an example table and return matching rows', async () => { + const output = execSync( + `${crudCmd} query ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk/); + }); + + // read_data + it('should read an example table', async () => { + const output = execSync( + `${crudCmd} read ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk/); + }); + + // add_column + it('should add a column to a table', async () => { + const output = execSync( + `${schemaCmd} addColumn ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Waiting for operation to complete\.\.\./); + assert.match(output, /Added the MarketingBudget column\./); + }); + + // update_data + it('should update existing rows in an example table', async () => { + const output = execSync( + `${crudCmd} update ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Updated data\./); + }); + + // read_stale_data + it('should read stale data from an example table', async () => { + // read-stale-data reads data that is exactly 15 seconds old. So, make sure + // 15 seconds have elapsed since the update_data test. + await new Promise(r => setTimeout(r, 16000)); + const output = execSync( + `${crudCmd} read-stale ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk, MarketingBudget: 100000/ + ); + assert.match( + output, + /SingerId: 2, AlbumId: 2, AlbumTitle: Forever Hold your Peace, MarketingBudget: 500000/ + ); + }); + + // query_data_with_new_column + it('should query an example table with an additional column and return matching rows', async () => { + const output = execSync( + `${schemaCmd} queryNewColumn ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /SingerId: 1, AlbumId: 1, MarketingBudget: 100000/); + assert.match(output, /SingerId: 2, AlbumId: 2, MarketingBudget: 500000/); + }); + + // create_index + it('should create an index in an example table', async () => { + const output = execSync( + `node index-create ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Waiting for operation to complete\.\.\./); + assert.match(output, /Added the AlbumsByAlbumTitle index\./); + }); + + // create_storing_index + it('should create a storing index in an example table', async function () { + this.retries(5); + // Delay the start of the test, if this is a retry. + await delay(this.test); + + const output = execSync( + `node index-create-storing ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Waiting for operation to complete\.\.\./); + assert.match(output, /Added the AlbumsByAlbumTitle2 index\./); + }); + + // query_data_with_index + it.skip('should query an example table with an index and return matching rows', async () => { + const output = execSync( + `node index-query-data ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /AlbumId: 2, AlbumTitle: Go, Go, Go, MarketingBudget:/ + ); + assert.notMatch( + output, + /AlbumId: 1, AlbumTitle: Total Junk, MarketingBudget:/ + ); + }); + + it.skip('should respect query boundaries when querying an example table with an index', async () => { + const output = execSync( + `node index-query-data ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID} "Ardvark" "Zoo"` + ); + assert.match( + output, + /AlbumId: 1, AlbumTitle: Total Junk, MarketingBudget:/ + ); + assert.match( + output, + /AlbumId: 2, AlbumTitle: Go, Go, Go, MarketingBudget:/ + ); + }); + + // read_data_with_index + it.skip('should read an example table with an index', async () => { + const output = execSync( + `node index-read-data ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /AlbumId: 1, AlbumTitle: Total Junk/); + }); + + // read_data_with_storing_index + it.skip('should read an example table with a storing index', async () => { + const output = execSync( + `node index-read-data-with-storing ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /AlbumId: 1, AlbumTitle: Total Junk/); + }); + + // spanner_create_client_with_query_options + it.skip('should use query options from a database reference', async () => { + const output = execSync( + `${queryOptionsCmd} databaseWithQueryOptions ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /AlbumId: 2, AlbumTitle: Forever Hold your Peace, MarketingBudget:/ + ); + }); + + // spanner_query_with_query_options + it.skip('should use query options on request', async () => { + const output = execSync( + `${queryOptionsCmd} queryWithQueryOptions ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /AlbumId: 2, AlbumTitle: Forever Hold your Peace, MarketingBudget:/ + ); + }); + + // query with RPC priority for run command + it.skip('should use RPC priority from request options for run command', async () => { + const output = execSync( + `${rpcPriorityRunCommand} ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /Successfully fetched \d rows using low RPC priority\./ + ); + assert.match( + output, + /AlbumId: 2, AlbumTitle: Forever Hold your Peace, MarketingBudget:/ + ); + }); + + // query with RPC priority for Read command + it.skip('should use RPC priority from request options for read command', async () => { + const output = execSync( + `${rpcPriorityReadCommand} ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /Successfully fetched \d rows using low RPC priority\./ + ); + assert.match(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk/); + }); + + // query with RPC priority for transaction command + it.skip('should use RPC priority from request options for transaction command', async () => { + const output = execSync( + `${rpcPriorityTransactionCommand} ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /Successfully inserted 1 record into the Singers table using low RPC priority\./ + ); + }); + + // query with RPC priority for batch DML command + it.skip('should use RPC priority from request options for batch DML command', async () => { + const output = execSync( + `${rpcPriorityBatchDMLCommand} ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /Successfully executed 2 SQL statements using Batch DML using low RPC priority\./ + ); + }); + + // query with RPC priority for partitioned DML command + it.skip('should use RPC priority from request options for partitioned DML command', async () => { + const output = execSync( + `${rpcPriorityPartitionedDMLCommand} ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('Successfully updated (\\d+) records using low RPC priority.') + ); + }); + + // query with RPC priority for Query partitions command + it.skip('should use RPC priority from request options for Query partition command', async () => { + const output = execSync( + `${rpcPriorityQueryPartitionsCommand} ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /Successfully created \d query partitions using low RPC priority\./ + ); + assert.match(output, /Successfully received \d from executed partitions\./); + }); + + // read_only_transactioni + it.skip('should read an example table using transactions', async () => { + const output = execSync( + `${transactionCmd} readOnly ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk/); + assert.match(output, /Successfully executed read-only transaction\./); + }); + + // read_write_transaction + it.skip('should read from and write to an example table using transactions', async () => { + let output = execSync( + `${transactionCmd} readWrite ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /The first album's marketing budget: 100000/); + assert.match(output, /The second album's marketing budget: 500000/); + assert.match( + output, + /Successfully executed read-write transaction to transfer 200000 from Album 2 to Album 1./ + ); + output = execSync( + `${schemaCmd} queryNewColumn ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /SingerId: 1, AlbumId: 1, MarketingBudget: 300000/); + assert.match(output, /SingerId: 2, AlbumId: 2, MarketingBudget: 300000/); + }); + + // batch_client + it.skip('should create and execute query partitions', async () => { + const output = execSync( + `${batchCmd} create-and-execute-query-partitions ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Successfully created \d query partitions\./); + assert.match(output, /Successfully received \d from executed partitions\./); + }); + + // execute_partition + it.skip('should execute a partition', async () => { + const instance = spanner.instance(INSTANCE_ID); + const database = instance.database(DATABASE_ID); + const [transaction] = await database.createBatchTransaction(); + const identifier = JSON.stringify(transaction.identifier()); + + const query = 'SELECT SingerId FROM Albums'; + const [partitions] = await transaction.createQueryPartitions(query); + const partition = JSON.stringify(partitions[0]); + + const output = execSync( + `${batchCmd} execute-partition ${INSTANCE_ID} ${DATABASE_ID} '${identifier}' '${partition}' ${PROJECT_ID}` + ); + assert.match(output, /Successfully received \d from executed partition\./); + await transaction.close(); + }); + + // add_timestamp_column + it.skip('should add a timestamp column to a table', async () => { + const output = execSync( + `${timestampCmd} addTimestampColumn ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Waiting for operation to complete\.\.\./); + assert.match( + output, + /Added LastUpdateTime as a commit timestamp column in Albums table\./ + ); + }); + + // update_data_with_timestamp_column + it.skip('should update existing rows in an example table with commit timestamp column', async () => { + const output = execSync( + `${timestampCmd} updateWithTimestamp ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Updated data\./); + }); + + // query_data_with_timestamp_column + it.skip('should query an example table with an additional timestamp column and return matching rows', async () => { + const output = execSync( + `${timestampCmd} queryWithTimestamp ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /SingerId: 1, AlbumId: 1, MarketingBudget: 1000000, LastUpdateTime:/ + ); + assert.match( + output, + /SingerId: 2, AlbumId: 2, MarketingBudget: 750000, LastUpdateTime:/ + ); + }); + + // create_table_with_timestamp_column + it.skip('should create an example table with a timestamp column', async () => { + const output = execSync( + `${timestampCmd} createTableWithTimestamp "${INSTANCE_ID}" "${DATABASE_ID}" ${PROJECT_ID}` + ); + + assert.match( + output, + new RegExp(`Waiting for operation on ${DATABASE_ID} to complete...`) + ); + assert.match( + output, + new RegExp(`Created table Performances in database ${DATABASE_ID}.`) + ); + }); + + // insert_data_with_timestamp + it.skip('should insert rows into an example table with timestamp column', async () => { + const output = execSync( + `${timestampCmd} insertWithTimestamp ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Inserted data\./); + }); + + // query_new_table_with_timestamp + it.skip('should query an example table with a non-null timestamp column and return matching rows', async () => { + const output = execSync( + `${timestampCmd} queryTableWithTimestamp ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /SingerId: 1, VenueId: 4, EventDate:/); + assert.match(output, /Revenue: 15000, LastUpdateTime:/); + }); + + // write_data_for_struct_queries + it.skip('should insert rows into an example table for use with struct query examples', async () => { + const output = execSync( + `${structCmd} writeDataForStructQueries ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Inserted data\./); + }); + + // query_with_struct_param + it.skip('should query an example table with a STRUCT param', async () => { + const output = execSync( + `${structCmd} queryDataWithStruct ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /SingerId: 6/); + }); + + // query_with_array_of_struct_param + it.skip('should query an example table with an array of STRUCT param', async () => { + const output = execSync( + `${structCmd} queryWithArrayOfStruct ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /SingerId: 6\nSingerId: 7\nSingerId: 8/); + }); + + // query_with_struct_field_param + it.skip('should query an example table with a STRUCT field param', async () => { + const output = execSync( + `${structCmd} queryStructField ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /SingerId: 6/); + }); + + // query_with_nested_struct_param + it.skip('should query an example table with a nested STRUCT param', async () => { + const output = execSync( + `${structCmd} queryNestedStructField ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /SingerId: 6, SongName: Imagination\nSingerId: 9, SongName: Imagination/ + ); + }); + + // dml_standard_insert + it.skip('should insert rows into an example table using a DML statement', async () => { + const output = execSync( + `${dmlCmd} insertUsingDml ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /Successfully inserted 1 record into the Singers table/ + ); + }); + + // dml_standard_update + it.skip('should update a row in an example table using a DML statement', async () => { + const output = execSync( + `${dmlCmd} updateUsingDml ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Successfully updated 1 record/); + }); + + // dml_standard_delete + it.skip('should delete a row from an example table using a DML statement', async () => { + const output = execSync( + `${dmlCmd} deleteUsingDml ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Successfully deleted 1 record\./); + }); + + // dml_standard_update_with_timestamp + it.skip('should update the timestamp of multiple records in an example table using a DML statement', async () => { + const output = execSync( + `${dmlCmd} updateUsingDmlWithTimestamp ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Successfully updated 3 records/); + }); + + // dml_write_then_read + it.skip('should insert a record in an example table using a DML statement and then query the record', async () => { + const output = execSync( + `${dmlCmd} writeAndReadUsingDml ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Timothy Campbell/); + }); + + // dml_structs + it.skip('should update a record in an example table using a DML statement along with a struct value', async () => { + const output = execSync( + `${dmlCmd} updateUsingDmlWithStruct ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Successfully updated 1 record/); + }); + + // dml_getting_started_insert + it.skip('should insert multiple records into an example table using a DML statement', async () => { + const output = execSync( + `${dmlCmd} writeUsingDml ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /4 records inserted/); + }); + + // dml_query_with_parameter + it.skip('should use a parameter query to query record that was inserted using a DML statement', async () => { + const output = execSync( + `${dmlCmd} queryWithParameter ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /SingerId: 12, FirstName: Melissa, LastName: Garcia/); + }); + + // dml_getting_started_update + it.skip('should transfer value from one record to another using DML statements within a transaction', async () => { + const output = execSync( + `${dmlCmd} writeWithTransactionUsingDml ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /Successfully executed read-write transaction using DML to transfer 200000 from Album 2 to Album 1/ + ); + }); + + // dml_partitioned_update + it.skip('should update multiple records using a partitioned DML statement', async () => { + const output = execSync( + `${dmlCmd} updateUsingPartitionedDml ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Successfully updated 3 records/); + }); + + // dml_partitioned_delete + it.skip('should delete multiple records using a partitioned DML statement', async () => { + const output = execSync( + `${dmlCmd} deleteUsingPartitionedDml ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Successfully deleted 6 records/); + }); + + // dml_batch_update + it.skip('should insert and update records using Batch DML', async () => { + const output = execSync( + `${dmlCmd} updateUsingBatchDml ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /Successfully executed 2 SQL statements using Batch DML/ + ); + }); + + // dml_returning_insert + it.skip('should insert records using DML Returning', async () => { + const output = execSync( + `node dml-returning-insert ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('Successfully inserted 1 record into the Singers table') + ); + assert.match(output, new RegExp('Virginia Watson')); + }); + + // dml_returning_update + it.skip('should update records using DML Returning', async () => { + const output = execSync( + `node dml-returning-update ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('Successfully updated 1 record into the Albums table') + ); + assert.match(output, new RegExp('2000000')); + }); + + // dml_returning_delete + it.skip('should delete records using DML Returning', async () => { + const output = execSync( + `node dml-returning-delete ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('Successfully deleted 1 record from the Singers table') + ); + assert.match(output, new RegExp('Virginia Watson')); + }); + + // batch_write + it.skip('should perform CRUD operations using batch write', async () => { + const output = execSync( + `${batchWriteCmd} ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ).toString(); + + const successRegex = + /Mutation group indexes [\d,]+ have been applied with commit timestamp \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/; + const failureRegex = + /Mutation group indexes [\d,]+, could not be applied with error code \d+, and error message .+/; + + const successMatch = successRegex.test(output); + const errorMatch = failureRegex.test(output); + + if (successMatch || errorMatch) { + assert.include(output, 'Request completed successfully'); + } else { + assert.ifError(output); + } + }); + + // create_table_with_datatypes + it.skip('should create Venues example table with supported datatype columns', async () => { + const output = execSync( + `${datatypesCmd} createVenuesTable "${INSTANCE_ID}" "${DATABASE_ID}" ${PROJECT_ID}` + ); + + assert.match( + output, + new RegExp(`Waiting for operation on ${DATABASE_ID} to complete...`) + ); + assert.match( + output, + new RegExp(`Created table Venues in database ${DATABASE_ID}.`) + ); + }); + + // insert_datatypes_data + it.skip('should insert multiple records into Venues example table', async () => { + const output = execSync( + `${datatypesCmd} insertData ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Inserted data./); + }); + + // query_with_array_parameter + it.skip('should use an ARRAY query parameter to query record from the Venues example table', async () => { + const output = execSync( + `${datatypesCmd} queryWithArray ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /VenueId: 19, VenueName: Venue 19, AvailableDate: 2020-11-01/ + ); + assert.match( + output, + /VenueId: 42, VenueName: Venue 42, AvailableDate: 2020-10-01/ + ); + }); + + // query_with_bool_parameter + it.skip('should use a BOOL query parameter to query record from the Venues example table', async () => { + const output = execSync( + `${datatypesCmd} queryWithBool ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /VenueId: 19, VenueName: Venue 19, OutdoorVenue: true/ + ); + }); + + // query_with_bytes_parameter + it.skip('should use a BYTES query parameter to query record from the Venues example table', async () => { + const output = execSync( + `${datatypesCmd} queryWithBytes ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /VenueId: 4, VenueName: Venue 4/); + }); + + // query_with_date_parameter + it.skip('should use a DATE query parameter to query record from the Venues example table', async () => { + const output = execSync( + `${datatypesCmd} queryWithDate ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /VenueId: 4, VenueName: Venue 4, LastContactDate: 2018-09-02/ + ); + assert.match( + output, + /VenueId: 42, VenueName: Venue 42, LastContactDate: 2018-10-01/ + ); + }); + + // query_with_float_parameter + it.skip('should use a FLOAT64 query parameter to query record from the Venues example table', async () => { + const output = execSync( + `${datatypesCmd} queryWithFloat ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + /VenueId: 4, VenueName: Venue 4, PopularityScore: 0.8/ + ); + assert.match( + output, + /VenueId: 19, VenueName: Venue 19, PopularityScore: 0.9/ + ); + }); + + // query_with_int_parameter + it.skip('should use a INT64 query parameter to query record from the Venues example table', async () => { + const output = execSync( + `${datatypesCmd} queryWithInt ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /VenueId: 19, VenueName: Venue 19, Capacity: 6300/); + assert.match(output, /VenueId: 42, VenueName: Venue 42, Capacity: 3000/); + }); + + // query_with_string_parameter + it.skip('should use a STRING query parameter to query record from the Venues example table', async () => { + const output = execSync( + `${datatypesCmd} queryWithString ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /VenueId: 42, VenueName: Venue 42/); + }); + + // query_with_timestamp_parameter + it.skip('should use a TIMESTAMP query parameter to query record from the Venues example table', async () => { + const output = execSync( + `${datatypesCmd} queryWithTimestamp ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /VenueId: 4, VenueName: Venue 4, LastUpdateTime:/); + assert.match(output, /VenueId: 19, VenueName: Venue 19, LastUpdateTime:/); + assert.match(output, /VenueId: 42, VenueName: Venue 42, LastUpdateTime:/); + }); + + // add_numeric_column + it.skip('should add a Revenue column to Venues example table', async () => { + const output = execSync( + `${datatypesCmd} addNumericColumn "${INSTANCE_ID}" "${DATABASE_ID}" ${PROJECT_ID}` + ); + + assert.include( + output, + `Waiting for operation on ${DATABASE_ID} to complete...` + ); + assert.include( + output, + `Added Revenue column to Venues table in database ${DATABASE_ID}.` + ); + }); + + // update_data_with_numeric + it.skip('should update rows in Venues example table to add data in Revenue column', async () => { + const output = execSync( + `${datatypesCmd} updateWithNumericData ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Updated data./); + }); + + // query_with_numeric_parameter + it.skip('should use a NUMERIC query parameter to query records from the Venues example table', async () => { + const output = execSync( + `${datatypesCmd} queryWithNumericParameter ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /VenueId: 4, Revenue: 35000/); + }); + + // query with request tag + it.skip('should execute a query with a request tag', async () => { + const output = execSync( + `${requestTagCommand} ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /SingerId: 1, AlbumId: 1, AlbumTitle: Total Junk/); + }); + + // read_write_transaction with transaction tag + it.skip('should execute a read/write transaction with a transaction tag', async () => { + const output = execSync( + `${transactionTagCommand} ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.include(output, 'Inserted new outdoor venue'); + }); + + // read_write_transaction with transaction timeout + it.skip('should execute a read/write transaction with a transaction timeout of 60 seconds', async () => { + const output = execSync( + `${transactionTimeoutCommand} ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.include(output, '1 record inserted.'); + }); + + // read_write_transaction with statement timeout + it.skip('should execute a read/write transaction with a statement timeout of 60 seconds', async () => { + const output = execSync( + `${statementTimeoutCommand} ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.include(output, '1 record inserted.'); + }); + + // add_json_column + it.skip('should add a VenueDetails column to Venues example table', async () => { + const output = execSync( + `${datatypesCmd} addJsonColumn "${INSTANCE_ID}" "${DATABASE_ID}" ${PROJECT_ID}` + ); + + assert.include( + output, + `Waiting for operation on ${DATABASE_ID} to complete...` + ); + assert.include( + output, + `Added VenueDetails column to Venues table in database ${DATABASE_ID}.` + ); + }); + + // update_data_with_json + it.skip('should update rows in Venues example table to add data in VenueDetails column', async () => { + const output = execSync( + `${datatypesCmd} updateWithJsonData ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Updated data./); + }); + + // query_with_json_parameter + it.skip('should use a JSON query parameter to query records from the Venues example table', async () => { + const output = execSync( + `${datatypesCmd} queryWithJsonParameter ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /VenueId: 19, Details: {"open":true,"rating":9}/); + }); + + // isolation_level_option + it.skip('should run read-write transaction with isolation level option set', () => { + const output = execSync( + `node repeatable-reads.js ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, new RegExp('previous album title Total Junk')); + assert.match( + output, + new RegExp('Successfully updated 1 record in Albums table.') + ); + assert.match( + output, + new RegExp( + 'Successfully executed read-write transaction with isolationLevel option.' + ) + ); + }); + + // read_lock_mode_option + it.skip('should run read-write transaction with read lock mode option set', () => { + const output = execSync( + `node read-lock-mode.js ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, new RegExp('previous album title Green')); + assert.match( + output, + new RegExp('Successfully updated 1 record in Albums table.') + ); + assert.match( + output, + new RegExp( + 'Successfully executed read-write transaction with readLockMode option.' + ) + ); + }); + + // add_and_drop_new_database_role + it.skip('should add and drop new database roles', async () => { + const output = execSync( + `node add-and-drop-new-database-role.js ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, new RegExp('Waiting for operation to complete...')); + assert.match( + output, + new RegExp('Created roles child and parent and granted privileges') + ); + assert.match( + output, + new RegExp('Revoked privileges and dropped role child') + ); + }); + + // read_data_with_database_role + it.skip('should read data with database role', async () => { + const output = execSync( + `node read-data-with-database-role.js ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('SingerId: 1, FirstName: Marc, LastName: Richards') + ); + }); + + // get_database_roles + it.skip('should list database roles', async () => { + const output = execSync( + `node get-database-roles.js ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Role: projects/${PROJECT_ID}/instances/${INSTANCE_ID}/databases/${DATABASE_ID}/databaseRoles/public` + ) + ); + }); + + describe.skip('backups', async () => { + before(async function () { + if ( + SKIP_BACKUPS === 'true' || + (KOKORO_JOB_NAME && KOKORO_JOB_NAME.includes('presubmit')) + ) { + this.skip(); + } + }); + + it('shoud create a full backup schedule', async () => { + const output = execSync( + `node create-full-backup-schedule.js ${PROJECT_ID} ${INSTANCE_ID} ${DATABASE_ID} full-backup-schedule` + ); + assert.match(output, new RegExp('Created full backup schedule')); + assert.match( + output, + new RegExp( + `projects/${PROJECT_ID}/instances/${INSTANCE_ID}/databases/${DATABASE_ID}/backupSchedules/full-backup-schedule` + ) + ); + }); + + it('shoud create an incremental backup schedule', async () => { + const output = execSync( + `node create-incremental-backup-schedule.js ${PROJECT_ID} ${INSTANCE_ID} ${DATABASE_ID} incremental-backup-schedule` + ); + assert.match(output, new RegExp('Created incremental backup schedule')); + assert.match( + output, + new RegExp( + `projects/${PROJECT_ID}/instances/${INSTANCE_ID}/databases/${DATABASE_ID}/backupSchedules/incremental-backup-schedule` + ) + ); + }); + + it('shoud list backup schedules', async () => { + const output = execSync( + `node list-backup-schedules.js ${PROJECT_ID} ${INSTANCE_ID} ${DATABASE_ID}` + ); + assert.match( + output, + new RegExp( + `projects/${PROJECT_ID}/instances/${INSTANCE_ID}/databases/${DATABASE_ID}/backupSchedules/full-backup-schedule` + ) + ); + assert.match( + output, + new RegExp( + `projects/${PROJECT_ID}/instances/${INSTANCE_ID}/databases/${DATABASE_ID}/backupSchedules/incremental-backup-schedule` + ) + ); + }); + + it('shoud get a backup schedule', async () => { + const output = execSync( + `node get-backup-schedule.js ${PROJECT_ID} ${INSTANCE_ID} ${DATABASE_ID} full-backup-schedule` + ); + assert.match( + output, + new RegExp( + `projects/${PROJECT_ID}/instances/${INSTANCE_ID}/databases/${DATABASE_ID}/backupSchedules/full-backup-schedule` + ) + ); + }); + + it('shoud update a backup schedule', async () => { + const output = execSync( + `node update-backup-schedule.js ${PROJECT_ID} ${INSTANCE_ID} ${DATABASE_ID} full-backup-schedule` + ); + assert.match(output, new RegExp('Updated backup schedule')); + assert.match( + output, + new RegExp( + `projects/${PROJECT_ID}/instances/${INSTANCE_ID}/databases/${DATABASE_ID}/backupSchedules/full-backup-schedule` + ) + ); + }); + + it('shoud delete a backup schedule', async () => { + const output = execSync( + `node delete-backup-schedule.js ${PROJECT_ID} ${INSTANCE_ID} ${DATABASE_ID} full-backup-schedule` + ); + assert.match(output, new RegExp('Deleted backup schedule')); + }); + + // create_backup + it('should create a backup of the database', async () => { + const instance = spanner.instance(INSTANCE_ID); + const database = instance.database(DATABASE_ID); + const query = { + sql: 'SELECT CURRENT_TIMESTAMP() as Timestamp', + }; + const [rows] = await database.run(query); + const versionTime = rows[0].toJSON().Timestamp.toISOString(); + + const output = execSync( + `${backupsCmd} createBackup ${INSTANCE_ID} ${DATABASE_ID} ${BACKUP_ID} ${PROJECT_ID} ${versionTime}` + ); + assert.match(output, new RegExp(`Backup (.+)${BACKUP_ID} of size`)); + }); + + // create_backup_with_encryption_key + it('should create an encrypted backup of the database', async () => { + const key = await getCryptoKey(KEY_LOCATION_ID1); + + const output = execSync( + `${backupsCmd} createBackupWithEncryptionKey ${INSTANCE_ID} ${DATABASE_ID} ${ENCRYPTED_BACKUP_ID} ${PROJECT_ID} ${key.name}` + ); + assert.match( + output, + new RegExp(`Backup (.+)${ENCRYPTED_BACKUP_ID} of size`) + ); + assert.include(output, `using encryption key ${key.name}`); + }); + + // copy_backup + it('should create a copy of a backup', async () => { + const sourceBackupPath = `projects/${PROJECT_ID}/instances/${INSTANCE_ID}/backups/${BACKUP_ID}`; + const output = execSync( + `node backups-copy.js ${INSTANCE_ID} ${COPY_BACKUP_ID} ${sourceBackupPath} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp(`(.*)Backup copy(.*)${COPY_BACKUP_ID} of size(.*)`) + ); + }); + + // cancel_backup + it('should cancel a backup of the database', async () => { + const output = execSync( + `${backupsCmd} cancelBackup ${INSTANCE_ID} ${DATABASE_ID} ${CANCELLED_BACKUP_ID} ${PROJECT_ID}` + ); + assert.match(output, /Backup cancelled./); + }); + + // get_backups + it('should list backups in the instance', async () => { + const output = execSync( + `${backupsCmd} getBackups ${INSTANCE_ID} ${DATABASE_ID} ${BACKUP_ID} ${PROJECT_ID}` + ); + assert.include(output, 'All backups:'); + assert.include(output, 'Backups matching backup name:'); + assert.include(output, 'Backups expiring within 30 days:'); + assert.include(output, 'Backups matching database name:'); + assert.include(output, 'Backups filtered by size:'); + assert.include(output, 'Ready backups filtered by create time:'); + assert.include(output, 'Get backups paginated:'); + const count = (output.match(new RegExp(`${BACKUP_ID}`, 'g')) || []) + .length; + assert.equal(count, 14); + }); + + // list_backup_operations + it('should list backup operations in the instance', async () => { + const output = execSync( + `${backupsCmd} getBackupOperations ${INSTANCE_ID} ${DATABASE_ID} ${BACKUP_ID} ${PROJECT_ID}` + ); + assert.match(output, /Create Backup Operations:/); + assert.match( + output, + new RegExp(`Backup (.+)${BACKUP_ID} (.+) is 100% complete`) + ); + assert.match(output, /Copy Backup Operations:/); + assert.match( + output, + new RegExp(`Backup (.+)${COPY_BACKUP_ID} (.+) is 100% complete`) + ); + }); + + // update_backup_expire_time + it('should update the expire time of a backup', async () => { + const output = execSync( + `${backupsCmd} updateBackup ${INSTANCE_ID} ${BACKUP_ID} ${PROJECT_ID}` + ); + assert.match(output, /Expire time updated./); + }); + + // restore_backup + it('should restore database from a backup', async function () { + // Restoring a backup can be a slow operation so the test may timeout and + // we'll have to retry. + this.retries(3); + // Delay the start of the test, if this is a retry. + await delay(this.test, async () => { + await cleanupDatabase(INSTANCE_ID, RESTORE_DATABASE_ID); + }); + + const output = execSync( + `${backupsCmd} restoreBackup ${INSTANCE_ID} ${RESTORE_DATABASE_ID} ${BACKUP_ID} ${PROJECT_ID}` + ); + assert.match(output, /Database restored from backup./); + assert.match( + output, + new RegExp( + `Database (.+) was restored to ${RESTORE_DATABASE_ID} from backup ` + + `(.+)${BACKUP_ID} with version time (.+)` + ) + ); + }); + + // restore_backup_with_encryption_key + it('should restore database from a backup using an encryption key', async function () { + // Restoring a backup can be a slow operation so the test may timeout and + // we'll have to retry. + this.retries(3); + // Delay the start of the test, if this is a retry. + await delay(this.test, async () => { + await cleanupDatabase(INSTANCE_ID, ENCRYPTED_RESTORE_DATABASE_ID); + }); + + const key = await getCryptoKey(KEY_LOCATION_ID1); + + const output = execSync( + `${backupsCmd} restoreBackupWithEncryptionKey ${INSTANCE_ID} ${ENCRYPTED_RESTORE_DATABASE_ID} ${ENCRYPTED_BACKUP_ID} ${PROJECT_ID} ${key.name}` + ); + assert.match(output, /Database restored from backup./); + assert.match( + output, + new RegExp( + `Database (.+) was restored to ${ENCRYPTED_RESTORE_DATABASE_ID} from backup ` + + `(.+)${ENCRYPTED_BACKUP_ID} using encryption key ${key.name}` + ) + ); + }); + + // list_database_operations + it('should list database operations in the instance', async () => { + const output = execSync( + `${backupsCmd} getDatabaseOperations ${INSTANCE_ID} ${PROJECT_ID}` + ); + assert.match(output, /Optimize Database Operations:/); + assert.match( + output, + new RegExp( + `Database (.+)${RESTORE_DATABASE_ID} restored from backup is (\\d+)% ` + + 'optimized' + ) + ); + }); + + // delete_backup + it('should delete a backup', async () => { + function sleep(timeMillis) { + return new Promise(resolve => setTimeout(resolve, timeMillis)); + } + + // Wait for database to finish optimizing - cannot delete a backup if a database restored from it + const instance = spanner.instance(INSTANCE_ID); + const database = instance.database(RESTORE_DATABASE_ID); + while ((await database.getState()) === 'READY_OPTIMIZING') { + await sleep(1000); + } + + const output = execSync( + `${backupsCmd} deleteBackup ${INSTANCE_ID} ${BACKUP_ID} ${PROJECT_ID}` + ); + assert.match(output, /Backup deleted./); + }); + }); + + // custom_timeout_and_retry + it.skip('should insert with custom timeout and retry settings', async () => { + const output = execSync( + `${dmlCmd} insertWithCustomTimeoutAndRetrySettings ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, /record inserted./); + }); + + // get_commit_stats + it.skip('should update rows in Albums example table and return CommitStats', async () => { + const output = execSync( + `${crudCmd} getCommitStats ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, new RegExp('Updated data with (\\d+) mutations')); + }); + + // create_database_with_version_retention_period + it.skip('should create a database with a version retention period', async () => { + const output = execSync( + `${schemaCmd} createDatabaseWithVersionRetentionPeriod "${INSTANCE_ID}" "${VERSION_RETENTION_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Waiting for operation on ${VERSION_RETENTION_DATABASE_ID} to complete...` + ) + ); + assert.match( + output, + new RegExp( + `Created database ${VERSION_RETENTION_DATABASE_ID} with version retention period.` + ) + ); + assert.include(output, 'Version retention period: 1d'); + assert.include(output, 'Earliest version time:'); + }); + + it.skip('should create a table with foreign key delete cascade', async () => { + const output = execSync( + `${createTableWithForeignKeyDeleteCascadeCommand} "${INSTANCE_ID}" "${DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp(`Waiting for operation on ${DATABASE_ID} to complete...`) + ); + assert.match( + output, + new RegExp( + 'Created Customers and ShoppingCarts table with FKShoppingCartsCustomerId' + ) + ); + }); + + it.skip('should alter a table with foreign key delete cascade', async () => { + const output = execSync( + `${alterTableWithForeignKeyDeleteCascadeCommand} "${INSTANCE_ID}" "${DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp(`Waiting for operation on ${DATABASE_ID} to complete...`) + ); + assert.match( + output, + new RegExp('Altered ShoppingCarts table with FKShoppingCartsCustomerName') + ); + }); + + it.skip('should drop a foreign key constraint delete cascade', async () => { + const output = execSync( + `${dropForeignKeyConstraintDeleteCascaseCommand} "${INSTANCE_ID}" "${DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp(`Waiting for operation on ${DATABASE_ID} to complete...`) + ); + assert.match( + output, + new RegExp( + 'Altered ShoppingCarts table to drop FKShoppingCartsCustomerName' + ) + ); + }); + + describe.skip('observability', () => { + it('traces', () => { + const output = execSync( + `${traceObservabilityCommand} ${PROJECT_ID} ${INSTANCE_ID} ${DATABASE_ID}` + ); + assert.match(output, /Query: \d+ found./); + }); + }); + + describe.skip('leader options', () => { + before(async () => { + const instance = spanner.instance(SAMPLE_INSTANCE_ID); + const [, operation] = await instance.create({ + config: 'nam6', + nodes: 1, + displayName: 'Multi-region options test', + labels: { + ['cloud_spanner_samples']: 'true', + created: Math.round(Date.now() / 1000).toString(), // current time + }, + }); + await operation.promise(); + }); + + after(async () => { + const instance = spanner.instance(SAMPLE_INSTANCE_ID); + await instance.delete(); + }); + + // create_instance_config + it('should create an example custom instance config', async () => { + const output = execSync( + `node instance-config-create.js ${SAMPLE_INSTANCE_CONFIG_ID} ${BASE_INSTANCE_CONFIG_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Waiting for create operation for ${SAMPLE_INSTANCE_CONFIG_ID} to complete...` + ) + ); + assert.match( + output, + new RegExp(`Created instance config ${SAMPLE_INSTANCE_CONFIG_ID}.`) + ); + }); + + // update_instance_config + it('should update an example custom instance config', async () => { + const output = execSync( + `node instance-config-update.js ${SAMPLE_INSTANCE_CONFIG_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Waiting for update operation for ${SAMPLE_INSTANCE_CONFIG_ID} to complete...` + ) + ); + assert.match( + output, + new RegExp(`Updated instance config ${SAMPLE_INSTANCE_CONFIG_ID}.`) + ); + }); + + // delete_instance_config + it('should delete an example custom instance config', async () => { + const output = execSync( + `node instance-config-delete.js ${SAMPLE_INSTANCE_CONFIG_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp(`Deleting ${SAMPLE_INSTANCE_CONFIG_ID}...`) + ); + assert.match( + output, + new RegExp(`Deleted instance config ${SAMPLE_INSTANCE_CONFIG_ID}.`) + ); + }); + + // list_instance_config_operations + it('should list all instance config operations', async () => { + const output = execSync( + `node instance-config-get-operations.js ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Available instance config operations for project ${PROJECT_ID}:` + ) + ); + assert.include(output, 'Instance config operation for'); + assert.include( + output, + 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceConfigMetadata' + ); + }); + + // list_instance_configs + it('should list available instance configs', async () => { + const output = execSync(`node list-instance-configs.js ${PROJECT_ID}`); + assert.match( + output, + new RegExp(`Available instance configs for project ${PROJECT_ID}:`) + ); + assert.include(output, 'Available leader options for instance config'); + }); + + // get_instance_config + // TODO: Enable when the feature has been released. + it('should get a specific instance config', async () => { + const output = execSync(`node get-instance-config.js ${PROJECT_ID}`); + assert.include(output, 'Available leader options for instance config'); + }); + + // create_database_with_default_leader + it('should create a database with a default leader', async () => { + const output = execSync( + `node database-create-with-default-leader.js "${SAMPLE_INSTANCE_ID}" "${DEFAULT_LEADER_DATABASE_ID}" "${DEFAULT_LEADER}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Waiting for creation of ${DEFAULT_LEADER_DATABASE_ID} to complete...` + ) + ); + assert.match( + output, + new RegExp( + `Created database ${DEFAULT_LEADER_DATABASE_ID} with default leader ${DEFAULT_LEADER}.` + ) + ); + }); + + // update_database_with_default_leader + it('should update a database with a default leader', async () => { + const output = execSync( + `node database-update-default-leader.js "${SAMPLE_INSTANCE_ID}" "${DEFAULT_LEADER_DATABASE_ID}" "${DEFAULT_LEADER_2}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Waiting for updating of ${DEFAULT_LEADER_DATABASE_ID} to complete...` + ) + ); + assert.match( + output, + new RegExp( + `Updated database ${DEFAULT_LEADER_DATABASE_ID} with default leader ${DEFAULT_LEADER_2}.` + ) + ); + }); + + // get_default_leader + it('should get the default leader option of a database', async () => { + const output = execSync( + `node database-get-default-leader.js "${SAMPLE_INSTANCE_ID}" "${DEFAULT_LEADER_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.include( + output, + `The default_leader for ${DEFAULT_LEADER_DATABASE_ID} is ${DEFAULT_LEADER_2}` + ); + }); + + // list_databases + it('should list databases on the instance', async () => { + const output = execSync( + `node list-databases.js "${SAMPLE_INSTANCE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Databases for projects/${PROJECT_ID}/instances/${SAMPLE_INSTANCE_ID}:` + ) + ); + assert.include(output, `(default leader = ${DEFAULT_LEADER_2}`); + }); + + // get_database_ddl + it('should get the ddl of a database', async () => { + const output = execSync( + `node database-get-ddl.js "${SAMPLE_INSTANCE_ID}" "${DEFAULT_LEADER_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Retrieved database DDL for projects/${PROJECT_ID}/instances/${SAMPLE_INSTANCE_ID}/databases/${DEFAULT_LEADER_DATABASE_ID}:` + ) + ); + assert.include(output, 'CREATE TABLE Singers'); + }); + + // max_commit_delay + it('should update rows in Albums example table when max commit delay is set', async () => { + const output = execSync( + `node max-commit-delay.js "${INSTANCE_ID}" "${DATABASE_ID}" "${PROJECT_ID}"` + ); + assert.match( + output, + new RegExp( + 'Successfully inserted (\\d+) record into the Singers table.' + ) + ); + }); + }); + + describe.skip('sequence', () => { + before(async () => { + const instance = spanner.instance(INSTANCE_ID); + const database = instance.database(SEQUENCE_DATABASE_ID); + const [, operation_seq] = await database.create(); + await operation_seq.promise(); + }); + + after(async () => { + await spanner + .instance(INSTANCE_ID) + .database(SEQUENCE_DATABASE_ID) + .delete(); + }); + + // create_sequence + it('should create a sequence', async () => { + const output = execSync( + `node sequence-create.js "${INSTANCE_ID}" "${SEQUENCE_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('Created Seq sequence and Customers table') + ); + assert.match( + output, + new RegExp('Number of customer records inserted is: 3') + ); + }); + + // alter_sequence + it('should alter a sequence', async () => { + const output = execSync( + `node sequence-alter.js "${INSTANCE_ID}" "${SEQUENCE_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + 'Altered Seq sequence to skip an inclusive range between 1000 and 5000000.' + ) + ); + assert.match( + output, + new RegExp('Number of customer records inserted is: 3') + ); + }); + + // drop_sequence + it('should drop a sequence', async () => { + const output = execSync( + `node sequence-drop.js "${INSTANCE_ID}" "${SEQUENCE_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + 'Altered Customers table to drop DEFAULT from CustomerId column and dropped the Seq sequence.' + ) + ); + }); + }); + + describe.skip('instance partition', () => { + before(async () => { + const instance = spanner.instance(SAMPLE_INSTANCE_ID); + const [, operation] = await instance.create({ + config: 'regional-us-central1', + nodes: 1, + edition: 'ENTERPRISE_PLUS', + displayName: 'Instance partitions test', + labels: { + ['cloud_spanner_samples']: 'true', + created: Math.round(Date.now() / 1000).toString(), // current time + }, + }); + await operation.promise(); + }); + + after(async () => { + const instance = spanner.instance(SAMPLE_INSTANCE_ID); + await instance.delete(); + }); + + // create_instance_partition + it('should create an instance partition', async () => { + const output = execSync( + `node instance-partition-create.js "${SAMPLE_INSTANCE_ID}" "my-instance-partition" "${PROJECT_ID}"` + ); + assert.match( + output, + new RegExp( + 'Waiting for operation on my-instance-partition to complete...' + ) + ); + assert.match( + output, + new RegExp('Created instance partition my-instance-partition.') + ); + }); + }); + + describe.skip('postgreSQL', () => { + before(async () => { + const instance = spanner.instance(SAMPLE_INSTANCE_ID); + const [, operation] = await instance.create({ + config: PG_LOCATION_ID, + nodes: 1, + displayName: 'PostgreSQL Test', + labels: { + ['cloud_spanner_samples']: 'true', + created: Math.round(Date.now() / 1000).toString(), // current time + }, + }); + await operation.promise(); + }); + + after(async () => { + const instance = spanner.instance(SAMPLE_INSTANCE_ID); + await instance.delete(); + }); + + // create_pg_database + it('should create an example PostgreSQL database', async () => { + const output = execSync( + `node pg-database-create.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp(`Waiting for operation on ${PG_DATABASE_ID} to complete...`) + ); + assert.match( + output, + new RegExp( + `Created database ${PG_DATABASE_ID} on instance ${SAMPLE_INSTANCE_ID} with dialect POSTGRESQL.` + ) + ); + }); + + // pg_interleaving + it('should create an interleaved table hierarchy using PostgreSQL dialect', async () => { + const output = execSync( + `node pg-interleaving.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp(`Waiting for operation on ${PG_DATABASE_ID} to complete...`) + ); + assert.match( + output, + new RegExp( + `Created an interleaved table hierarchy in database ${PG_DATABASE_ID} using PostgreSQL dialect.` + ) + ); + }); + + // pg_dml_with_parameter + it('should execute a DML statement with parameters on a Spanner PostgreSQL database', async () => { + const output = execSync( + `node pg-dml-with-parameter.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('Successfully executed 1 postgreSQL statements using DML') + ); + }); + + // pg_dml_batch + it('should execute a batch of DML statements on a Spanner PostgreSQL database', async () => { + const output = execSync( + `node pg-dml-batch.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + 'Successfully executed 3 postgreSQL statements using Batch DML.' + ) + ); + }); + + // pg_dml_partitioned + it('should execute a partitioned DML on a Spanner PostgreSQL database', async () => { + const output = execSync( + `node pg-dml-partitioned.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, new RegExp('Successfully deleted 1 record.')); + }); + + // pg_query_with_parameters + it('should execute a query with parameters on a Spanner PostgreSQL database.', async () => { + const output = execSync( + `node pg-query-parameter.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('SingerId: 1, FirstName: Alice, LastName: Henderson') + ); + }); + + // pg_dml_update + it('should update a table using parameterized queries on a Spanner PostgreSQL database.', async () => { + const output = execSync( + `node pg-dml-getting-started-update.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('Successfully updated 1 record in the Singers table.') + ); + }); + + // pg_add_column + it('should add a column to a table in the Spanner PostgreSQL database.', async () => { + const output = execSync( + `node pg-add-column.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Added MarketingBudget column to Albums table in database ${PG_DATABASE_ID}` + ) + ); + }); + + //pg_create_index + it('should create an index in the Spanner PostgreSQL database.', async () => { + const output = execSync( + `node pg-index-create-storing.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, new RegExp('Added the AlbumsByAlbumTitle index.')); + }); + + // pg_schema_information + it('should query the information schema metadata in a Spanner PostgreSQL database', async () => { + const output = execSync( + `node pg-schema-information.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, new RegExp('Table: public.albums')); + assert.match(output, new RegExp('Table: public.author')); + assert.match(output, new RegExp('Table: public.book')); + assert.match(output, new RegExp('Table: public.singers')); + }); + + // pg_ordering_nulls + it('should order nulls as per clause in a Spanner PostgreSQL database', async () => { + const output = execSync( + `node pg-ordering-nulls.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, new RegExp('Author ORDER BY FirstName')); + assert.match(output, new RegExp('Author ORDER BY FirstName DESC')); + assert.match(output, new RegExp('Author ORDER BY FirstName NULLS FIRST')); + assert.match( + output, + new RegExp('Author ORDER BY FirstName DESC NULLS LAST') + ); + }); + + // pg_numeric_data_type + it('should create a table, insert and query pg numeric data', async () => { + const output = execSync( + `node pg-numeric-data-type.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp(`Waiting for operation on ${PG_DATABASE_ID} to complete...`) + ); + assert.match( + output, + new RegExp(`Added table venues to database ${PG_DATABASE_ID}.`) + ); + assert.match(output, new RegExp('Inserted data.')); + assert.match(output, new RegExp('VenueId: 4, Revenue: 97372.3863')); + assert.match(output, new RegExp('VenueId: 19, Revenue: 7629')); + assert.match(output, new RegExp('VenueId: 398, Revenue: 0.000000123')); + }); + + // pg_jsonb_add_column + it('should add a jsonb column to a table', async () => { + const output = execSync( + `node pg-jsonb-add-column.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp(`Waiting for operation on ${PG_DATABASE_ID} to complete...`) + ); + assert.match( + output, + new RegExp( + `Added jsonb column to table venues to database ${PG_DATABASE_ID}.` + ) + ); + }); + + // pg_jsonb_insert_data + it('should insert pg jsonb data', async () => { + const output = execSync( + `node pg-jsonb-update-data.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, new RegExp('Updated data.')); + }); + + // pg_jsonb_query_data + it('should query pg jsonb data', async () => { + const output = execSync( + `node pg-jsonb-query-parameter.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('VenueId: 19, Details: {"value":{"open":true,"rating":9}}') + ); + }); + + // pg_case_sensitivity + it('should create case sensitive table and query the information in a Spanner PostgreSQL database', async () => { + const output = execSync( + `node pg-case-sensitivity.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Created table with case sensitive names in database ${PG_DATABASE_ID} using PostgreSQL dialect.` + ) + ); + assert.match(output, new RegExp('Inserted data using mutations.')); + assert.match(output, new RegExp('Concerts Table Data using Mutations:')); + assert.match(output, new RegExp('Concerts Table Data using Aliases:')); + assert.match(output, new RegExp('Inserted data using DML.')); + }); + + // pg_datatypes_casting + it('should use cast operator to cast from one data type to another in a Spanner PostgreSQL database', async () => { + const output = execSync( + `node pg-datatypes-casting.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, new RegExp('Data types after casting')); + }); + + // pg_functions + it('should call a server side function on a Spanner PostgreSQL database.', async () => { + const output = execSync( + `node pg-functions.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, new RegExp('1284352323 seconds after epoch is')); + }); + + // pg_dml_returning_insert + it('should insert records using DML Returning in a Spanner PostgreSQL database', async () => { + const output = execSync( + `node pg-dml-returning-insert ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('Successfully inserted 1 record into the Singers table') + ); + assert.match(output, new RegExp('Virginia Watson')); + }); + + // pg_dml_returning_update + it('should update records using DML Returning in a Spanner PostgreSQL database', async () => { + const output = execSync( + `node pg-dml-returning-update ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('Successfully updated 1 record into the Singers table') + ); + assert.match(output, new RegExp('Virginia1 Watson1')); + }); + + // pg_dml_returning_delete + it('should delete records using DML Returning in a Spanner PostgreSQL database', async () => { + const output = execSync( + `node pg-dml-returning-delete ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('Successfully deleted 1 record from the Singers table') + ); + assert.match(output, new RegExp('Virginia1 Watson1')); + }); + + // pg_create_sequence + it('should create a sequence', async () => { + const output = execSync( + `node pg-sequence-create.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp('Created Seq sequence and Customers table') + ); + assert.match( + output, + new RegExp('Number of customer records inserted is: 3') + ); + }); + + // pg_alter_sequence + it('should alter a sequence', async () => { + const output = execSync( + `node pg-sequence-alter.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + 'Altered Seq sequence to skip an inclusive range between 1000 and 5000000.' + ) + ); + assert.match( + output, + new RegExp('Number of customer records inserted is: 3') + ); + }); + + // pg_drop_sequence + it('should drop a sequence', async () => { + const output = execSync( + `node pg-sequence-drop.js ${SAMPLE_INSTANCE_ID} ${PG_DATABASE_ID} ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + 'Altered Customers table to drop DEFAULT from CustomerId column and dropped the Seq sequence.' + ) + ); + }); + + // directed_read_options + it('should run read-only transaction with directed read options set', async () => { + const output = execSync( + `node directed-reads.js ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + console.log(output); + assert.match( + output, + new RegExp( + 'SingerId: 2, AlbumId: 2, AlbumTitle: Forever Hold your Peace' + ) + ); + assert.match( + output, + new RegExp( + 'Successfully executed read-only transaction with directedReadOptions' + ) + ); + }); + }); + + describe.skip('proto columns', () => { + before(async () => { + // Setup database for Proto columns + const databaseAdminClient = spanner.getDatabaseAdminClient(); + const [operation] = await databaseAdminClient.createDatabase({ + createStatement: 'CREATE DATABASE `' + PROTO_DATABASE_ID + '`', + extraStatements: [ + ` + CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + ) PRIMARY KEY (SingerId)`, + `CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE`, + ], + parent: databaseAdminClient.instancePath(PROJECT_ID, INSTANCE_ID), + }); + + console.log( + `Waiting for creation of ${PROTO_DATABASE_ID} to complete...` + ); + await operation.promise(); + console.log( + `Created database ${PROTO_DATABASE_ID} on instance ${INSTANCE_ID}.` + ); + + // Insert seed data into the database tables + execSync( + `${crudCmd} insert ${INSTANCE_ID} ${PROTO_DATABASE_ID} ${PROJECT_ID}` + ); + }); + + after(async () => { + await spanner.instance(INSTANCE_ID).database(PROTO_DATABASE_ID).delete(); + }); + + it('should add proto message and enum columns', async () => { + const output = execSync( + `node proto-type-add-column.js "${INSTANCE_ID}" "${PROTO_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Altered table "Singers" on database ${PROTO_DATABASE_ID} on instance ${INSTANCE_ID} with proto descriptors.` + ) + ); + }); + + it('update data with proto message and enum columns', async () => { + const output = execSync( + `node proto-update-data.js "${INSTANCE_ID}" "${PROTO_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match(output, new RegExp('Data updated')); + }); + + it('update data with proto message and enum columns using DML', async () => { + const output = execSync( + `node proto-update-data-dml.js "${INSTANCE_ID}" "${PROTO_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.include(output, '1 record updated.'); + }); + + it('query data with proto message and enum columns', async () => { + const output = execSync( + `node proto-query-data.js "${INSTANCE_ID}" "${PROTO_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match(output, new RegExp('SingerId: 2')); + }); + }); + + // Skipping KMS test suite as tests are getting timed out frequently. + describe.skip('encrypted database and backups with multiple KMS keys', () => { + const MR_CMEK_DB = `test-mr-${CURRENT_TIME}-db`; + const MR_CMEK_BACKUP = `test-mr-${CURRENT_TIME}-backup`; + const MR_CMEK_COPIED = `test-mr-${CURRENT_TIME}-copied`; + const MR_CMEK_RESTORED = `test-mr-${CURRENT_TIME}-restored`; + let instance_already_exists = false; + let key1, key2, key3; + let shouldCleanup = false; + before(async function () { + if ( + SKIP_BACKUPS === 'true' || + (KOKORO_JOB_NAME && KOKORO_JOB_NAME.includes('presubmit')) + ) { + this.skip(); + } + shouldCleanup = true; + // Create multiple KMS keys covering `nam3`. + key1 = await getCryptoKey(KEY_LOCATION_ID1); + key2 = await getCryptoKey(KEY_LOCATION_ID2); + key3 = await getCryptoKey(KEY_LOCATION_ID3); + + const multi_region_instance = spanner.instance(MULTI_REGION_INSTANCE_ID); + [instance_already_exists] = await multi_region_instance.exists(); + if (!instance_already_exists) { + const [, operation] = await multi_region_instance.create({ + config: MULTI_REGION_LOCATION_ID, + nodes: 1, + labels: { + [LABEL]: 'true', + created: CURRENT_TIME, + }, + gaxOptions: GAX_OPTIONS, + }); + await operation.promise(); + console.log( + `Created temp instance, using + ${multi_region_instance.formattedName_}...` + ); + } else { + console.log( + `Not creating temp instance, using + ${multi_region_instance.formattedName_}...` + ); + } + }); + + after(async () => { + if (!shouldCleanup) { + return; + } + const instance = spanner.instance(MULTI_REGION_INSTANCE_ID); + const restored_db = instance.database(MR_CMEK_RESTORED); + function sleep(timeMillis) { + return new Promise(resolve => setTimeout(resolve, timeMillis)); + } + // Backup cannot be deleted when restored db is not in READY_OPTIMIZING state. + while ((await restored_db.getState()) === 'READY_OPTIMIZING') { + await sleep(1000); + } + await Promise.all([ + instance.database(MR_CMEK_DB).delete(GAX_OPTIONS), + instance.database(MR_CMEK_RESTORED).delete(), + instance.backup(MR_CMEK_BACKUP).delete(GAX_OPTIONS), + instance.backup(MR_CMEK_COPIED).delete(GAX_OPTIONS), + ]); + if (!instance_already_exists) { + await spanner.instance(MULTI_REGION_INSTANCE_ID).delete(GAX_OPTIONS); + } + }); + + it('should create a database with multiple KMS keys', async () => { + const output = execSync( + `node database-create-with-multiple-kms-keys.js \ + "${MULTI_REGION_INSTANCE_ID}" \ + "${MR_CMEK_DB}" \ + "${PROJECT_ID}" \ + "${key1.name},${key2.name},${key3.name}"` + ); + assert.match( + output, + new RegExp(`Waiting for operation on ${MR_CMEK_DB} to complete...`) + ); + assert.match( + output, + new RegExp( + `Created database ${MR_CMEK_DB} on instance ${MULTI_REGION_INSTANCE_ID}.` + ) + ); + assert.match(output, new RegExp('Database encrypted with keys')); + }); + + it('should create backup with multiple KMS keys', async () => { + const output = execSync( + `node backups-create-with-multiple-kms-keys.js \ + ${MULTI_REGION_INSTANCE_ID} \ + ${MR_CMEK_DB} \ + ${MR_CMEK_BACKUP} \ + ${PROJECT_ID} \ + "${key1.name},${key2.name},${key3.name}"` + ); + assert.match(output, new RegExp(`Backup (.+)${MR_CMEK_BACKUP} of size`)); + assert.include(output, 'using encryption key'); + }); + + it('should copy backup with multiple KMS keys', async () => { + const sourceBackupPath = `projects/${PROJECT_ID}/instances/${MULTI_REGION_INSTANCE_ID}/backups/${MR_CMEK_BACKUP}`; + const output = execSync( + `node backups-copy-with-multiple-kms-keys.js \ + ${MULTI_REGION_INSTANCE_ID} \ + ${MR_CMEK_COPIED} \ + ${sourceBackupPath} \ + ${PROJECT_ID} \ + "${key1.name},${key2.name},${key3.name}"` + ); + assert.match( + output, + new RegExp(`(.*)Backup copy(.*)${MR_CMEK_COPIED} of size(.*)`) + ); + }); + + it('should restore backup with multiple KMS keys', async function () { + // Restoring a backup can be a slow operation so the test may timeout and + // we'll have to retry. + this.retries(5); + // Delay the start of the test, if this is a retry. + await delay(this.test, async () => { + await cleanupDatabase(MULTI_REGION_INSTANCE_ID, MR_CMEK_RESTORED); + }); + + const output = execSync( + `node backups-restore-with-multiple-kms-keys.js \ + ${MULTI_REGION_INSTANCE_ID} \ + ${MR_CMEK_RESTORED} \ + ${MR_CMEK_BACKUP} \ + ${PROJECT_ID} \ + "${key1.name},${key2.name},${key3.name}"` + ); + assert.match(output, /Database restored from backup./); + assert.match( + output, + new RegExp( + `Database (.+) was restored to ${MR_CMEK_RESTORED} from backup ` + + `(.+)${MR_CMEK_BACKUP}.` + ) + ); + }); + + // add split points, enable when drop database automatically reclaims quota + it.skip('should add split points', async () => { + const output = execSync( + `node database-add-split-points.js ${INSTANCE_ID} ${DATABASE_ID} ${PROJECT_ID}` + ); + assert.match(output, new RegExp('Added Split Points')); + }); + }); +}); diff --git a/storage/getBucketEncryptionEnforcementConfig.js b/storage/getBucketEncryptionEnforcementConfig.js new file mode 100644 index 0000000000..aa791ab1bb --- /dev/null +++ b/storage/getBucketEncryptionEnforcementConfig.js @@ -0,0 +1,76 @@ +// Copyright 2026 Google LLC +// +// 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. + +'use strict'; + +// sample-metadata: +// title: Get Bucket Encryption Enforcement +// description: Retrieves the current encryption enforcement configurations for a bucket. +// usage: node getBucketEncryptionEnforcementConfig.js + +function main(bucketName = 'my-bucket') { + // [START storage_get_encryption_enforcement_config] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function getBucketEncryptionEnforcementConfig() { + const [metadata] = await storage.bucket(bucketName).getMetadata(); + + console.log( + `Encryption enforcement configuration for bucket ${bucketName}.` + ); + const enc = metadata.encryption; + if (!enc) { + console.log( + 'No encryption configuration found (Default GMEK is active).' + ); + return; + } + console.log(`Default KMS Key: ${enc.defaultKmsKeyName || 'None'}`); + + const printConfig = (label, config) => { + if (config) { + console.log(`${label}:`); + console.log(` Mode: ${config.restrictionMode}`); + console.log(` Effective: ${config.effectiveTime}`); + } + }; + + printConfig( + 'Google Managed (GMEK) Enforcement', + enc.googleManagedEncryptionEnforcementConfig + ); + printConfig( + 'Customer Managed (CMEK) Enforcement', + enc.customerManagedEncryptionEnforcementConfig + ); + printConfig( + 'Customer Supplied (CSEK) Enforcement', + enc.customerSuppliedEncryptionEnforcementConfig + ); + } + + getBucketEncryptionEnforcementConfig().catch(console.error); + // [END storage_get_encryption_enforcement_config] +} +main(...process.argv.slice(2)); diff --git a/storage/setBucketEncryptionEnforcementConfig.js b/storage/setBucketEncryptionEnforcementConfig.js new file mode 100644 index 0000000000..625ada0629 --- /dev/null +++ b/storage/setBucketEncryptionEnforcementConfig.js @@ -0,0 +1,93 @@ +// Copyright 2026 Google LLC +// +// 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. + +'use strict'; + +// sample-metadata: +// title: Set Bucket Encryption Enforcement +// description: Configures a bucket to enforce specific encryption types (e.g., CMEK-only). +// usage: node setBucketEncryptionEnforcementConfig.js + +function main( + bucketName = 'my-bucket', + defaultKmsKeyName = process.env.GOOGLE_CLOUD_KMS_KEY_ASIA +) { + // [START storage_set_encryption_enforcement_config] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // The name of the KMS key to be used as the default + // const defaultKmsKeyName = 'my-key'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function setBucketEncryptionEnforcementConfig() { + const options = { + encryption: { + defaultKmsKeyName, + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + customerSuppliedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + customerManagedEncryptionEnforcementConfig: { + restrictionMode: 'NotRestricted', + }, + }, + }; + + const [metadata] = await storage.bucket(bucketName).setMetadata(options); + + console.log( + `Encryption enforcement configuration updated for bucket ${bucketName}.` + ); + const enc = metadata.encryption; + if (enc) { + console.log(`Default KMS Key: ${enc.defaultKmsKeyName}`); + + const logEnforcement = (label, config) => { + if (config) { + console.log(`${label}:`); + console.log(` Mode: ${config.restrictionMode}`); + console.log(` Effective: ${config.effectiveTime}`); + } + }; + + logEnforcement( + 'Google Managed (GMEK) Enforcement', + enc.googleManagedEncryptionEnforcementConfig + ); + logEnforcement( + 'Customer Managed (CMEK) Enforcement', + enc.customerManagedEncryptionEnforcementConfig + ); + logEnforcement( + 'Customer Supplied (CSEK) Enforcement', + enc.customerSuppliedEncryptionEnforcementConfig + ); + } + } + + setBucketEncryptionEnforcementConfig().catch(console.error); + // [END storage_set_encryption_enforcement_config] +} +main(...process.argv.slice(2)); diff --git a/storage/system-test/buckets.test.js b/storage/system-test/buckets.test.js new file mode 100644 index 0000000000..c9438e9a6f --- /dev/null +++ b/storage/system-test/buckets.test.js @@ -0,0 +1,112 @@ +// Copyright 2019 Google LLC +// +// 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. + +'use strict'; + +const {Storage} = require('@google-cloud/storage'); +const {assert} = require('chai'); +const {before, after, afterEach, it} = require('mocha'); +const cp = require('child_process'); +const uuid = require('uuid'); + +const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'}); + +const storage = new Storage(); +const samplesTestBucketPrefix = `nodejs-storage-samples-${uuid.v4()}`; +const bucketName = `${samplesTestBucketPrefix}-a`; +const defaultKmsKeyName = process.env.GOOGLE_CLOUD_KMS_KEY_ASIA; +const bucket = storage.bucket(bucketName); + +before(async () => { + await storage.createBucket(bucketName); +}); + +async function deleteAllBucketsAsync() { + const [buckets] = await storage.getBuckets({prefix: samplesTestBucketPrefix}); + + for (const bucket of buckets) { + await bucket.deleteFiles({force: true}); + await bucket.delete({ignoreNotFound: true}); + } +} + +after(deleteAllBucketsAsync); +afterEach(async () => { + await new Promise(res => setTimeout(res, 1000)); +}); + +it('should set bucket encryption enforcement configuration', async () => { + const output = execSync( + `node setBucketEncryptionEnforcementConfig.js ${bucketName} ${defaultKmsKeyName}` + ); + + assert.include( + output, + `Encryption enforcement configuration updated for bucket ${bucketName}.` + ); + + assert.include(output, `Default KMS Key: ${defaultKmsKeyName}`); + + assert.include(output, 'Google Managed (GMEK) Enforcement:'); + assert.include(output, 'Mode: FullyRestricted'); + + assert.include(output, 'Customer Managed (CMEK) Enforcement:'); + assert.include(output, 'Mode: NotRestricted'); + + assert.include(output, 'Customer Supplied (CSEK) Enforcement:'); + assert.include(output, 'Mode: FullyRestricted'); + + assert.match(output, new RegExp('Effective:')); + + const [metadata] = await bucket.getMetadata(); + assert.strictEqual( + metadata.encryption.googleManagedEncryptionEnforcementConfig + .restrictionMode, + 'FullyRestricted' + ); +}); + +it('should get bucket encryption enforcement configuration', async () => { + const output = execSync( + `node getBucketEncryptionEnforcementConfig.js ${bucketName}` + ); + + assert.include( + output, + `Encryption enforcement configuration for bucket ${bucketName}.` + ); + assert.include(output, `Default KMS Key: ${defaultKmsKeyName}`); + + assert.include(output, 'Google Managed (GMEK) Enforcement:'); + assert.include(output, 'Mode: FullyRestricted'); + assert.match(output, /Effective:/); +}); + +it('should update and then remove bucket encryption enforcement configuration', async () => { + const output = execSync( + `node updateBucketEncryptionEnforcementConfig.js ${bucketName}` + ); + + assert.include( + output, + `Google-managed encryption enforcement set to FullyRestricted for ${bucketName}.` + ); + assert.include( + output, + `All encryption enforcement configurations removed from bucket ${bucketName}.` + ); + + const [metadata] = await bucket.getMetadata(); + assert.ok(!metadata.encryption); +}); diff --git a/storage/updateBucketEncryptionEnforcementConfig.js b/storage/updateBucketEncryptionEnforcementConfig.js new file mode 100644 index 0000000000..1bf5feb839 --- /dev/null +++ b/storage/updateBucketEncryptionEnforcementConfig.js @@ -0,0 +1,74 @@ +// Copyright 2026 Google LLC +// +// 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. + +'use strict'; + +// sample-metadata: +// title: Update Bucket Encryption Enforcement Config +// description: Updates and then removes encryption enforcement configurations from a bucket. +// usage: node updateBucketEncryptionEnforcementConfig.js + +function main(bucketName = 'my-bucket') { + // [START storage_update_bucket_encryption_enforcement_config] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // The ID of your GCS bucket + // const bucketName = 'your-unique-bucket-name'; + + // Imports the Google Cloud client library + const {Storage} = require('@google-cloud/storage'); + + // Creates a client + const storage = new Storage(); + + async function updateBucketEncryptionEnforcementConfig() { + const bucket = storage.bucket(bucketName); + + // Update a specific encryption type's restriction mode + // This partial update preserves other existing encryption settings. + const updateOptions = { + encryption: { + googleManagedEncryptionEnforcementConfig: { + restrictionMode: 'FullyRestricted', + }, + }, + }; + + await bucket.setMetadata(updateOptions); + console.log( + `Google-managed encryption enforcement set to FullyRestricted for ${bucketName}.` + ); + + // Remove all encryption enforcement configurations altogether + // Setting these values to null removes the policies from the bucket metadata. + const clearOptions = { + encryption: { + defaultKmsKeyName: null, + googleManagedEncryptionEnforcementConfig: null, + customerSuppliedEncryptionEnforcementConfig: null, + customerManagedEncryptionEnforcementConfig: null, + }, + }; + + await bucket.setMetadata(clearOptions); + console.log( + `All encryption enforcement configurations removed from bucket ${bucketName}.` + ); + } + + updateBucketEncryptionEnforcementConfig().catch(console.error); + // [END storage_update_bucket_encryption_enforcement_config] +} +main(...process.argv.slice(2));