From 217d2564727df5d0bedab4d876263a7bf2368653 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Sun, 21 Jul 2024 15:32:13 +0200 Subject: [PATCH 1/4] chore(deps): update actions/checkout digest to 692973e (#2031) --- .github/workflows/system-tests-against-emulator.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/system-tests-against-emulator.yaml b/.github/workflows/system-tests-against-emulator.yaml index b9785c922..9a07e1d4b 100644 --- a/.github/workflows/system-tests-against-emulator.yaml +++ b/.github/workflows/system-tests-against-emulator.yaml @@ -16,7 +16,7 @@ jobs: - 9020:9020 steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: actions/setup-node@v4 with: node-version: 14 From b91e2849056df9894e0590cb71e21c13319e6d70 Mon Sep 17 00:00:00 2001 From: Varun Naik Date: Thu, 25 Jul 2024 23:20:22 -0700 Subject: [PATCH 2/4] feat(spanner): add samples for instance partitions (#2083) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://togithub.com/googleapis/nodejs-spanner/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [x] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes # 🦕 --- README.md | 1 + samples/README.md | 18 ++++++ samples/instance-partition-create.js | 84 ++++++++++++++++++++++++++++ samples/system-test/spanner.test.js | 38 +++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 samples/instance-partition-create.js diff --git a/README.md b/README.md index 14f4c8c8d..8e88ef2e6 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/nodejs-spanner/tre | Deletes a user-managed instance configuration. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/instance-config-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/instance-config-delete.js,samples/README.md) | | Lists the instance configuration operations. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/instance-config-get-operations.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/instance-config-get-operations.js,samples/README.md) | | Updates a user-managed instance configuration. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/instance-config-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/instance-config-update.js,samples/README.md) | +| Creates a new instance partition | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/instance-partition-create.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/instance-partition-create.js,samples/README.md) | | Creates a instance with autoscaling config. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/instance-with-autoscaling-config.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/instance-with-autoscaling-config.js,samples/README.md) | | Instance-with-processing-units | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/instance-with-processing-units.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/instance-with-processing-units.js,samples/README.md) | | Instance | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/instance.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/instance.js,samples/README.md) | diff --git a/samples/README.md b/samples/README.md index 17d42d085..5276e8710 100644 --- a/samples/README.md +++ b/samples/README.md @@ -57,6 +57,7 @@ and automatic, synchronous replication for high availability. * [Deletes a user-managed instance configuration.](#deletes-a-user-managed-instance-configuration.) * [Lists the instance configuration operations.](#lists-the-instance-configuration-operations.) * [Updates a user-managed instance configuration.](#updates-a-user-managed-instance-configuration.) + * [Creates a new instance partition](#creates-a-new-instance-partition) * [Creates a instance with autoscaling config.](#creates-a-instance-with-autoscaling-config.) * [Instance-with-processing-units](#instance-with-processing-units) * [Instance](#instance) @@ -865,6 +866,23 @@ __Usage:__ +### Creates a new instance partition + +View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/instance-partition-create.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/instance-partition-create.js,samples/README.md) + +__Usage:__ + + +`node instance-partition-create.js ` + + +----- + + + + ### Creates a instance with autoscaling config. View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/instance-with-autoscaling-config.js). diff --git a/samples/instance-partition-create.js b/samples/instance-partition-create.js new file mode 100644 index 000000000..b7536fe93 --- /dev/null +++ b/samples/instance-partition-create.js @@ -0,0 +1,84 @@ +/** + * 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 instance partition +// usage: node instance-partition-create.js + +'use strict'; + +function main( + instanceId = 'my-instance', + instancePartitionId = 'my-instance-partition', + projectId = 'my-project-id' +) { + async function createInstancePartition() { + // [START spanner_create_instance_partition] + // 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 instancePartitionId = 'my-instance-partition'; + + // Creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + // Get the instance admin client + const instanceAdminClient = spanner.getInstanceAdminClient(); + + // Creates a new instance partition + try { + console.log( + `Creating instance partition ${instanceAdminClient.instancePartitionPath( + projectId, + instanceId, + instancePartitionId + )}.` + ); + const [operation] = await instanceAdminClient.createInstancePartition({ + instancePartitionId: instancePartitionId, + parent: instanceAdminClient.instancePath(projectId, instanceId), + instancePartition: { + config: instanceAdminClient.instanceConfigPath(projectId, 'nam3'), + nodeCount: 1, + displayName: 'Test instance partition', + }, + }); + + console.log( + `Waiting for operation on ${instancePartitionId} to complete...` + ); + await operation.promise(); + + console.log(`Created instance partition ${instancePartitionId}.`); + } catch (err) { + console.error('ERROR:', err); + } + // [END spanner_create_instance_partition] + } + createInstancePartition(); +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/samples/system-test/spanner.test.js b/samples/system-test/spanner.test.js index a5e27a174..1208b012b 100644 --- a/samples/system-test/spanner.test.js +++ b/samples/system-test/spanner.test.js @@ -1732,6 +1732,44 @@ describe('Autogenerated Admin Clients', () => { }); }); + describe('instance partition', () => { + before(async () => { + const instance = spanner.instance(SAMPLE_INSTANCE_ID); + const [, operation] = await instance.create({ + config: 'regional-us-central1', + nodes: 1, + 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('postgreSQL', () => { before(async () => { const instance = spanner.instance(SAMPLE_INSTANCE_ID); From 62fc0a47327017c115466b9e89e53dbd778579af Mon Sep 17 00:00:00 2001 From: alkatrivedi <58396306+alkatrivedi@users.noreply.github.com> Date: Mon, 29 Jul 2024 06:17:08 +0000 Subject: [PATCH 3/4] feat: add support for blind writes (#2065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * sample: blind write * sample: blind write * refactor * add class mutation * add class mutation * feat: blind-writes * refactor * fix: lint errors * refactor * fix: lint errors * fix: header checks * refactor the blind write * feat: add support for blind writes chore(main): release 7.9.0 (#2053) :robot: I have created a release *beep* *boop* --- * **spanner:** Add support for batchWrite ([#2054](https://togithub.com/googleapis/nodejs-spanner/issues/2054)) ([06aab6e](https://togithub.com/googleapis/nodejs-spanner/commit/06aab6e39bbce9e3786f1ac631c80e8909197e92)) * **deps:** Update dependency google-gax to v4.3.4 ([#2051](https://togithub.com/googleapis/nodejs-spanner/issues/2051)) ([80abf06](https://togithub.com/googleapis/nodejs-spanner/commit/80abf06ba8ef9497318ffc597b83fb63e4408f9c)) * **deps:** Update dependency google-gax to v4.3.5 ([#2055](https://togithub.com/googleapis/nodejs-spanner/issues/2055)) ([702c9b0](https://togithub.com/googleapis/nodejs-spanner/commit/702c9b0f34e6cc34233c5aa52b97601b19f70980)) * **deps:** Update dependency google-gax to v4.3.6 ([#2057](https://togithub.com/googleapis/nodejs-spanner/issues/2057)) ([74ebf1e](https://togithub.com/googleapis/nodejs-spanner/commit/74ebf1e45cddf614c180295f3a761a8f84c5cb32)) * **deps:** Update dependency google-gax to v4.3.7 ([#2068](https://togithub.com/googleapis/nodejs-spanner/issues/2068)) ([28fec6c](https://togithub.com/googleapis/nodejs-spanner/commit/28fec6ca505d78d725efc123950be978e0c84ab7)) --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). refactor: blind write method fix: lint errors fix: Retry with timeout (#2071) Use `gaxOptions.timeout` during retry in streaming calls. Earlier the timeout value was only used for a single RPC not for the whole operation including retries. Now if RPC returns `Unavailable` error and the timeout value has been reached, library will throw an Deadline exceeded error. ``` const query = { sql: 'Select 1', gaxOptions: {timeout: 500} } const [rows] = await database.run(query); ``` chore(main): release 7.9.1 (#2072) :robot: I have created a release *beep* *boop* --- * Retry with timeout ([#2071](https://togithub.com/googleapis/nodejs-spanner/issues/2071)) ([a943257](https://togithub.com/googleapis/nodejs-spanner/commit/a943257a0402b26fd80196057a9724fd28fc5c1b)) --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). refactor: blind write method test: unit test for blind write test: unit test for blind write refactor fix: lint errors feat: add support for change streams transaction exclusion option for Batch Write (#2070) * feat: change stream transaction exclusion option for Batch Write * refactor docs: add doc to blindWrite method docs: add doc to the setQueuedMutations refactor: doc setQueuedMutations fix: presubmit error fix(deps): update dependency google-gax to v4.3.8 (#2077) [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [google-gax](https://togithub.com/googleapis/gax-nodejs) ([source](https://togithub.com/googleapis/gax-nodejs/tree/HEAD/gax)) | [`4.3.7` -> `4.3.8`](https://renovatebot.com/diffs/npm/google-gax/4.3.7/4.3.8) | [![age](https://developer.mend.io/api/mc/badges/age/npm/google-gax/4.3.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/google-gax/4.3.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/google-gax/4.3.7/4.3.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/google-gax/4.3.7/4.3.8?slim=true)](https://docs.renovatebot.com/merge-confidence/) | ---
googleapis/gax-nodejs (google-gax) [Compare Source](https://togithub.com/googleapis/gax-nodejs/compare/google-gax-v4.3.7...google-gax-v4.3.8) - **deps:** remove rimraf in favor of native node rm function ([#​1626](https://togithub.com/googleapis/gax-nodejs/issues/1626)) ([dd87646](https://togithub.com/googleapis/gax-nodejs/commit/dd87646618d5026549920e224df7f85cbb5ff6a8))
--- 📅 **Schedule**: Branch creation - "after 9am and before 3pm" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/googleapis/nodejs-spanner). updated updated lint refactor * fix: presubmit error * refactor: docs of the method writeAtLeastOnce * test: unit test using mockspanner * fix: lint errors * docs refactor * refactor * refactor --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: surbhigarg92 --- src/database.ts | 93 ++++++++++++++++++++++++++++++++++++ src/index.ts | 10 ++++ src/transaction.ts | 115 +++++++++++++++++++++++++++++++++++++++++++++ test/database.ts | 105 ++++++++++++++++++++++++++++++++++++++++- test/spanner.ts | 41 ++++++++++++++++ 5 files changed, 363 insertions(+), 1 deletion(-) diff --git a/src/database.ts b/src/database.ts index fb85e053e..b3b38a239 100644 --- a/src/database.ts +++ b/src/database.ts @@ -61,8 +61,11 @@ import { import {CreateTableCallback, CreateTableResponse, Table} from './table'; import { BatchWriteOptions, + CommitCallback, + CommitResponse, ExecuteSqlRequest, MutationGroup, + MutationSet, RunCallback, RunResponse, RunUpdateCallback, @@ -3346,6 +3349,95 @@ class Database extends common.GrpcServiceObject { return proxyStream as NodeJS.ReadableStream; } + /** + * Write mutations using a single RPC invocation without replay protection. + * + * writeAtLeastOnce writes mutations to Spanner using a single Commit RPC. + * These requests are not replay protected, meaning that it may apply mutations more + * than once, if the mutations are not idempotent, this may lead to a failure being + * reported when the mutation was applied once. Replays non-idempotent mutations may + * have undesirable effects. For example, replays of an insert mutation may produce an + * already exists error. For this reason, most users of the library will prefer to use + * {@link runTransaction} instead. + * + * However, {@link writeAtLeastOnce()} requires only a single RPC, whereas {@link runTransaction()} + * requires two RPCs (one of which may be performed in advance), and so this method may be + * appropriate for latency sensitive and/or high throughput blind writing. + * + * We recommend structuring your mutation set to be idempotent to avoid this issue. + * + * @param {MutationSet} [mutations] Set of Mutations to be applied. + * @param {CallOptions} [options] Options object for blind write request. + * @param {CommitCallback} [callback] Callback function for blind write request. + * + * @returns {Promise} + * + * @example + * ``` + * const {Spanner} = require('@google-cloud/spanner'); + * const spanner = new Spanner(); + * + * const instance = spanner.instance('my-instance'); + * const database = instance.database('my-database'); + * const mutations = new MutationSet(); + * mutations.upsert('Singers', { + * SingerId: 1, + * FirstName: 'Scarlet', + * LastName: 'Terry', + * }); + * mutations.upsert('Singers', { + * SingerId: 2, + * FirstName: 'Marc', + * LastName: 'Richards', + * }); + * + * try { + * const [response, err] = await database.writeAtLeastOnce(mutations, {}); + * console.log(response.commitTimestamp); + * } catch(err) { + * console.log("Error: ", err); + * } + * ``` + */ + writeAtLeastOnce(mutations: MutationSet): Promise; + writeAtLeastOnce( + mutations: MutationSet, + options: CallOptions + ): Promise; + writeAtLeastOnce(mutations: MutationSet, callback: CommitCallback): void; + writeAtLeastOnce( + mutations: MutationSet, + options: CallOptions, + callback: CommitCallback + ): void; + writeAtLeastOnce( + mutations: MutationSet, + optionsOrCallback?: CallOptions | CommitCallback, + callback?: CommitCallback + ): void | Promise { + const cb = + typeof optionsOrCallback === 'function' + ? (optionsOrCallback as CommitCallback) + : callback; + const options = + typeof optionsOrCallback === 'object' && optionsOrCallback + ? (optionsOrCallback as CallOptions) + : {}; + this.pool_.getSession((err, session?, transaction?) => { + if (err && isSessionNotFoundError(err as grpc.ServiceError)) { + this.writeAtLeastOnce(mutations, options, cb!); + return; + } + if (err) { + cb!(err as grpc.ServiceError); + return; + } + this._releaseOnEnd(session!, transaction!); + transaction?.setQueuedMutations(mutations.proto()); + return transaction?.commit(options, cb!); + }); + } + /** * Create a Session object. * @@ -3674,6 +3766,7 @@ callbackifyAll(Database, { 'batchCreateSessions', 'batchTransaction', 'batchWriteAtLeastOnce', + 'writeAtLeastOnce', 'close', 'createBatchTransaction', 'createSession', diff --git a/src/index.ts b/src/index.ts index d6107aed1..983d7f9ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -72,6 +72,7 @@ import {SessionPool} from './session-pool'; import {Table} from './table'; import { MutationGroup, + MutationSet, PartitionedDml, Snapshot, Transaction, @@ -2025,6 +2026,15 @@ export {Transaction}; */ export {MutationGroup}; +/** + * {@link MutationSet} class. + * + * @name Spanner.MutationSet + * @see MutationSet + * @type {Constructor} + */ +export {MutationSet}; + /** * @type {object} * @property {constructor} DatabaseAdminClient diff --git a/src/transaction.ts b/src/transaction.ts index 35d5e90f7..5617b4a3c 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -1854,6 +1854,17 @@ export class Transaction extends Dml { return undefined; } + /** + * This method updates the _queuedMutations property of the transaction. + * + * @public + * + * @param {spannerClient.spanner.v1.Mutation[]} [mutation] + */ + setQueuedMutations(mutation: spannerClient.spanner.v1.Mutation[]): void { + this._queuedMutations = mutation; + } + /** * @typedef {object} CommitOptions * @property {google.spanner.v1.IRequestOptions} requestOptions The request options to include @@ -2530,6 +2541,110 @@ function buildDeleteMutation( return mutation as spannerClient.spanner.v1.Mutation; } +/** + * MutationSet represent a set of changes to be applied atomically to a Cloud Spanner + * database with a {@link Transaction}. + * Mutations are used to insert, update, upsert(insert or update), replace, or + * delete rows within tables. + * + * Mutations are added to a {@link Transaction} and are not executed until the + * transaction is committed via {@link Transaction#commit}. + * + * If the transaction is rolled back or encounters an error, the mutations are + * discarded. + * + * @example + * ``` + * const {Spanner, Mutation} = require('@google-cloud/spanner'); + * const spanner = new Spanner(); + * + * const instance = spanner.instance('my-instance'); + * const database = instance.database('my-database'); + * + * const mutations = new MutationSet(); + * mutations.insert('Singers', {SingerId: '123', FirstName: 'David'}); + * mutations.update('Singers', {SingerId: '123', FirstName: 'Marc'}); + * + * try { + * database.writeAtLeastOnce(mutations, (err, res) => { + * console.log("RESPONSE: ", res); + * }); + * } catch(err) { + * console.log("ERROR: ", err); + * } + * ``` + */ +export class MutationSet { + /** + * An array to store the mutations. + */ + private _queuedMutations: spannerClient.spanner.v1.Mutation[]; + + /** + * Creates a new Mutation object. + */ + constructor() { + this._queuedMutations = []; + } + + /** + * Adds an insert operation to the mutation set. + * @param {string} table. The name of the table to insert into. + * @param {object|object[]} rows. A single row object or an array of row objects to insert. + */ + insert(table: string, rows: object | object[]): void { + this._queuedMutations.push(buildMutation('insert', table, rows)); + } + + /** + * Adds an update operation to the mutation set. + * @param {string} table. The name of the table to update. + * @param {object|object[]} rows. A single row object or an array of row objects to update. + * Each row object must contain the primary key values to indentify the row to update. + */ + update(table: string, rows: object | object[]): void { + this._queuedMutations.push(buildMutation('update', table, rows)); + } + + /** + * Adds an upsert operation to the mutation set. + * An upsert will insert a new row if it does not exist or update an existing row if it does. + * @param {string} table. The name of the table to upsert. + * @param {object|object[]} rows. A single row object or an array of row objects to upsert. + */ + upsert(table: string, rows: object | object[]): void { + this._queuedMutations.push(buildMutation('insertOrUpdate', table, rows)); + } + + /** + * Adds a replace operation to the mutation set. + * A replace operation deletes the existing row (if it exists) and inserts the new row. + * @param {string} table. The name of the table to replace. + * @param {object|object[]} rows. A single row object or an array of row objects to replace. + */ + replace(table: string, rows: object | object[]): void { + this._queuedMutations.push(buildMutation('replace', table, rows)); + } + + /** + * Adds a deleteRows operation to the mutation set. + * This operation deletes rows from the specified table based on their primary keys. + * @param {string} table. The name of the table to deleteRows from. + * @param {key[]} key. An array of key objects, each represeting the primary key of a row to delete. + */ + deleteRows(table: string, keys: Key[]): void { + this._queuedMutations.push(buildDeleteMutation(table, keys)); + } + + /** + * Returns the internal representation of the queued mutations as a protobuf message. + * @returns {spannerClient.spanner.v1.Mutation[]}. The protobuf message representing the mutations. + */ + proto(): spannerClient.spanner.v1.Mutation[] { + return this._queuedMutations; + } +} + /** * A group of mutations to be committed together. * Related mutations should be placed in a group. diff --git a/test/database.ts b/test/database.ts index 059f704fe..e061b0ef5 100644 --- a/test/database.ts +++ b/test/database.ts @@ -40,7 +40,13 @@ import {protos} from '../src'; import * as inst from '../src/instance'; import RequestOptions = google.spanner.v1.RequestOptions; import EncryptionType = google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.EncryptionType; -import {BatchWriteOptions} from '../src/transaction'; +import { + BatchWriteOptions, + CommitCallback, + CommitOptions, + MutationSet, +} from '../src/transaction'; +import {error} from 'is'; let promisified = false; const fakePfy = extend({}, pfy, { @@ -125,10 +131,12 @@ class FakeTable { class FakeTransaction extends EventEmitter { calledWith_: IArguments; _options!: google.spanner.v1.ITransactionOptions; + private _queuedMutations: google.spanner.v1.Mutation[]; constructor(options) { super(); this._options = options; this.calledWith_ = arguments; + this._queuedMutations = []; } begin() {} end() {} @@ -136,6 +144,18 @@ class FakeTransaction extends EventEmitter { return through.obj(); } runUpdate() {} + setQueuedMutations(mutation) { + this._queuedMutations = mutation; + } + commit( + options?: CommitOptions, + callback?: CommitCallback + ): void | Promise { + if (callback) { + callback(null, {commitTimestamp: {seconds: 1, nanos: 0}}); + } + return Promise.resolve({commitTimestamp: {seconds: 1, nanos: 0}}); + } } let fakeTransactionRunner: FakeTransactionRunner; @@ -762,6 +782,89 @@ describe('Database', () => { }); }); + describe('writeAtLeastOnce', () => { + const mutations = new MutationSet(); + mutations.insert('MyTable', { + Key: 'k3', + Thing: 'xyz', + }); + + const SESSION = new FakeSession(); + const RESPONSE = {commitTimestamp: {seconds: 1, nanos: 0}}; + const TRANSACTION = new FakeTransaction( + {} as google.spanner.v1.TransactionOptions.ReadWrite + ); + + let pool: FakeSessionPool; + + beforeEach(() => { + pool = database.pool_; + (sandbox.stub(pool, 'getSession') as sinon.SinonStub).callsFake( + callback => { + callback(null, SESSION, TRANSACTION); + } + ); + }); + + it('should return any errors getting a session', done => { + const fakeErr = new Error('err'); + + (pool.getSession as sinon.SinonStub).callsFake(callback => + callback(fakeErr, null, null) + ); + + database.writeAtLeastOnce(mutations, err => { + assert.deepStrictEqual(err, fakeErr); + done(); + }); + }); + + it('should return successful CommitResponse when passing an empty mutation', done => { + const fakeMutations = new MutationSet(); + try { + database.writeAtLeastOnce(fakeMutations, (err, response) => { + assert.ifError(err); + assert.deepStrictEqual( + response.commitTimestamp, + RESPONSE.commitTimestamp + ); + }); + done(); + } catch (error) { + assert(error instanceof Error); + } + }); + + it('should return an error when passing null mutation', done => { + const fakeError = new Error('err'); + try { + database.writeAtLeastOnce(null, (err, res) => {}); + } catch (err) { + (err as grpc.ServiceError).message.includes( + "Cannot read properties of null (reading 'proto')" + ); + done(); + } + }); + + it('should return CommitResponse on successful write using Callback', done => { + database.writeAtLeastOnce(mutations, (err, res) => { + assert.deepStrictEqual(err, null); + assert.deepStrictEqual(res, RESPONSE); + done(); + }); + }); + + it('should return CommitResponse on successful write using await', async () => { + sinon.stub(database, 'writeAtLeastOnce').resolves([RESPONSE]); + const [response, err] = await database.writeAtLeastOnce(mutations, {}); + assert.deepStrictEqual( + response.commitTimestamp, + RESPONSE.commitTimestamp + ); + }); + }); + describe('close', () => { const FAKE_ID = 'a/c/b/d'; diff --git a/test/spanner.ts b/test/spanner.ts index e2e715710..4acb15fdf 100644 --- a/test/spanner.ts +++ b/test/spanner.ts @@ -20,6 +20,7 @@ import {grpc, Status, ServiceError} from 'google-gax'; import { Database, Instance, + MutationSet, SessionPool, Snapshot, Spanner, @@ -3260,6 +3261,46 @@ describe('Spanner with mock server', () => { assert.strictEqual(commitRequest.mutations.length, 1); }); + it('should apply blind writes only once with mutations', async () => { + const database = newTestDatabase(); + const mutations = new MutationSet(); + mutations.upsert('Singers', { + SingerId: 1, + FirstName: 'Scarlet', + LastName: 'Terry', + }); + mutations.upsert('Singers', { + SingerId: 2, + FirstName: 'Marc', + }); + await database.writeAtLeastOnce(mutations, {}); + await database.close(); + + // Verify that we don't have a BeginTransaction request for the transaction. + const beginTxnRequest = spannerMock.getRequests().find(val => { + return (val as v1.BeginTransactionRequest).options?.readWrite; + }) as v1.BeginTransactionRequest; + assert.deepStrictEqual(beginTxnRequest, undefined); + + // Verify that we have a single Commit request, and that the Commit request + // contains only two mutations and uses a single-use read/write transaction. + assert.strictEqual( + 1, + spannerMock.getRequests().filter(val => { + return (val as v1.CommitRequest).mutations; + }).length + ); + const commitRequest = spannerMock.getRequests().find(val => { + const request = val as v1.CommitRequest; + return request.mutations || request.singleUseTransaction?.readWrite; + }) as v1.CommitRequest; + assert.ok(commitRequest, 'Commit was called'); + assert.strictEqual(commitRequest.mutations.length, 2); + assert.deepStrictEqual(commitRequest.singleUseTransaction?.readWrite, { + readLockMode: 'READ_LOCK_MODE_UNSPECIFIED', + }); + }); + it('should apply blind writes only once with excludeTxnFromChangeStreams option', async () => { const database = newTestDatabase(); await database.runTransactionAsync( From f34f464ebf4a14a66751d4720ea023f3aacd4a32 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 07:22:17 +0000 Subject: [PATCH 4/4] chore(main): release 7.11.0 (#2084) :robot: I have created a release *beep* *boop* --- ## [7.11.0](https://togithub.com/googleapis/nodejs-spanner/compare/v7.10.0...v7.11.0) (2024-07-29) ### Features * Add support for blind writes ([#2065](https://togithub.com/googleapis/nodejs-spanner/issues/2065)) ([62fc0a4](https://togithub.com/googleapis/nodejs-spanner/commit/62fc0a47327017c115466b9e89e53dbd778579af)) * **spanner:** Add samples for instance partitions ([#2083](https://togithub.com/googleapis/nodejs-spanner/issues/2083)) ([b91e284](https://togithub.com/googleapis/nodejs-spanner/commit/b91e2849056df9894e0590cb71e21c13319e6d70)) --- This PR was generated with [Release Please](https://togithub.com/googleapis/release-please). See [documentation](https://togithub.com/googleapis/release-please#release-please). --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- samples/package.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfef2cbc1..c8ac9cf08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ [1]: https://www.npmjs.com/package/nodejs-spanner?activeTab=versions +## [7.11.0](https://github.com/googleapis/nodejs-spanner/compare/v7.10.0...v7.11.0) (2024-07-29) + + +### Features + +* Add support for blind writes ([#2065](https://github.com/googleapis/nodejs-spanner/issues/2065)) ([62fc0a4](https://github.com/googleapis/nodejs-spanner/commit/62fc0a47327017c115466b9e89e53dbd778579af)) +* **spanner:** Add samples for instance partitions ([#2083](https://github.com/googleapis/nodejs-spanner/issues/2083)) ([b91e284](https://github.com/googleapis/nodejs-spanner/commit/b91e2849056df9894e0590cb71e21c13319e6d70)) + ## [7.10.0](https://github.com/googleapis/nodejs-spanner/compare/v7.9.1...v7.10.0) (2024-07-19) diff --git a/package.json b/package.json index f3e59edc1..a4bbbbf62 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@google-cloud/spanner", "description": "Cloud Spanner Client Library for Node.js", - "version": "7.10.0", + "version": "7.11.0", "license": "Apache-2.0", "author": "Google Inc.", "engines": { diff --git a/samples/package.json b/samples/package.json index 2114e3dd6..e07a78984 100644 --- a/samples/package.json +++ b/samples/package.json @@ -17,7 +17,7 @@ "dependencies": { "@google-cloud/kms": "^4.0.0", "@google-cloud/precise-date": "^4.0.0", - "@google-cloud/spanner": "^7.10.0", + "@google-cloud/spanner": "^7.11.0", "yargs": "^17.0.0", "protobufjs": "^7.0.0" },