diff --git a/.github/.OwlBot.lock.yaml b/.github/.OwlBot.lock.yaml index 6c41b3088..788f7a9fd 100644 --- a/.github/.OwlBot.lock.yaml +++ b/.github/.OwlBot.lock.yaml @@ -13,4 +13,4 @@ # limitations under the License. docker: image: gcr.io/cloud-devrel-public-resources/owlbot-nodejs:latest - digest: sha256:f59941869d508c6825deeffce180579545fd528f359f549a80a18ec0458d7094 + digest: sha256:fe04ae044dadf5ad88d979dbcc85e0e99372fb5d6316790341e6aca5e4e3fbc8 diff --git a/.jsdoc.js b/.jsdoc.js index b936415de..e37e0473a 100644 --- a/.jsdoc.js +++ b/.jsdoc.js @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ module.exports = { includePattern: '\\.js$' }, templates: { - copyright: 'Copyright 2022 Google LLC', + copyright: 'Copyright 2023 Google LLC', includeDate: false, sourceFiles: false, systemName: '@google-cloud/spanner', diff --git a/.kokoro/continuous/node12/system-test.cfg b/.kokoro/continuous/node12/system-test.cfg index 8beea6587..f8dd221bf 100644 --- a/.kokoro/continuous/node12/system-test.cfg +++ b/.kokoro/continuous/node12/system-test.cfg @@ -5,3 +5,8 @@ env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: "github/nodejs-spanner/.kokoro/system-test.sh" } + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/presubmit/node12/system-test.cfg b/.kokoro/presubmit/node12/system-test.cfg index 8beea6587..f8dd221bf 100644 --- a/.kokoro/presubmit/node12/system-test.cfg +++ b/.kokoro/presubmit/node12/system-test.cfg @@ -5,3 +5,8 @@ env_vars: { key: "TRAMPOLINE_BUILD_FILE" value: "github/nodejs-spanner/.kokoro/system-test.sh" } + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "long-door-651-kokoro-system-test-service-account" +} \ No newline at end of file diff --git a/.kokoro/system-test.sh b/.kokoro/system-test.sh index 87fa0653d..0201e9dfd 100755 --- a/.kokoro/system-test.sh +++ b/.kokoro/system-test.sh @@ -19,7 +19,7 @@ set -eo pipefail export NPM_CONFIG_PREFIX=${HOME}/.npm-global # Setup service account credentials. -export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/service-account.json +export GOOGLE_APPLICATION_CREDENTIALS=${KOKORO_GFILE_DIR}/secret_manager/long-door-651-kokoro-system-test-service-account export GCLOUD_PROJECT=long-door-651 cd $(dirname $0)/.. diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a56beaf..238eaad49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ [1]: https://www.npmjs.com/package/nodejs-spanner?activeTab=versions +## [6.7.0](https://github.com/googleapis/nodejs-spanner/compare/v6.6.0...v6.7.0) (2023-01-17) + + +### Features + +* Added SuggestConversationSummary RPC ([#1744](https://github.com/googleapis/nodejs-spanner/issues/1744)) ([14346f3](https://github.com/googleapis/nodejs-spanner/commit/14346f3cf8ed0cb0a93c255dc520dc62887c0e1a)) + ## [6.6.0](https://github.com/googleapis/nodejs-spanner/compare/v6.5.0...v6.6.0) (2022-12-16) diff --git a/README.md b/README.md index ef77aca62..587b4337e 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,9 @@ Samples are in the [`samples/`](https://github.com/googleapis/nodejs-spanner/tre | Gets the default leader option of an existing database | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/database-get-default-leader.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/database-get-default-leader.js,samples/README.md) | | Updates the default leader of an existing database | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/database-update-default-leader.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/database-update-default-leader.js,samples/README.md) | | Datatypes | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/datatypes.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/datatypes.js,samples/README.md) | +| Delete using DML returning. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml-returning-delete.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/dml-returning-delete.js,samples/README.md) | +| Insert using DML returning. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml-returning-insert.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/dml-returning-insert.js,samples/README.md) | +| Update using DML returning. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml-returning-update.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/dml-returning-update.js,samples/README.md) | | DML | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/dml.js,samples/README.md) | | Enable fine grained access control | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/enable-fine-grained-access.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/enable-fine-grained-access.js,samples/README.md) | | Get-commit-stats | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/get-commit-stats.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/get-commit-stats.js,samples/README.md) | @@ -143,6 +146,9 @@ Samples are in the [`samples/`](https://github.com/googleapis/nodejs-spanner/tre | Execute a batch of DML statements on a Spanner PostgreSQL database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-dml-batch.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/pg-dml-batch.js,samples/README.md) | | Updates data in a table in a Spanner PostgreSQL database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-dml-getting-started-update.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/pg-dml-getting-started-update.js,samples/README.md) | | Execute a Partitioned DML on a Spanner PostgreSQL database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-dml-partitioned.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/pg-dml-partitioned.js,samples/README.md) | +| Delete using DML returning on a Spanner PostgreSQL database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-dml-returning-delete.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/pg-dml-returning-delete.js,samples/README.md) | +| Insert using DML returning on a Spanner PostgreSQL database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-dml-returning-insert.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/pg-dml-returning-insert.js,samples/README.md) | +| Update using DML returning on a Spanner PostgreSQL database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-dml-returning-update.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/pg-dml-returning-update.js,samples/README.md) | | Execute a DML statement with parameters on a Spanner PostgreSQL database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-dml-with-parameter.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/pg-dml-with-parameter.js,samples/README.md) | | Calls a server side function on a Spanner PostgreSQL database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-functions.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/pg-functions.js,samples/README.md) | | Creates a new storing index in a Spanner PostgreSQL database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-index-create-storing.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/pg-index-create-storing.js,samples/README.md) | diff --git a/package.json b/package.json index fe3b0cc52..81488fedf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@google-cloud/spanner", "description": "Cloud Spanner Client Library for Node.js", - "version": "6.6.0", + "version": "6.7.0", "license": "Apache-2.0", "author": "Google Inc.", "engines": { @@ -109,7 +109,7 @@ "ncp": "^2.0.0", "p-limit": "^3.0.1", "proxyquire": "^2.0.1", - "sinon": "^14.0.0", + "sinon": "^15.0.0", "stats-lite": "^2.1.1", "time-span": "^4.0.0", "tmp": "^0.2.0", diff --git a/protos/protos.d.ts b/protos/protos.d.ts index 9e61c9850..91552afe3 100644 --- a/protos/protos.d.ts +++ b/protos/protos.d.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/protos/protos.js b/protos/protos.js index 82874db74..ad74f311f 100644 --- a/protos/protos.js +++ b/protos/protos.js @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/samples/README.md b/samples/README.md index 322bf9cc1..13160934b 100644 --- a/samples/README.md +++ b/samples/README.md @@ -36,6 +36,9 @@ and automatic, synchronous replication for high availability. * [Gets the default leader option of an existing database](#gets-the-default-leader-option-of-an-existing-database) * [Updates the default leader of an existing database](#updates-the-default-leader-of-an-existing-database) * [Datatypes](#datatypes) + * [Delete using DML returning.](#delete-using-dml-returning.) + * [Insert using DML returning.](#insert-using-dml-returning.) + * [Update using DML returning.](#update-using-dml-returning.) * [DML](#dml) * [Enable fine grained access control](#enable-fine-grained-access-control) * [Get-commit-stats](#get-commit-stats) @@ -68,6 +71,9 @@ and automatic, synchronous replication for high availability. * [Execute a batch of DML statements on a Spanner PostgreSQL database.](#execute-a-batch-of-dml-statements-on-a-spanner-postgresql-database.) * [Updates data in a table in a Spanner PostgreSQL database.](#updates-data-in-a-table-in-a-spanner-postgresql-database.) * [Execute a Partitioned DML on a Spanner PostgreSQL database.](#execute-a-partitioned-dml-on-a-spanner-postgresql-database.) + * [Delete using DML returning on a Spanner PostgreSQL database.](#delete-using-dml-returning-on-a-spanner-postgresql-database.) + * [Insert using DML returning on a Spanner PostgreSQL database.](#insert-using-dml-returning-on-a-spanner-postgresql-database.) + * [Update using DML returning on a Spanner PostgreSQL database.](#update-using-dml-returning-on-a-spanner-postgresql-database.) * [Execute a DML statement with parameters on a Spanner PostgreSQL database.](#execute-a-dml-statement-with-parameters-on-a-spanner-postgresql-database.) * [Calls a server side function on a Spanner PostgreSQL database.](#calls-a-server-side-function-on-a-spanner-postgresql-database.) * [Creates a new storing index in a Spanner PostgreSQL database.](#creates-a-new-storing-index-in-a-spanner-postgresql-database.) @@ -484,6 +490,57 @@ __Usage:__ +### Delete using DML returning. + +View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml-returning-delete.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/dml-returning-delete.js,samples/README.md) + +__Usage:__ + + +`node dml-returning-delete.js ` + + +----- + + + + +### Insert using DML returning. + +View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml-returning-insert.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/dml-returning-insert.js,samples/README.md) + +__Usage:__ + + +`node dml-returning-insert.js ` + + +----- + + + + +### Update using DML returning. + +View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml-returning-update.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/dml-returning-update.js,samples/README.md) + +__Usage:__ + + +`node dml-returning-update.js ` + + +----- + + + + ### DML View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/dml.js). @@ -1028,6 +1085,57 @@ __Usage:__ +### Delete using DML returning on a Spanner PostgreSQL database. + +View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-dml-returning-delete.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/pg-dml-returning-delete.js,samples/README.md) + +__Usage:__ + + +`node pg-dml-returning-delete.js ` + + +----- + + + + +### Insert using DML returning on a Spanner PostgreSQL database. + +View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-dml-returning-insert.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/pg-dml-returning-insert.js,samples/README.md) + +__Usage:__ + + +`node pg-dml-returning-insert.js ` + + +----- + + + + +### Update using DML returning on a Spanner PostgreSQL database. + +View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-dml-returning-update.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-spanner&page=editor&open_in_editor=samples/pg-dml-returning-update.js,samples/README.md) + +__Usage:__ + + +`node pg-dml-returning-update.js ` + + +----- + + + + ### Execute a DML statement with parameters on a Spanner PostgreSQL database. View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-dml-with-parameter.js). diff --git a/samples/dml-returning-delete.js b/samples/dml-returning-delete.js new file mode 100644 index 000000000..a1d754eb7 --- /dev/null +++ b/samples/dml-returning-delete.js @@ -0,0 +1,78 @@ +// Copyright 2022 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: Delete using DML returning. +// usage: node dml-returning-delete.js + +'use strict'; + +async function main(instanceId, databaseId, projectId) { + // [START spanner_delete_dml_returning] + // 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, + }); + + function deleteUsingDmlReturning(instanceId, databaseId) { + // Gets a reference to a Cloud Spanner instance and database. + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + database.runTransaction(async (err, transaction) => { + if (err) { + console.error(err); + return; + } + try { + const [rows, stats] = await transaction.run({ + sql: 'DELETE FROM Singers WHERE SingerId = 18 THEN RETURN FullName', + }); + + const rowCount = Math.floor(stats[stats.rowCount]); + console.log( + `Successfully deleted ${rowCount} record from the Singers table.` + ); + rows.forEach(row => { + console.log(row.toJSON().FullName); + }); + + await transaction.commit(); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + database.close(); + } + }); + } + deleteUsingDmlReturning(instanceId, databaseId); + // [END spanner_delete_dml_returning] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/samples/dml-returning-insert.js b/samples/dml-returning-insert.js new file mode 100644 index 000000000..272c98594 --- /dev/null +++ b/samples/dml-returning-insert.js @@ -0,0 +1,83 @@ +// Copyright 2022 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: Insert using DML returning. +// usage: node dml-returning-insert.js + +'use strict'; + +async function main(instanceId, databaseId, projectId) { + // [START spanner_insert_dml_returning] + // 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, + }); + + function insertUsingDmlReturning(instanceId, databaseId) { + // Gets a reference to a Cloud Spanner instance and database. + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + database.runTransaction(async (err, transaction) => { + if (err) { + console.error(err); + return; + } + try { + const [rows, stats] = await transaction.run({ + sql: 'INSERT Singers (SingerId, FirstName, LastName) VALUES (@id, @firstName, @lastName) THEN RETURN FullName', + params: { + id: 18, + firstName: 'Virginia', + lastName: 'Watson', + }, + }); + + const rowCount = Math.floor(stats[stats.rowCount]); + console.log( + `Successfully inserted ${rowCount} record into the Singers table.` + ); + rows.forEach(row => { + console.log(row.toJSON().FullName); + }); + + await transaction.commit(); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + database.close(); + } + }); + } + insertUsingDmlReturning(instanceId, databaseId); + // [END spanner_insert_dml_returning] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/samples/dml-returning-update.js b/samples/dml-returning-update.js new file mode 100644 index 000000000..51cb8d2cf --- /dev/null +++ b/samples/dml-returning-update.js @@ -0,0 +1,78 @@ +// Copyright 2022 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: Update using DML returning. +// usage: node dml-returning-update.js + +'use strict'; + +async function main(instanceId, databaseId, projectId) { + // [START spanner_update_dml_returning] + // 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, + }); + + function updateUsingDmlReturning(instanceId, databaseId) { + // Gets a reference to a Cloud Spanner instance and database. + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + database.runTransaction(async (err, transaction) => { + if (err) { + console.error(err); + return; + } + try { + const [rows, stats] = await transaction.run({ + sql: 'UPDATE Albums SET MarketingBudget = 2000000 WHERE SingerId = 1 and AlbumId = 1 THEN RETURN MarketingBudget', + }); + + const rowCount = Math.floor(stats[stats.rowCount]); + console.log( + `Successfully updated ${rowCount} record into the Albums table.` + ); + rows.forEach(row => { + console.log(row.toJSON().MarketingBudget); + }); + + await transaction.commit(); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + database.close(); + } + }); + } + updateUsingDmlReturning(instanceId, databaseId); + // [END spanner_update_dml_returning] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/samples/package.json b/samples/package.json index fee7d8ca6..fb532cc06 100644 --- a/samples/package.json +++ b/samples/package.json @@ -16,7 +16,7 @@ "dependencies": { "@google-cloud/kms": "^3.0.0", "@google-cloud/precise-date": "^3.0.0", - "@google-cloud/spanner": "^6.6.0", + "@google-cloud/spanner": "^6.7.0", "yargs": "^16.0.0" }, "devDependencies": { diff --git a/samples/pg-database-create.js b/samples/pg-database-create.js index c39e930c9..4eb5abc7d 100644 --- a/samples/pg-database-create.js +++ b/samples/pg-database-create.js @@ -70,6 +70,7 @@ function main( FirstName varchar(1024), LastName varchar(1024), SingerInfo bytea, + FullName character varying(2048) GENERATED ALWAYS AS (FirstName || ' ' || LastName) STORED, PRIMARY KEY (SingerId) ); CREATE TABLE Albums diff --git a/samples/pg-dml-returning-delete.js b/samples/pg-dml-returning-delete.js new file mode 100644 index 000000000..3d7cfcac8 --- /dev/null +++ b/samples/pg-dml-returning-delete.js @@ -0,0 +1,78 @@ +// Copyright 2022 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: Delete using DML returning on a Spanner PostgreSQL database. +// usage: node pg-dml-returning-delete.js + +'use strict'; + +async function main(instanceId, databaseId, projectId) { + // [START spanner_postgresql_delete_dml_returning] + // 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, + }); + + function pgDeleteUsingDmlReturning(instanceId, databaseId) { + // Gets a reference to a Cloud Spanner instance and database. + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + database.runTransaction(async (err, transaction) => { + if (err) { + console.error(err); + return; + } + try { + const [rows, stats] = await transaction.run({ + sql: 'DELETE FROM Singers WHERE SingerId = 18 RETURNING FullName', + }); + + const rowCount = Math.floor(stats[stats.rowCount]); + console.log( + `Successfully deleted ${rowCount} record from the Singers table.` + ); + rows.forEach(row => { + console.log(row.toJSON().fullname); + }); + + await transaction.commit(); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + database.close(); + } + }); + } + pgDeleteUsingDmlReturning(instanceId, databaseId); + // [END spanner_postgresql_delete_dml_returning] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/samples/pg-dml-returning-insert.js b/samples/pg-dml-returning-insert.js new file mode 100644 index 000000000..cf32d7b01 --- /dev/null +++ b/samples/pg-dml-returning-insert.js @@ -0,0 +1,83 @@ +// Copyright 2022 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: Insert using DML returning on a Spanner PostgreSQL database. +// usage: node pg-dml-returning-insert.js + +'use strict'; + +async function main(instanceId, databaseId, projectId) { + // [START spanner_postgresql_insert_dml_returning] + // 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, + }); + + function pgInsertUsingDmlReturning(instanceId, databaseId) { + // Gets a reference to a Cloud Spanner instance and database. + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + database.runTransaction(async (err, transaction) => { + if (err) { + console.error(err); + return; + } + try { + const [rows, stats] = await transaction.run({ + sql: 'INSERT Into Singers (SingerId, FirstName, LastName) VALUES ($1, $2, $3) RETURNING FullName', + params: { + p1: 18, + p2: 'Virginia', + p3: 'Watson', + }, + }); + + const rowCount = Math.floor(stats[stats.rowCount]); + console.log( + `Successfully inserted ${rowCount} record into the Singers table.` + ); + rows.forEach(row => { + console.log(row.toJSON().fullname); + }); + + await transaction.commit(); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + database.close(); + } + }); + } + pgInsertUsingDmlReturning(instanceId, databaseId); + // [END spanner_postgresql_insert_dml_returning] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/samples/pg-dml-returning-update.js b/samples/pg-dml-returning-update.js new file mode 100644 index 000000000..98ad85c4e --- /dev/null +++ b/samples/pg-dml-returning-update.js @@ -0,0 +1,83 @@ +// Copyright 2022 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: Update using DML returning on a Spanner PostgreSQL database. +// usage: node pg-dml-returning-update.js + +'use strict'; + +async function main(instanceId, databaseId, projectId) { + // [START spanner_postgresql_update_dml_returning] + // 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, + }); + + function pgUpdateUsingDmlReturning(instanceId, databaseId) { + // Gets a reference to a Cloud Spanner instance and database. + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + database.runTransaction(async (err, transaction) => { + if (err) { + console.error(err); + return; + } + try { + const [rows, stats] = await transaction.run({ + sql: 'UPDATE singers SET FirstName = $1, LastName = $2 WHERE singerid = $3 RETURNING FullName', + params: { + p1: 'Virginia1', + p2: 'Watson1', + p3: 18, + }, + }); + + const rowCount = Math.floor(stats[stats.rowCount]); + console.log( + `Successfully updated ${rowCount} record into the Singers table.` + ); + rows.forEach(row => { + console.log(row.toJSON().fullname); + }); + + await transaction.commit(); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + database.close(); + } + }); + } + pgUpdateUsingDmlReturning(instanceId, databaseId); + // [END spanner_postgresql_update_dml_returning] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/samples/schema.js b/samples/schema.js index 462755660..59ffc0e2e 100644 --- a/samples/schema.js +++ b/samples/schema.js @@ -42,7 +42,8 @@ async function createDatabase(instanceId, databaseId, projectId) { SingerId INT64 NOT NULL, FirstName STRING(1024), LastName STRING(1024), - SingerInfo BYTES(MAX) + SingerInfo BYTES(MAX), + FullName STRING(2048) AS (ARRAY_TO_STRING([FirstName, LastName], " ")) STORED, ) PRIMARY KEY (SingerId)`, `CREATE TABLE Albums ( SingerId INT64 NOT NULL, diff --git a/samples/system-test/spanner.test.js b/samples/system-test/spanner.test.js index da25a0a27..95f07187d 100644 --- a/samples/system-test/spanner.test.js +++ b/samples/system-test/spanner.test.js @@ -871,6 +871,42 @@ describe('Spanner', () => { ); }); + // dml_returning_insert + it('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('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('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')); + }); + // create_table_with_datatypes it('should create Venues example table with supported datatype columns', async () => { const output = execSync( @@ -1735,5 +1771,41 @@ describe('Spanner', () => { ); 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')); + }); }); }); diff --git a/src/database.ts b/src/database.ts index 77d56b07b..97a3e79dc 100644 --- a/src/database.ts +++ b/src/database.ts @@ -2841,6 +2841,9 @@ class Database extends common.GrpcServiceObject { runFn!(err as grpc.ServiceError); return; } + if (options.optimisticLock) { + transaction!.useOptimisticLock(); + } const release = this.pool_.release.bind(this.pool_, session!); const runner = new TransactionRunner( @@ -2952,6 +2955,9 @@ class Database extends common.GrpcServiceObject { transaction.requestOptions || {}, options.requestOptions ); + if (options.optimisticLock) { + transaction.useOptimisticLock(); + } const runner = new AsyncTransactionRunner( session, transaction, diff --git a/src/transaction-runner.ts b/src/transaction-runner.ts index 91b43787c..24a53939d 100644 --- a/src/transaction-runner.ts +++ b/src/transaction-runner.ts @@ -44,6 +44,7 @@ const RetryInfo = Root.fromJSON(jsonProtos).lookup('google.rpc.RetryInfo'); export interface RunTransactionOptions { timeout?: number; requestOptions?: Pick; + optimisticLock?: boolean; } /** @@ -195,6 +196,9 @@ export abstract class Runner { const transaction = this.session.transaction( (this.session.parent as Database).queryOptions_ ); + if (this.options.optimisticLock) { + transaction.useOptimisticLock(); + } if (this.attempts > 0) { await transaction.begin(); } diff --git a/src/transaction.ts b/src/transaction.ts index aad555b29..853bfb77f 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -39,6 +39,7 @@ import IAny = google.protobuf.IAny; import IQueryOptions = google.spanner.v1.ExecuteSqlRequest.IQueryOptions; import IRequestOptions = google.spanner.v1.IRequestOptions; import {Database} from '.'; +import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; export type Rows = Array; const RETRY_INFO_TYPE = 'type.googleapis.com/google.rpc.retryinfo'; @@ -2278,6 +2279,20 @@ export class Transaction extends Dml { useInRunner(): void { this._useInRunner = true; } + + /** + * Use optimistic concurrency control for the transaction. + * + * In this concurrency mode, operations during the execution phase, i.e., + * reads and queries, are performed without acquiring locks, and transactional + * consistency is ensured by running a validation process in the commit phase + * (when any needed locks are acquired). The validation process succeeds only + * if there are no conflicting committed transactions (that committed + * mutations to the read data at a commit timestamp after the read timestamp). + */ + useOptimisticLock(): void { + this._options.readWrite!.readLockMode = ReadLockMode.OPTIMISTIC; + } } /*! Developer Documentation diff --git a/src/v1/database_admin_client.ts b/src/v1/database_admin_client.ts index dc72ea201..cb77a1133 100644 --- a/src/v1/database_admin_client.ts +++ b/src/v1/database_admin_client.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -129,6 +129,9 @@ export class DatabaseAdminClient { (typeof window !== 'undefined' && typeof window?.fetch === 'function'); opts = Object.assign({servicePath, port, clientConfig, fallback}, opts); + // Request numeric enum values if REST transport is used. + opts.numericEnums = true; + // If scopes are unset in options and we're connecting to a non-default endpoint, set scopes just in case. if (servicePath !== staticMembers.servicePath && !('scopes' in opts)) { opts['scopes'] = staticMembers.scopes; diff --git a/src/v1/instance_admin_client.ts b/src/v1/instance_admin_client.ts index 2213bf9ea..e39e95c91 100644 --- a/src/v1/instance_admin_client.ts +++ b/src/v1/instance_admin_client.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -143,6 +143,9 @@ export class InstanceAdminClient { (typeof window !== 'undefined' && typeof window?.fetch === 'function'); opts = Object.assign({servicePath, port, clientConfig, fallback}, opts); + // Request numeric enum values if REST transport is used. + opts.numericEnums = true; + // If scopes are unset in options and we're connecting to a non-default endpoint, set scopes just in case. if (servicePath !== staticMembers.servicePath && !('scopes' in opts)) { opts['scopes'] = staticMembers.scopes; diff --git a/src/v1/spanner_client.ts b/src/v1/spanner_client.ts index e93060a87..a50db356b 100644 --- a/src/v1/spanner_client.ts +++ b/src/v1/spanner_client.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -123,6 +123,9 @@ export class SpannerClient { (typeof window !== 'undefined' && typeof window?.fetch === 'function'); opts = Object.assign({servicePath, port, clientConfig, fallback}, opts); + // Request numeric enum values if REST transport is used. + opts.numericEnums = true; + // If scopes are unset in options and we're connecting to a non-default endpoint, set scopes just in case. if (servicePath !== staticMembers.servicePath && !('scopes' in opts)) { opts['scopes'] = staticMembers.scopes; diff --git a/system-test/spanner.ts b/system-test/spanner.ts index cf05f853f..39f5d01d1 100644 --- a/system-test/spanner.ts +++ b/system-test/spanner.ts @@ -46,6 +46,8 @@ import CreateBackupMetadata = google.spanner.admin.database.v1.CreateBackupMetad import CreateInstanceConfigMetadata = google.spanner.admin.instance.v1.CreateInstanceConfigMetadata; const SKIP_BACKUPS = process.env.SKIP_BACKUPS; +const SKIP_FGAC_TESTS = (process.env.SKIP_FGAC_TESTS || 'false').toLowerCase(); + const IAM_MEMBER = process.env.IAM_MEMBER; const PREFIX = 'gcloud-tests-'; const RUN_ID = shortUUID(); @@ -330,8 +332,17 @@ describe('Spanner', () => { "StringValue" VARCHAR, "TimestampValue" TIMESTAMPTZ, "DateValue" DATE, - "CommitTimestamp" SPANNER.COMMIT_TIMESTAMP, - "JsonbValue" JSONB + "JsonbValue" JSONB, + "BytesArray" BYTEA[], + "BoolArray" BOOL[], + "FloatArray" DOUBLE PRECISION[], + "IntArray" BIGINT[], + "NumericArray" NUMERIC[], + "StringArray" VARCHAR[], + "TimestampArray" TIMESTAMPTZ[], + "DateArray" DATE[], + "JsonbArray" JSONB[], + "CommitTimestamp" SPANNER.COMMIT_TIMESTAMP ); ` ); @@ -536,6 +547,17 @@ describe('Spanner', () => { }); }); + it('POSTGRESQL should write empty boolean array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({BoolArray: []}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().BoolArray, []); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write null boolean array values', done => { insert({BoolArray: [null]}, Spanner.GOOGLE_STANDARD_SQL, (err, row) => { assert.ifError(err); @@ -544,6 +566,17 @@ describe('Spanner', () => { }); }); + it('POSTGRESQL should write null boolean array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({BoolArray: [null]}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().BoolArray, [null]); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write boolean array values', done => { insert( {BoolArray: [true, false]}, @@ -555,6 +588,17 @@ describe('Spanner', () => { } ); }); + + it('POSTGRESQL should write boolean array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({BoolArray: [true, false]}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().BoolArray, [true, false]); + done(); + }); + }); }); describe('int64s', () => { @@ -647,6 +691,17 @@ describe('Spanner', () => { }); }); + it('POSTGRESQL should write empty in64 array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({IntArray: []}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().IntArray, []); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write null int64 array values', done => { insert({IntArray: [null]}, Spanner.GOOGLE_STANDARD_SQL, (err, row) => { assert.ifError(err); @@ -655,6 +710,17 @@ describe('Spanner', () => { }); }); + it('POSTGRESQL should write null int64 array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({IntArray: [null]}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().IntArray, [null]); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write int64 array values', done => { const values = [1, 2, 3]; @@ -664,6 +730,19 @@ describe('Spanner', () => { done(); }); }); + + it('POSTGRESQL should write int64 array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const values = [1, 2, 3]; + + insert({IntArray: values}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().IntArray, values); + done(); + }); + }); }); describe('float64s', () => { @@ -752,6 +831,17 @@ describe('Spanner', () => { }); }); + it('POSTGRESQL should write empty float64 array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({FloatArray: []}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().FloatArray, []); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write null float64 array values', done => { insert( {FloatArray: [null]}, @@ -764,6 +854,17 @@ describe('Spanner', () => { ); }); + it('POSTGRESQL should write null float64 array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({FloatArray: [null]}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().FloatArray, [null]); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write float64 array values', done => { const values = [1.2, 2.3, 3.4]; @@ -777,6 +878,19 @@ describe('Spanner', () => { } ); }); + + it('POSTGRESQL should write float64 array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const values = [1.2, 2.3, 3.4]; + + insert({FloatArray: values}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().FloatArray, values); + done(); + }); + }); }); describe('numerics', () => { @@ -859,6 +973,17 @@ describe('Spanner', () => { }); }); + it('POSTGRESQL should write empty numeric array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({NumericArray: []}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().NumericArray, []); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write null numeric array values', done => { insert( {NumericArray: [null]}, @@ -871,6 +996,17 @@ describe('Spanner', () => { ); }); + it('POSTGRESQL should write null numeric array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({NumericArray: [null]}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().NumericArray, [null]); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write numeric array values', done => { const values = [ Spanner.numeric('-99999999999999999999999999999.999999999'), @@ -888,6 +1024,23 @@ describe('Spanner', () => { } ); }); + + it('POSTGRESQL should write numeric array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const values = [ + Spanner.pgNumeric('-99999999999999999999999999999.999999999'), + Spanner.pgNumeric('3.141592653'), + Spanner.pgNumeric('99999999999999999999999999999.999999999'), + ]; + + insert({NumericArray: values}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().NumericArray, values); + done(); + }); + }); }); describe('strings', () => { @@ -929,6 +1082,17 @@ describe('Spanner', () => { }); }); + it('POSTGRESQL should write empty string array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({StringArray: []}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().StringArray, []); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write null string array values', done => { insert( {StringArray: [null]}, @@ -941,6 +1105,17 @@ describe('Spanner', () => { ); }); + it('POSTGRESQL should write null string array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({StringArray: [null]}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().StringArray, [null]); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write string array values', done => { insert( {StringArray: ['abc', 'def']}, @@ -952,6 +1127,21 @@ describe('Spanner', () => { } ); }); + + it('POSTGRESQL should write string array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert( + {StringArray: ['abc', 'def']}, + Spanner.POSTGRESQL, + (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().StringArray, ['abc', 'def']); + done(); + } + ); + }); }); describe('bytes', () => { @@ -993,6 +1183,17 @@ describe('Spanner', () => { }); }); + it('POSTGRESQL should write empty bytes array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({BytesArray: []}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().BytesArray, []); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write null bytes array values', done => { insert( {BytesArray: [null]}, @@ -1005,6 +1206,17 @@ describe('Spanner', () => { ); }); + it('POSTGRESQL should write null bytes array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({BytesArray: [null]}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().BytesArray, [null]); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write bytes array values', done => { const values = [Buffer.from('a'), Buffer.from('b')]; @@ -1018,6 +1230,19 @@ describe('Spanner', () => { } ); }); + + it('POSTGRESQL should write bytes array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const values = [Buffer.from('a'), Buffer.from('b')]; + + insert({BytesArray: values}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().BytesArray, values); + done(); + }); + }); }); describe('jsons', () => { @@ -1136,6 +1361,17 @@ describe('Spanner', () => { ); }); + it('POSTGRESQL should write empty timestamp array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({TimestampArray: []}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().TimestampArray, []); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write null timestamp array values', done => { insert( {TimestampArray: [null]}, @@ -1148,6 +1384,17 @@ describe('Spanner', () => { ); }); + it('POSTGRESQL should write null timestamp array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({TimestampArray: [null]}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().TimestampArray, [null]); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write timestamp array values', done => { const values = [Spanner.timestamp(), Spanner.timestamp('3-3-1933')]; @@ -1161,6 +1408,19 @@ describe('Spanner', () => { } ); }); + + it('POSTGRESQL should write timestamp array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const values = [Spanner.timestamp(), Spanner.timestamp('3-3-1933')]; + + insert({TimestampArray: values}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().TimestampArray, values); + done(); + }); + }); }); describe('dates', () => { @@ -1213,6 +1473,17 @@ describe('Spanner', () => { }); }); + it('POSTGRESQL should write empty date array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({DateArray: []}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().DateArray, []); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write null date array values', done => { insert({DateArray: [null]}, Spanner.GOOGLE_STANDARD_SQL, (err, row) => { assert.ifError(err); @@ -1221,6 +1492,17 @@ describe('Spanner', () => { }); }); + it('POSTGRESQL should write null date array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + insert({DateArray: [null]}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().DateArray, [null]); + done(); + }); + }); + it('GOOGLE_STANDARD_SQL should write date array values', done => { const values = [Spanner.date(), Spanner.date('3-3-1933')]; @@ -1231,6 +1513,20 @@ describe('Spanner', () => { done(); }); }); + + it('POSTGRESQL should write date array values', function (done) { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + const values = [Spanner.date(), Spanner.date('3-3-1933')]; + + insert({DateArray: values}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + const {DateArray} = row.toJSON(); + assert.deepStrictEqual(DateArray, values); + done(); + }); + }); }); describe('jsonb', () => { @@ -1240,27 +1536,55 @@ describe('Spanner', () => { } }); - const jsonbInsert = (done, dialect, value) => { - insert({JsonbValue: value}, dialect, (err, row) => { + it('POSTGRESQL should write jsonb values', done => { + const value = Spanner.pgJsonb({ + key1: 'value1', + key2: 'value2', + }); + insert({JsonbValue: value}, Spanner.POSTGRESQL, (err, row) => { assert.ifError(err); assert.deepStrictEqual(row.toJSON().JsonbValue, value); done(); }); - }; - - it('POSTGRESQL should write jsonb values', done => { - jsonbInsert( - done, - Spanner.POSTGRESQL, - Spanner.pgJsonb({ - key1: 'value1', - key2: 'value2', - }) - ); }); it('POSTGRESQL should write null jsonb values', done => { - jsonbInsert(done, Spanner.POSTGRESQL, null); + insert({JsonbValue: null}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().JsonbValue, null); + done(); + }); + }); + + it('POSTGRESQL should write empty json array values', done => { + insert({JsonbArray: []}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().JsonbArray, []); + done(); + }); + }); + + it('POSTGRESQL should write null json array values', done => { + insert({JsonbArray: [null]}, Spanner.POSTGRESQL, (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().JsonbArray, [null]); + done(); + }); + }); + + it('POSTGRESQL should write json array values', done => { + insert( + {JsonbArray: [{key1: 'value1'}, {key2: 'value2'}]}, + Spanner.POSTGRESQL, + (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().JsonbArray, [ + Spanner.pgJsonb({key1: 'value1'}), + Spanner.pgJsonb({key2: 'value2'}), + ]); + done(); + } + ); }); }); @@ -1816,6 +2140,11 @@ describe('Spanner', () => { }); describe('FineGrainedAccessControl', () => { + before(function () { + if (SKIP_FGAC_TESTS === 'true') { + this.skip(); + } + }); const createUserDefinedDatabaseRole = async (database, query) => { database.updateSchema( [query], diff --git a/test/gapic_database_admin_v1.ts b/test/gapic_database_admin_v1.ts index a469b988e..b2f3bc4d6 100644 --- a/test/gapic_database_admin_v1.ts +++ b/test/gapic_database_admin_v1.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/gapic_instance_admin_v1.ts b/test/gapic_instance_admin_v1.ts index e259c6c96..9e78ad37b 100644 --- a/test/gapic_instance_admin_v1.ts +++ b/test/gapic_instance_admin_v1.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/gapic_spanner_v1.ts b/test/gapic_spanner_v1.ts index b1d607ad0..d1974d5cd 100644 --- a/test/gapic_spanner_v1.ts +++ b/test/gapic_spanner_v1.ts @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/spanner.ts b/test/spanner.ts index 181cb1968..daf560999 100644 --- a/test/spanner.ts +++ b/test/spanner.ts @@ -403,7 +403,10 @@ describe('Spanner with mock server', () => { it('should execute read with requestOptions in a read/write transaction', async () => { const database = newTestDatabase(); await database.runTransactionAsync( - {requestOptions: {transactionTag: 'transaction-tag'}}, + { + optimisticLock: true, + requestOptions: {transactionTag: 'transaction-tag'}, + }, async tx => { try { return await tx.read('foo', { @@ -438,7 +441,13 @@ describe('Spanner with mock server', () => { request.requestOptions!.transactionTag, 'transaction-tag' ); - assert.ok(request.transaction!.begin!.readWrite, 'ReadWrite is not set'); + const beginTxnRequest = spannerMock.getRequests().find(val => { + return (val as v1.BeginTransactionRequest).options?.readWrite; + }) as v1.BeginTransactionRequest; + assert.strictEqual( + beginTxnRequest.options?.readWrite!.readLockMode, + 'OPTIMISTIC' + ); }); it('should return an array of json objects', async () => { @@ -3088,6 +3097,75 @@ describe('Spanner with mock server', () => { assert.ok(!beginTxnRequest, 'beginTransaction was called'); }); + it('should use optimistic lock for runTransactionAsync', async () => { + const database = newTestDatabase(); + await database.runTransactionAsync( + { + optimisticLock: true, + }, + async tx => { + await tx!.run(selectSql); + await tx.commit(); + } + ); + await database.close(); + + const request = spannerMock.getRequests().find(val => { + return (val as v1.ExecuteSqlRequest).sql; + }) as v1.ExecuteSqlRequest; + assert.ok(request, 'no ExecuteSqlRequest found'); + assert.strictEqual( + request.transaction!.begin!.readWrite!.readLockMode, + 'OPTIMISTIC' + ); + }); + + it('should use optimistic lock for runTransaction', done => { + const database = newTestDatabase(); + database.runTransaction({optimisticLock: true}, async (err, tx) => { + assert.ifError(err); + await tx!.run(selectSql); + await tx!.commit(); + await database.close(); + + const request = spannerMock.getRequests().find(val => { + return (val as v1.ExecuteSqlRequest).sql; + }) as v1.ExecuteSqlRequest; + assert.ok(request, 'no ExecuteSqlRequest found'); + assert.strictEqual( + request.transaction!.begin!.readWrite!.readLockMode, + 'OPTIMISTIC' + ); + done(); + }); + }); + + it('should reuse a session for optimistic and pessimistic locks', async () => { + const database = newTestDatabase({min: 1, max: 1}); + let session1; + let session2; + await database.runTransactionAsync({optimisticLock: true}, async tx => { + session1 = tx!.session.id; + await tx!.run(selectSql); + await tx.commit(); + }); + spannerMock.resetRequests(); + await database.runTransactionAsync(async tx => { + session2 = tx!.session.id; + await tx!.run(selectSql); + await tx.commit(); + }); + assert.strictEqual(session1, session2); + const request = spannerMock.getRequests().find(val => { + return (val as v1.ExecuteSqlRequest).sql; + }) as v1.ExecuteSqlRequest; + assert.ok(request, 'no ExecuteSqlRequest found'); + assert.notStrictEqual( + request.transaction!.begin!.readWrite!.readLockMode, + 'OPTIMISTIC' + ); + }); + it('should only inline one begin transaction', async () => { const database = newTestDatabase(); await database.runTransactionAsync(async tx => { @@ -3141,6 +3219,30 @@ describe('Spanner with mock server', () => { assert.ok(beginTxnRequest, 'beginTransaction was called'); }); + it('should use beginTransaction on retry with optimistic lock', async () => { + const database = newTestDatabase(); + let attempts = 0; + await database.runTransactionAsync({optimisticLock: true}, async tx => { + await tx!.run(selectSql); + if (!attempts) { + spannerMock.abortTransaction(tx); + } + attempts++; + await tx!.run(insertSql); + await tx.commit(); + }); + await database.close(); + + const beginTxnRequest = spannerMock.getRequests().find(val => { + return (val as v1.BeginTransactionRequest).options?.readWrite; + }) as v1.BeginTransactionRequest; + assert.ok(beginTxnRequest, 'beginTransaction was called'); + assert.strictEqual( + beginTxnRequest.options!.readWrite!.readLockMode, + 'OPTIMISTIC' + ); + }); + it('should use beginTransaction on retry for unknown reason', async () => { const database = newTestDatabase(); await database.runTransactionAsync(async tx => { diff --git a/test/transaction.ts b/test/transaction.ts index 739b68d0c..0dbefaf46 100644 --- a/test/transaction.ts +++ b/test/transaction.ts @@ -1362,6 +1362,26 @@ describe('Transaction', () => { }; transaction.begin(assert.ifError); }); + + it('should set optimistic lock', () => { + const rw = { + readLockMode: + google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode + .OPTIMISTIC, + }; + transaction = new Transaction(SESSION); + transaction.useOptimisticLock(); + const stub = sandbox.stub(transaction, 'request'); + transaction.begin(); + + const expectedOptions = {readWrite: rw}; + const {client, method, reqOpts, headers} = stub.lastCall.args[0]; + + assert.strictEqual(client, 'SpannerClient'); + assert.strictEqual(method, 'beginTransaction'); + assert.deepStrictEqual(reqOpts.options, expectedOptions); + assert.deepStrictEqual(headers, transaction.resourceHeader_); + }); }); describe('commit', () => {