diff --git a/CHANGELOG.md b/CHANGELOG.md index b9c7d82c2..86d3f1ef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ [1]: https://www.npmjs.com/package/nodejs-spanner?activeTab=versions +## [7.8.0](https://github.com/googleapis/nodejs-spanner/compare/v7.7.0...v7.8.0) (2024-05-24) + + +### Features + +* Add `RESOURCE_EXHAUSTED` to the list of retryable error codes ([#2032](https://github.com/googleapis/nodejs-spanner/issues/2032)) ([a4623c5](https://github.com/googleapis/nodejs-spanner/commit/a4623c560c16fa1f37a06cb57a5e47a1d6759d27)) +* Add support for multi region encryption config ([81fa610](https://github.com/googleapis/nodejs-spanner/commit/81fa610895fe709cbb7429896493a67407a6343c)) +* Add support for Proto columns ([#1991](https://github.com/googleapis/nodejs-spanner/issues/1991)) ([ae59c7f](https://github.com/googleapis/nodejs-spanner/commit/ae59c7f957660e08cd5965b5e67694fa1ccc0057)) +* **spanner:** Add support for change streams transaction exclusion option ([#2049](https://github.com/googleapis/nodejs-spanner/issues/2049)) ([d95cab5](https://github.com/googleapis/nodejs-spanner/commit/d95cab5abe50cdb56cbc1d6d935aee29526e1096)) + + +### Bug Fixes + +* **deps:** Update dependency google-gax to v4.3.3 ([#2038](https://github.com/googleapis/nodejs-spanner/issues/2038)) ([d86c1b0](https://github.com/googleapis/nodejs-spanner/commit/d86c1b0c21c7c95e3110221b3ca6ff9ff3b4a088)) +* Drop table statement ([#2036](https://github.com/googleapis/nodejs-spanner/issues/2036)) ([f31d7b2](https://github.com/googleapis/nodejs-spanner/commit/f31d7b205d74d4a783f0d5159dd5b62efe968fe6)) + ## [7.7.0](https://github.com/googleapis/nodejs-spanner/compare/v7.6.0...v7.7.0) (2024-04-17) diff --git a/README.md b/README.md index e40782b4e..12878220b 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,10 @@ Samples are in the [`samples/`](https://github.com/googleapis/nodejs-spanner/tre | Alters a sequence in a PostgreSQL database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-sequence-alter.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-sequence-alter.js,samples/README.md) | | Creates sequence in PostgreSQL database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-sequence-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/pg-sequence-create.js,samples/README.md) | | Drops a sequence in PostgreSQL database. | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/pg-sequence-drop.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-sequence-drop.js,samples/README.md) | +| Proto-query-data | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/proto-query-data.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/proto-query-data.js,samples/README.md) | +| Creates a new database with a proto column and enum | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/proto-type-add-column.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/proto-type-add-column.js,samples/README.md) | +| Proto-update-data-dml | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/proto-update-data-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/proto-update-data-dml.js,samples/README.md) | +| Proto-update-data | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/proto-update-data.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/proto-update-data.js,samples/README.md) | | Queryoptions | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/queryoptions.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/queryoptions.js,samples/README.md) | | Quickstart | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/quickstart.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/quickstart.js,samples/README.md) | | Read data with database role | [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/read-data-with-database-role.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/read-data-with-database-role.js,samples/README.md) | diff --git a/package.json b/package.json index 8b8cdca08..5b5963e6f 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.7.0", + "version": "7.8.0", "license": "Apache-2.0", "author": "Google Inc.", "engines": { @@ -38,7 +38,7 @@ "ycsb": "node ./benchmark/ycsb.js run -P ./benchmark/workloada -p table=usertable -p cloudspanner.instance=ycsb-instance -p operationcount=100 -p cloudspanner.database=ycsb", "fix": "gts fix", "clean": "gts clean", - "compile": "tsc -p . && cp -r protos build", + "compile": "tsc -p . && cp -r protos build && cp -r test/data build/test", "prepare": "npm run compile-protos && npm run compile", "pretest": "npm run compile", "presystem-test": "npm run compile", @@ -66,7 +66,7 @@ "events-intercept": "^2.0.0", "extend": "^3.0.2", "google-auth-library": "^9.0.0", - "google-gax": "4.3.2", + "google-gax": "4.3.3", "grpc-gcp": "^1.0.0", "is": "^3.2.1", "lodash.snakecase": "^4.1.1", diff --git a/protos/google/spanner/admin/database/v1/backup.proto b/protos/google/spanner/admin/database/v1/backup.proto index fce69a2f3..bb8ef4d55 100644 --- a/protos/google/spanner/admin/database/v1/backup.proto +++ b/protos/google/spanner/admin/database/v1/backup.proto @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. @@ -51,14 +51,14 @@ message Backup { READY = 2; } - // Required for the [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] operation. - // Name of the database from which this backup was - // created. This needs to be in the same instance as the backup. - // Values are of the form + // Required for the + // [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] + // operation. Name of the database from which this backup was created. This + // needs to be in the same instance as the backup. Values are of the form // `projects//instances//databases/`. string database = 2 [(google.api.resource_reference) = { - type: "spanner.googleapis.com/Database" - }]; + type: "spanner.googleapis.com/Database" + }]; // The backup will contain an externally consistent copy of the database at // the timestamp specified by `version_time`. If `version_time` is not @@ -66,7 +66,8 @@ message Backup { // backup. google.protobuf.Timestamp version_time = 9; - // Required for the [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] + // Required for the + // [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] // operation. The expiration time of the backup, with microseconds // granularity that must be at least 6 hours and at most 366 days // from the time the CreateBackup request is processed. Once the `expire_time` @@ -74,8 +75,11 @@ message Backup { // Spanner to free the resources used by the backup. google.protobuf.Timestamp expire_time = 3; - // Output only for the [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] operation. - // Required for the [UpdateBackup][google.spanner.admin.database.v1.DatabaseAdmin.UpdateBackup] operation. + // Output only for the + // [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] + // operation. Required for the + // [UpdateBackup][google.spanner.admin.database.v1.DatabaseAdmin.UpdateBackup] + // operation. // // A globally unique identifier for the backup which cannot be // changed. Values are of the form @@ -89,10 +93,12 @@ message Backup { // `projects//instances/`. string name = 1; - // Output only. The time the [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] + // Output only. The time the + // [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] // request is received. If the request does not specify `version_time`, the // `version_time` of the backup will be equivalent to the `create_time`. - google.protobuf.Timestamp create_time = 4 [(google.api.field_behavior) = OUTPUT_ONLY]; + google.protobuf.Timestamp create_time = 4 + [(google.api.field_behavior) = OUTPUT_ONLY]; // Output only. Size of the backup in bytes. int64 size_bytes = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; @@ -115,10 +121,21 @@ message Backup { ]; // Output only. The encryption information for the backup. - EncryptionInfo encryption_info = 8 [(google.api.field_behavior) = OUTPUT_ONLY]; + EncryptionInfo encryption_info = 8 + [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Output only. The encryption information for the backup, whether it is + // protected by one or more KMS keys. The information includes all Cloud + // KMS key versions used to encrypt the backup. The `encryption_status' field + // inside of each `EncryptionInfo` is not populated. At least one of the key + // versions must be available for the backup to be restored. If a key version + // is revoked in the middle of a restore, the restore behavior is undefined. + repeated EncryptionInfo encryption_information = 13 + [(google.api.field_behavior) = OUTPUT_ONLY]; // Output only. The database dialect information for the backup. - DatabaseDialect database_dialect = 10 [(google.api.field_behavior) = OUTPUT_ONLY]; + DatabaseDialect database_dialect = 10 + [(google.api.field_behavior) = OUTPUT_ONLY]; // Output only. The names of the destination backups being created by copying // this source backup. The backup names are of the form @@ -129,9 +146,7 @@ message Backup { // destination backup is deleted), the reference to the backup is removed. repeated string referencing_backups = 11 [ (google.api.field_behavior) = OUTPUT_ONLY, - (google.api.resource_reference) = { - type: "spanner.googleapis.com/Backup" - } + (google.api.resource_reference) = { type: "spanner.googleapis.com/Backup" } ]; // Output only. The max allowed expiration time of the backup, with @@ -139,10 +154,12 @@ message Backup { // multiple APIs: CreateBackup, UpdateBackup, CopyBackup. When updating or // copying an existing backup, the expiration time specified must be // less than `Backup.max_expire_time`. - google.protobuf.Timestamp max_expire_time = 12 [(google.api.field_behavior) = OUTPUT_ONLY]; + google.protobuf.Timestamp max_expire_time = 12 + [(google.api.field_behavior) = OUTPUT_ONLY]; } -// The request for [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup]. +// The request for +// [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup]. message CreateBackupRequest { // Required. The name of the instance in which the backup will be // created. This must be the same instance that contains the database the @@ -165,29 +182,31 @@ message CreateBackupRequest { // Required. The backup to create. Backup backup = 3 [(google.api.field_behavior) = REQUIRED]; - // Optional. The encryption configuration used to encrypt the backup. If this field is - // not specified, the backup will use the same - // encryption configuration as the database by default, namely - // [encryption_type][google.spanner.admin.database.v1.CreateBackupEncryptionConfig.encryption_type] = - // `USE_DATABASE_ENCRYPTION`. - CreateBackupEncryptionConfig encryption_config = 4 [(google.api.field_behavior) = OPTIONAL]; + // Optional. The encryption configuration used to encrypt the backup. If this + // field is not specified, the backup will use the same encryption + // configuration as the database by default, namely + // [encryption_type][google.spanner.admin.database.v1.CreateBackupEncryptionConfig.encryption_type] + // = `USE_DATABASE_ENCRYPTION`. + CreateBackupEncryptionConfig encryption_config = 4 + [(google.api.field_behavior) = OPTIONAL]; } // Metadata type for the operation returned by // [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup]. message CreateBackupMetadata { // The name of the backup being created. - string name = 1 [(google.api.resource_reference) = { - type: "spanner.googleapis.com/Backup" - }]; + string name = 1 [ + (google.api.resource_reference) = { type: "spanner.googleapis.com/Backup" } + ]; // The name of the database the backup is created from. string database = 2 [(google.api.resource_reference) = { - type: "spanner.googleapis.com/Database" - }]; + type: "spanner.googleapis.com/Database" + }]; // The progress of the - // [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] operation. + // [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] + // operation. OperationProgress progress = 3; // The time at which cancellation of this operation was received. @@ -205,10 +224,11 @@ message CreateBackupMetadata { google.protobuf.Timestamp cancel_time = 4; } -// The request for [CopyBackup][google.spanner.admin.database.v1.DatabaseAdmin.CopyBackup]. +// The request for +// [CopyBackup][google.spanner.admin.database.v1.DatabaseAdmin.CopyBackup]. message CopyBackupRequest { - // Required. The name of the destination instance that will contain the backup copy. - // Values are of the form: `projects//instances/`. + // Required. The name of the destination instance that will contain the backup + // copy. Values are of the form: `projects//instances/`. string parent = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { @@ -229,9 +249,7 @@ message CopyBackupRequest { // `projects//instances//backups/`. string source_backup = 3 [ (google.api.field_behavior) = REQUIRED, - (google.api.resource_reference) = { - type: "spanner.googleapis.com/Backup" - } + (google.api.resource_reference) = { type: "spanner.googleapis.com/Backup" } ]; // Required. The expiration time of the backup in microsecond granularity. @@ -239,35 +257,38 @@ message CopyBackupRequest { // from the `create_time` of the source backup. Once the `expire_time` has // passed, the backup is eligible to be automatically deleted by Cloud Spanner // to free the resources used by the backup. - google.protobuf.Timestamp expire_time = 4 [(google.api.field_behavior) = REQUIRED]; - - // Optional. The encryption configuration used to encrypt the backup. If this field is - // not specified, the backup will use the same - // encryption configuration as the source backup by default, namely - // [encryption_type][google.spanner.admin.database.v1.CopyBackupEncryptionConfig.encryption_type] = - // `USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION`. - CopyBackupEncryptionConfig encryption_config = 5 [(google.api.field_behavior) = OPTIONAL]; + google.protobuf.Timestamp expire_time = 4 + [(google.api.field_behavior) = REQUIRED]; + + // Optional. The encryption configuration used to encrypt the backup. If this + // field is not specified, the backup will use the same encryption + // configuration as the source backup by default, namely + // [encryption_type][google.spanner.admin.database.v1.CopyBackupEncryptionConfig.encryption_type] + // = `USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION`. + CopyBackupEncryptionConfig encryption_config = 5 + [(google.api.field_behavior) = OPTIONAL]; } -// Metadata type for the google.longrunning.Operation returned by +// Metadata type for the operation returned by // [CopyBackup][google.spanner.admin.database.v1.DatabaseAdmin.CopyBackup]. message CopyBackupMetadata { // The name of the backup being created through the copy operation. // Values are of the form // `projects//instances//backups/`. - string name = 1 [(google.api.resource_reference) = { - type: "spanner.googleapis.com/Backup" - }]; + string name = 1 [ + (google.api.resource_reference) = { type: "spanner.googleapis.com/Backup" } + ]; // The name of the source backup that is being copied. // Values are of the form // `projects//instances//backups/`. - string source_backup = 2 [(google.api.resource_reference) = { - type: "spanner.googleapis.com/Backup" - }]; + string source_backup = 2 [ + (google.api.resource_reference) = { type: "spanner.googleapis.com/Backup" } + ]; // The progress of the - // [CopyBackup][google.spanner.admin.database.v1.DatabaseAdmin.CopyBackup] operation. + // [CopyBackup][google.spanner.admin.database.v1.DatabaseAdmin.CopyBackup] + // operation. OperationProgress progress = 3; // The time at which cancellation of CopyBackup operation was received. @@ -285,7 +306,8 @@ message CopyBackupMetadata { google.protobuf.Timestamp cancel_time = 4; } -// The request for [UpdateBackup][google.spanner.admin.database.v1.DatabaseAdmin.UpdateBackup]. +// The request for +// [UpdateBackup][google.spanner.admin.database.v1.DatabaseAdmin.UpdateBackup]. message UpdateBackupRequest { // Required. The backup to update. `backup.name`, and the fields to be updated // as specified by `update_mask` are required. Other fields are ignored. @@ -298,36 +320,36 @@ message UpdateBackupRequest { // resource, not to the request message. The field mask must always be // specified; this prevents any future fields from being erased accidentally // by clients that do not know about them. - google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; + google.protobuf.FieldMask update_mask = 2 + [(google.api.field_behavior) = REQUIRED]; } -// The request for [GetBackup][google.spanner.admin.database.v1.DatabaseAdmin.GetBackup]. +// The request for +// [GetBackup][google.spanner.admin.database.v1.DatabaseAdmin.GetBackup]. message GetBackupRequest { // Required. Name of the backup. // Values are of the form // `projects//instances//backups/`. string name = 1 [ (google.api.field_behavior) = REQUIRED, - (google.api.resource_reference) = { - type: "spanner.googleapis.com/Backup" - } + (google.api.resource_reference) = { type: "spanner.googleapis.com/Backup" } ]; } -// The request for [DeleteBackup][google.spanner.admin.database.v1.DatabaseAdmin.DeleteBackup]. +// The request for +// [DeleteBackup][google.spanner.admin.database.v1.DatabaseAdmin.DeleteBackup]. message DeleteBackupRequest { // Required. Name of the backup to delete. // Values are of the form // `projects//instances//backups/`. string name = 1 [ (google.api.field_behavior) = REQUIRED, - (google.api.resource_reference) = { - type: "spanner.googleapis.com/Backup" - } + (google.api.resource_reference) = { type: "spanner.googleapis.com/Backup" } ]; } -// The request for [ListBackups][google.spanner.admin.database.v1.DatabaseAdmin.ListBackups]. +// The request for +// [ListBackups][google.spanner.admin.database.v1.DatabaseAdmin.ListBackups]. message ListBackupsRequest { // Required. The instance to list backups from. Values are of the // form `projects//instances/`. @@ -346,7 +368,9 @@ message ListBackupsRequest { // must be one of: `<`, `>`, `<=`, `>=`, `!=`, `=`, or `:`. // Colon `:` is the contains operator. Filter rules are not case sensitive. // - // The following fields in the [Backup][google.spanner.admin.database.v1.Backup] are eligible for filtering: + // The following fields in the + // [Backup][google.spanner.admin.database.v1.Backup] are eligible for + // filtering: // // * `name` // * `database` @@ -380,21 +404,23 @@ message ListBackupsRequest { int32 page_size = 3; // If non-empty, `page_token` should contain a - // [next_page_token][google.spanner.admin.database.v1.ListBackupsResponse.next_page_token] from a - // previous [ListBackupsResponse][google.spanner.admin.database.v1.ListBackupsResponse] to the same `parent` and with the same - // `filter`. + // [next_page_token][google.spanner.admin.database.v1.ListBackupsResponse.next_page_token] + // from a previous + // [ListBackupsResponse][google.spanner.admin.database.v1.ListBackupsResponse] + // to the same `parent` and with the same `filter`. string page_token = 4; } -// The response for [ListBackups][google.spanner.admin.database.v1.DatabaseAdmin.ListBackups]. +// The response for +// [ListBackups][google.spanner.admin.database.v1.DatabaseAdmin.ListBackups]. message ListBackupsResponse { // The list of matching backups. Backups returned are ordered by `create_time` // in descending order, starting from the most recent `create_time`. repeated Backup backups = 1; // `next_page_token` can be sent in a subsequent - // [ListBackups][google.spanner.admin.database.v1.DatabaseAdmin.ListBackups] call to fetch more - // of the matching backups. + // [ListBackups][google.spanner.admin.database.v1.DatabaseAdmin.ListBackups] + // call to fetch more of the matching backups. string next_page_token = 2; } @@ -424,7 +450,9 @@ message ListBackupOperationsRequest { // * `name` - The name of the long-running operation // * `done` - False if the operation is in progress, else true. // * `metadata.@type` - the type of metadata. For example, the type string - // for [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata] is + // for + // [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata] + // is // `type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata`. // * `metadata.` - any field in metadata.value. // `metadata.@type` must be specified first if filtering on metadata @@ -442,14 +470,15 @@ message ListBackupOperationsRequest { // * `done:true` - The operation is complete. // * `(metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ // `metadata.database:prod` - Returns operations where: - // * The operation's metadata type is [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. - // * The database the backup was taken from has a name containing the - // string "prod". + // * The operation's metadata type is + // [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. + // * The source database name of backup contains the string "prod". // * `(metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ // `(metadata.name:howl) AND` \ // `(metadata.progress.start_time < \"2018-03-28T14:50:00Z\") AND` \ // `(error:*)` - Returns operations where: - // * The operation's metadata type is [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. + // * The operation's metadata type is + // [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. // * The backup name contains the string "howl". // * The operation started before 2018-03-28T14:50:00Z. // * The operation resulted in an error. @@ -457,9 +486,9 @@ message ListBackupOperationsRequest { // `(metadata.source_backup:test) AND` \ // `(metadata.progress.start_time < \"2022-01-18T14:50:00Z\") AND` \ // `(error:*)` - Returns operations where: - // * The operation's metadata type is [CopyBackupMetadata][google.spanner.admin.database.v1.CopyBackupMetadata]. - // * The source backup of the copied backup name contains the string - // "test". + // * The operation's metadata type is + // [CopyBackupMetadata][google.spanner.admin.database.v1.CopyBackupMetadata]. + // * The source backup name contains the string "test". // * The operation started before 2022-01-18T14:50:00Z. // * The operation resulted in an error. // * `((metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ @@ -469,12 +498,13 @@ message ListBackupOperationsRequest { // `(metadata.source_backup:test_bkp)) AND` \ // `(error:*)` - Returns operations where: // * The operation's metadata matches either of criteria: - // * The operation's metadata type is [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata] AND the - // database the backup was taken from has name containing string + // * The operation's metadata type is + // [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata] + // AND the source database name of the backup contains the string // "test_db" - // * The operation's metadata type is [CopyBackupMetadata][google.spanner.admin.database.v1.CopyBackupMetadata] AND the - // backup the backup was copied from has name containing string - // "test_bkp" + // * The operation's metadata type is + // [CopyBackupMetadata][google.spanner.admin.database.v1.CopyBackupMetadata] + // AND the source backup name contains the string "test_bkp" // * The operation resulted in an error. string filter = 2; @@ -484,8 +514,9 @@ message ListBackupOperationsRequest { // If non-empty, `page_token` should contain a // [next_page_token][google.spanner.admin.database.v1.ListBackupOperationsResponse.next_page_token] - // from a previous [ListBackupOperationsResponse][google.spanner.admin.database.v1.ListBackupOperationsResponse] to the - // same `parent` and with the same `filter`. + // from a previous + // [ListBackupOperationsResponse][google.spanner.admin.database.v1.ListBackupOperationsResponse] + // to the same `parent` and with the same `filter`. string page_token = 4; } @@ -512,25 +543,26 @@ message ListBackupOperationsResponse { // Information about a backup. message BackupInfo { // Name of the backup. - string backup = 1 [(google.api.resource_reference) = { - type: "spanner.googleapis.com/Backup" - }]; + string backup = 1 [ + (google.api.resource_reference) = { type: "spanner.googleapis.com/Backup" } + ]; // The backup contains an externally consistent copy of `source_database` at // the timestamp specified by `version_time`. If the - // [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] request did not specify - // `version_time`, the `version_time` of the backup is equivalent to the - // `create_time`. + // [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] + // request did not specify `version_time`, the `version_time` of the backup is + // equivalent to the `create_time`. google.protobuf.Timestamp version_time = 4; - // The time the [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] request was - // received. + // The time the + // [CreateBackup][google.spanner.admin.database.v1.DatabaseAdmin.CreateBackup] + // request was received. google.protobuf.Timestamp create_time = 2; // Name of the database the backup was created from. string source_database = 3 [(google.api.resource_reference) = { - type: "spanner.googleapis.com/Database" - }]; + type: "spanner.googleapis.com/Database" + }]; } // Encryption configuration for the backup to create. @@ -542,9 +574,10 @@ message CreateBackupEncryptionConfig { // Use the same encryption configuration as the database. This is the // default option when - // [encryption_config][google.spanner.admin.database.v1.CreateBackupEncryptionConfig] is empty. - // For example, if the database is using `Customer_Managed_Encryption`, the - // backup will be using the same Cloud KMS key as the database. + // [encryption_config][google.spanner.admin.database.v1.CreateBackupEncryptionConfig] + // is empty. For example, if the database is using + // `Customer_Managed_Encryption`, the backup will be using the same Cloud + // KMS key as the database. USE_DATABASE_ENCRYPTION = 1; // Use Google default encryption. @@ -560,8 +593,8 @@ message CreateBackupEncryptionConfig { // Optional. The Cloud KMS key that will be used to protect the backup. // This field should be set only when - // [encryption_type][google.spanner.admin.database.v1.CreateBackupEncryptionConfig.encryption_type] is - // `CUSTOMER_MANAGED_ENCRYPTION`. Values are of the form + // [encryption_type][google.spanner.admin.database.v1.CreateBackupEncryptionConfig.encryption_type] + // is `CUSTOMER_MANAGED_ENCRYPTION`. Values are of the form // `projects//locations//keyRings//cryptoKeys/`. string kms_key_name = 2 [ (google.api.field_behavior) = OPTIONAL, @@ -569,6 +602,28 @@ message CreateBackupEncryptionConfig { type: "cloudkms.googleapis.com/CryptoKey" } ]; + + // Optional. Specifies the KMS configuration for the one or more keys used to + // protect the backup. Values are of the form + // `projects//locations//keyRings//cryptoKeys/`. + // + // The keys referenced by kms_key_names must fully cover all + // regions of the backup's instance configuration. Some examples: + // * For single region instance configs, specify a single regional + // location KMS key. + // * For multi-regional instance configs of type GOOGLE_MANAGED, + // either specify a multi-regional location KMS key or multiple regional + // location KMS keys that cover all regions in the instance config. + // * For an instance config of type USER_MANAGED, please specify only + // regional location KMS keys to cover each region in the instance config. + // Multi-regional location KMS keys are not supported for USER_MANAGED + // instance configs. + repeated string kms_key_names = 3 [ + (google.api.field_behavior) = OPTIONAL, + (google.api.resource_reference) = { + type: "cloudkms.googleapis.com/CryptoKey" + } + ]; } // Encryption configuration for the copied backup. @@ -578,17 +633,20 @@ message CopyBackupEncryptionConfig { // Unspecified. Do not use. ENCRYPTION_TYPE_UNSPECIFIED = 0; - // This is the default option for [CopyBackup][google.spanner.admin.database.v1.DatabaseAdmin.CopyBackup] - // when [encryption_config][google.spanner.admin.database.v1.CopyBackupEncryptionConfig] is not specified. - // For example, if the source backup is using `Customer_Managed_Encryption`, - // the backup will be using the same Cloud KMS key as the source backup. + // This is the default option for + // [CopyBackup][google.spanner.admin.database.v1.DatabaseAdmin.CopyBackup] + // when + // [encryption_config][google.spanner.admin.database.v1.CopyBackupEncryptionConfig] + // is not specified. For example, if the source backup is using + // `Customer_Managed_Encryption`, the backup will be using the same Cloud + // KMS key as the source backup. USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION = 1; // Use Google default encryption. GOOGLE_DEFAULT_ENCRYPTION = 2; - // Use customer managed encryption. If specified, `kms_key_name` - // must contain a valid Cloud KMS key. + // Use customer managed encryption. If specified, either `kms_key_name` or + // `kms_key_names` must contain valid Cloud KMS key(s). CUSTOMER_MANAGED_ENCRYPTION = 3; } @@ -597,8 +655,8 @@ message CopyBackupEncryptionConfig { // Optional. The Cloud KMS key that will be used to protect the backup. // This field should be set only when - // [encryption_type][google.spanner.admin.database.v1.CopyBackupEncryptionConfig.encryption_type] is - // `CUSTOMER_MANAGED_ENCRYPTION`. Values are of the form + // [encryption_type][google.spanner.admin.database.v1.CopyBackupEncryptionConfig.encryption_type] + // is `CUSTOMER_MANAGED_ENCRYPTION`. Values are of the form // `projects//locations//keyRings//cryptoKeys/`. string kms_key_name = 2 [ (google.api.field_behavior) = OPTIONAL, @@ -606,4 +664,27 @@ message CopyBackupEncryptionConfig { type: "cloudkms.googleapis.com/CryptoKey" } ]; + + // Optional. Specifies the KMS configuration for the one or more keys used to + // protect the backup. Values are of the form + // `projects//locations//keyRings//cryptoKeys/`. + // Kms keys specified can be in any order. + // + // The keys referenced by kms_key_names must fully cover all + // regions of the backup's instance configuration. Some examples: + // * For single region instance configs, specify a single regional + // location KMS key. + // * For multi-regional instance configs of type GOOGLE_MANAGED, + // either specify a multi-regional location KMS key or multiple regional + // location KMS keys that cover all regions in the instance config. + // * For an instance config of type USER_MANAGED, please specify only + // regional location KMS keys to cover each region in the instance config. + // Multi-regional location KMS keys are not supported for USER_MANAGED + // instance configs. + repeated string kms_key_names = 3 [ + (google.api.field_behavior) = OPTIONAL, + (google.api.resource_reference) = { + type: "cloudkms.googleapis.com/CryptoKey" + } + ]; } diff --git a/protos/google/spanner/admin/database/v1/common.proto b/protos/google/spanner/admin/database/v1/common.proto index 32d7519e3..a91012306 100644 --- a/protos/google/spanner/admin/database/v1/common.proto +++ b/protos/google/spanner/admin/database/v1/common.proto @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. @@ -58,8 +58,27 @@ message EncryptionConfig { // the database. Values are of the form // `projects//locations//keyRings//cryptoKeys/`. string kms_key_name = 2 [(google.api.resource_reference) = { - type: "cloudkms.googleapis.com/CryptoKey" - }]; + type: "cloudkms.googleapis.com/CryptoKey" + }]; + + // Specifies the KMS configuration for the one or more keys used to encrypt + // the database. Values are of the form + // `projects//locations//keyRings//cryptoKeys/`. + // + // The keys referenced by kms_key_names must fully cover all + // regions of the database instance configuration. Some examples: + // * For single region database instance configs, specify a single regional + // location KMS key. + // * For multi-regional database instance configs of type GOOGLE_MANAGED, + // either specify a multi-regional location KMS key or multiple regional + // location KMS keys that cover all regions in the instance config. + // * For a database instance config of type USER_MANAGED, please specify only + // regional location KMS keys to cover each region in the instance config. + // Multi-regional location KMS keys are not supported for USER_MANAGED + // instance configs. + repeated string kms_key_names = 3 [(google.api.resource_reference) = { + type: "cloudkms.googleapis.com/CryptoKey" + }]; } // Encryption information for a Cloud Spanner database or backup. @@ -83,13 +102,14 @@ message EncryptionInfo { // Output only. The type of encryption. Type encryption_type = 3 [(google.api.field_behavior) = OUTPUT_ONLY]; - // Output only. If present, the status of a recent encrypt/decrypt call on underlying data - // for this database or backup. Regardless of status, data is always encrypted - // at rest. - google.rpc.Status encryption_status = 4 [(google.api.field_behavior) = OUTPUT_ONLY]; + // Output only. If present, the status of a recent encrypt/decrypt call on + // underlying data for this database or backup. Regardless of status, data is + // always encrypted at rest. + google.rpc.Status encryption_status = 4 + [(google.api.field_behavior) = OUTPUT_ONLY]; - // Output only. A Cloud KMS key version that is being used to protect the database or - // backup. + // Output only. A Cloud KMS key version that is being used to protect the + // database or backup. string kms_key_version = 2 [ (google.api.field_behavior) = OUTPUT_ONLY, (google.api.resource_reference) = { @@ -104,7 +124,7 @@ enum DatabaseDialect { // GOOGLE_STANDARD_SQL dialect. DATABASE_DIALECT_UNSPECIFIED = 0; - // Google standard SQL. + // GoogleSQL supported SQL. GOOGLE_STANDARD_SQL = 1; // PostgreSQL supported SQL. diff --git a/protos/google/spanner/admin/database/v1/spanner_database_admin.proto b/protos/google/spanner/admin/database/v1/spanner_database_admin.proto index a522c08c1..0d4e26170 100644 --- a/protos/google/spanner/admin/database/v1/spanner_database_admin.proto +++ b/protos/google/spanner/admin/database/v1/spanner_database_admin.proto @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// 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. @@ -46,7 +46,7 @@ option (google.api.resource_definition) = { // The Cloud Spanner Database Admin API can be used to: // * create, drop, and list databases // * update the schema of pre-existing databases -// * create, delete and list backups for a database +// * create, delete, copy and list backups for a database // * restore a database from an existing backup service DatabaseAdmin { option (google.api.default_host) = "spanner.googleapis.com"; @@ -67,10 +67,11 @@ service DatabaseAdmin { // have a name of the format `/operations/` and // can be used to track preparation of the database. The // [metadata][google.longrunning.Operation.metadata] field type is - // [CreateDatabaseMetadata][google.spanner.admin.database.v1.CreateDatabaseMetadata]. The - // [response][google.longrunning.Operation.response] field type is + // [CreateDatabaseMetadata][google.spanner.admin.database.v1.CreateDatabaseMetadata]. + // The [response][google.longrunning.Operation.response] field type is // [Database][google.spanner.admin.database.v1.Database], if successful. - rpc CreateDatabase(CreateDatabaseRequest) returns (google.longrunning.Operation) { + rpc CreateDatabase(CreateDatabaseRequest) + returns (google.longrunning.Operation) { option (google.api.http) = { post: "/v1/{parent=projects/*/instances/*}/databases" body: "*" @@ -145,8 +146,10 @@ service DatabaseAdmin { // the format `/operations/` and can be used to // track execution of the schema change(s). The // [metadata][google.longrunning.Operation.metadata] field type is - // [UpdateDatabaseDdlMetadata][google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata]. The operation has no response. - rpc UpdateDatabaseDdl(UpdateDatabaseDdlRequest) returns (google.longrunning.Operation) { + // [UpdateDatabaseDdlMetadata][google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata]. + // The operation has no response. + rpc UpdateDatabaseDdl(UpdateDatabaseDdlRequest) + returns (google.longrunning.Operation) { option (google.api.http) = { patch: "/v1/{database=projects/*/instances/*/databases/*}/ddl" body: "*" @@ -187,7 +190,8 @@ service DatabaseAdmin { // permission on [resource][google.iam.v1.SetIamPolicyRequest.resource]. // For backups, authorization requires `spanner.backups.setIamPolicy` // permission on [resource][google.iam.v1.SetIamPolicyRequest.resource]. - rpc SetIamPolicy(google.iam.v1.SetIamPolicyRequest) returns (google.iam.v1.Policy) { + rpc SetIamPolicy(google.iam.v1.SetIamPolicyRequest) + returns (google.iam.v1.Policy) { option (google.api.http) = { post: "/v1/{resource=projects/*/instances/*/databases/*}:setIamPolicy" body: "*" @@ -207,7 +211,8 @@ service DatabaseAdmin { // [resource][google.iam.v1.GetIamPolicyRequest.resource]. // For backups, authorization requires `spanner.backups.getIamPolicy` // permission on [resource][google.iam.v1.GetIamPolicyRequest.resource]. - rpc GetIamPolicy(google.iam.v1.GetIamPolicyRequest) returns (google.iam.v1.Policy) { + rpc GetIamPolicy(google.iam.v1.GetIamPolicyRequest) + returns (google.iam.v1.Policy) { option (google.api.http) = { post: "/v1/{resource=projects/*/instances/*/databases/*}:getIamPolicy" body: "*" @@ -229,7 +234,8 @@ service DatabaseAdmin { // Calling this method on a backup that does not exist will // result in a NOT_FOUND error if the user has // `spanner.backups.list` permission on the containing instance. - rpc TestIamPermissions(google.iam.v1.TestIamPermissionsRequest) returns (google.iam.v1.TestIamPermissionsResponse) { + rpc TestIamPermissions(google.iam.v1.TestIamPermissionsRequest) + returns (google.iam.v1.TestIamPermissionsResponse) { option (google.api.http) = { post: "/v1/{resource=projects/*/instances/*/databases/*}:testIamPermissions" body: "*" @@ -251,12 +257,12 @@ service DatabaseAdmin { // `projects//instances//backups//operations/` // and can be used to track creation of the backup. The // [metadata][google.longrunning.Operation.metadata] field type is - // [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. The - // [response][google.longrunning.Operation.response] field type is - // [Backup][google.spanner.admin.database.v1.Backup], if successful. Cancelling the returned operation will stop the - // creation and delete the backup. - // There can be only one pending backup creation per database. Backup creation - // of different databases can run concurrently. + // [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. + // The [response][google.longrunning.Operation.response] field type is + // [Backup][google.spanner.admin.database.v1.Backup], if successful. + // Cancelling the returned operation will stop the creation and delete the + // backup. There can be only one pending backup creation per database. Backup + // creation of different databases can run concurrently. rpc CreateBackup(CreateBackupRequest) returns (google.longrunning.Operation) { option (google.api.http) = { post: "/v1/{parent=projects/*/instances/*}/backups" @@ -278,22 +284,25 @@ service DatabaseAdmin { // The [metadata][google.longrunning.Operation.metadata] field type is // [CopyBackupMetadata][google.spanner.admin.database.v1.CopyBackupMetadata]. // The [response][google.longrunning.Operation.response] field type is - // [Backup][google.spanner.admin.database.v1.Backup], if successful. Cancelling the returned operation will stop the - // copying and delete the backup. - // Concurrent CopyBackup requests can run on the same source backup. + // [Backup][google.spanner.admin.database.v1.Backup], if successful. + // Cancelling the returned operation will stop the copying and delete the + // destination backup. Concurrent CopyBackup requests can run on the same + // source backup. rpc CopyBackup(CopyBackupRequest) returns (google.longrunning.Operation) { option (google.api.http) = { post: "/v1/{parent=projects/*/instances/*}/backups:copy" body: "*" }; - option (google.api.method_signature) = "parent,backup_id,source_backup,expire_time"; + option (google.api.method_signature) = + "parent,backup_id,source_backup,expire_time"; option (google.longrunning.operation_info) = { response_type: "google.spanner.admin.database.v1.Backup" metadata_type: "google.spanner.admin.database.v1.CopyBackupMetadata" }; } - // Gets metadata on a pending or completed [Backup][google.spanner.admin.database.v1.Backup]. + // Gets metadata on a pending or completed + // [Backup][google.spanner.admin.database.v1.Backup]. rpc GetBackup(GetBackupRequest) returns (Backup) { option (google.api.http) = { get: "/v1/{name=projects/*/instances/*/backups/*}" @@ -301,7 +310,8 @@ service DatabaseAdmin { option (google.api.method_signature) = "name"; } - // Updates a pending or completed [Backup][google.spanner.admin.database.v1.Backup]. + // Updates a pending or completed + // [Backup][google.spanner.admin.database.v1.Backup]. rpc UpdateBackup(UpdateBackupRequest) returns (Backup) { option (google.api.http) = { patch: "/v1/{backup.name=projects/*/instances/*/backups/*}" @@ -310,7 +320,8 @@ service DatabaseAdmin { option (google.api.method_signature) = "backup,update_mask"; } - // Deletes a pending or completed [Backup][google.spanner.admin.database.v1.Backup]. + // Deletes a pending or completed + // [Backup][google.spanner.admin.database.v1.Backup]. rpc DeleteBackup(DeleteBackupRequest) returns (google.protobuf.Empty) { option (google.api.http) = { delete: "/v1/{name=projects/*/instances/*/backups/*}" @@ -345,7 +356,8 @@ service DatabaseAdmin { // Once the restore operation completes, a new restore operation can be // initiated, without waiting for the optimize operation associated with the // first restore to complete. - rpc RestoreDatabase(RestoreDatabaseRequest) returns (google.longrunning.Operation) { + rpc RestoreDatabase(RestoreDatabaseRequest) + returns (google.longrunning.Operation) { option (google.api.http) = { post: "/v1/{parent=projects/*/instances/*}/databases:restore" body: "*" @@ -365,7 +377,8 @@ service DatabaseAdmin { // `metadata.type_url` describes the type of the metadata. Operations returned // include those that have completed/failed/canceled within the last 7 days, // and pending operations. - rpc ListDatabaseOperations(ListDatabaseOperationsRequest) returns (ListDatabaseOperationsResponse) { + rpc ListDatabaseOperations(ListDatabaseOperationsRequest) + returns (ListDatabaseOperationsResponse) { option (google.api.http) = { get: "/v1/{parent=projects/*/instances/*}/databaseOperations" }; @@ -382,7 +395,8 @@ service DatabaseAdmin { // and pending operations. Operations returned are ordered by // `operation.metadata.value.progress.start_time` in descending order starting // from the most recently started operation. - rpc ListBackupOperations(ListBackupOperationsRequest) returns (ListBackupOperationsResponse) { + rpc ListBackupOperations(ListBackupOperationsRequest) + returns (ListBackupOperationsResponse) { option (google.api.http) = { get: "/v1/{parent=projects/*/instances/*}/backupOperations" }; @@ -390,7 +404,8 @@ service DatabaseAdmin { } // Lists Cloud Spanner database roles. - rpc ListDatabaseRoles(ListDatabaseRolesRequest) returns (ListDatabaseRolesResponse) { + rpc ListDatabaseRoles(ListDatabaseRolesRequest) + returns (ListDatabaseRolesResponse) { option (google.api.http) = { get: "/v1/{parent=projects/*/instances/*/databases/*}/databaseRoles" }; @@ -452,7 +467,8 @@ message Database { State state = 2 [(google.api.field_behavior) = OUTPUT_ONLY]; // Output only. If exists, the time at which the database creation started. - google.protobuf.Timestamp create_time = 3 [(google.api.field_behavior) = OUTPUT_ONLY]; + google.protobuf.Timestamp create_time = 3 + [(google.api.field_behavior) = OUTPUT_ONLY]; // Output only. Applicable only for restored databases. Contains information // about the restore source. @@ -462,32 +478,37 @@ message Database { // field contains the encryption configuration for the database. // For databases that are using Google default or other types of encryption, // this field is empty. - EncryptionConfig encryption_config = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; + EncryptionConfig encryption_config = 5 + [(google.api.field_behavior) = OUTPUT_ONLY]; // Output only. For databases that are using customer managed encryption, this // field contains the encryption information for the database, such as - // encryption state and the Cloud KMS key versions that are in use. + // all Cloud KMS key versions that are in use. The `encryption_status' field + // inside of each `EncryptionInfo` is not populated. // // For databases that are using Google default or other types of encryption, // this field is empty. // // This field is propagated lazily from the backend. There might be a delay // from when a key version is being used and when it appears in this field. - repeated EncryptionInfo encryption_info = 8 [(google.api.field_behavior) = OUTPUT_ONLY]; + repeated EncryptionInfo encryption_info = 8 + [(google.api.field_behavior) = OUTPUT_ONLY]; // Output only. The period in which Cloud Spanner retains all versions of data // for the database. This is the same as the value of version_retention_period // database option set using - // [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl]. Defaults to 1 hour, - // if not set. - string version_retention_period = 6 [(google.api.field_behavior) = OUTPUT_ONLY]; + // [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl]. + // Defaults to 1 hour, if not set. + string version_retention_period = 6 + [(google.api.field_behavior) = OUTPUT_ONLY]; // Output only. Earliest timestamp at which older versions of the data can be // read. This value is continuously updated by Cloud Spanner and becomes stale // the moment it is queried. If you are using this value to recover data, make // sure to account for the time from the moment when the value is queried to // the moment when you initiate the recovery. - google.protobuf.Timestamp earliest_version_time = 7 [(google.api.field_behavior) = OUTPUT_ONLY]; + google.protobuf.Timestamp earliest_version_time = 7 + [(google.api.field_behavior) = OUTPUT_ONLY]; // Output only. The read-write region which contains the database's leader // replicas. @@ -498,10 +519,13 @@ message Database { string default_leader = 9 [(google.api.field_behavior) = OUTPUT_ONLY]; // Output only. The dialect of the Cloud Spanner Database. - DatabaseDialect database_dialect = 10 [(google.api.field_behavior) = OUTPUT_ONLY]; + DatabaseDialect database_dialect = 10 + [(google.api.field_behavior) = OUTPUT_ONLY]; // Whether drop protection is enabled for this database. Defaults to false, - // if not set. + // if not set. For more details, please see how to [prevent accidental + // database + // deletion](https://cloud.google.com/spanner/docs/prevent-database-deletion). bool enable_drop_protection = 11; // Output only. If true, the database is being updated. If false, there are no @@ -509,7 +533,8 @@ message Database { bool reconciling = 12 [(google.api.field_behavior) = OUTPUT_ONLY]; } -// The request for [ListDatabases][google.spanner.admin.database.v1.DatabaseAdmin.ListDatabases]. +// The request for +// [ListDatabases][google.spanner.admin.database.v1.DatabaseAdmin.ListDatabases]. message ListDatabasesRequest { // Required. The instance whose databases should be listed. // Values are of the form `projects//instances/`. @@ -525,23 +550,26 @@ message ListDatabasesRequest { int32 page_size = 3; // If non-empty, `page_token` should contain a - // [next_page_token][google.spanner.admin.database.v1.ListDatabasesResponse.next_page_token] from a - // previous [ListDatabasesResponse][google.spanner.admin.database.v1.ListDatabasesResponse]. + // [next_page_token][google.spanner.admin.database.v1.ListDatabasesResponse.next_page_token] + // from a previous + // [ListDatabasesResponse][google.spanner.admin.database.v1.ListDatabasesResponse]. string page_token = 4; } -// The response for [ListDatabases][google.spanner.admin.database.v1.DatabaseAdmin.ListDatabases]. +// The response for +// [ListDatabases][google.spanner.admin.database.v1.DatabaseAdmin.ListDatabases]. message ListDatabasesResponse { // Databases that matched the request. repeated Database databases = 1; // `next_page_token` can be sent in a subsequent - // [ListDatabases][google.spanner.admin.database.v1.DatabaseAdmin.ListDatabases] call to fetch more - // of the matching databases. + // [ListDatabases][google.spanner.admin.database.v1.DatabaseAdmin.ListDatabases] + // call to fetch more of the matching databases. string next_page_token = 2; } -// The request for [CreateDatabase][google.spanner.admin.database.v1.DatabaseAdmin.CreateDatabase]. +// The request for +// [CreateDatabase][google.spanner.admin.database.v1.DatabaseAdmin.CreateDatabase]. message CreateDatabaseRequest { // Required. The name of the instance that will serve the new database. // Values are of the form `projects//instances/`. @@ -565,10 +593,11 @@ message CreateDatabaseRequest { // if there is an error in any statement, the database is not created. repeated string extra_statements = 3 [(google.api.field_behavior) = OPTIONAL]; - // Optional. The encryption configuration for the database. If this field is not - // specified, Cloud Spanner will encrypt/decrypt all data at rest using + // Optional. The encryption configuration for the database. If this field is + // not specified, Cloud Spanner will encrypt/decrypt all data at rest using // Google default encryption. - EncryptionConfig encryption_config = 4 [(google.api.field_behavior) = OPTIONAL]; + EncryptionConfig encryption_config = 4 + [(google.api.field_behavior) = OPTIONAL]; // Optional. The dialect of the Cloud Spanner Database. DatabaseDialect database_dialect = 5 [(google.api.field_behavior) = OPTIONAL]; @@ -596,11 +625,12 @@ message CreateDatabaseRequest { message CreateDatabaseMetadata { // The database being created. string database = 1 [(google.api.resource_reference) = { - type: "spanner.googleapis.com/Database" - }]; + type: "spanner.googleapis.com/Database" + }]; } -// The request for [GetDatabase][google.spanner.admin.database.v1.DatabaseAdmin.GetDatabase]. +// The request for +// [GetDatabase][google.spanner.admin.database.v1.DatabaseAdmin.GetDatabase]. message GetDatabaseRequest { // Required. The name of the requested database. Values are of the form // `projects//instances//databases/`. @@ -657,8 +687,8 @@ message UpdateDatabaseMetadata { // Each batch of statements is assigned a name which can be used with // the [Operations][google.longrunning.Operations] API to monitor // progress. See the -// [operation_id][google.spanner.admin.database.v1.UpdateDatabaseDdlRequest.operation_id] field for more -// details. +// [operation_id][google.spanner.admin.database.v1.UpdateDatabaseDdlRequest.operation_id] +// field for more details. message UpdateDatabaseDdlRequest { // Required. The database to update. string database = 1 [ @@ -678,18 +708,20 @@ message UpdateDatabaseDdlRequest { // // Specifying an explicit operation ID simplifies determining // whether the statements were executed in the event that the - // [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl] call is replayed, - // or the return value is otherwise lost: the [database][google.spanner.admin.database.v1.UpdateDatabaseDdlRequest.database] and - // `operation_id` fields can be combined to form the + // [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl] + // call is replayed, or the return value is otherwise lost: the + // [database][google.spanner.admin.database.v1.UpdateDatabaseDdlRequest.database] + // and `operation_id` fields can be combined to form the // [name][google.longrunning.Operation.name] of the resulting - // [longrunning.Operation][google.longrunning.Operation]: `/operations/`. + // [longrunning.Operation][google.longrunning.Operation]: + // `/operations/`. // // `operation_id` should be unique within the database, and must be // a valid identifier: `[a-z][a-z0-9_]*`. Note that // automatically-generated operation IDs always begin with an // underscore. If the named operation already exists, - // [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl] returns - // `ALREADY_EXISTS`. + // [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl] + // returns `ALREADY_EXISTS`. string operation_id = 3; // Optional. Proto descriptors used by CREATE/ALTER PROTO BUNDLE statements. @@ -735,8 +767,8 @@ message DdlStatementActionInfo { message UpdateDatabaseDdlMetadata { // The database being modified. string database = 1 [(google.api.resource_reference) = { - type: "spanner.googleapis.com/Database" - }]; + type: "spanner.googleapis.com/Database" + }]; // For an update this list contains all the statements. For an // individual statement, this list contains only that statement. @@ -766,7 +798,8 @@ message UpdateDatabaseDdlMetadata { repeated DdlStatementActionInfo actions = 6; } -// The request for [DropDatabase][google.spanner.admin.database.v1.DatabaseAdmin.DropDatabase]. +// The request for +// [DropDatabase][google.spanner.admin.database.v1.DatabaseAdmin.DropDatabase]. message DropDatabaseRequest { // Required. The database to be dropped. string database = 1 [ @@ -777,7 +810,8 @@ message DropDatabaseRequest { ]; } -// The request for [GetDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.GetDatabaseDdl]. +// The request for +// [GetDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.GetDatabaseDdl]. message GetDatabaseDdlRequest { // Required. The database whose schema we wish to get. // Values are of the form @@ -790,7 +824,8 @@ message GetDatabaseDdlRequest { ]; } -// The response for [GetDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.GetDatabaseDdl]. +// The response for +// [GetDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.GetDatabaseDdl]. message GetDatabaseDdlResponse { // A list of formatted DDL statements defining the schema of the database // specified in the request. @@ -830,7 +865,9 @@ message ListDatabaseOperationsRequest { // * `name` - The name of the long-running operation // * `done` - False if the operation is in progress, else true. // * `metadata.@type` - the type of metadata. For example, the type string - // for [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata] is + // for + // [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata] + // is // `type.googleapis.com/google.spanner.admin.database.v1.RestoreDatabaseMetadata`. // * `metadata.` - any field in metadata.value. // `metadata.@type` must be specified first, if filtering on metadata @@ -852,7 +889,8 @@ message ListDatabaseOperationsRequest { // `(metadata.name:restored_howl) AND` \ // `(metadata.progress.start_time < \"2018-03-28T14:50:00Z\") AND` \ // `(error:*)` - Return operations where: - // * The operation's metadata type is [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. + // * The operation's metadata type is + // [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. // * The database is restored from a backup. // * The backup name contains "backup_howl". // * The restored database's name contains "restored_howl". @@ -866,8 +904,9 @@ message ListDatabaseOperationsRequest { // If non-empty, `page_token` should contain a // [next_page_token][google.spanner.admin.database.v1.ListDatabaseOperationsResponse.next_page_token] - // from a previous [ListDatabaseOperationsResponse][google.spanner.admin.database.v1.ListDatabaseOperationsResponse] to the - // same `parent` and with the same `filter`. + // from a previous + // [ListDatabaseOperationsResponse][google.spanner.admin.database.v1.ListDatabaseOperationsResponse] + // to the same `parent` and with the same `filter`. string page_token = 4; } @@ -913,17 +952,18 @@ message RestoreDatabaseRequest { // Name of the backup from which to restore. Values are of the form // `projects//instances//backups/`. string backup = 3 [(google.api.resource_reference) = { - type: "spanner.googleapis.com/Backup" - }]; + type: "spanner.googleapis.com/Backup" + }]; } - // Optional. An encryption configuration describing the encryption type and key - // resources in Cloud KMS used to encrypt/decrypt the database to restore to. - // If this field is not specified, the restored database will use - // the same encryption configuration as the backup by default, namely - // [encryption_type][google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.encryption_type] = - // `USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION`. - RestoreDatabaseEncryptionConfig encryption_config = 4 [(google.api.field_behavior) = OPTIONAL]; + // Optional. An encryption configuration describing the encryption type and + // key resources in Cloud KMS used to encrypt/decrypt the database to restore + // to. If this field is not specified, the restored database will use the same + // encryption configuration as the backup by default, namely + // [encryption_type][google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.encryption_type] + // = `USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION`. + RestoreDatabaseEncryptionConfig encryption_config = 4 + [(google.api.field_behavior) = OPTIONAL]; } // Encryption configuration for the restored database. @@ -934,7 +974,8 @@ message RestoreDatabaseEncryptionConfig { ENCRYPTION_TYPE_UNSPECIFIED = 0; // This is the default option when - // [encryption_config][google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig] is not specified. + // [encryption_config][google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig] + // is not specified. USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION = 1; // Use Google default encryption. @@ -948,10 +989,10 @@ message RestoreDatabaseEncryptionConfig { // Required. The encryption type of the restored database. EncryptionType encryption_type = 1 [(google.api.field_behavior) = REQUIRED]; - // Optional. The Cloud KMS key that will be used to encrypt/decrypt the restored - // database. This field should be set only when - // [encryption_type][google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.encryption_type] is - // `CUSTOMER_MANAGED_ENCRYPTION`. Values are of the form + // Optional. The Cloud KMS key that will be used to encrypt/decrypt the + // restored database. This field should be set only when + // [encryption_type][google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.encryption_type] + // is `CUSTOMER_MANAGED_ENCRYPTION`. Values are of the form // `projects//locations//keyRings//cryptoKeys/`. string kms_key_name = 2 [ (google.api.field_behavior) = OPTIONAL, @@ -959,6 +1000,28 @@ message RestoreDatabaseEncryptionConfig { type: "cloudkms.googleapis.com/CryptoKey" } ]; + + // Optional. Specifies the KMS configuration for the one or more keys used to + // encrypt the database. Values are of the form + // `projects//locations//keyRings//cryptoKeys/`. + // + // The keys referenced by kms_key_names must fully cover all + // regions of the database instance configuration. Some examples: + // * For single region database instance configs, specify a single regional + // location KMS key. + // * For multi-regional database instance configs of type GOOGLE_MANAGED, + // either specify a multi-regional location KMS key or multiple regional + // location KMS keys that cover all regions in the instance config. + // * For a database instance config of type USER_MANAGED, please specify only + // regional location KMS keys to cover each region in the instance config. + // Multi-regional location KMS keys are not supported for USER_MANAGED + // instance configs. + repeated string kms_key_names = 3 [ + (google.api.field_behavior) = OPTIONAL, + (google.api.resource_reference) = { + type: "cloudkms.googleapis.com/CryptoKey" + } + ]; } // Metadata type for the long-running operation returned by @@ -966,14 +1029,15 @@ message RestoreDatabaseEncryptionConfig { message RestoreDatabaseMetadata { // Name of the database being created and restored to. string name = 1 [(google.api.resource_reference) = { - type: "spanner.googleapis.com/Database" - }]; + type: "spanner.googleapis.com/Database" + }]; // The type of the restore source. RestoreSourceType source_type = 2; // Information about the source used to restore the database, as specified by - // `source` in [RestoreDatabaseRequest][google.spanner.admin.database.v1.RestoreDatabaseRequest]. + // `source` in + // [RestoreDatabaseRequest][google.spanner.admin.database.v1.RestoreDatabaseRequest]. oneof source_info { // Information about the backup used to restore the database. BackupInfo backup_info = 3; @@ -994,7 +1058,8 @@ message RestoreDatabaseMetadata { // operation completed despite cancellation. On successful cancellation, // the operation is not deleted; instead, it becomes an operation with // an [Operation.error][google.longrunning.Operation.error] value with a - // [google.rpc.Status.code][google.rpc.Status.code] of 1, corresponding to `Code.CANCELLED`. + // [google.rpc.Status.code][google.rpc.Status.code] of 1, corresponding to + // `Code.CANCELLED`. google.protobuf.Timestamp cancel_time = 5; // If exists, the name of the long-running operation that will be used to @@ -1004,10 +1069,10 @@ message RestoreDatabaseMetadata { // `projects//instances//databases//operations/` // where the is the name of database being created and restored to. // The metadata type of the long-running operation is - // [OptimizeRestoredDatabaseMetadata][google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata]. This long-running operation will be - // automatically created by the system after the RestoreDatabase long-running - // operation completes successfully. This operation will not be created if the - // restore was not successful. + // [OptimizeRestoredDatabaseMetadata][google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata]. + // This long-running operation will be automatically created by the system + // after the RestoreDatabase long-running operation completes successfully. + // This operation will not be created if the restore was not successful. string optimize_database_operation_name = 6; } @@ -1018,8 +1083,8 @@ message RestoreDatabaseMetadata { message OptimizeRestoredDatabaseMetadata { // Name of the restored database being optimized. string name = 1 [(google.api.resource_reference) = { - type: "spanner.googleapis.com/Database" - }]; + type: "spanner.googleapis.com/Database" + }]; // The progress of the post-restore optimizations. OperationProgress progress = 2; @@ -1042,18 +1107,17 @@ message DatabaseRole { }; // Required. The name of the database role. Values are of the form - // `projects//instances//databases//databaseRoles/ - // {role}`, where `` is as specified in the `CREATE ROLE` - // DDL statement. This name can be passed to Get/Set IAMPolicy methods to - // identify the database role. + // `projects//instances//databases//databaseRoles/` + // where `` is as specified in the `CREATE ROLE` DDL statement. string name = 1 [(google.api.field_behavior) = REQUIRED]; } -// The request for [ListDatabaseRoles][google.spanner.admin.database.v1.DatabaseAdmin.ListDatabaseRoles]. +// The request for +// [ListDatabaseRoles][google.spanner.admin.database.v1.DatabaseAdmin.ListDatabaseRoles]. message ListDatabaseRolesRequest { // Required. The database whose roles should be listed. // Values are of the form - // `projects//instances//databases//databaseRoles`. + // `projects//instances//databases/`. string parent = 1 [ (google.api.field_behavior) = REQUIRED, (google.api.resource_reference) = { @@ -1066,12 +1130,14 @@ message ListDatabaseRolesRequest { int32 page_size = 2; // If non-empty, `page_token` should contain a - // [next_page_token][google.spanner.admin.database.v1.ListDatabaseRolesResponse.next_page_token] from a - // previous [ListDatabaseRolesResponse][google.spanner.admin.database.v1.ListDatabaseRolesResponse]. + // [next_page_token][google.spanner.admin.database.v1.ListDatabaseRolesResponse.next_page_token] + // from a previous + // [ListDatabaseRolesResponse][google.spanner.admin.database.v1.ListDatabaseRolesResponse]. string page_token = 3; } -// The response for [ListDatabaseRoles][google.spanner.admin.database.v1.DatabaseAdmin.ListDatabaseRoles]. +// The response for +// [ListDatabaseRoles][google.spanner.admin.database.v1.DatabaseAdmin.ListDatabaseRoles]. message ListDatabaseRolesResponse { // Database roles that matched the request. repeated DatabaseRole database_roles = 1; diff --git a/protos/google/spanner/admin/instance/v1/common.proto b/protos/google/spanner/admin/instance/v1/common.proto index ab6293acf..075b45fea 100644 --- a/protos/google/spanner/admin/instance/v1/common.proto +++ b/protos/google/spanner/admin/instance/v1/common.proto @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// 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. diff --git a/protos/google/spanner/executor/v1/cloud_executor.proto b/protos/google/spanner/executor/v1/cloud_executor.proto index 3ad36e3ee..73474a650 100644 --- a/protos/google/spanner/executor/v1/cloud_executor.proto +++ b/protos/google/spanner/executor/v1/cloud_executor.proto @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// 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. diff --git a/protos/google/spanner/v1/commit_response.proto b/protos/google/spanner/v1/commit_response.proto index 436a002b8..d44aad63b 100644 --- a/protos/google/spanner/v1/commit_response.proto +++ b/protos/google/spanner/v1/commit_response.proto @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. diff --git a/protos/google/spanner/v1/keys.proto b/protos/google/spanner/v1/keys.proto index 8fb4757f5..82f073b96 100644 --- a/protos/google/spanner/v1/keys.proto +++ b/protos/google/spanner/v1/keys.proto @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. diff --git a/protos/google/spanner/v1/mutation.proto b/protos/google/spanner/v1/mutation.proto index cced61f33..7fbf93f8a 100644 --- a/protos/google/spanner/v1/mutation.proto +++ b/protos/google/spanner/v1/mutation.proto @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. diff --git a/protos/google/spanner/v1/query_plan.proto b/protos/google/spanner/v1/query_plan.proto index c0903bdd7..ba18055e3 100644 --- a/protos/google/spanner/v1/query_plan.proto +++ b/protos/google/spanner/v1/query_plan.proto @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. diff --git a/protos/google/spanner/v1/result_set.proto b/protos/google/spanner/v1/result_set.proto index cfa5719c4..f392c1d7d 100644 --- a/protos/google/spanner/v1/result_set.proto +++ b/protos/google/spanner/v1/result_set.proto @@ -1,4 +1,4 @@ -// Copyright 2022 Google LLC +// 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. diff --git a/protos/google/spanner/v1/spanner.proto b/protos/google/spanner/v1/spanner.proto index 440ebf785..25c67f155 100644 --- a/protos/google/spanner/v1/spanner.proto +++ b/protos/google/spanner/v1/spanner.proto @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// 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. diff --git a/protos/google/spanner/v1/transaction.proto b/protos/google/spanner/v1/transaction.proto index e3f22ee3c..8af513d15 100644 --- a/protos/google/spanner/v1/transaction.proto +++ b/protos/google/spanner/v1/transaction.proto @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// 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. diff --git a/protos/google/spanner/v1/type.proto b/protos/google/spanner/v1/type.proto index 8e28fa7fd..4e77106c9 100644 --- a/protos/google/spanner/v1/type.proto +++ b/protos/google/spanner/v1/type.proto @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// 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. diff --git a/protos/protos.d.ts b/protos/protos.d.ts index 98bfcb679..0003e5dd4 100644 --- a/protos/protos.d.ts +++ b/protos/protos.d.ts @@ -6558,6 +6558,9 @@ export namespace google { /** Backup encryptionInfo */ encryptionInfo?: (google.spanner.admin.database.v1.IEncryptionInfo|null); + /** Backup encryptionInformation */ + encryptionInformation?: (google.spanner.admin.database.v1.IEncryptionInfo[]|null); + /** Backup databaseDialect */ databaseDialect?: (google.spanner.admin.database.v1.DatabaseDialect|keyof typeof google.spanner.admin.database.v1.DatabaseDialect|null); @@ -6604,6 +6607,9 @@ export namespace google { /** Backup encryptionInfo. */ public encryptionInfo?: (google.spanner.admin.database.v1.IEncryptionInfo|null); + /** Backup encryptionInformation. */ + public encryptionInformation: google.spanner.admin.database.v1.IEncryptionInfo[]; + /** Backup databaseDialect. */ public databaseDialect: (google.spanner.admin.database.v1.DatabaseDialect|keyof typeof google.spanner.admin.database.v1.DatabaseDialect); @@ -8023,6 +8029,9 @@ export namespace google { /** CreateBackupEncryptionConfig kmsKeyName */ kmsKeyName?: (string|null); + + /** CreateBackupEncryptionConfig kmsKeyNames */ + kmsKeyNames?: (string[]|null); } /** Represents a CreateBackupEncryptionConfig. */ @@ -8040,6 +8049,9 @@ export namespace google { /** CreateBackupEncryptionConfig kmsKeyName. */ public kmsKeyName: string; + /** CreateBackupEncryptionConfig kmsKeyNames. */ + public kmsKeyNames: string[]; + /** * Creates a new CreateBackupEncryptionConfig instance using the specified properties. * @param [properties] Properties to set @@ -8137,6 +8149,9 @@ export namespace google { /** CopyBackupEncryptionConfig kmsKeyName */ kmsKeyName?: (string|null); + + /** CopyBackupEncryptionConfig kmsKeyNames */ + kmsKeyNames?: (string[]|null); } /** Represents a CopyBackupEncryptionConfig. */ @@ -8154,6 +8169,9 @@ export namespace google { /** CopyBackupEncryptionConfig kmsKeyName. */ public kmsKeyName: string; + /** CopyBackupEncryptionConfig kmsKeyNames. */ + public kmsKeyNames: string[]; + /** * Creates a new CopyBackupEncryptionConfig instance using the specified properties. * @param [properties] Properties to set @@ -8357,6 +8375,9 @@ export namespace google { /** EncryptionConfig kmsKeyName */ kmsKeyName?: (string|null); + + /** EncryptionConfig kmsKeyNames */ + kmsKeyNames?: (string[]|null); } /** Represents an EncryptionConfig. */ @@ -8371,6 +8392,9 @@ export namespace google { /** EncryptionConfig kmsKeyName. */ public kmsKeyName: string; + /** EncryptionConfig kmsKeyNames. */ + public kmsKeyNames: string[]; + /** * Creates a new EncryptionConfig instance using the specified properties. * @param [properties] Properties to set @@ -11036,6 +11060,9 @@ export namespace google { /** RestoreDatabaseEncryptionConfig kmsKeyName */ kmsKeyName?: (string|null); + + /** RestoreDatabaseEncryptionConfig kmsKeyNames */ + kmsKeyNames?: (string[]|null); } /** Represents a RestoreDatabaseEncryptionConfig. */ @@ -11053,6 +11080,9 @@ export namespace google { /** RestoreDatabaseEncryptionConfig kmsKeyName. */ public kmsKeyName: string; + /** RestoreDatabaseEncryptionConfig kmsKeyNames. */ + public kmsKeyNames: string[]; + /** * Creates a new RestoreDatabaseEncryptionConfig instance using the specified properties. * @param [properties] Properties to set diff --git a/protos/protos.js b/protos/protos.js index feb8d0198..239a3074d 100644 --- a/protos/protos.js +++ b/protos/protos.js @@ -17698,6 +17698,7 @@ * @property {google.spanner.admin.database.v1.Backup.State|null} [state] Backup state * @property {Array.|null} [referencingDatabases] Backup referencingDatabases * @property {google.spanner.admin.database.v1.IEncryptionInfo|null} [encryptionInfo] Backup encryptionInfo + * @property {Array.|null} [encryptionInformation] Backup encryptionInformation * @property {google.spanner.admin.database.v1.DatabaseDialect|null} [databaseDialect] Backup databaseDialect * @property {Array.|null} [referencingBackups] Backup referencingBackups * @property {google.protobuf.ITimestamp|null} [maxExpireTime] Backup maxExpireTime @@ -17713,6 +17714,7 @@ */ function Backup(properties) { this.referencingDatabases = []; + this.encryptionInformation = []; this.referencingBackups = []; if (properties) for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) @@ -17792,6 +17794,14 @@ */ Backup.prototype.encryptionInfo = null; + /** + * Backup encryptionInformation. + * @member {Array.} encryptionInformation + * @memberof google.spanner.admin.database.v1.Backup + * @instance + */ + Backup.prototype.encryptionInformation = $util.emptyArray; + /** * Backup databaseDialect. * @member {google.spanner.admin.database.v1.DatabaseDialect} databaseDialect @@ -17866,6 +17876,9 @@ writer.uint32(/* id 11, wireType 2 =*/90).string(message.referencingBackups[i]); if (message.maxExpireTime != null && Object.hasOwnProperty.call(message, "maxExpireTime")) $root.google.protobuf.Timestamp.encode(message.maxExpireTime, writer.uint32(/* id 12, wireType 2 =*/98).fork()).ldelim(); + if (message.encryptionInformation != null && message.encryptionInformation.length) + for (var i = 0; i < message.encryptionInformation.length; ++i) + $root.google.spanner.admin.database.v1.EncryptionInfo.encode(message.encryptionInformation[i], writer.uint32(/* id 13, wireType 2 =*/106).fork()).ldelim(); return writer; }; @@ -17938,6 +17951,12 @@ message.encryptionInfo = $root.google.spanner.admin.database.v1.EncryptionInfo.decode(reader, reader.uint32()); break; } + case 13: { + if (!(message.encryptionInformation && message.encryptionInformation.length)) + message.encryptionInformation = []; + message.encryptionInformation.push($root.google.spanner.admin.database.v1.EncryptionInfo.decode(reader, reader.uint32())); + break; + } case 10: { message.databaseDialect = reader.int32(); break; @@ -18032,6 +18051,15 @@ if (error) return "encryptionInfo." + error; } + if (message.encryptionInformation != null && message.hasOwnProperty("encryptionInformation")) { + if (!Array.isArray(message.encryptionInformation)) + return "encryptionInformation: array expected"; + for (var i = 0; i < message.encryptionInformation.length; ++i) { + var error = $root.google.spanner.admin.database.v1.EncryptionInfo.verify(message.encryptionInformation[i]); + if (error) + return "encryptionInformation." + error; + } + } if (message.databaseDialect != null && message.hasOwnProperty("databaseDialect")) switch (message.databaseDialect) { default: @@ -18128,6 +18156,16 @@ throw TypeError(".google.spanner.admin.database.v1.Backup.encryptionInfo: object expected"); message.encryptionInfo = $root.google.spanner.admin.database.v1.EncryptionInfo.fromObject(object.encryptionInfo); } + if (object.encryptionInformation) { + if (!Array.isArray(object.encryptionInformation)) + throw TypeError(".google.spanner.admin.database.v1.Backup.encryptionInformation: array expected"); + message.encryptionInformation = []; + for (var i = 0; i < object.encryptionInformation.length; ++i) { + if (typeof object.encryptionInformation[i] !== "object") + throw TypeError(".google.spanner.admin.database.v1.Backup.encryptionInformation: object expected"); + message.encryptionInformation[i] = $root.google.spanner.admin.database.v1.EncryptionInfo.fromObject(object.encryptionInformation[i]); + } + } switch (object.databaseDialect) { default: if (typeof object.databaseDialect === "number") { @@ -18179,6 +18217,7 @@ if (options.arrays || options.defaults) { object.referencingDatabases = []; object.referencingBackups = []; + object.encryptionInformation = []; } if (options.defaults) { object.name = ""; @@ -18229,6 +18268,11 @@ } if (message.maxExpireTime != null && message.hasOwnProperty("maxExpireTime")) object.maxExpireTime = $root.google.protobuf.Timestamp.toObject(message.maxExpireTime, options); + if (message.encryptionInformation && message.encryptionInformation.length) { + object.encryptionInformation = []; + for (var j = 0; j < message.encryptionInformation.length; ++j) + object.encryptionInformation[j] = $root.google.spanner.admin.database.v1.EncryptionInfo.toObject(message.encryptionInformation[j], options); + } return object; }; @@ -21408,6 +21452,7 @@ * @interface ICreateBackupEncryptionConfig * @property {google.spanner.admin.database.v1.CreateBackupEncryptionConfig.EncryptionType|null} [encryptionType] CreateBackupEncryptionConfig encryptionType * @property {string|null} [kmsKeyName] CreateBackupEncryptionConfig kmsKeyName + * @property {Array.|null} [kmsKeyNames] CreateBackupEncryptionConfig kmsKeyNames */ /** @@ -21419,6 +21464,7 @@ * @param {google.spanner.admin.database.v1.ICreateBackupEncryptionConfig=} [properties] Properties to set */ function CreateBackupEncryptionConfig(properties) { + this.kmsKeyNames = []; if (properties) for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) @@ -21441,6 +21487,14 @@ */ CreateBackupEncryptionConfig.prototype.kmsKeyName = ""; + /** + * CreateBackupEncryptionConfig kmsKeyNames. + * @member {Array.} kmsKeyNames + * @memberof google.spanner.admin.database.v1.CreateBackupEncryptionConfig + * @instance + */ + CreateBackupEncryptionConfig.prototype.kmsKeyNames = $util.emptyArray; + /** * Creates a new CreateBackupEncryptionConfig instance using the specified properties. * @function create @@ -21469,6 +21523,9 @@ writer.uint32(/* id 1, wireType 0 =*/8).int32(message.encryptionType); if (message.kmsKeyName != null && Object.hasOwnProperty.call(message, "kmsKeyName")) writer.uint32(/* id 2, wireType 2 =*/18).string(message.kmsKeyName); + if (message.kmsKeyNames != null && message.kmsKeyNames.length) + for (var i = 0; i < message.kmsKeyNames.length; ++i) + writer.uint32(/* id 3, wireType 2 =*/26).string(message.kmsKeyNames[i]); return writer; }; @@ -21511,6 +21568,12 @@ message.kmsKeyName = reader.string(); break; } + case 3: { + if (!(message.kmsKeyNames && message.kmsKeyNames.length)) + message.kmsKeyNames = []; + message.kmsKeyNames.push(reader.string()); + break; + } default: reader.skipType(tag & 7); break; @@ -21559,6 +21622,13 @@ if (message.kmsKeyName != null && message.hasOwnProperty("kmsKeyName")) if (!$util.isString(message.kmsKeyName)) return "kmsKeyName: string expected"; + if (message.kmsKeyNames != null && message.hasOwnProperty("kmsKeyNames")) { + if (!Array.isArray(message.kmsKeyNames)) + return "kmsKeyNames: array expected"; + for (var i = 0; i < message.kmsKeyNames.length; ++i) + if (!$util.isString(message.kmsKeyNames[i])) + return "kmsKeyNames: string[] expected"; + } return null; }; @@ -21600,6 +21670,13 @@ } if (object.kmsKeyName != null) message.kmsKeyName = String(object.kmsKeyName); + if (object.kmsKeyNames) { + if (!Array.isArray(object.kmsKeyNames)) + throw TypeError(".google.spanner.admin.database.v1.CreateBackupEncryptionConfig.kmsKeyNames: array expected"); + message.kmsKeyNames = []; + for (var i = 0; i < object.kmsKeyNames.length; ++i) + message.kmsKeyNames[i] = String(object.kmsKeyNames[i]); + } return message; }; @@ -21616,6 +21693,8 @@ if (!options) options = {}; var object = {}; + if (options.arrays || options.defaults) + object.kmsKeyNames = []; if (options.defaults) { object.encryptionType = options.enums === String ? "ENCRYPTION_TYPE_UNSPECIFIED" : 0; object.kmsKeyName = ""; @@ -21624,6 +21703,11 @@ object.encryptionType = options.enums === String ? $root.google.spanner.admin.database.v1.CreateBackupEncryptionConfig.EncryptionType[message.encryptionType] === undefined ? message.encryptionType : $root.google.spanner.admin.database.v1.CreateBackupEncryptionConfig.EncryptionType[message.encryptionType] : message.encryptionType; if (message.kmsKeyName != null && message.hasOwnProperty("kmsKeyName")) object.kmsKeyName = message.kmsKeyName; + if (message.kmsKeyNames && message.kmsKeyNames.length) { + object.kmsKeyNames = []; + for (var j = 0; j < message.kmsKeyNames.length; ++j) + object.kmsKeyNames[j] = message.kmsKeyNames[j]; + } return object; }; @@ -21682,6 +21766,7 @@ * @interface ICopyBackupEncryptionConfig * @property {google.spanner.admin.database.v1.CopyBackupEncryptionConfig.EncryptionType|null} [encryptionType] CopyBackupEncryptionConfig encryptionType * @property {string|null} [kmsKeyName] CopyBackupEncryptionConfig kmsKeyName + * @property {Array.|null} [kmsKeyNames] CopyBackupEncryptionConfig kmsKeyNames */ /** @@ -21693,6 +21778,7 @@ * @param {google.spanner.admin.database.v1.ICopyBackupEncryptionConfig=} [properties] Properties to set */ function CopyBackupEncryptionConfig(properties) { + this.kmsKeyNames = []; if (properties) for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) @@ -21715,6 +21801,14 @@ */ CopyBackupEncryptionConfig.prototype.kmsKeyName = ""; + /** + * CopyBackupEncryptionConfig kmsKeyNames. + * @member {Array.} kmsKeyNames + * @memberof google.spanner.admin.database.v1.CopyBackupEncryptionConfig + * @instance + */ + CopyBackupEncryptionConfig.prototype.kmsKeyNames = $util.emptyArray; + /** * Creates a new CopyBackupEncryptionConfig instance using the specified properties. * @function create @@ -21743,6 +21837,9 @@ writer.uint32(/* id 1, wireType 0 =*/8).int32(message.encryptionType); if (message.kmsKeyName != null && Object.hasOwnProperty.call(message, "kmsKeyName")) writer.uint32(/* id 2, wireType 2 =*/18).string(message.kmsKeyName); + if (message.kmsKeyNames != null && message.kmsKeyNames.length) + for (var i = 0; i < message.kmsKeyNames.length; ++i) + writer.uint32(/* id 3, wireType 2 =*/26).string(message.kmsKeyNames[i]); return writer; }; @@ -21785,6 +21882,12 @@ message.kmsKeyName = reader.string(); break; } + case 3: { + if (!(message.kmsKeyNames && message.kmsKeyNames.length)) + message.kmsKeyNames = []; + message.kmsKeyNames.push(reader.string()); + break; + } default: reader.skipType(tag & 7); break; @@ -21833,6 +21936,13 @@ if (message.kmsKeyName != null && message.hasOwnProperty("kmsKeyName")) if (!$util.isString(message.kmsKeyName)) return "kmsKeyName: string expected"; + if (message.kmsKeyNames != null && message.hasOwnProperty("kmsKeyNames")) { + if (!Array.isArray(message.kmsKeyNames)) + return "kmsKeyNames: array expected"; + for (var i = 0; i < message.kmsKeyNames.length; ++i) + if (!$util.isString(message.kmsKeyNames[i])) + return "kmsKeyNames: string[] expected"; + } return null; }; @@ -21874,6 +21984,13 @@ } if (object.kmsKeyName != null) message.kmsKeyName = String(object.kmsKeyName); + if (object.kmsKeyNames) { + if (!Array.isArray(object.kmsKeyNames)) + throw TypeError(".google.spanner.admin.database.v1.CopyBackupEncryptionConfig.kmsKeyNames: array expected"); + message.kmsKeyNames = []; + for (var i = 0; i < object.kmsKeyNames.length; ++i) + message.kmsKeyNames[i] = String(object.kmsKeyNames[i]); + } return message; }; @@ -21890,6 +22007,8 @@ if (!options) options = {}; var object = {}; + if (options.arrays || options.defaults) + object.kmsKeyNames = []; if (options.defaults) { object.encryptionType = options.enums === String ? "ENCRYPTION_TYPE_UNSPECIFIED" : 0; object.kmsKeyName = ""; @@ -21898,6 +22017,11 @@ object.encryptionType = options.enums === String ? $root.google.spanner.admin.database.v1.CopyBackupEncryptionConfig.EncryptionType[message.encryptionType] === undefined ? message.encryptionType : $root.google.spanner.admin.database.v1.CopyBackupEncryptionConfig.EncryptionType[message.encryptionType] : message.encryptionType; if (message.kmsKeyName != null && message.hasOwnProperty("kmsKeyName")) object.kmsKeyName = message.kmsKeyName; + if (message.kmsKeyNames && message.kmsKeyNames.length) { + object.kmsKeyNames = []; + for (var j = 0; j < message.kmsKeyNames.length; ++j) + object.kmsKeyNames[j] = message.kmsKeyNames[j]; + } return object; }; @@ -22215,6 +22339,7 @@ * @memberof google.spanner.admin.database.v1 * @interface IEncryptionConfig * @property {string|null} [kmsKeyName] EncryptionConfig kmsKeyName + * @property {Array.|null} [kmsKeyNames] EncryptionConfig kmsKeyNames */ /** @@ -22226,6 +22351,7 @@ * @param {google.spanner.admin.database.v1.IEncryptionConfig=} [properties] Properties to set */ function EncryptionConfig(properties) { + this.kmsKeyNames = []; if (properties) for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) @@ -22240,6 +22366,14 @@ */ EncryptionConfig.prototype.kmsKeyName = ""; + /** + * EncryptionConfig kmsKeyNames. + * @member {Array.} kmsKeyNames + * @memberof google.spanner.admin.database.v1.EncryptionConfig + * @instance + */ + EncryptionConfig.prototype.kmsKeyNames = $util.emptyArray; + /** * Creates a new EncryptionConfig instance using the specified properties. * @function create @@ -22266,6 +22400,9 @@ writer = $Writer.create(); if (message.kmsKeyName != null && Object.hasOwnProperty.call(message, "kmsKeyName")) writer.uint32(/* id 2, wireType 2 =*/18).string(message.kmsKeyName); + if (message.kmsKeyNames != null && message.kmsKeyNames.length) + for (var i = 0; i < message.kmsKeyNames.length; ++i) + writer.uint32(/* id 3, wireType 2 =*/26).string(message.kmsKeyNames[i]); return writer; }; @@ -22304,6 +22441,12 @@ message.kmsKeyName = reader.string(); break; } + case 3: { + if (!(message.kmsKeyNames && message.kmsKeyNames.length)) + message.kmsKeyNames = []; + message.kmsKeyNames.push(reader.string()); + break; + } default: reader.skipType(tag & 7); break; @@ -22342,6 +22485,13 @@ if (message.kmsKeyName != null && message.hasOwnProperty("kmsKeyName")) if (!$util.isString(message.kmsKeyName)) return "kmsKeyName: string expected"; + if (message.kmsKeyNames != null && message.hasOwnProperty("kmsKeyNames")) { + if (!Array.isArray(message.kmsKeyNames)) + return "kmsKeyNames: array expected"; + for (var i = 0; i < message.kmsKeyNames.length; ++i) + if (!$util.isString(message.kmsKeyNames[i])) + return "kmsKeyNames: string[] expected"; + } return null; }; @@ -22359,6 +22509,13 @@ var message = new $root.google.spanner.admin.database.v1.EncryptionConfig(); if (object.kmsKeyName != null) message.kmsKeyName = String(object.kmsKeyName); + if (object.kmsKeyNames) { + if (!Array.isArray(object.kmsKeyNames)) + throw TypeError(".google.spanner.admin.database.v1.EncryptionConfig.kmsKeyNames: array expected"); + message.kmsKeyNames = []; + for (var i = 0; i < object.kmsKeyNames.length; ++i) + message.kmsKeyNames[i] = String(object.kmsKeyNames[i]); + } return message; }; @@ -22375,10 +22532,17 @@ if (!options) options = {}; var object = {}; + if (options.arrays || options.defaults) + object.kmsKeyNames = []; if (options.defaults) object.kmsKeyName = ""; if (message.kmsKeyName != null && message.hasOwnProperty("kmsKeyName")) object.kmsKeyName = message.kmsKeyName; + if (message.kmsKeyNames && message.kmsKeyNames.length) { + object.kmsKeyNames = []; + for (var j = 0; j < message.kmsKeyNames.length; ++j) + object.kmsKeyNames[j] = message.kmsKeyNames[j]; + } return object; }; @@ -28487,6 +28651,7 @@ * @interface IRestoreDatabaseEncryptionConfig * @property {google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.EncryptionType|null} [encryptionType] RestoreDatabaseEncryptionConfig encryptionType * @property {string|null} [kmsKeyName] RestoreDatabaseEncryptionConfig kmsKeyName + * @property {Array.|null} [kmsKeyNames] RestoreDatabaseEncryptionConfig kmsKeyNames */ /** @@ -28498,6 +28663,7 @@ * @param {google.spanner.admin.database.v1.IRestoreDatabaseEncryptionConfig=} [properties] Properties to set */ function RestoreDatabaseEncryptionConfig(properties) { + this.kmsKeyNames = []; if (properties) for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) @@ -28520,6 +28686,14 @@ */ RestoreDatabaseEncryptionConfig.prototype.kmsKeyName = ""; + /** + * RestoreDatabaseEncryptionConfig kmsKeyNames. + * @member {Array.} kmsKeyNames + * @memberof google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig + * @instance + */ + RestoreDatabaseEncryptionConfig.prototype.kmsKeyNames = $util.emptyArray; + /** * Creates a new RestoreDatabaseEncryptionConfig instance using the specified properties. * @function create @@ -28548,6 +28722,9 @@ writer.uint32(/* id 1, wireType 0 =*/8).int32(message.encryptionType); if (message.kmsKeyName != null && Object.hasOwnProperty.call(message, "kmsKeyName")) writer.uint32(/* id 2, wireType 2 =*/18).string(message.kmsKeyName); + if (message.kmsKeyNames != null && message.kmsKeyNames.length) + for (var i = 0; i < message.kmsKeyNames.length; ++i) + writer.uint32(/* id 3, wireType 2 =*/26).string(message.kmsKeyNames[i]); return writer; }; @@ -28590,6 +28767,12 @@ message.kmsKeyName = reader.string(); break; } + case 3: { + if (!(message.kmsKeyNames && message.kmsKeyNames.length)) + message.kmsKeyNames = []; + message.kmsKeyNames.push(reader.string()); + break; + } default: reader.skipType(tag & 7); break; @@ -28638,6 +28821,13 @@ if (message.kmsKeyName != null && message.hasOwnProperty("kmsKeyName")) if (!$util.isString(message.kmsKeyName)) return "kmsKeyName: string expected"; + if (message.kmsKeyNames != null && message.hasOwnProperty("kmsKeyNames")) { + if (!Array.isArray(message.kmsKeyNames)) + return "kmsKeyNames: array expected"; + for (var i = 0; i < message.kmsKeyNames.length; ++i) + if (!$util.isString(message.kmsKeyNames[i])) + return "kmsKeyNames: string[] expected"; + } return null; }; @@ -28679,6 +28869,13 @@ } if (object.kmsKeyName != null) message.kmsKeyName = String(object.kmsKeyName); + if (object.kmsKeyNames) { + if (!Array.isArray(object.kmsKeyNames)) + throw TypeError(".google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.kmsKeyNames: array expected"); + message.kmsKeyNames = []; + for (var i = 0; i < object.kmsKeyNames.length; ++i) + message.kmsKeyNames[i] = String(object.kmsKeyNames[i]); + } return message; }; @@ -28695,6 +28892,8 @@ if (!options) options = {}; var object = {}; + if (options.arrays || options.defaults) + object.kmsKeyNames = []; if (options.defaults) { object.encryptionType = options.enums === String ? "ENCRYPTION_TYPE_UNSPECIFIED" : 0; object.kmsKeyName = ""; @@ -28703,6 +28902,11 @@ object.encryptionType = options.enums === String ? $root.google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.EncryptionType[message.encryptionType] === undefined ? message.encryptionType : $root.google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.EncryptionType[message.encryptionType] : message.encryptionType; if (message.kmsKeyName != null && message.hasOwnProperty("kmsKeyName")) object.kmsKeyName = message.kmsKeyName; + if (message.kmsKeyNames && message.kmsKeyNames.length) { + object.kmsKeyNames = []; + for (var j = 0; j < message.kmsKeyNames.length; ++j) + object.kmsKeyNames[j] = message.kmsKeyNames[j]; + } return object; }; diff --git a/protos/protos.json b/protos/protos.json index 2c8db5995..f1daeaffc 100644 --- a/protos/protos.json +++ b/protos/protos.json @@ -1690,6 +1690,14 @@ "(google.api.field_behavior)": "OUTPUT_ONLY" } }, + "encryptionInformation": { + "rule": "repeated", + "type": "EncryptionInfo", + "id": 13, + "options": { + "(google.api.field_behavior)": "OUTPUT_ONLY" + } + }, "databaseDialect": { "type": "DatabaseDialect", "id": 10, @@ -2008,6 +2016,15 @@ "(google.api.field_behavior)": "OPTIONAL", "(google.api.resource_reference).type": "cloudkms.googleapis.com/CryptoKey" } + }, + "kmsKeyNames": { + "rule": "repeated", + "type": "string", + "id": 3, + "options": { + "(google.api.field_behavior)": "OPTIONAL", + "(google.api.resource_reference).type": "cloudkms.googleapis.com/CryptoKey" + } } }, "nested": { @@ -2037,6 +2054,15 @@ "(google.api.field_behavior)": "OPTIONAL", "(google.api.resource_reference).type": "cloudkms.googleapis.com/CryptoKey" } + }, + "kmsKeyNames": { + "rule": "repeated", + "type": "string", + "id": 3, + "options": { + "(google.api.field_behavior)": "OPTIONAL", + "(google.api.resource_reference).type": "cloudkms.googleapis.com/CryptoKey" + } } }, "nested": { @@ -2074,6 +2100,14 @@ "options": { "(google.api.resource_reference).type": "cloudkms.googleapis.com/CryptoKey" } + }, + "kmsKeyNames": { + "rule": "repeated", + "type": "string", + "id": 3, + "options": { + "(google.api.resource_reference).type": "cloudkms.googleapis.com/CryptoKey" + } } } }, @@ -3052,6 +3086,15 @@ "(google.api.field_behavior)": "OPTIONAL", "(google.api.resource_reference).type": "cloudkms.googleapis.com/CryptoKey" } + }, + "kmsKeyNames": { + "rule": "repeated", + "type": "string", + "id": 3, + "options": { + "(google.api.field_behavior)": "OPTIONAL", + "(google.api.resource_reference).type": "cloudkms.googleapis.com/CryptoKey" + } } }, "nested": { diff --git a/samples/README.md b/samples/README.md index 5e99af7cf..d60b9cdc2 100644 --- a/samples/README.md +++ b/samples/README.md @@ -92,6 +92,10 @@ and automatic, synchronous replication for high availability. * [Alters a sequence in a PostgreSQL database.](#alters-a-sequence-in-a-postgresql-database.) * [Creates sequence in PostgreSQL database.](#creates-sequence-in-postgresql-database.) * [Drops a sequence in PostgreSQL database.](#drops-a-sequence-in-postgresql-database.) + * [Proto-query-data](#proto-query-data) + * [Creates a new database with a proto column and enum](#creates-a-new-database-with-a-proto-column-and-enum) + * [Proto-update-data-dml](#proto-update-data-dml) + * [Proto-update-data](#proto-update-data) * [Queryoptions](#queryoptions) * [Quickstart](#quickstart) * [Read data with database role](#read-data-with-database-role) @@ -1455,6 +1459,74 @@ __Usage:__ +### Proto-query-data + +View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/proto-query-data.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/proto-query-data.js,samples/README.md) + +__Usage:__ + + +`node samples/proto-query-data.js` + + +----- + + + + +### Creates a new database with a proto column and enum + +View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/proto-type-add-column.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/proto-type-add-column.js,samples/README.md) + +__Usage:__ + + +`node proto-type-add-column.js ` + + +----- + + + + +### Proto-update-data-dml + +View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/proto-update-data-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/proto-update-data-dml.js,samples/README.md) + +__Usage:__ + + +`node samples/proto-update-data-dml.js` + + +----- + + + + +### Proto-update-data + +View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/proto-update-data.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/proto-update-data.js,samples/README.md) + +__Usage:__ + + +`node samples/proto-update-data.js` + + +----- + + + + ### Queryoptions View the [source code](https://github.com/googleapis/nodejs-spanner/blob/main/samples/queryoptions.js). diff --git a/samples/package.json b/samples/package.json index ea6e99d57..58c78db82 100644 --- a/samples/package.json +++ b/samples/package.json @@ -17,8 +17,9 @@ "dependencies": { "@google-cloud/kms": "^4.0.0", "@google-cloud/precise-date": "^4.0.0", - "@google-cloud/spanner": "^7.7.0", - "yargs": "^17.0.0" + "@google-cloud/spanner": "^7.8.0", + "yargs": "^17.0.0", + "protobufjs": "^7.0.0" }, "devDependencies": { "chai": "^4.2.0", diff --git a/samples/proto-query-data.js b/samples/proto-query-data.js new file mode 100644 index 000000000..3ee51cd34 --- /dev/null +++ b/samples/proto-query-data.js @@ -0,0 +1,100 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +// eslint-disable-next-line node/no-unpublished-require +const singer = require('./resource/singer.js'); +const music = singer.examples.spanner.music; + +function main( + instanceId = 'my-instance', + databaseId = 'my-database', + projectId = 'my-project-id' +) { + // [START spanner_query_with_proto_types_parameter] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + // const projectId = 'my-project-id'; + + // Imports the Google Cloud Spanner client library + const {Spanner} = require('@google-cloud/spanner'); + + // Instantiates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + async function queryDataWithProtoTypes() { + // Gets a reference to a Cloud Spanner instance and database. + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + const query = { + sql: `SELECT SingerId, + SingerInfo, + SingerInfo.nationality, + SingerInfoArray, + SingerGenre, + SingerGenreArray + FROM Singers + WHERE SingerInfo.nationality = @country + and SingerGenre=@singerGenre`, + params: { + country: 'Country2', + singerGenre: music.Genre.FOLK, + }, + /* `columnsMetadata` is an optional parameter and is used to deserialize the + proto message and enum object back from bytearray and int respectively. + If columnsMetadata is not passed for proto messages and enums, then the data + types for these columns will be bytes and int respectively. */ + columnsMetadata: { + SingerInfo: music.SingerInfo, + SingerInfoArray: music.SingerInfo, + SingerGenre: music.Genre, + SingerGenreArray: music.Genre, + }, + }; + + // Queries rows from the Singers table. + try { + const [rows] = await database.run(query); + + rows.forEach(row => { + const json = row.toJSON(); + console.log( + `SingerId: ${json.SingerId}, SingerInfo: ${json.SingerInfo}, SingerGenre: ${json.SingerGenre}, + SingerInfoArray: ${json.SingerInfoArray}, SingerGenreArray: ${json.SingerGenreArray}` + ); + }); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + database.close(); + } + } + + queryDataWithProtoTypes(); + // [END spanner_query_with_proto_types_parameter] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/samples/proto-type-add-column.js b/samples/proto-type-add-column.js new file mode 100644 index 000000000..76c77aa5a --- /dev/null +++ b/samples/proto-type-add-column.js @@ -0,0 +1,90 @@ +/** + * 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 database with a proto column and enum +// usage: node proto-type-add-column.js + +'use strict'; + +const fs = require('fs'); + +function main( + instanceId = 'my-instance', + databaseId = 'my-database', + projectId = 'my-project-id' +) { + // [START spanner_add_proto_type_columns] + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const projectId = 'my-project-id'; + // const instanceId = 'my-instance-id'; + // const databaseId = 'my-database-id'; + + // Imports the Google Cloud client library + const {Spanner} = require('@google-cloud/spanner'); + + // Creates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + const databaseAdminClient = spanner.getDatabaseAdminClient(); + async function protoTypeAddColumn() { + // Adds a new Proto Message column and Proto Enum column to the Singers table. + + const request = [ + `CREATE PROTO BUNDLE ( + examples.spanner.music.SingerInfo, + examples.spanner.music.Genre, + )`, + 'ALTER TABLE Singers ADD COLUMN SingerInfo examples.spanner.music.SingerInfo', + 'ALTER TABLE Singers ADD COLUMN SingerInfoArray ARRAY', + 'ALTER TABLE Singers ADD COLUMN SingerGenre examples.spanner.music.Genre', + 'ALTER TABLE Singers ADD COLUMN SingerGenreArray ARRAY', + ]; + + // Read a proto descriptor file and convert it to a base64 string + const protoDescriptor = fs + .readFileSync('./resource/descriptors.pb') + .toString('base64'); + + // Alter existing table to add a column. + const [operation] = await databaseAdminClient.updateDatabaseDdl({ + database: databaseAdminClient.databasePath( + projectId, + instanceId, + databaseId + ), + statements: request, + protoDescriptors: protoDescriptor, + }); + + console.log(`Waiting for operation on ${databaseId} to complete...`); + await operation.promise(); + console.log( + `Altered table "Singers" on database ${databaseId} on instance ${instanceId} with proto descriptors.` + ); + } + protoTypeAddColumn(); + // [END spanner_add_proto_type_columns] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/samples/proto-update-data-dml.js b/samples/proto-update-data-dml.js new file mode 100644 index 000000000..e5f9f7ac8 --- /dev/null +++ b/samples/proto-update-data-dml.js @@ -0,0 +1,128 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +// eslint-disable-next-line node/no-unpublished-require +const singer = require('./resource/singer.js'); +const music = singer.examples.spanner.music; + +function main( + instanceId = 'my-instance', + databaseId = 'my-database', + projectId = 'my-project-id' +) { + // [START spanner_update_data_with_proto_types_with_dml] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + // const projectId = 'my-project-id'; + + // Imports the Google Cloud Spanner client library + const {Spanner} = require('@google-cloud/spanner'); + + // Instantiates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + async function updateDataUsingDmlWithProtoTypes() { + /* + Updates Singers tables in the database with the ProtoMessage + and ProtoEnum column. + This updates the `SingerInfo`, `SingerInfoArray`, `SingerGenre` and + SingerGenreArray` columns which must be created before running this sample. + You can add the column by running the `add_proto_type_columns` sample or + by running this DDL statement against your database: + + ALTER TABLE Singers ADD COLUMN SingerInfo examples.spanner.music.SingerInfo\n + ALTER TABLE Singers ADD COLUMN SingerInfoArray ARRAY\n + ALTER TABLE Singers ADD COLUMN SingerGenre examples.spanner.music.Genre\n + ALTER TABLE Singers ADD COLUMN SingerGenreArray ARRAY\n + */ + + // Gets a reference to a Cloud Spanner instance and database. + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + const genre = music.Genre.ROCK; + const singerInfo = music.SingerInfo.create({ + singerId: 1, + genre: genre, + birthDate: 'January', + nationality: 'Country1', + }); + + const protoMessage = Spanner.protoMessage({ + value: singerInfo, + messageFunction: music.SingerInfo, + fullName: 'examples.spanner.music.SingerInfo', + }); + + const protoEnum = Spanner.protoEnum({ + value: genre, + enumObject: music.Genre, + fullName: 'examples.spanner.music.Genre', + }); + + database.runTransaction(async (err, transaction) => { + if (err) { + console.error(err); + return; + } + try { + const [, stats] = await transaction.run({ + sql: `UPDATE Singers SET SingerInfo=@singerInfo, SingerInfoArray=@singerInfoArray, + SingerGenre=@singerGenre, SingerGenreArray=@singerGenreArray WHERE SingerId = 1`, + params: { + singerInfo: protoMessage, + singerInfoArray: [protoMessage, null], + singerGenre: genre, + singerGenreArray: [protoEnum, null], + }, + }); + + const rowCount = stats[stats.rowCount]; + console.log(`${rowCount} record updated.`); + + const [, stats1] = await transaction.run({ + sql: 'UPDATE Singers SET SingerInfo.nationality=@singerNationality WHERE SingerId = 1', + params: { + singerNationality: 'Country2', + }, + }); + const rowCount1 = stats1[stats1.rowCount]; + console.log(`${rowCount1} record updated.`); + + await transaction.commit(); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + database.close(); + } + }); + } + + updateDataUsingDmlWithProtoTypes(); + // [END spanner_update_data_with_proto_types_with_dml] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/samples/proto-update-data.js b/samples/proto-update-data.js new file mode 100644 index 000000000..004e5938a --- /dev/null +++ b/samples/proto-update-data.js @@ -0,0 +1,120 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +// eslint-disable-next-line node/no-unpublished-require +const singer = require('./resource/singer.js'); +const music = singer.examples.spanner.music; + +function main( + instanceId = 'my-instance', + databaseId = 'my-database', + projectId = 'my-project-id' +) { + // [START spanner_update_data_with_proto_types] + /** + * TODO(developer): Uncomment these variables before running the sample. + */ + // const instanceId = 'my-instance'; + // const databaseId = 'my-database'; + // const projectId = 'my-project-id'; + + // Imports the Google Cloud Spanner client library + const {Spanner} = require('@google-cloud/spanner'); + + // Instantiates a client + const spanner = new Spanner({ + projectId: projectId, + }); + + async function updateDataWithProtoTypes() { + /* + Updates Singers tables in the database with the ProtoMessage + and ProtoEnum column. + This updates the `SingerInfo`, `SingerInfoArray`, `SingerGenre` and + SingerGenreArray` columns which must be created before running this sample. + You can add the column by running the `add_proto_type_columns` sample or + by running this DDL statement against your database: + + ALTER TABLE Singers ADD COLUMN SingerInfo examples.spanner.music.SingerInfo\n + ALTER TABLE Singers ADD COLUMN SingerInfoArray ARRAY\n + ALTER TABLE Singers ADD COLUMN SingerGenre examples.spanner.music.Genre\n + ALTER TABLE Singers ADD COLUMN SingerGenreArray ARRAY\n + */ + + // Gets a reference to a Cloud Spanner instance and database. + const instance = spanner.instance(instanceId); + const database = instance.database(databaseId); + + const genre = music.Genre.FOLK; + const singerInfo = music.SingerInfo.create({ + singerId: 2, + genre: genre, + birthDate: 'February', + nationality: 'Country2', + }); + + const protoMessage = Spanner.protoMessage({ + value: singerInfo, + messageFunction: music.SingerInfo, + fullName: 'examples.spanner.music.SingerInfo', + }); + + const protoEnum = Spanner.protoEnum({ + value: genre, + enumObject: music.Genre, + fullName: 'examples.spanner.music.Genre', + }); + + // Get a reference to the Singers table + const table = database.table('Singers'); + + const data = [ + { + SingerId: 2, + SingerInfo: protoMessage, + SingerInfoArray: [protoMessage], + SingerGenre: protoEnum, + SingerGenreArray: [protoEnum], + }, + { + SingerId: 3, + SingerInfo: null, + SingerInfoArray: null, + SingerGenre: null, + SingerGenreArray: null, + }, + ]; + + try { + await table.update(data); + console.log('Data updated.'); + } catch (err) { + console.error('ERROR:', err); + } finally { + // Close the database when finished. + await database.close(); + } + } + + updateDataWithProtoTypes(); + // [END spanner_update_data_with_proto_types] +} + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); +main(...process.argv.slice(2)); diff --git a/samples/resource/README.md b/samples/resource/README.md new file mode 100644 index 000000000..980e6a3d7 --- /dev/null +++ b/samples/resource/README.md @@ -0,0 +1,8 @@ +#### To generate singer.js and singer.d.ts file from singer.proto +```shell +npm install -g protobufjs-cli +cd samples/resource +pbjs -t static-module -w commonjs -o singer.js singer.proto +pbts -o singer.d.ts singer.js +protoc --proto_path=. --include_imports --descriptor_set_out=descriptors.pb singer.proto +``` diff --git a/samples/resource/descriptors.pb b/samples/resource/descriptors.pb new file mode 100644 index 000000000..10193075e Binary files /dev/null and b/samples/resource/descriptors.pb differ diff --git a/samples/resource/singer.d.ts b/samples/resource/singer.d.ts new file mode 100644 index 000000000..87d1cfc11 --- /dev/null +++ b/samples/resource/singer.d.ts @@ -0,0 +1,175 @@ +// 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. + +import * as $protobuf from 'protobufjs'; +import Long = require('long'); +/** Namespace examples. */ +export namespace examples { + /** Namespace spanner. */ + namespace spanner { + /** Namespace music. */ + namespace music { + /** Properties of a SingerInfo. */ + interface ISingerInfo { + /** SingerInfo singerId */ + singerId?: number | Long | null; + + /** SingerInfo birthDate */ + birthDate?: string | null; + + /** SingerInfo nationality */ + nationality?: string | null; + + /** SingerInfo genre */ + genre?: examples.spanner.music.Genre | null; + } + + /** Represents a SingerInfo. */ + class SingerInfo implements ISingerInfo { + /** + * Constructs a new SingerInfo. + * @param [properties] Properties to set + */ + constructor(properties?: examples.spanner.music.ISingerInfo); + + /** SingerInfo singerId. */ + public singerId?: number | Long | null; + + /** SingerInfo birthDate. */ + public birthDate?: string | null; + + /** SingerInfo nationality. */ + public nationality?: string | null; + + /** SingerInfo genre. */ + public genre?: examples.spanner.music.Genre | null; + + /** SingerInfo _singerId. */ + public _singerId?: 'singerId'; + + /** SingerInfo _birthDate. */ + public _birthDate?: 'birthDate'; + + /** SingerInfo _nationality. */ + public _nationality?: 'nationality'; + + /** SingerInfo _genre. */ + public _genre?: 'genre'; + + /** + * Creates a new SingerInfo instance using the specified properties. + * @param [properties] Properties to set + * @returns SingerInfo instance + */ + public static create( + properties?: examples.spanner.music.ISingerInfo + ): examples.spanner.music.SingerInfo; + + /** + * Encodes the specified SingerInfo message. Does not implicitly {@link examples.spanner.music.SingerInfo.verify|verify} messages. + * @param message SingerInfo message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode( + message: examples.spanner.music.ISingerInfo, + writer?: $protobuf.Writer + ): $protobuf.Writer; + + /** + * Encodes the specified SingerInfo message, length delimited. Does not implicitly {@link examples.spanner.music.SingerInfo.verify|verify} messages. + * @param message SingerInfo message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited( + message: examples.spanner.music.ISingerInfo, + writer?: $protobuf.Writer + ): $protobuf.Writer; + + /** + * Decodes a SingerInfo message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns SingerInfo + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode( + reader: $protobuf.Reader | Uint8Array, + length?: number + ): examples.spanner.music.SingerInfo; + + /** + * Decodes a SingerInfo message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns SingerInfo + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited( + reader: $protobuf.Reader | Uint8Array + ): examples.spanner.music.SingerInfo; + + /** + * Verifies a SingerInfo message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: {[k: string]: any}): string | null; + + /** + * Creates a SingerInfo message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns SingerInfo + */ + public static fromObject(object: { + [k: string]: any; + }): examples.spanner.music.SingerInfo; + + /** + * Creates a plain object from a SingerInfo message. Also converts values to other types if specified. + * @param message SingerInfo + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject( + message: examples.spanner.music.SingerInfo, + options?: $protobuf.IConversionOptions + ): {[k: string]: any}; + + /** + * Converts this SingerInfo to JSON. + * @returns JSON object + */ + public toJSON(): {[k: string]: any}; + + /** + * Gets the default type url for SingerInfo + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } + + /** Genre enum. */ + enum Genre { + POP = 0, + JAZZ = 1, + FOLK = 2, + ROCK = 3, + } + } + } +} diff --git a/samples/resource/singer.js b/samples/resource/singer.js new file mode 100644 index 000000000..203791b41 --- /dev/null +++ b/samples/resource/singer.js @@ -0,0 +1,505 @@ +// 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. + +/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/ +'use strict'; + +var $protobuf = require('protobufjs/minimal'); + +// Common aliases +var $Reader = $protobuf.Reader, + $Writer = $protobuf.Writer, + $util = $protobuf.util; + +// Exported root namespace +var $root = $protobuf.roots['default'] || ($protobuf.roots['default'] = {}); + +$root.examples = (function () { + /** + * Namespace examples. + * @exports examples + * @namespace + */ + var examples = {}; + + examples.spanner = (function () { + /** + * Namespace spanner. + * @memberof examples + * @namespace + */ + var spanner = {}; + + spanner.music = (function () { + /** + * Namespace music. + * @memberof examples.spanner + * @namespace + */ + var music = {}; + + music.SingerInfo = (function () { + /** + * Properties of a SingerInfo. + * @memberof examples.spanner.music + * @interface ISingerInfo + * @property {number|Long|null} [singerId] SingerInfo singerId + * @property {string|null} [birthDate] SingerInfo birthDate + * @property {string|null} [nationality] SingerInfo nationality + * @property {examples.spanner.music.Genre|null} [genre] SingerInfo genre + */ + + /** + * Constructs a new SingerInfo. + * @memberof examples.spanner.music + * @classdesc Represents a SingerInfo. + * @implements ISingerInfo + * @constructor + * @param {examples.spanner.music.ISingerInfo=} [properties] Properties to set + */ + function SingerInfo(properties) { + if (properties) + for ( + var keys = Object.keys(properties), i = 0; + i < keys.length; + ++i + ) + if (properties[keys[i]] !== null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * SingerInfo singerId. + * @member {number|Long|null|undefined} singerId + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + SingerInfo.prototype.singerId = null; + + /** + * SingerInfo birthDate. + * @member {string|null|undefined} birthDate + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + SingerInfo.prototype.birthDate = null; + + /** + * SingerInfo nationality. + * @member {string|null|undefined} nationality + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + SingerInfo.prototype.nationality = null; + + /** + * SingerInfo genre. + * @member {examples.spanner.music.Genre|null|undefined} genre + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + SingerInfo.prototype.genre = null; + + // OneOf field names bound to virtual getters and setters + var $oneOfFields; + + /** + * SingerInfo _singerId. + * @member {"singerId"|undefined} _singerId + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + Object.defineProperty(SingerInfo.prototype, '_singerId', { + get: $util.oneOfGetter(($oneOfFields = ['singerId'])), + set: $util.oneOfSetter($oneOfFields), + }); + + /** + * SingerInfo _birthDate. + * @member {"birthDate"|undefined} _birthDate + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + Object.defineProperty(SingerInfo.prototype, '_birthDate', { + get: $util.oneOfGetter(($oneOfFields = ['birthDate'])), + set: $util.oneOfSetter($oneOfFields), + }); + + /** + * SingerInfo _nationality. + * @member {"nationality"|undefined} _nationality + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + Object.defineProperty(SingerInfo.prototype, '_nationality', { + get: $util.oneOfGetter(($oneOfFields = ['nationality'])), + set: $util.oneOfSetter($oneOfFields), + }); + + /** + * SingerInfo _genre. + * @member {"genre"|undefined} _genre + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + Object.defineProperty(SingerInfo.prototype, '_genre', { + get: $util.oneOfGetter(($oneOfFields = ['genre'])), + set: $util.oneOfSetter($oneOfFields), + }); + + /** + * Creates a new SingerInfo instance using the specified properties. + * @function create + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {examples.spanner.music.ISingerInfo=} [properties] Properties to set + * @returns {examples.spanner.music.SingerInfo} SingerInfo instance + */ + SingerInfo.create = function create(properties) { + return new SingerInfo(properties); + }; + + /** + * Encodes the specified SingerInfo message. Does not implicitly {@link examples.spanner.music.SingerInfo.verify|verify} messages. + * @function encode + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {examples.spanner.music.ISingerInfo} message SingerInfo message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + SingerInfo.encode = function encode(message, writer) { + if (!writer) writer = $Writer.create(); + if ( + message.singerId !== null && + Object.hasOwnProperty.call(message, 'singerId') + ) + writer.uint32(/* id 1, wireType 0 =*/ 8).int64(message.singerId); + if ( + message.birthDate !== null && + Object.hasOwnProperty.call(message, 'birthDate') + ) + writer.uint32(/* id 2, wireType 2 =*/ 18).string(message.birthDate); + if ( + message.nationality !== null && + Object.hasOwnProperty.call(message, 'nationality') + ) + writer + .uint32(/* id 3, wireType 2 =*/ 26) + .string(message.nationality); + if ( + message.genre !== null && + Object.hasOwnProperty.call(message, 'genre') + ) + writer.uint32(/* id 4, wireType 0 =*/ 32).int32(message.genre); + return writer; + }; + + /** + * Encodes the specified SingerInfo message, length delimited. Does not implicitly {@link examples.spanner.music.SingerInfo.verify|verify} messages. + * @function encodeDelimited + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {examples.spanner.music.ISingerInfo} message SingerInfo message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + SingerInfo.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a SingerInfo message from the specified reader or buffer. + * @function decode + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {examples.spanner.music.SingerInfo} SingerInfo + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + SingerInfo.decode = function decode(reader, length) { + if (!(reader instanceof $Reader)) reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, + message = new $root.examples.spanner.music.SingerInfo(); + while (reader.pos < end) { + var tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + message.singerId = reader.int64(); + break; + } + case 2: { + message.birthDate = reader.string(); + break; + } + case 3: { + message.nationality = reader.string(); + break; + } + case 4: { + message.genre = reader.int32(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a SingerInfo message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {examples.spanner.music.SingerInfo} SingerInfo + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + SingerInfo.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a SingerInfo message. + * @function verify + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + SingerInfo.verify = function verify(message) { + if (typeof message !== 'object' || message === null) + return 'object expected'; + var properties = {}; + if (message.singerId !== null && message.hasOwnProperty('singerId')) { + properties._singerId = 1; + if ( + !$util.isInteger(message.singerId) && + !( + message.singerId && + $util.isInteger(message.singerId.low) && + $util.isInteger(message.singerId.high) + ) + ) + return 'singerId: integer|Long expected'; + } + if ( + message.birthDate !== null && + message.hasOwnProperty('birthDate') + ) { + properties._birthDate = 1; + if (!$util.isString(message.birthDate)) + return 'birthDate: string expected'; + } + if ( + message.nationality !== null && + message.hasOwnProperty('nationality') + ) { + properties._nationality = 1; + if (!$util.isString(message.nationality)) + return 'nationality: string expected'; + } + if (message.genre !== null && message.hasOwnProperty('genre')) { + properties._genre = 1; + switch (message.genre) { + default: + return 'genre: enum value expected'; + case 0: + case 1: + case 2: + case 3: + break; + } + } + return null; + }; + + /** + * Creates a SingerInfo message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {Object.} object Plain object + * @returns {examples.spanner.music.SingerInfo} SingerInfo + */ + SingerInfo.fromObject = function fromObject(object) { + if (object instanceof $root.examples.spanner.music.SingerInfo) + return object; + var message = new $root.examples.spanner.music.SingerInfo(); + if (object.singerId !== null) + if ($util.Long) + (message.singerId = $util.Long.fromValue( + object.singerId + )).unsigned = false; + else if (typeof object.singerId === 'string') + message.singerId = parseInt(object.singerId, 10); + else if (typeof object.singerId === 'number') + message.singerId = object.singerId; + else if (typeof object.singerId === 'object') + message.singerId = new $util.LongBits( + object.singerId.low >>> 0, + object.singerId.high >>> 0 + ).toNumber(); + if (object.birthDate !== null) + message.birthDate = String(object.birthDate); + if (object.nationality !== null) + message.nationality = String(object.nationality); + switch (object.genre) { + default: + if (typeof object.genre === 'number') { + message.genre = object.genre; + break; + } + break; + case 'POP': + case 0: + message.genre = 0; + break; + case 'JAZZ': + case 1: + message.genre = 1; + break; + case 'FOLK': + case 2: + message.genre = 2; + break; + case 'ROCK': + case 3: + message.genre = 3; + break; + } + return message; + }; + + /** + * Creates a plain object from a SingerInfo message. Also converts values to other types if specified. + * @function toObject + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {examples.spanner.music.SingerInfo} message SingerInfo + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + SingerInfo.toObject = function toObject(message, options) { + if (!options) options = {}; + var object = {}; + if (message.singerId !== null && message.hasOwnProperty('singerId')) { + if (typeof message.singerId === 'number') + object.singerId = + options.longs === String + ? String(message.singerId) + : message.singerId; + else + object.singerId = + options.longs === String + ? $util.Long.prototype.toString.call(message.singerId) + : options.longs === Number + ? new $util.LongBits( + message.singerId.low >>> 0, + message.singerId.high >>> 0 + ).toNumber() + : message.singerId; + if (options.oneofs) object._singerId = 'singerId'; + } + if ( + message.birthDate !== null && + message.hasOwnProperty('birthDate') + ) { + object.birthDate = message.birthDate; + if (options.oneofs) object._birthDate = 'birthDate'; + } + if ( + message.nationality !== null && + message.hasOwnProperty('nationality') + ) { + object.nationality = message.nationality; + if (options.oneofs) object._nationality = 'nationality'; + } + if (message.genre !== null && message.hasOwnProperty('genre')) { + object.genre = + options.enums === String + ? $root.examples.spanner.music.Genre[message.genre] === + undefined + ? message.genre + : $root.examples.spanner.music.Genre[message.genre] + : message.genre; + if (options.oneofs) object._genre = 'genre'; + } + return object; + }; + + /** + * Converts this SingerInfo to JSON. + * @function toJSON + * @memberof examples.spanner.music.SingerInfo + * @instance + * @returns {Object.} JSON object + */ + SingerInfo.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for SingerInfo + * @function getTypeUrl + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + SingerInfo.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = 'type.googleapis.com'; + } + return typeUrlPrefix + '/examples.spanner.music.SingerInfo'; + }; + + return SingerInfo; + })(); + + /** + * Genre enum. + * @name examples.spanner.music.Genre + * @enum {number} + * @property {number} POP=0 POP value + * @property {number} JAZZ=1 JAZZ value + * @property {number} FOLK=2 FOLK value + * @property {number} ROCK=3 ROCK value + */ + music.Genre = (function () { + var valuesById = {}, + values = Object.create(valuesById); + values[(valuesById[0] = 'POP')] = 0; + values[(valuesById[1] = 'JAZZ')] = 1; + values[(valuesById[2] = 'FOLK')] = 2; + values[(valuesById[3] = 'ROCK')] = 3; + return values; + })(); + + return music; + })(); + + return spanner; + })(); + + return examples; +})(); + +module.exports = $root; diff --git a/samples/resource/singer.proto b/samples/resource/singer.proto new file mode 100644 index 000000000..adc79d18c --- /dev/null +++ b/samples/resource/singer.proto @@ -0,0 +1,31 @@ +// 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. +// 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. + +syntax = "proto3"; + +package examples.spanner.music; + +message SingerInfo { + optional int64 singer_id = 1; + optional string birth_date = 2; + optional string nationality = 3; + optional Genre genre = 4; +} + +enum Genre { + POP = 0; + JAZZ = 1; + FOLK = 2; + ROCK = 3; +} diff --git a/samples/system-test/spanner.test.js b/samples/system-test/spanner.test.js index 3d18b8493..8ce55029b 100644 --- a/samples/system-test/spanner.test.js +++ b/samples/system-test/spanner.test.js @@ -67,6 +67,7 @@ const VERSION_RETENTION_DATABASE_ID = `test-database-${CURRENT_TIME}-v`; const ENCRYPTED_DATABASE_ID = `test-database-${CURRENT_TIME}-enc`; const DEFAULT_LEADER_DATABASE_ID = `test-database-${CURRENT_TIME}-dl`; const SEQUENCE_DATABASE_ID = `test-seq-database-${CURRENT_TIME}-r`; +const PROTO_DATABASE_ID = `test-db${CURRENT_TIME}-proto1`; const BACKUP_ID = `test-backup-${CURRENT_TIME}`; const COPY_BACKUP_ID = `test-copy-backup-${CURRENT_TIME}`; const ENCRYPTED_BACKUP_ID = `test-backup-${CURRENT_TIME}-enc`; @@ -2052,4 +2053,79 @@ describe('Autogenerated Admin Clients', () => { ); }); }); + + describe('proto columns', () => { + before(async () => { + // Setup database for Proto columns + const databaseAdminClient = spanner.getDatabaseAdminClient(); + const [operation] = await databaseAdminClient.createDatabase({ + createStatement: 'CREATE DATABASE `' + PROTO_DATABASE_ID + '`', + extraStatements: [ + ` + CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + ) PRIMARY KEY (SingerId)`, + `CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE`, + ], + parent: databaseAdminClient.instancePath(PROJECT_ID, INSTANCE_ID), + }); + + console.log( + `Waiting for creation of ${PROTO_DATABASE_ID} to complete...` + ); + await operation.promise(); + console.log( + `Created database ${PROTO_DATABASE_ID} on instance ${INSTANCE_ID}.` + ); + + // Insert seed data into the database tables + execSync( + `${crudCmd} insert ${INSTANCE_ID} ${PROTO_DATABASE_ID} ${PROJECT_ID}` + ); + }); + + after(async () => { + await spanner.instance(INSTANCE_ID).database(PROTO_DATABASE_ID).delete(); + }); + + it('should add proto message and enum columns', async () => { + const output = execSync( + `node proto-type-add-column.js "${INSTANCE_ID}" "${PROTO_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match( + output, + new RegExp( + `Altered table "Singers" on database ${PROTO_DATABASE_ID} on instance ${INSTANCE_ID} with proto descriptors.` + ) + ); + }); + + it('update data with proto message and enum columns', async () => { + const output = execSync( + `node proto-update-data.js "${INSTANCE_ID}" "${PROTO_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match(output, new RegExp('Data updated')); + }); + + it('update data with proto message and enum columns using DML', async () => { + const output = execSync( + `node proto-update-data-dml.js "${INSTANCE_ID}" "${PROTO_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.include(output, '1 record updated.'); + }); + + it('query data with proto message and enum columns', async () => { + const output = execSync( + `node proto-query-data.js "${INSTANCE_ID}" "${PROTO_DATABASE_ID}" ${PROJECT_ID}` + ); + assert.match(output, new RegExp('SingerId: 2')); + }); + }); }); diff --git a/src/codec.ts b/src/codec.ts index 53c643200..1c49d8599 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -30,6 +30,40 @@ export interface Field { value: Value; } +export interface IProtoMessageParams { + // Supports proto message serialized binary data as a `Buffer` or pass a + // message object created using the functions generated by protobufjs-cli. + value: object; + // Fully qualified name of the proto representing the message definition + fullName: string; + /** + * Provide a First Class function that includes nested functions named + * "encode" for serialization and "decode" for deserialization of the proto + * message. The function should be sourced from the JS file generated by + * protobufjs-cli for the proto message. + */ + messageFunction?: Function; +} + +export interface IProtoEnumParams { + // Supports proto enum integer constant or pass the enum string + // the functions generated by protobufjs-cli. + value: string | number; + // Fully qualified name of the proto representing the enum definition + fullName: string; + /** + * An object containing enum string to id mapping. + * @example: { POP: 0, JAZZ: 1, FOLK: 2, ROCK: 3 } + * + * The object should be sourced from the JS file generated by + * protobufjs-cli for the proto message. Additionally, please review the + * sample at {@link www.samples.com} for guidance. + * + * ToDo: Update the link + */ + enumObject?: object; +} + export interface Json { [field: string]: Value; } @@ -246,6 +280,83 @@ export class PGNumeric { } } +/** + * @typedef ProtoMessage + * @see Spanner.protoMessage + */ +export class ProtoMessage { + value: Buffer; + fullName: string; + messageFunction?: Function; + + constructor(protoMessageParams: IProtoMessageParams) { + this.fullName = protoMessageParams.fullName; + this.messageFunction = protoMessageParams.messageFunction; + + if (protoMessageParams.value instanceof Buffer) { + this.value = protoMessageParams.value; + } else if (protoMessageParams.messageFunction) { + this.value = protoMessageParams.messageFunction['encode']( + protoMessageParams.value + ).finish(); + } else { + throw new GoogleError(`protoMessageParams cannot be used to construct + the ProtoMessage. Pass the serialized buffer of the + proto message as the value or provide the message object along with the + corresponding messageFunction generated by protobufjs-cli.`); + } + } + + toJSON(): string { + if (this.messageFunction) { + return this.messageFunction['toObject']( + this.messageFunction['decode'](this.value) + ); + } + return this.value.toString(); + } +} + +/** + * @typedef ProtoEnum + * @see Spanner.protoEnum + */ +export class ProtoEnum { + value: string; + fullName: string; + enumObject?: object; + + constructor(protoEnumParams: IProtoEnumParams) { + this.fullName = protoEnumParams.fullName; + this.enumObject = protoEnumParams.enumObject; + + /** + * @code{IProtoEnumParams} can accept either a number or a string as a value so + * converting to string and checking whether it's numeric using regex. + */ + if (/^\d+$/.test(protoEnumParams.value.toString())) { + this.value = protoEnumParams.value.toString(); + } else if ( + protoEnumParams.enumObject && + protoEnumParams.enumObject[protoEnumParams.value] + ) { + this.value = protoEnumParams.enumObject[protoEnumParams.value]; + } else { + throw new GoogleError(`protoEnumParams cannot be used for constructing the + ProtoEnum. Pass the number as the value or provide the enum string + constant as the value along with the corresponding enumObject generated + by protobufjs-cli.`); + } + } + + toJSON(): string { + if (this.enumObject) { + return Object.getPrototypeOf(this.enumObject)[this.value]; + } + return this.value.toString(); + } +} + /** * @typedef PGJsonb * @see Spanner.pgJsonb @@ -367,6 +478,10 @@ function convertValueToJson(value: Value, options: JSONOptions): Value { return value.map(child => convertValueToJson(child, options)); } + if (value instanceof ProtoMessage || value instanceof ProtoEnum) { + return value.toJSON(); + } + return value; } @@ -377,9 +492,14 @@ function convertValueToJson(value: Value, options: JSONOptions): Value { * * @param {*} value Value to decode * @param {object[]} type Value type object. + * @param columnMetadata Optional parameter to deserialize data * @returns {*} */ -function decode(value: Value, type: spannerClient.spanner.v1.Type): Value { +function decode( + value: Value, + type: spannerClient.spanner.v1.Type, + columnMetadata?: object +): Value { if (is.null(value)) { return null; } @@ -392,6 +512,23 @@ function decode(value: Value, type: spannerClient.spanner.v1.Type): Value { case 'BYTES': decoded = Buffer.from(decoded, 'base64'); break; + case spannerClient.spanner.v1.TypeCode.PROTO: + case 'PROTO': + decoded = Buffer.from(decoded, 'base64'); + decoded = new ProtoMessage({ + value: decoded, + fullName: type.protoTypeFqn, + messageFunction: columnMetadata as Function, + }); + break; + case spannerClient.spanner.v1.TypeCode.ENUM: + case 'ENUM': + decoded = new ProtoEnum({ + value: decoded, + fullName: type.protoTypeFqn, + enumObject: columnMetadata as object, + }); + break; case spannerClient.spanner.v1.TypeCode.FLOAT32: case 'FLOAT32': decoded = new Float32(decoded); @@ -449,7 +586,8 @@ function decode(value: Value, type: spannerClient.spanner.v1.Type): Value { decoded = decoded.map(value => { return decode( value, - type.arrayElementType! as spannerClient.spanner.v1.Type + type.arrayElementType! as spannerClient.spanner.v1.Type, + columnMetadata ); }); break; @@ -458,7 +596,8 @@ function decode(value: Value, type: spannerClient.spanner.v1.Type): Value { fields = type.structType!.fields!.map(({name, type}, index) => { const value = decode( (!Array.isArray(decoded) && decoded[name!]) || decoded[index], - type as spannerClient.spanner.v1.Type + type as spannerClient.spanner.v1.Type, + columnMetadata ); return {name, value}; }); @@ -518,6 +657,14 @@ function encodeValue(value: Value): Value { return value.toString('base64'); } + if (value instanceof ProtoMessage) { + return value.value.toString('base64'); + } + + if (value instanceof ProtoEnum) { + return value.value; + } + if (value instanceof Struct) { return Array.from(value).map(field => encodeValue(field.value)); } @@ -560,6 +707,8 @@ const TypeCode: { bytes: 'BYTES', json: 'JSON', jsonb: 'JSON', + proto: 'PROTO', + enum: 'ENUM', array: 'ARRAY', struct: 'STRUCT', }; @@ -574,6 +723,7 @@ export interface Type { type: string; fields?: FieldType[]; child?: Type; + fullName?: string; } interface FieldType extends Type { @@ -595,6 +745,8 @@ interface FieldType extends Type { * - string * - bytes * - json + * - proto + * - enum * - timestamp * - date * - struct @@ -650,6 +802,14 @@ function getType(value: Value): Type { return {type: 'pgOid'}; } + if (value instanceof ProtoMessage) { + return {type: 'proto', fullName: value.fullName}; + } + + if (value instanceof ProtoEnum) { + return {type: 'enum', fullName: value.fullName}; + } + if (is.boolean(value)) { return {type: 'bool'}; } @@ -776,6 +936,10 @@ function createTypeObject( code, } as spannerClient.spanner.v1.Type; + if (code === 'PROTO' || code === 'ENUM') { + type.protoTypeFqn = config.fullName || ''; + } + if (code === 'ARRAY') { type.arrayElementType = codec.createTypeObject(config.child); } @@ -811,6 +975,8 @@ export const codec = { Numeric, PGNumeric, PGJsonb, + ProtoMessage, + ProtoEnum, PGOid, convertFieldsToJson, decode, diff --git a/src/database.ts b/src/database.ts index c35f1557e..c0b5fd12f 100644 --- a/src/database.ts +++ b/src/database.ts @@ -133,11 +133,19 @@ export interface SessionPoolConstructor { ): SessionPoolInterface; } +export type GetDatabaseDialectCallback = NormalCallback< + EnumKey +>; + export interface SetIamPolicyRequest { policy: Policy | null; updateMask?: FieldMask | null; } +export interface RunPartitionedUpdateOptions extends ExecuteSqlRequest { + excludeTxnFromChangeStreams?: boolean; +} + export type UpdateSchemaCallback = ResourceCallback< GaxOperation, databaseAdmin.longrunning.IOperation @@ -166,7 +174,15 @@ type IDatabaseTranslatedEnum = Omit< typeof databaseAdmin.spanner.admin.database.v1.Database.State >, 'restoreInfo' -> & {restoreInfo?: IRestoreInfoTranslatedEnum | null}; +> & + Omit< + TranslateEnumKeys< + databaseAdmin.spanner.admin.database.v1.IDatabase, + 'databaseDialect', + typeof databaseAdmin.spanner.admin.database.v1.DatabaseDialect + >, + 'restoreInfo' + > & {restoreInfo?: IRestoreInfoTranslatedEnum | null}; /** * IRestoreInfo structure with restore source type enum translated to string form. @@ -312,6 +328,9 @@ class Database extends common.GrpcServiceObject { resourceHeader_: {[k: string]: string}; request: DatabaseRequest; databaseRole?: string | null; + databaseDialect?: EnumKey< + typeof databaseAdmin.spanner.admin.database.v1.DatabaseDialect + > | null; constructor( instance: Instance, name: string, @@ -1476,6 +1495,61 @@ class Database extends common.GrpcServiceObject { return metadata.state || undefined; } + /** + * Retrieves the dialect of the database + * + * @see {@link #getMetadata} + * + * @method Database#getDatabaseDialect + * + * @param {object} [gaxOptions] Request configuration options, + * See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} + * for more details. + * @param {GetDatabaseDialectCallback} [callback] Callback function. + * @returns {Promise | undefined>} + * When resolved, contains the database dialect of the database if the dialect is defined. + * @example + * const {Spanner} = require('@google-cloud/spanner'); + * const spanner = new Spanner(); + * const instance = spanner.instance('my-instance'); + * const database = instance.database('my-database'); + * const dialect = await database.getDatabaseDialect(); + * const isGoogleSQL = (dialect === 'GOOGLE_STANDARD_SQL'); + * const isPostgreSQL = (dialect === 'POSTGRESQL'); + */ + + getDatabaseDialect( + options?: CallOptions + ): Promise< + | EnumKey + | undefined + >; + getDatabaseDialect(callback: GetDatabaseDialectCallback): void; + getDatabaseDialect( + options: CallOptions, + callback: GetDatabaseDialectCallback + ): void; + async getDatabaseDialect( + optionsOrCallback?: CallOptions | GetDatabaseDialectCallback, + cb?: GetDatabaseDialectCallback + ): Promise< + | EnumKey + | undefined + > { + const gaxOptions = + typeof optionsOrCallback === 'object' ? optionsOrCallback : {}; + + if ( + this.databaseDialect === 'DATABASE_DIALECT_UNSPECIFIED' || + this.databaseDialect === null || + this.databaseDialect === undefined + ) { + const [metadata] = await this.getMetadata(gaxOptions); + this.databaseDialect = metadata.databaseDialect; + } + return this.databaseDialect || undefined; + } + /** * @typedef {array} GetSchemaResponse * @property {string[]} 0 An array of database DDL statements. @@ -2022,6 +2096,9 @@ class Database extends common.GrpcServiceObject { if (options.optimisticLock) { transaction!.useOptimisticLock(); } + if (options.excludeTxnFromChangeStreams) { + transaction!.excludeTxnFromChangeStreams(); + } if (!err) { this._releaseOnEnd(session!, transaction!); } @@ -2641,13 +2718,15 @@ class Database extends common.GrpcServiceObject { * @param {RunUpdateCallback} [callback] Callback function. * @returns {Promise} */ - runPartitionedUpdate(query: string | ExecuteSqlRequest): Promise<[number]>; runPartitionedUpdate( - query: string | ExecuteSqlRequest, + query: string | RunPartitionedUpdateOptions + ): Promise<[number]>; + runPartitionedUpdate( + query: string | RunPartitionedUpdateOptions, callback?: RunUpdateCallback ): void; runPartitionedUpdate( - query: string | ExecuteSqlRequest, + query: string | RunPartitionedUpdateOptions, callback?: RunUpdateCallback ): void | Promise<[number]> { this.pool_.getSession((err, session) => { @@ -2662,11 +2741,14 @@ class Database extends common.GrpcServiceObject { _runPartitionedUpdate( session: Session, - query: string | ExecuteSqlRequest, + query: string | RunPartitionedUpdateOptions, callback?: RunUpdateCallback ): void | Promise { const transaction = session.partitionedDml(); + if (typeof query !== 'string' && query.excludeTxnFromChangeStreams) { + transaction.excludeTxnFromChangeStreams(); + } transaction.begin(err => { if (err) { this.pool_.release(session!); @@ -2989,6 +3071,9 @@ class Database extends common.GrpcServiceObject { if (options.optimisticLock) { transaction!.useOptimisticLock(); } + if (options.excludeTxnFromChangeStreams) { + transaction!.excludeTxnFromChangeStreams(); + } const release = this.pool_.release.bind(this.pool_, session!); const runner = new TransactionRunner( @@ -3103,6 +3188,9 @@ class Database extends common.GrpcServiceObject { if (options.optimisticLock) { transaction.useOptimisticLock(); } + if (options.excludeTxnFromChangeStreams) { + transaction.excludeTxnFromChangeStreams(); + } const runner = new AsyncTransactionRunner( session, transaction, @@ -3429,6 +3517,7 @@ promisifyAll(Database, { 'batchTransaction', 'getRestoreInfo', 'getState', + 'getDatabaseDialect', 'getOperations', 'runTransaction', 'runTransactionAsync', diff --git a/src/index.ts b/src/index.ts index 62e3fc23d..71370fce2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,6 +34,10 @@ import { PGJsonb, SpannerDate, Struct, + ProtoMessage, + ProtoEnum, + IProtoMessageParams, + IProtoEnumParams, } from './codec'; import {Backup} from './backup'; import {Database} from './database'; @@ -1772,6 +1776,62 @@ class Spanner extends GrpcService { static pgJsonb(value): PGJsonb { return new codec.PGJsonb(value); } + + /** + * @typedef IProtoMessageParams + * @property {object} value Proto Message value as serialized-buffer or message object. + * @property {string} fullName Fully-qualified path name of proto message. + * @property {Function} [messageFunction] Function generated by protobufs containing + * helper methods for deserializing and serializing messages. + */ + /** + * Helper function to get a Cloud Spanner proto Message object. + * + * @param {IProtoMessageParams} params The proto message value params + * @returns {ProtoMessage} + * + * @example + * ``` + * const {Spanner} = require('@google-cloud/spanner'); + * const protoMessage = Spanner.protoMessage({ + * value: singerInfo, + * messageFunction: music.SingerInfo, + * fullName: "examples.spanner.music.SingerInfo" + * }); + * ``` + */ + static protoMessage(params: IProtoMessageParams): ProtoMessage { + return new codec.ProtoMessage(params); + } + + /** + * @typedef IProtoEnumParams + * @property {string | number} value Proto Enum value as a string constant or + * an integer constant. + * @property {string} fullName Fully-qualified path name of proto enum. + * @property {object} [enumObject] An enum object generated by protobufjs-cli. + */ + /** + * Helper function to get a Cloud Spanner proto enum object. + * + * @param {IProtoEnumParams} params The proto enum value params in the format of + * @code{IProtoEnumParams} + * @returns {ProtoEnum} + * + * @example + * ``` + * const {Spanner} = require('@google-cloud/spanner'); + * const protoEnum = Spanner.protoEnum({ + * value: 'ROCK', + * enumObject: music.Genre, + * fullName: "examples.spanner.music.Genre" + * }); + * ``` + */ + static protoEnum(params: IProtoEnumParams): ProtoEnum { + return new codec.ProtoEnum(params); + } + /** * Helper function to get a Cloud Spanner Struct object. * diff --git a/src/partial-result-stream.ts b/src/partial-result-stream.ts index e03539219..616cdc88b 100644 --- a/src/partial-result-stream.ts +++ b/src/partial-result-stream.ts @@ -50,11 +50,52 @@ interface RequestFunction { * that it is not ready for any more data. Increase this value if you * experience 'Stream is still not ready to receive data' errors as a * result of a slow writer in your receiving stream. + * @property {object} [columnsMetadata] An object map that can be used to pass + * additional properties for each column type which can help in deserializing + * the data coming from backend. (Eg: We need to pass Proto Function and Enum + * map to deserialize proto messages and enums, respectively.) */ export interface RowOptions { json?: boolean; jsonOptions?: JSONOptions; maxResumeRetries?: number; + /** + * An object where column names as keys and custom objects as corresponding + * values for deserialization. It's specifically useful for data types like + * protobuf where deserialization logic is on user-specific code. When provided, + * the custom object enables deserialization of backend-received column data. + * If not provided, data remains serialized as buffer for Proto Messages and + * integer for Proto Enums. + * + * @example + * To obtain Proto Messages and Proto Enums as JSON objects, you must supply + * additional metadata. This metadata should include the protobufjs-cli + * generated proto message function and enum object. It encompasses the essential + * logic for proper data deserialization. + * + * Eg: To read data from Proto Columns in json format using DQL, you should pass + * columnsMetadata where key is the name of the column and value is the protobufjs-cli + * generated proto message function and enum object. + * + * const query = { + * sql: `SELECT SingerId, + * FirstName, + * LastName, + * SingerInfo, + * SingerGenre, + * SingerInfoArray, + * SingerGenreArray + * FROM Singers + * WHERE SingerId = 6`, + * columnsMetadata: { + * SingerInfo: music.SingerInfo, + * SingerInfoArray: music.SingerInfo, + * SingerGenre: music.Genre, + * SingerGenreArray: music.Genre, + * }, + * }; + */ + columnsMetadata?: object; } /** @@ -334,7 +375,15 @@ export class PartialResultStream extends Transform implements ResultEvents { private _createRow(values: Value[]): Row { const fields = values.map((value, index) => { const {name, type} = this._fields[index]; - return {name, value: codec.decode(value, type as google.spanner.v1.Type)}; + const columnMetadata = this._options.columnsMetadata?.[name]; + return { + name, + value: codec.decode( + value, + type as google.spanner.v1.Type, + columnMetadata + ), + }; }); Object.defineProperty(fields, 'toJSON', { diff --git a/src/table.ts b/src/table.ts index c737bf96b..343d4364b 100644 --- a/src/table.ts +++ b/src/table.ts @@ -46,6 +46,7 @@ export type DropTableCallback = UpdateSchemaCallback; interface MutateRowsOptions extends CommitOptions { requestOptions?: Omit; + excludeTxnFromChangeStreams?: boolean; } export type DeleteRowsCallback = CommitCallback; @@ -68,6 +69,8 @@ export type UpsertRowsCallback = CommitCallback; export type UpsertRowsResponse = CommitResponse; export type UpsertRowsOptions = MutateRowsOptions; +const GOOGLE_STANDARD_SQL = 'GOOGLE_STANDARD_SQL'; +const POSTGRESQL = 'POSTGRESQL'; /** * Create a Table object to interact with a table in a Cloud Spanner * database. @@ -342,11 +345,37 @@ class Table { const callback = typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb!; - return this.database.updateSchema( - 'DROP TABLE `' + this.name + '`', - gaxOptions, - callback! - ); + const [schema, table] = this.name.includes('.') + ? this.name.split('.') + : [null, this.name]; + + let dropStatement = 'DROP TABLE `' + table + '`'; + + const performDelete = async (): Promise => { + const result = await this.database.getDatabaseDialect(gaxOptions); + + if (result && result.includes(POSTGRESQL)) { + dropStatement = schema + ? `DROP TABLE "${schema}"."${table}"` + : `DROP TABLE "${table}"`; + } else if (result && result.includes(GOOGLE_STANDARD_SQL)) { + dropStatement = schema + ? 'DROP TABLE `' + schema + '`.`' + table + '`' + : dropStatement; + } + + const updateSchemaResult = callback + ? this.database.updateSchema(dropStatement, gaxOptions, callback) + : this.database.updateSchema(dropStatement, gaxOptions); + + return updateSchemaResult as Promise; + }; + + if (!callback) { + return performDelete() as Promise; + } else { + performDelete(); + } } /** * @typedef {array} DeleteRowsResponse @@ -1045,15 +1074,27 @@ class Table { ): void { const requestOptions = 'requestOptions' in options ? options.requestOptions : {}; - this.database.runTransaction({requestOptions}, (err, transaction) => { - if (err) { - callback(err); - return; - } - transaction![method](this.name, rows as Key[]); - transaction!.commit(options, callback); - }); + const excludeTxnFromChangeStreams = + 'excludeTxnFromChangeStreams' in options + ? options.excludeTxnFromChangeStreams + : false; + + this.database.runTransaction( + { + requestOptions: requestOptions, + excludeTxnFromChangeStreams: excludeTxnFromChangeStreams, + }, + (err, transaction) => { + if (err) { + callback(err); + return; + } + + transaction![method](this.name, rows as Key[]); + transaction!.commit(options, callback); + } + ); } } diff --git a/src/transaction-runner.ts b/src/transaction-runner.ts index 7f697cd86..61d979e8c 100644 --- a/src/transaction-runner.ts +++ b/src/transaction-runner.ts @@ -45,6 +45,7 @@ export interface RunTransactionOptions { timeout?: number; requestOptions?: Pick; optimisticLock?: boolean; + excludeTxnFromChangeStreams?: boolean; } /** @@ -204,6 +205,9 @@ export abstract class Runner { if (this.options.optimisticLock) { transaction.useOptimisticLock(); } + if (this.options.excludeTxnFromChangeStreams) { + transaction.excludeTxnFromChangeStreams(); + } if (this.attempts > 0) { await transaction.begin(); } diff --git a/src/transaction.ts b/src/transaction.ts index 11eb03657..1231a55bf 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -64,6 +64,43 @@ export interface RequestOptions { jsonOptions?: JSONOptions; gaxOptions?: CallOptions; maxResumeRetries?: number; + /** + * An object where column names as keys and custom objects as corresponding + * values for deserialization. This is only needed for proto columns + * where deserialization logic is on user-specific code. When provided, + * the custom object enables deserialization of backend-received column data. + * If not provided, data remains serialized as buffer for Proto Messages and + * integer for Proto Enums. + * + * @example + * To obtain Proto Messages and Proto Enums as JSON objects, you must supply + * additional metadata. This metadata should include the protobufjs-cli + * generated proto message function and enum object. It encompasses the essential + * logic for proper data deserialization. + * + * Eg: To read data from Proto Columns in json format using DQL, you should pass + * columnsMetadata where key is the name of the column and value is the protobufjs-cli + * generated proto message function and enum object. + * + * const query = { + * sql: `SELECT SingerId, + * FirstName, + * LastName, + * SingerInfo, + * SingerGenre, + * SingerInfoArray, + * SingerGenreArray + * FROM Singers + * WHERE SingerId = 6`, + * columnsMetadata: { + * SingerInfo: music.SingerInfo, + * SingerInfoArray: music.SingerInfo, + * SingerGenre: music.Genre, + * SingerGenreArray: music.Genre, + * }, + * }; + */ + columnsMetadata?: object; } export interface CommitOptions { @@ -583,8 +620,14 @@ export class Snapshot extends EventEmitter { table: string, request = {} as ReadRequest ): PartialResultStream { - const {gaxOptions, json, jsonOptions, maxResumeRetries, requestOptions} = - request; + const { + gaxOptions, + json, + jsonOptions, + maxResumeRetries, + requestOptions, + columnsMetadata, + } = request; const keySet = Snapshot.encodeKeySet(request); const transaction: spannerClient.spanner.v1.ITransactionSelector = {}; @@ -610,6 +653,7 @@ export class Snapshot extends EventEmitter { delete request.ranges; delete request.requestOptions; delete request.directedReadOptions; + delete request.columnsMetadata; const reqOpts: spannerClient.spanner.v1.IReadRequest = Object.assign( request, @@ -654,6 +698,7 @@ export class Snapshot extends EventEmitter { json, jsonOptions, maxResumeRetries, + columnsMetadata, }) ?.on('response', response => { if (response.metadata && response.metadata!.transaction && !this.id) { @@ -1077,8 +1122,14 @@ export class Snapshot extends EventEmitter { query.queryOptions ); - const {gaxOptions, json, jsonOptions, maxResumeRetries, requestOptions} = - query; + const { + gaxOptions, + json, + jsonOptions, + maxResumeRetries, + requestOptions, + columnsMetadata, + } = query; let reqOpts; const directedReadOptions = this._getDirectedReadOptions( @@ -1103,6 +1154,7 @@ export class Snapshot extends EventEmitter { delete query.requestOptions; delete query.types; delete query.directedReadOptions; + delete query.columnsMetadata; reqOpts = Object.assign(query, { session: this.session.formattedName_!, @@ -1152,6 +1204,7 @@ export class Snapshot extends EventEmitter { json, jsonOptions, maxResumeRetries, + columnsMetadata, }) .on('response', response => { if (response.metadata && response.metadata!.transaction && !this.id) { @@ -2417,6 +2470,18 @@ export class Transaction extends Dml { useOptimisticLock(): void { this._options.readWrite!.readLockMode = ReadLockMode.OPTIMISTIC; } + + /** + * Use option excludeTxnFromChangeStreams to exclude read/write transactions + * from being tracked in change streams. + * + * Enabling this options to true will effectively disable change stream tracking + * for a specified transaction, allowing read/write transaction to operate without being + * included in change streams. + */ + excludeTxnFromChangeStreams(): void { + this._options.excludeTxnFromChangeStreams = true; + } } /*! Developer Documentation @@ -2450,6 +2515,17 @@ export class PartitionedDml extends Dml { super(session); this._options = {partitionedDml: options}; } + /** + * Use option excludeTxnFromChangeStreams to exclude partitionedDml + * queries from being tracked in change streams. + * + * Enabling this options to true will effectively disable change stream tracking + * for a specified partitionedDml query, allowing write queries to operate + * without being included in change streams. + */ + excludeTxnFromChangeStreams(): void { + this._options.excludeTxnFromChangeStreams = true; + } /** * Execute a DML statement and get the affected row count. Unlike diff --git a/src/v1/database_admin_client.ts b/src/v1/database_admin_client.ts index a4e26200a..3fae88a1d 100644 --- a/src/v1/database_admin_client.ts +++ b/src/v1/database_admin_client.ts @@ -46,7 +46,7 @@ const version = require('../../../package.json').version; * The Cloud Spanner Database Admin API can be used to: * * create, drop, and list databases * * update the schema of pre-existing databases - * * create, delete and list backups for a database + * * create, delete, copy and list backups for a database * * restore a database from an existing backup * @class * @memberof v1 @@ -1139,7 +1139,8 @@ export class DatabaseAdminClient { return this.innerApiCalls.testIamPermissions(request, options, callback); } /** - * Gets metadata on a pending or completed {@link protos.google.spanner.admin.database.v1.Backup|Backup}. + * Gets metadata on a pending or completed + * {@link protos.google.spanner.admin.database.v1.Backup|Backup}. * * @param {Object} request * The request object that will be sent. @@ -1229,7 +1230,8 @@ export class DatabaseAdminClient { return this.innerApiCalls.getBackup(request, options, callback); } /** - * Updates a pending or completed {@link protos.google.spanner.admin.database.v1.Backup|Backup}. + * Updates a pending or completed + * {@link protos.google.spanner.admin.database.v1.Backup|Backup}. * * @param {Object} request * The request object that will be sent. @@ -1326,7 +1328,8 @@ export class DatabaseAdminClient { return this.innerApiCalls.updateBackup(request, options, callback); } /** - * Deletes a pending or completed {@link protos.google.spanner.admin.database.v1.Backup|Backup}. + * Deletes a pending or completed + * {@link protos.google.spanner.admin.database.v1.Backup|Backup}. * * @param {Object} request * The request object that will be sent. @@ -1422,8 +1425,8 @@ export class DatabaseAdminClient { * have a name of the format `/operations/` and * can be used to track preparation of the database. The * {@link protos.google.longrunning.Operation.metadata|metadata} field type is - * {@link protos.google.spanner.admin.database.v1.CreateDatabaseMetadata|CreateDatabaseMetadata}. The - * {@link protos.google.longrunning.Operation.response|response} field type is + * {@link protos.google.spanner.admin.database.v1.CreateDatabaseMetadata|CreateDatabaseMetadata}. + * The {@link protos.google.longrunning.Operation.response|response} field type is * {@link protos.google.spanner.admin.database.v1.Database|Database}, if successful. * * @param {Object} request @@ -1443,8 +1446,8 @@ export class DatabaseAdminClient { * statements execute atomically with the creation of the database: * if there is an error in any statement, the database is not created. * @param {google.spanner.admin.database.v1.EncryptionConfig} [request.encryptionConfig] - * Optional. The encryption configuration for the database. If this field is not - * specified, Cloud Spanner will encrypt/decrypt all data at rest using + * Optional. The encryption configuration for the database. If this field is + * not specified, Cloud Spanner will encrypt/decrypt all data at rest using * Google default encryption. * @param {google.spanner.admin.database.v1.DatabaseDialect} [request.databaseDialect] * Optional. The dialect of the Cloud Spanner Database. @@ -1768,7 +1771,8 @@ export class DatabaseAdminClient { * the format `/operations/` and can be used to * track execution of the schema change(s). The * {@link protos.google.longrunning.Operation.metadata|metadata} field type is - * {@link protos.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata|UpdateDatabaseDdlMetadata}. The operation has no response. + * {@link protos.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata|UpdateDatabaseDdlMetadata}. + * The operation has no response. * * @param {Object} request * The request object that will be sent. @@ -1784,18 +1788,20 @@ export class DatabaseAdminClient { * * Specifying an explicit operation ID simplifies determining * whether the statements were executed in the event that the - * {@link protos.google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl|UpdateDatabaseDdl} call is replayed, - * or the return value is otherwise lost: the {@link protos.google.spanner.admin.database.v1.UpdateDatabaseDdlRequest.database|database} and - * `operation_id` fields can be combined to form the + * {@link protos.google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl|UpdateDatabaseDdl} + * call is replayed, or the return value is otherwise lost: the + * {@link protos.google.spanner.admin.database.v1.UpdateDatabaseDdlRequest.database|database} + * and `operation_id` fields can be combined to form the * {@link protos.google.longrunning.Operation.name|name} of the resulting - * {@link protos.google.longrunning.Operation|longrunning.Operation}: `/operations/`. + * {@link protos.google.longrunning.Operation|longrunning.Operation}: + * `/operations/`. * * `operation_id` should be unique within the database, and must be * a valid identifier: `{@link protos.a-z0-9_|a-z}*`. Note that * automatically-generated operation IDs always begin with an * underscore. If the named operation already exists, - * {@link protos.google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl|UpdateDatabaseDdl} returns - * `ALREADY_EXISTS`. + * {@link protos.google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl|UpdateDatabaseDdl} + * returns `ALREADY_EXISTS`. * @param {Buffer} [request.protoDescriptors] * Optional. Proto descriptors used by CREATE/ALTER PROTO BUNDLE statements. * Contains a protobuf-serialized @@ -1943,12 +1949,12 @@ export class DatabaseAdminClient { * `projects//instances//backups//operations/` * and can be used to track creation of the backup. The * {@link protos.google.longrunning.Operation.metadata|metadata} field type is - * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. The - * {@link protos.google.longrunning.Operation.response|response} field type is - * {@link protos.google.spanner.admin.database.v1.Backup|Backup}, if successful. Cancelling the returned operation will stop the - * creation and delete the backup. - * There can be only one pending backup creation per database. Backup creation - * of different databases can run concurrently. + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. + * The {@link protos.google.longrunning.Operation.response|response} field type is + * {@link protos.google.spanner.admin.database.v1.Backup|Backup}, if successful. + * Cancelling the returned operation will stop the creation and delete the + * backup. There can be only one pending backup creation per database. Backup + * creation of different databases can run concurrently. * * @param {Object} request * The request object that will be sent. @@ -1966,11 +1972,11 @@ export class DatabaseAdminClient { * @param {google.spanner.admin.database.v1.Backup} request.backup * Required. The backup to create. * @param {google.spanner.admin.database.v1.CreateBackupEncryptionConfig} [request.encryptionConfig] - * Optional. The encryption configuration used to encrypt the backup. If this field is - * not specified, the backup will use the same - * encryption configuration as the database by default, namely - * {@link protos.google.spanner.admin.database.v1.CreateBackupEncryptionConfig.encryption_type|encryption_type} = - * `USE_DATABASE_ENCRYPTION`. + * Optional. The encryption configuration used to encrypt the backup. If this + * field is not specified, the backup will use the same encryption + * configuration as the database by default, namely + * {@link protos.google.spanner.admin.database.v1.CreateBackupEncryptionConfig.encryption_type|encryption_type} + * = `USE_DATABASE_ENCRYPTION`. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. @@ -2106,15 +2112,16 @@ export class DatabaseAdminClient { * The {@link protos.google.longrunning.Operation.metadata|metadata} field type is * {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata}. * The {@link protos.google.longrunning.Operation.response|response} field type is - * {@link protos.google.spanner.admin.database.v1.Backup|Backup}, if successful. Cancelling the returned operation will stop the - * copying and delete the backup. - * Concurrent CopyBackup requests can run on the same source backup. + * {@link protos.google.spanner.admin.database.v1.Backup|Backup}, if successful. + * Cancelling the returned operation will stop the copying and delete the + * destination backup. Concurrent CopyBackup requests can run on the same + * source backup. * * @param {Object} request * The request object that will be sent. * @param {string} request.parent - * Required. The name of the destination instance that will contain the backup copy. - * Values are of the form: `projects//instances/`. + * Required. The name of the destination instance that will contain the backup + * copy. Values are of the form: `projects//instances/`. * @param {string} request.backupId * Required. The id of the backup copy. * The `backup_id` appended to `parent` forms the full backup_uri of the form @@ -2133,11 +2140,11 @@ export class DatabaseAdminClient { * passed, the backup is eligible to be automatically deleted by Cloud Spanner * to free the resources used by the backup. * @param {google.spanner.admin.database.v1.CopyBackupEncryptionConfig} [request.encryptionConfig] - * Optional. The encryption configuration used to encrypt the backup. If this field is - * not specified, the backup will use the same - * encryption configuration as the source backup by default, namely - * {@link protos.google.spanner.admin.database.v1.CopyBackupEncryptionConfig.encryption_type|encryption_type} = - * `USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION`. + * Optional. The encryption configuration used to encrypt the backup. If this + * field is not specified, the backup will use the same encryption + * configuration as the source backup by default, namely + * {@link protos.google.spanner.admin.database.v1.CopyBackupEncryptionConfig.encryption_type|encryption_type} + * = `USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION`. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. @@ -2299,12 +2306,12 @@ export class DatabaseAdminClient { * Name of the backup from which to restore. Values are of the form * `projects//instances//backups/`. * @param {google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig} [request.encryptionConfig] - * Optional. An encryption configuration describing the encryption type and key - * resources in Cloud KMS used to encrypt/decrypt the database to restore to. - * If this field is not specified, the restored database will use - * the same encryption configuration as the backup by default, namely - * {@link protos.google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.encryption_type|encryption_type} = - * `USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION`. + * Optional. An encryption configuration describing the encryption type and + * key resources in Cloud KMS used to encrypt/decrypt the database to restore + * to. If this field is not specified, the restored database will use the same + * encryption configuration as the backup by default, namely + * {@link protos.google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.encryption_type|encryption_type} + * = `USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION`. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. @@ -2443,8 +2450,9 @@ export class DatabaseAdminClient { * defaults to the server's maximum allowed page size. * @param {string} request.pageToken * If non-empty, `page_token` should contain a - * {@link protos.google.spanner.admin.database.v1.ListDatabasesResponse.next_page_token|next_page_token} from a - * previous {@link protos.google.spanner.admin.database.v1.ListDatabasesResponse|ListDatabasesResponse}. + * {@link protos.google.spanner.admin.database.v1.ListDatabasesResponse.next_page_token|next_page_token} + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListDatabasesResponse|ListDatabasesResponse}. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. @@ -2544,8 +2552,9 @@ export class DatabaseAdminClient { * defaults to the server's maximum allowed page size. * @param {string} request.pageToken * If non-empty, `page_token` should contain a - * {@link protos.google.spanner.admin.database.v1.ListDatabasesResponse.next_page_token|next_page_token} from a - * previous {@link protos.google.spanner.admin.database.v1.ListDatabasesResponse|ListDatabasesResponse}. + * {@link protos.google.spanner.admin.database.v1.ListDatabasesResponse.next_page_token|next_page_token} + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListDatabasesResponse|ListDatabasesResponse}. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} @@ -2593,8 +2602,9 @@ export class DatabaseAdminClient { * defaults to the server's maximum allowed page size. * @param {string} request.pageToken * If non-empty, `page_token` should contain a - * {@link protos.google.spanner.admin.database.v1.ListDatabasesResponse.next_page_token|next_page_token} from a - * previous {@link protos.google.spanner.admin.database.v1.ListDatabasesResponse|ListDatabasesResponse}. + * {@link protos.google.spanner.admin.database.v1.ListDatabasesResponse.next_page_token|next_page_token} + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListDatabasesResponse|ListDatabasesResponse}. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Object} @@ -2645,7 +2655,9 @@ export class DatabaseAdminClient { * must be one of: `<`, `>`, `<=`, `>=`, `!=`, `=`, or `:`. * Colon `:` is the contains operator. Filter rules are not case sensitive. * - * The following fields in the {@link protos.google.spanner.admin.database.v1.Backup|Backup} are eligible for filtering: + * The following fields in the + * {@link protos.google.spanner.admin.database.v1.Backup|Backup} are eligible for + * filtering: * * * `name` * * `database` @@ -2677,9 +2689,10 @@ export class DatabaseAdminClient { * less, defaults to the server's maximum allowed page size. * @param {string} request.pageToken * If non-empty, `page_token` should contain a - * {@link protos.google.spanner.admin.database.v1.ListBackupsResponse.next_page_token|next_page_token} from a - * previous {@link protos.google.spanner.admin.database.v1.ListBackupsResponse|ListBackupsResponse} to the same `parent` and with the same - * `filter`. + * {@link protos.google.spanner.admin.database.v1.ListBackupsResponse.next_page_token|next_page_token} + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListBackupsResponse|ListBackupsResponse} + * to the same `parent` and with the same `filter`. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. @@ -2783,7 +2796,9 @@ export class DatabaseAdminClient { * must be one of: `<`, `>`, `<=`, `>=`, `!=`, `=`, or `:`. * Colon `:` is the contains operator. Filter rules are not case sensitive. * - * The following fields in the {@link protos.google.spanner.admin.database.v1.Backup|Backup} are eligible for filtering: + * The following fields in the + * {@link protos.google.spanner.admin.database.v1.Backup|Backup} are eligible for + * filtering: * * * `name` * * `database` @@ -2815,9 +2830,10 @@ export class DatabaseAdminClient { * less, defaults to the server's maximum allowed page size. * @param {string} request.pageToken * If non-empty, `page_token` should contain a - * {@link protos.google.spanner.admin.database.v1.ListBackupsResponse.next_page_token|next_page_token} from a - * previous {@link protos.google.spanner.admin.database.v1.ListBackupsResponse|ListBackupsResponse} to the same `parent` and with the same - * `filter`. + * {@link protos.google.spanner.admin.database.v1.ListBackupsResponse.next_page_token|next_page_token} + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListBackupsResponse|ListBackupsResponse} + * to the same `parent` and with the same `filter`. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} @@ -2869,7 +2885,9 @@ export class DatabaseAdminClient { * must be one of: `<`, `>`, `<=`, `>=`, `!=`, `=`, or `:`. * Colon `:` is the contains operator. Filter rules are not case sensitive. * - * The following fields in the {@link protos.google.spanner.admin.database.v1.Backup|Backup} are eligible for filtering: + * The following fields in the + * {@link protos.google.spanner.admin.database.v1.Backup|Backup} are eligible for + * filtering: * * * `name` * * `database` @@ -2901,9 +2919,10 @@ export class DatabaseAdminClient { * less, defaults to the server's maximum allowed page size. * @param {string} request.pageToken * If non-empty, `page_token` should contain a - * {@link protos.google.spanner.admin.database.v1.ListBackupsResponse.next_page_token|next_page_token} from a - * previous {@link protos.google.spanner.admin.database.v1.ListBackupsResponse|ListBackupsResponse} to the same `parent` and with the same - * `filter`. + * {@link protos.google.spanner.admin.database.v1.ListBackupsResponse.next_page_token|next_page_token} + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListBackupsResponse|ListBackupsResponse} + * to the same `parent` and with the same `filter`. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Object} @@ -2965,7 +2984,9 @@ export class DatabaseAdminClient { * * `name` - The name of the long-running operation * * `done` - False if the operation is in progress, else true. * * `metadata.@type` - the type of metadata. For example, the type string - * for {@link protos.google.spanner.admin.database.v1.RestoreDatabaseMetadata|RestoreDatabaseMetadata} is + * for + * {@link protos.google.spanner.admin.database.v1.RestoreDatabaseMetadata|RestoreDatabaseMetadata} + * is * `type.googleapis.com/google.spanner.admin.database.v1.RestoreDatabaseMetadata`. * * `metadata.` - any field in metadata.value. * `metadata.@type` must be specified first, if filtering on metadata @@ -2987,7 +3008,8 @@ export class DatabaseAdminClient { * `(metadata.name:restored_howl) AND` \ * `(metadata.progress.start_time < \"2018-03-28T14:50:00Z\") AND` \ * `(error:*)` - Return operations where: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.RestoreDatabaseMetadata|RestoreDatabaseMetadata}. + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.RestoreDatabaseMetadata|RestoreDatabaseMetadata}. * * The database is restored from a backup. * * The backup name contains "backup_howl". * * The restored database's name contains "restored_howl". @@ -2999,8 +3021,9 @@ export class DatabaseAdminClient { * @param {string} request.pageToken * If non-empty, `page_token` should contain a * {@link protos.google.spanner.admin.database.v1.ListDatabaseOperationsResponse.next_page_token|next_page_token} - * from a previous {@link protos.google.spanner.admin.database.v1.ListDatabaseOperationsResponse|ListDatabaseOperationsResponse} to the - * same `parent` and with the same `filter`. + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListDatabaseOperationsResponse|ListDatabaseOperationsResponse} + * to the same `parent` and with the same `filter`. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. @@ -3114,7 +3137,9 @@ export class DatabaseAdminClient { * * `name` - The name of the long-running operation * * `done` - False if the operation is in progress, else true. * * `metadata.@type` - the type of metadata. For example, the type string - * for {@link protos.google.spanner.admin.database.v1.RestoreDatabaseMetadata|RestoreDatabaseMetadata} is + * for + * {@link protos.google.spanner.admin.database.v1.RestoreDatabaseMetadata|RestoreDatabaseMetadata} + * is * `type.googleapis.com/google.spanner.admin.database.v1.RestoreDatabaseMetadata`. * * `metadata.` - any field in metadata.value. * `metadata.@type` must be specified first, if filtering on metadata @@ -3136,7 +3161,8 @@ export class DatabaseAdminClient { * `(metadata.name:restored_howl) AND` \ * `(metadata.progress.start_time < \"2018-03-28T14:50:00Z\") AND` \ * `(error:*)` - Return operations where: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.RestoreDatabaseMetadata|RestoreDatabaseMetadata}. + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.RestoreDatabaseMetadata|RestoreDatabaseMetadata}. * * The database is restored from a backup. * * The backup name contains "backup_howl". * * The restored database's name contains "restored_howl". @@ -3148,8 +3174,9 @@ export class DatabaseAdminClient { * @param {string} request.pageToken * If non-empty, `page_token` should contain a * {@link protos.google.spanner.admin.database.v1.ListDatabaseOperationsResponse.next_page_token|next_page_token} - * from a previous {@link protos.google.spanner.admin.database.v1.ListDatabaseOperationsResponse|ListDatabaseOperationsResponse} to the - * same `parent` and with the same `filter`. + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListDatabaseOperationsResponse|ListDatabaseOperationsResponse} + * to the same `parent` and with the same `filter`. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} @@ -3207,7 +3234,9 @@ export class DatabaseAdminClient { * * `name` - The name of the long-running operation * * `done` - False if the operation is in progress, else true. * * `metadata.@type` - the type of metadata. For example, the type string - * for {@link protos.google.spanner.admin.database.v1.RestoreDatabaseMetadata|RestoreDatabaseMetadata} is + * for + * {@link protos.google.spanner.admin.database.v1.RestoreDatabaseMetadata|RestoreDatabaseMetadata} + * is * `type.googleapis.com/google.spanner.admin.database.v1.RestoreDatabaseMetadata`. * * `metadata.` - any field in metadata.value. * `metadata.@type` must be specified first, if filtering on metadata @@ -3229,7 +3258,8 @@ export class DatabaseAdminClient { * `(metadata.name:restored_howl) AND` \ * `(metadata.progress.start_time < \"2018-03-28T14:50:00Z\") AND` \ * `(error:*)` - Return operations where: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.RestoreDatabaseMetadata|RestoreDatabaseMetadata}. + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.RestoreDatabaseMetadata|RestoreDatabaseMetadata}. * * The database is restored from a backup. * * The backup name contains "backup_howl". * * The restored database's name contains "restored_howl". @@ -3241,8 +3271,9 @@ export class DatabaseAdminClient { * @param {string} request.pageToken * If non-empty, `page_token` should contain a * {@link protos.google.spanner.admin.database.v1.ListDatabaseOperationsResponse.next_page_token|next_page_token} - * from a previous {@link protos.google.spanner.admin.database.v1.ListDatabaseOperationsResponse|ListDatabaseOperationsResponse} to the - * same `parent` and with the same `filter`. + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListDatabaseOperationsResponse|ListDatabaseOperationsResponse} + * to the same `parent` and with the same `filter`. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Object} @@ -3306,7 +3337,9 @@ export class DatabaseAdminClient { * * `name` - The name of the long-running operation * * `done` - False if the operation is in progress, else true. * * `metadata.@type` - the type of metadata. For example, the type string - * for {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata} is + * for + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata} + * is * `type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata`. * * `metadata.` - any field in metadata.value. * `metadata.@type` must be specified first if filtering on metadata @@ -3324,14 +3357,15 @@ export class DatabaseAdminClient { * * `done:true` - The operation is complete. * * `(metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ * `metadata.database:prod` - Returns operations where: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. - * * The database the backup was taken from has a name containing the - * string "prod". + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. + * * The source database name of backup contains the string "prod". * * `(metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ * `(metadata.name:howl) AND` \ * `(metadata.progress.start_time < \"2018-03-28T14:50:00Z\") AND` \ * `(error:*)` - Returns operations where: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. * * The backup name contains the string "howl". * * The operation started before 2018-03-28T14:50:00Z. * * The operation resulted in an error. @@ -3339,9 +3373,9 @@ export class DatabaseAdminClient { * `(metadata.source_backup:test) AND` \ * `(metadata.progress.start_time < \"2022-01-18T14:50:00Z\") AND` \ * `(error:*)` - Returns operations where: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata}. - * * The source backup of the copied backup name contains the string - * "test". + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata}. + * * The source backup name contains the string "test". * * The operation started before 2022-01-18T14:50:00Z. * * The operation resulted in an error. * * `((metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ @@ -3351,12 +3385,13 @@ export class DatabaseAdminClient { * `(metadata.source_backup:test_bkp)) AND` \ * `(error:*)` - Returns operations where: * * The operation's metadata matches either of criteria: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata} AND the - * database the backup was taken from has name containing string + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata} + * AND the source database name of the backup contains the string * "test_db" - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata} AND the - * backup the backup was copied from has name containing string - * "test_bkp" + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata} + * AND the source backup name contains the string "test_bkp" * * The operation resulted in an error. * @param {number} request.pageSize * Number of operations to be returned in the response. If 0 or @@ -3364,8 +3399,9 @@ export class DatabaseAdminClient { * @param {string} request.pageToken * If non-empty, `page_token` should contain a * {@link protos.google.spanner.admin.database.v1.ListBackupOperationsResponse.next_page_token|next_page_token} - * from a previous {@link protos.google.spanner.admin.database.v1.ListBackupOperationsResponse|ListBackupOperationsResponse} to the - * same `parent` and with the same `filter`. + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListBackupOperationsResponse|ListBackupOperationsResponse} + * to the same `parent` and with the same `filter`. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. @@ -3475,7 +3511,9 @@ export class DatabaseAdminClient { * * `name` - The name of the long-running operation * * `done` - False if the operation is in progress, else true. * * `metadata.@type` - the type of metadata. For example, the type string - * for {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata} is + * for + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata} + * is * `type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata`. * * `metadata.` - any field in metadata.value. * `metadata.@type` must be specified first if filtering on metadata @@ -3493,14 +3531,15 @@ export class DatabaseAdminClient { * * `done:true` - The operation is complete. * * `(metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ * `metadata.database:prod` - Returns operations where: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. - * * The database the backup was taken from has a name containing the - * string "prod". + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. + * * The source database name of backup contains the string "prod". * * `(metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ * `(metadata.name:howl) AND` \ * `(metadata.progress.start_time < \"2018-03-28T14:50:00Z\") AND` \ * `(error:*)` - Returns operations where: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. * * The backup name contains the string "howl". * * The operation started before 2018-03-28T14:50:00Z. * * The operation resulted in an error. @@ -3508,9 +3547,9 @@ export class DatabaseAdminClient { * `(metadata.source_backup:test) AND` \ * `(metadata.progress.start_time < \"2022-01-18T14:50:00Z\") AND` \ * `(error:*)` - Returns operations where: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata}. - * * The source backup of the copied backup name contains the string - * "test". + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata}. + * * The source backup name contains the string "test". * * The operation started before 2022-01-18T14:50:00Z. * * The operation resulted in an error. * * `((metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ @@ -3520,12 +3559,13 @@ export class DatabaseAdminClient { * `(metadata.source_backup:test_bkp)) AND` \ * `(error:*)` - Returns operations where: * * The operation's metadata matches either of criteria: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata} AND the - * database the backup was taken from has name containing string + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata} + * AND the source database name of the backup contains the string * "test_db" - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata} AND the - * backup the backup was copied from has name containing string - * "test_bkp" + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata} + * AND the source backup name contains the string "test_bkp" * * The operation resulted in an error. * @param {number} request.pageSize * Number of operations to be returned in the response. If 0 or @@ -3533,8 +3573,9 @@ export class DatabaseAdminClient { * @param {string} request.pageToken * If non-empty, `page_token` should contain a * {@link protos.google.spanner.admin.database.v1.ListBackupOperationsResponse.next_page_token|next_page_token} - * from a previous {@link protos.google.spanner.admin.database.v1.ListBackupOperationsResponse|ListBackupOperationsResponse} to the - * same `parent` and with the same `filter`. + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListBackupOperationsResponse|ListBackupOperationsResponse} + * to the same `parent` and with the same `filter`. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} @@ -3592,7 +3633,9 @@ export class DatabaseAdminClient { * * `name` - The name of the long-running operation * * `done` - False if the operation is in progress, else true. * * `metadata.@type` - the type of metadata. For example, the type string - * for {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata} is + * for + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata} + * is * `type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata`. * * `metadata.` - any field in metadata.value. * `metadata.@type` must be specified first if filtering on metadata @@ -3610,14 +3653,15 @@ export class DatabaseAdminClient { * * `done:true` - The operation is complete. * * `(metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ * `metadata.database:prod` - Returns operations where: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. - * * The database the backup was taken from has a name containing the - * string "prod". + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. + * * The source database name of backup contains the string "prod". * * `(metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ * `(metadata.name:howl) AND` \ * `(metadata.progress.start_time < \"2018-03-28T14:50:00Z\") AND` \ * `(error:*)` - Returns operations where: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata}. * * The backup name contains the string "howl". * * The operation started before 2018-03-28T14:50:00Z. * * The operation resulted in an error. @@ -3625,9 +3669,9 @@ export class DatabaseAdminClient { * `(metadata.source_backup:test) AND` \ * `(metadata.progress.start_time < \"2022-01-18T14:50:00Z\") AND` \ * `(error:*)` - Returns operations where: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata}. - * * The source backup of the copied backup name contains the string - * "test". + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata}. + * * The source backup name contains the string "test". * * The operation started before 2022-01-18T14:50:00Z. * * The operation resulted in an error. * * `((metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ @@ -3637,12 +3681,13 @@ export class DatabaseAdminClient { * `(metadata.source_backup:test_bkp)) AND` \ * `(error:*)` - Returns operations where: * * The operation's metadata matches either of criteria: - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata} AND the - * database the backup was taken from has name containing string + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CreateBackupMetadata|CreateBackupMetadata} + * AND the source database name of the backup contains the string * "test_db" - * * The operation's metadata type is {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata} AND the - * backup the backup was copied from has name containing string - * "test_bkp" + * * The operation's metadata type is + * {@link protos.google.spanner.admin.database.v1.CopyBackupMetadata|CopyBackupMetadata} + * AND the source backup name contains the string "test_bkp" * * The operation resulted in an error. * @param {number} request.pageSize * Number of operations to be returned in the response. If 0 or @@ -3650,8 +3695,9 @@ export class DatabaseAdminClient { * @param {string} request.pageToken * If non-empty, `page_token` should contain a * {@link protos.google.spanner.admin.database.v1.ListBackupOperationsResponse.next_page_token|next_page_token} - * from a previous {@link protos.google.spanner.admin.database.v1.ListBackupOperationsResponse|ListBackupOperationsResponse} to the - * same `parent` and with the same `filter`. + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListBackupOperationsResponse|ListBackupOperationsResponse} + * to the same `parent` and with the same `filter`. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Object} @@ -3691,14 +3737,15 @@ export class DatabaseAdminClient { * @param {string} request.parent * Required. The database whose roles should be listed. * Values are of the form - * `projects//instances//databases//databaseRoles`. + * `projects//instances//databases/`. * @param {number} request.pageSize * Number of database roles to be returned in the response. If 0 or less, * defaults to the server's maximum allowed page size. * @param {string} request.pageToken * If non-empty, `page_token` should contain a - * {@link protos.google.spanner.admin.database.v1.ListDatabaseRolesResponse.next_page_token|next_page_token} from a - * previous {@link protos.google.spanner.admin.database.v1.ListDatabaseRolesResponse|ListDatabaseRolesResponse}. + * {@link protos.google.spanner.admin.database.v1.ListDatabaseRolesResponse.next_page_token|next_page_token} + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListDatabaseRolesResponse|ListDatabaseRolesResponse}. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Promise} - The promise which resolves to an array. @@ -3793,14 +3840,15 @@ export class DatabaseAdminClient { * @param {string} request.parent * Required. The database whose roles should be listed. * Values are of the form - * `projects//instances//databases//databaseRoles`. + * `projects//instances//databases/`. * @param {number} request.pageSize * Number of database roles to be returned in the response. If 0 or less, * defaults to the server's maximum allowed page size. * @param {string} request.pageToken * If non-empty, `page_token` should contain a - * {@link protos.google.spanner.admin.database.v1.ListDatabaseRolesResponse.next_page_token|next_page_token} from a - * previous {@link protos.google.spanner.admin.database.v1.ListDatabaseRolesResponse|ListDatabaseRolesResponse}. + * {@link protos.google.spanner.admin.database.v1.ListDatabaseRolesResponse.next_page_token|next_page_token} + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListDatabaseRolesResponse|ListDatabaseRolesResponse}. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Stream} @@ -3843,14 +3891,15 @@ export class DatabaseAdminClient { * @param {string} request.parent * Required. The database whose roles should be listed. * Values are of the form - * `projects//instances//databases//databaseRoles`. + * `projects//instances//databases/`. * @param {number} request.pageSize * Number of database roles to be returned in the response. If 0 or less, * defaults to the server's maximum allowed page size. * @param {string} request.pageToken * If non-empty, `page_token` should contain a - * {@link protos.google.spanner.admin.database.v1.ListDatabaseRolesResponse.next_page_token|next_page_token} from a - * previous {@link protos.google.spanner.admin.database.v1.ListDatabaseRolesResponse|ListDatabaseRolesResponse}. + * {@link protos.google.spanner.admin.database.v1.ListDatabaseRolesResponse.next_page_token|next_page_token} + * from a previous + * {@link protos.google.spanner.admin.database.v1.ListDatabaseRolesResponse|ListDatabaseRolesResponse}. * @param {object} [options] * Call options. See {@link https://googleapis.dev/nodejs/google-gax/latest/interfaces/CallOptions.html|CallOptions} for more details. * @returns {Object} diff --git a/src/v1/spanner_client_config.json b/src/v1/spanner_client_config.json index f1ed7096b..3d5086946 100644 --- a/src/v1/spanner_client_config.json +++ b/src/v1/spanner_client_config.json @@ -7,7 +7,8 @@ "DEADLINE_EXCEEDED", "UNAVAILABLE" ], - "unavailable": [ + "resource_exhausted_unavailable": [ + "RESOURCE_EXHAUSTED", "UNAVAILABLE" ] }, @@ -34,32 +35,32 @@ "methods": { "CreateSession": { "timeout_millis": 30000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "BatchCreateSessions": { "timeout_millis": 60000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "GetSession": { "timeout_millis": 30000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "ListSessions": { "timeout_millis": 3600000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "DeleteSession": { "timeout_millis": 30000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "ExecuteSql": { "timeout_millis": 30000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "ExecuteStreamingSql": { @@ -69,12 +70,12 @@ }, "ExecuteBatchDml": { "timeout_millis": 30000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "Read": { "timeout_millis": 30000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "StreamingRead": { @@ -84,27 +85,27 @@ }, "BeginTransaction": { "timeout_millis": 30000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "Commit": { "timeout_millis": 3600000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "Rollback": { "timeout_millis": 30000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "PartitionQuery": { "timeout_millis": 30000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "PartitionRead": { "timeout_millis": 30000, - "retry_codes_name": "unavailable", + "retry_codes_name": "resource_exhausted_unavailable", "retry_params_name": "9442ca297df43f7314712e1a19d003838e738a45" }, "BatchWrite": { diff --git a/system-test/spanner.ts b/system-test/spanner.ts index 041920a77..2a4df13aa 100644 --- a/system-test/spanner.ts +++ b/system-test/spanner.ts @@ -46,6 +46,11 @@ import {google} from '../protos/protos'; import CreateDatabaseMetadata = google.spanner.admin.database.v1.CreateDatabaseMetadata; import CreateBackupMetadata = google.spanner.admin.database.v1.CreateBackupMetadata; import CreateInstanceConfigMetadata = google.spanner.admin.instance.v1.CreateInstanceConfigMetadata; +const singer = require('../test/data/singer'); +const music = singer.examples.spanner.music; +import {util} from 'protobufjs'; +import Long = util.Long; +const fs = require('fs'); const SKIP_BACKUPS = process.env.SKIP_BACKUPS; const SKIP_FGAC_TESTS = (process.env.SKIP_FGAC_TESTS || 'false').toLowerCase(); @@ -124,15 +129,42 @@ describe('Spanner', () => { `Not creating temp instance, using + ${instance.formattedName_}...` ); } - const [, googleSqlOperation1] = await DATABASE.create({ - schema: ` + + if (IS_EMULATOR_ENABLED) { + const [, googleSqlOperation1] = await DATABASE.create({ + schema: ` CREATE TABLE ${TABLE_NAME} ( SingerId STRING(1024) NOT NULL, Name STRING(1024), ) PRIMARY KEY(SingerId)`, - gaxOptions: GAX_OPTIONS, - }); - await googleSqlOperation1.promise(); + gaxOptions: GAX_OPTIONS, + }); + await googleSqlOperation1.promise(); + } else { + // Reading proto descriptor file + const protoDescriptor = fs + .readFileSync('test/data/descriptors.pb') + .toString('base64'); + + const [, googleSqlOperation1] = await DATABASE.create({ + schema: [ + ` + CREATE PROTO BUNDLE ( + examples.spanner.music.SingerInfo, + examples.spanner.music.Genre, + )`, + ` + CREATE TABLE ${TABLE_NAME} ( + SingerId STRING(1024) NOT NULL, + Name STRING(1024), + ) PRIMARY KEY(SingerId)`, + ], + gaxOptions: GAX_OPTIONS, + protoDescriptors: protoDescriptor, + }); + + await googleSqlOperation1.promise(); + } RESOURCES_TO_CLEAN.push(DATABASE); const [, googleSqlOperation2] = await DATABASE_DROP_PROTECTION.create({ @@ -346,7 +378,20 @@ describe('Spanner', () => { const googleSqlTable = DATABASE.table(TABLE_NAME); const postgreSqlTable = PG_DATABASE.table(TABLE_NAME); - function insert(insertData, dialect, callback) { + /** + * + * @param insertData data to insert + * @param dialect sql dialect + * @param callback + * @param columnsMetadataForRead Optional parameter use for read/query for + * deserializing Proto messages and enum + */ + function insert( + insertData, + dialect, + callback, + columnsMetadataForRead?: {} + ) { const id = generateName('id'); insertData.Key = id; @@ -357,6 +402,7 @@ describe('Spanner', () => { params: { id, }, + columnsMetadata: columnsMetadataForRead, }; let database = DATABASE; if (dialect === Spanner.POSTGRESQL) { @@ -430,6 +476,8 @@ describe('Spanner', () => { NumericValue NUMERIC, StringValue STRING( MAX), TimestampValue TIMESTAMP, + ProtoMessageValue examples.spanner.music.SingerInfo, + ProtoEnumValue examples.spanner.music.Genre, BytesArray ARRAY, BoolArray ARRAY, DateArray ARRAY< DATE >, @@ -439,6 +487,8 @@ describe('Spanner', () => { NumericArray ARRAY< NUMERIC >, StringArray ARRAY, TimestampArray ARRAY< TIMESTAMP >, + ProtoMessageArray ARRAY, + ProtoEnumArray ARRAY, CommitTimestamp TIMESTAMP OPTIONS (allow_commit_timestamp= true) ) PRIMARY KEY (Key) ` @@ -1844,6 +1894,219 @@ describe('Spanner', () => { }); }); + describe('protoMessage', () => { + before(async function () { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + }); + + const protoMessageParams = { + value: music.SingerInfo.create({ + singerId: new Long(1), + genre: music.Genre.POP, + birthDate: 'January', + nationality: 'Country1', + }), + messageFunction: music.SingerInfo, + fullName: 'examples.spanner.music.SingerInfo', + }; + + it('GOOGLE_STANDARD_SQL should write protoMessage values', done => { + const value = Spanner.protoMessage(protoMessageParams); + insert( + {ProtoMessageValue: value}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.deepStrictEqual( + row.toJSON().ProtoMessageValue, + music.SingerInfo.toObject(protoMessageParams.value) + ); + done(); + }, + {ProtoMessageValue: music.SingerInfo} + ); + }); + + it('GOOGLE_STANDARD_SQL should write bytes in the protoMessage column', done => { + const value = music.SingerInfo.encode( + protoMessageParams.value + ).finish(); + insert( + {ProtoMessageValue: value}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.deepStrictEqual( + row.toJSON().ProtoMessageValue, + value.toString() + ); + done(); + } + ); + }); + + it('GOOGLE_STANDARD_SQL should write null in the protoMessage column', done => { + insert( + {ProtoMessageValue: null}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.equal(row.toJSON().ProtoMessageValue, null); + done(); + } + ); + }); + + it('GOOGLE_STANDARD_SQL should write protoMessageArray', done => { + const value = Spanner.protoMessage(protoMessageParams); + insert( + {ProtoMessageArray: [value]}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().ProtoMessageArray, [ + music.SingerInfo.toObject(protoMessageParams.value), + ]); + done(); + }, + {ProtoMessageArray: music.SingerInfo} + ); + }); + + it('GOOGLE_STANDARD_SQL should write bytes array in the protoMessageArray column', done => { + const value = music.SingerInfo.encode( + protoMessageParams.value + ).finish(); + insert( + {ProtoMessageArray: [value]}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().ProtoMessageArray, [ + value.toString(), + ]); + done(); + } + ); + }); + + it('GOOGLE_STANDARD_SQL should write null in the protoMessageArray column', done => { + insert( + {ProtoMessageArray: null}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.equal(row.toJSON().ProtoMessageArray, null); + done(); + } + ); + }); + }); + + describe('protoEnum', () => { + before(async function () { + if (IS_EMULATOR_ENABLED) { + this.skip(); + } + }); + + const enumParams = { + value: music.Genre.JAZZ, + enumObject: music.Genre, + fullName: 'examples.spanner.music.Genre', + }; + + it('GOOGLE_STANDARD_SQL should write protoEnum values', done => { + const value = Spanner.protoEnum(enumParams); + insert( + {ProtoEnumValue: value}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.deepStrictEqual( + row.toJSON().ProtoEnumValue, + Object.getPrototypeOf(music.Genre)[enumParams.value] + ); + done(); + }, + {ProtoEnumValue: music.Genre} + ); + }); + + it('GOOGLE_STANDARD_SQL should write int in the protoEnum column', done => { + const value = 2; + insert( + {ProtoEnumValue: value}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.deepStrictEqual( + row.toJSON().ProtoEnumValue, + value.toString() + ); + done(); + } + ); + }); + + it('GOOGLE_STANDARD_SQL should write null in the protoEnum column', done => { + insert( + {ProtoEnumValue: null}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.equal(row.toJSON().ProtoEnumValue, null); + done(); + } + ); + }); + + it('GOOGLE_STANDARD_SQL should write protoEnumArray', done => { + const value = Spanner.protoEnum(enumParams); + insert( + {ProtoEnumArray: [value]}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().ProtoEnumArray, [ + Object.getPrototypeOf(music.Genre)[enumParams.value], + ]); + done(); + }, + {ProtoEnumArray: music.Genre} + ); + }); + + it('GOOGLE_STANDARD_SQL should write int array in the protoEnumArray column', done => { + const value = 3; + insert( + {ProtoEnumArray: [value]}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.deepStrictEqual(row.toJSON().ProtoEnumArray, [ + value.toString(), + ]); + done(); + } + ); + }); + + it('GOOGLE_STANDARD_SQL should write null in the protoEnumArray column', done => { + insert( + {ProtoEnumArray: null}, + Spanner.GOOGLE_STANDARD_SQL, + (err, row) => { + assert.ifError(err); + assert.equal(row.toJSON().ProtoEnumArray, null); + done(); + } + ); + }); + }); + describe('jsonb', () => { before(async function () { if (IS_EMULATOR_ENABLED) { diff --git a/test/codec.ts b/test/codec.ts index 73b9f2132..9a925be86 100644 --- a/test/codec.ts +++ b/test/codec.ts @@ -22,6 +22,12 @@ import {Big} from 'big.js'; import {PreciseDate} from '@google-cloud/precise-date'; import {GrpcService} from '../src/common-grpc/service'; import {google} from '../protos/protos'; +import {GoogleError} from 'google-gax'; +import {util} from 'protobufjs'; +import Long = util.Long; +const singer = require('./data/singer'); +const music = singer.examples.spanner.music; +const is = require('is'); describe('codec', () => { let codec; @@ -302,6 +308,100 @@ describe('codec', () => { }); }); + describe('ProtoMessage', () => { + const protoMessageParams = { + value: music.SingerInfo.create({ + singerId: new Long(1), + genre: music.Genre.POP, + birthDate: 'January', + nationality: 'Country1', + }), + messageFunction: music.SingerInfo, + fullName: 'examples.spanner.music.SingerInfo', + }; + + it('should store value as buffer', () => { + const protoMessage = new codec.ProtoMessage(protoMessageParams); + assert(Buffer.isBuffer(protoMessage.value)); + }); + + it('should throw an error when value is not object and protoMessage is not passed', () => { + assert.throws( + () => { + new codec.ProtoMessage({ + value: { + singerId: 1, + genre: music.Genre.POP, + birthDate: 'January', + }, + fullName: 'examples.spanner.music.SingerInfo', + }); + }, + new GoogleError(`protoMessageParams cannot be used to construct + the ProtoMessage. Pass the serialized buffer of the + proto message as the value or provide the message object along with the + corresponding messageFunction generated by protobufjs-cli.`) + ); + }); + + it('toJSON with messageFunction', () => { + assert.deepEqual( + new codec.ProtoMessage(protoMessageParams).toJSON(), + music.SingerInfo.toObject(protoMessageParams.value) + ); + }); + + it('toJSON without messageFunction', () => { + const message = new codec.ProtoMessage({ + value: music.SingerInfo.encode(protoMessageParams.value).finish(), + fullName: 'examples.spanner.music.SingerInfo', + }); + assert.deepEqual(message.toJSON(), message.value.toString()); + }); + }); + + describe('ProtoEnum', () => { + const enumParams = { + value: music.Genre.JAZZ, + enumObject: music.Genre, + fullName: 'examples.spanner.music.Genre', + }; + + it('should store value as string', () => { + const protoEnum = new codec.ProtoEnum(enumParams); + assert(is.string(protoEnum.value)); + }); + + it('should throw an error when value is non numeric string and enumObject is not passed', () => { + assert.throws( + () => { + new codec.ProtoEnum({ + value: 'POP', + fullName: 'examples.spanner.music.Genre', + }); + }, + new GoogleError(`protoEnumParams cannot be used for constructing the + ProtoEnum. Pass the number as the value or provide the enum string + constant as the value along with the corresponding enumObject generated + by protobufjs-cli.`) + ); + }); + + it('toJSON with enumObject', () => { + assert.deepEqual(new codec.ProtoEnum(enumParams).toJSON(), 'JAZZ'); + }); + + it('toJSON without enumObject', () => { + assert.deepEqual( + new codec.ProtoEnum({ + value: music.Genre.JAZZ, + fullName: 'examples.spanner.music.Genre', + }).toJSON(), + 1 + ); + }); + }); + describe('Struct', () => { describe('toJSON', () => { it('should covert the struct to JSON', () => { @@ -544,6 +644,42 @@ describe('codec', () => { assert.deepStrictEqual(decoded, expected); }); + it('should decode ProtoMessage', () => { + const expected = new codec.ProtoMessage({ + value: music.SingerInfo.create({ + singerId: 1, + genre: music.Genre.POP, + birthDate: 'January', + nationality: 'Country1', + }), + messageFunction: music.SingerInfo, + fullName: 'examples.spanner.music.SingerInfo', + }); + const encoded = expected.value.toString('base64'); + + const decoded = codec.decode( + encoded, + { + code: google.spanner.v1.TypeCode.PROTO, + protoTypeFqn: 'examples.spanner.music.SingerInfo', + }, + music.SingerInfo + ); + + assert.deepStrictEqual(decoded, expected); + }); + + it('should decode ProtoEnum', () => { + const expected = Buffer.from('bytes value'); + const encoded = expected.toString('base64'); + + const decoded = codec.decode(encoded, { + code: google.spanner.v1.TypeCode.BYTES, + }); + + assert.deepStrictEqual(decoded, expected); + }); + it.skip('should decode FLOAT32', () => { const value = 'Infinity'; @@ -804,6 +940,42 @@ describe('codec', () => { assert.strictEqual(encoded, value.toString('base64')); }); + it('should encode ProtoMessage', () => { + const genre = music.Genre.ROCK; + const singerInfo = music.SingerInfo.create({ + singerId: 1, + genre: genre, + birthDate: 'January', + nationality: 'Country1', + }); + + const protoMessage = new codec.ProtoMessage({ + value: singerInfo, + messageFunction: music.SingerInfo, + fullName: 'examples.spanner.music.SingerInfo', + }); + + const encoded = codec.encode(protoMessage); + + assert.strictEqual( + encoded, + music.SingerInfo.encode(singerInfo).finish().toString('base64') + ); + }); + + it('should encode ProtoEnum', () => { + const genre = music.Genre.ROCK; + const protoEnum = new codec.ProtoEnum({ + value: genre, + enumObject: music.Genre, + fullName: 'examples.spanner.music.Genre', + }); + + const encoded = codec.encode(protoEnum); + + assert.strictEqual(encoded, genre.toString()); + }); + it('should encode structs', () => { const value = codec.Struct.fromJSON({a: 'b', c: 'd'}); const encoded = codec.encode(value); diff --git a/test/data/README.md b/test/data/README.md new file mode 100644 index 000000000..6de5c0ae0 --- /dev/null +++ b/test/data/README.md @@ -0,0 +1,8 @@ +#### To generate singer.js and singer.d.ts file from singer.proto +```shell +npm install -g protobufjs-cli +cd test/data +pbjs -t static-module -w commonjs -o singer.js singer.proto +pbts -o singer.d.ts singer.js +protoc --proto_path=. --include_imports --descriptor_set_out=descriptors.pb singer.proto +``` diff --git a/test/data/descriptors.pb b/test/data/descriptors.pb new file mode 100644 index 000000000..10193075e Binary files /dev/null and b/test/data/descriptors.pb differ diff --git a/test/data/singer.d.ts b/test/data/singer.d.ts new file mode 100644 index 000000000..87d1cfc11 --- /dev/null +++ b/test/data/singer.d.ts @@ -0,0 +1,175 @@ +// 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. + +import * as $protobuf from 'protobufjs'; +import Long = require('long'); +/** Namespace examples. */ +export namespace examples { + /** Namespace spanner. */ + namespace spanner { + /** Namespace music. */ + namespace music { + /** Properties of a SingerInfo. */ + interface ISingerInfo { + /** SingerInfo singerId */ + singerId?: number | Long | null; + + /** SingerInfo birthDate */ + birthDate?: string | null; + + /** SingerInfo nationality */ + nationality?: string | null; + + /** SingerInfo genre */ + genre?: examples.spanner.music.Genre | null; + } + + /** Represents a SingerInfo. */ + class SingerInfo implements ISingerInfo { + /** + * Constructs a new SingerInfo. + * @param [properties] Properties to set + */ + constructor(properties?: examples.spanner.music.ISingerInfo); + + /** SingerInfo singerId. */ + public singerId?: number | Long | null; + + /** SingerInfo birthDate. */ + public birthDate?: string | null; + + /** SingerInfo nationality. */ + public nationality?: string | null; + + /** SingerInfo genre. */ + public genre?: examples.spanner.music.Genre | null; + + /** SingerInfo _singerId. */ + public _singerId?: 'singerId'; + + /** SingerInfo _birthDate. */ + public _birthDate?: 'birthDate'; + + /** SingerInfo _nationality. */ + public _nationality?: 'nationality'; + + /** SingerInfo _genre. */ + public _genre?: 'genre'; + + /** + * Creates a new SingerInfo instance using the specified properties. + * @param [properties] Properties to set + * @returns SingerInfo instance + */ + public static create( + properties?: examples.spanner.music.ISingerInfo + ): examples.spanner.music.SingerInfo; + + /** + * Encodes the specified SingerInfo message. Does not implicitly {@link examples.spanner.music.SingerInfo.verify|verify} messages. + * @param message SingerInfo message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode( + message: examples.spanner.music.ISingerInfo, + writer?: $protobuf.Writer + ): $protobuf.Writer; + + /** + * Encodes the specified SingerInfo message, length delimited. Does not implicitly {@link examples.spanner.music.SingerInfo.verify|verify} messages. + * @param message SingerInfo message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited( + message: examples.spanner.music.ISingerInfo, + writer?: $protobuf.Writer + ): $protobuf.Writer; + + /** + * Decodes a SingerInfo message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns SingerInfo + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode( + reader: $protobuf.Reader | Uint8Array, + length?: number + ): examples.spanner.music.SingerInfo; + + /** + * Decodes a SingerInfo message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns SingerInfo + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited( + reader: $protobuf.Reader | Uint8Array + ): examples.spanner.music.SingerInfo; + + /** + * Verifies a SingerInfo message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: {[k: string]: any}): string | null; + + /** + * Creates a SingerInfo message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns SingerInfo + */ + public static fromObject(object: { + [k: string]: any; + }): examples.spanner.music.SingerInfo; + + /** + * Creates a plain object from a SingerInfo message. Also converts values to other types if specified. + * @param message SingerInfo + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject( + message: examples.spanner.music.SingerInfo, + options?: $protobuf.IConversionOptions + ): {[k: string]: any}; + + /** + * Converts this SingerInfo to JSON. + * @returns JSON object + */ + public toJSON(): {[k: string]: any}; + + /** + * Gets the default type url for SingerInfo + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } + + /** Genre enum. */ + enum Genre { + POP = 0, + JAZZ = 1, + FOLK = 2, + ROCK = 3, + } + } + } +} diff --git a/test/data/singer.js b/test/data/singer.js new file mode 100644 index 000000000..203791b41 --- /dev/null +++ b/test/data/singer.js @@ -0,0 +1,505 @@ +// 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. + +/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/ +'use strict'; + +var $protobuf = require('protobufjs/minimal'); + +// Common aliases +var $Reader = $protobuf.Reader, + $Writer = $protobuf.Writer, + $util = $protobuf.util; + +// Exported root namespace +var $root = $protobuf.roots['default'] || ($protobuf.roots['default'] = {}); + +$root.examples = (function () { + /** + * Namespace examples. + * @exports examples + * @namespace + */ + var examples = {}; + + examples.spanner = (function () { + /** + * Namespace spanner. + * @memberof examples + * @namespace + */ + var spanner = {}; + + spanner.music = (function () { + /** + * Namespace music. + * @memberof examples.spanner + * @namespace + */ + var music = {}; + + music.SingerInfo = (function () { + /** + * Properties of a SingerInfo. + * @memberof examples.spanner.music + * @interface ISingerInfo + * @property {number|Long|null} [singerId] SingerInfo singerId + * @property {string|null} [birthDate] SingerInfo birthDate + * @property {string|null} [nationality] SingerInfo nationality + * @property {examples.spanner.music.Genre|null} [genre] SingerInfo genre + */ + + /** + * Constructs a new SingerInfo. + * @memberof examples.spanner.music + * @classdesc Represents a SingerInfo. + * @implements ISingerInfo + * @constructor + * @param {examples.spanner.music.ISingerInfo=} [properties] Properties to set + */ + function SingerInfo(properties) { + if (properties) + for ( + var keys = Object.keys(properties), i = 0; + i < keys.length; + ++i + ) + if (properties[keys[i]] !== null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * SingerInfo singerId. + * @member {number|Long|null|undefined} singerId + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + SingerInfo.prototype.singerId = null; + + /** + * SingerInfo birthDate. + * @member {string|null|undefined} birthDate + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + SingerInfo.prototype.birthDate = null; + + /** + * SingerInfo nationality. + * @member {string|null|undefined} nationality + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + SingerInfo.prototype.nationality = null; + + /** + * SingerInfo genre. + * @member {examples.spanner.music.Genre|null|undefined} genre + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + SingerInfo.prototype.genre = null; + + // OneOf field names bound to virtual getters and setters + var $oneOfFields; + + /** + * SingerInfo _singerId. + * @member {"singerId"|undefined} _singerId + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + Object.defineProperty(SingerInfo.prototype, '_singerId', { + get: $util.oneOfGetter(($oneOfFields = ['singerId'])), + set: $util.oneOfSetter($oneOfFields), + }); + + /** + * SingerInfo _birthDate. + * @member {"birthDate"|undefined} _birthDate + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + Object.defineProperty(SingerInfo.prototype, '_birthDate', { + get: $util.oneOfGetter(($oneOfFields = ['birthDate'])), + set: $util.oneOfSetter($oneOfFields), + }); + + /** + * SingerInfo _nationality. + * @member {"nationality"|undefined} _nationality + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + Object.defineProperty(SingerInfo.prototype, '_nationality', { + get: $util.oneOfGetter(($oneOfFields = ['nationality'])), + set: $util.oneOfSetter($oneOfFields), + }); + + /** + * SingerInfo _genre. + * @member {"genre"|undefined} _genre + * @memberof examples.spanner.music.SingerInfo + * @instance + */ + Object.defineProperty(SingerInfo.prototype, '_genre', { + get: $util.oneOfGetter(($oneOfFields = ['genre'])), + set: $util.oneOfSetter($oneOfFields), + }); + + /** + * Creates a new SingerInfo instance using the specified properties. + * @function create + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {examples.spanner.music.ISingerInfo=} [properties] Properties to set + * @returns {examples.spanner.music.SingerInfo} SingerInfo instance + */ + SingerInfo.create = function create(properties) { + return new SingerInfo(properties); + }; + + /** + * Encodes the specified SingerInfo message. Does not implicitly {@link examples.spanner.music.SingerInfo.verify|verify} messages. + * @function encode + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {examples.spanner.music.ISingerInfo} message SingerInfo message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + SingerInfo.encode = function encode(message, writer) { + if (!writer) writer = $Writer.create(); + if ( + message.singerId !== null && + Object.hasOwnProperty.call(message, 'singerId') + ) + writer.uint32(/* id 1, wireType 0 =*/ 8).int64(message.singerId); + if ( + message.birthDate !== null && + Object.hasOwnProperty.call(message, 'birthDate') + ) + writer.uint32(/* id 2, wireType 2 =*/ 18).string(message.birthDate); + if ( + message.nationality !== null && + Object.hasOwnProperty.call(message, 'nationality') + ) + writer + .uint32(/* id 3, wireType 2 =*/ 26) + .string(message.nationality); + if ( + message.genre !== null && + Object.hasOwnProperty.call(message, 'genre') + ) + writer.uint32(/* id 4, wireType 0 =*/ 32).int32(message.genre); + return writer; + }; + + /** + * Encodes the specified SingerInfo message, length delimited. Does not implicitly {@link examples.spanner.music.SingerInfo.verify|verify} messages. + * @function encodeDelimited + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {examples.spanner.music.ISingerInfo} message SingerInfo message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + SingerInfo.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a SingerInfo message from the specified reader or buffer. + * @function decode + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {examples.spanner.music.SingerInfo} SingerInfo + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + SingerInfo.decode = function decode(reader, length) { + if (!(reader instanceof $Reader)) reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, + message = new $root.examples.spanner.music.SingerInfo(); + while (reader.pos < end) { + var tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + message.singerId = reader.int64(); + break; + } + case 2: { + message.birthDate = reader.string(); + break; + } + case 3: { + message.nationality = reader.string(); + break; + } + case 4: { + message.genre = reader.int32(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a SingerInfo message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {examples.spanner.music.SingerInfo} SingerInfo + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + SingerInfo.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a SingerInfo message. + * @function verify + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + SingerInfo.verify = function verify(message) { + if (typeof message !== 'object' || message === null) + return 'object expected'; + var properties = {}; + if (message.singerId !== null && message.hasOwnProperty('singerId')) { + properties._singerId = 1; + if ( + !$util.isInteger(message.singerId) && + !( + message.singerId && + $util.isInteger(message.singerId.low) && + $util.isInteger(message.singerId.high) + ) + ) + return 'singerId: integer|Long expected'; + } + if ( + message.birthDate !== null && + message.hasOwnProperty('birthDate') + ) { + properties._birthDate = 1; + if (!$util.isString(message.birthDate)) + return 'birthDate: string expected'; + } + if ( + message.nationality !== null && + message.hasOwnProperty('nationality') + ) { + properties._nationality = 1; + if (!$util.isString(message.nationality)) + return 'nationality: string expected'; + } + if (message.genre !== null && message.hasOwnProperty('genre')) { + properties._genre = 1; + switch (message.genre) { + default: + return 'genre: enum value expected'; + case 0: + case 1: + case 2: + case 3: + break; + } + } + return null; + }; + + /** + * Creates a SingerInfo message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {Object.} object Plain object + * @returns {examples.spanner.music.SingerInfo} SingerInfo + */ + SingerInfo.fromObject = function fromObject(object) { + if (object instanceof $root.examples.spanner.music.SingerInfo) + return object; + var message = new $root.examples.spanner.music.SingerInfo(); + if (object.singerId !== null) + if ($util.Long) + (message.singerId = $util.Long.fromValue( + object.singerId + )).unsigned = false; + else if (typeof object.singerId === 'string') + message.singerId = parseInt(object.singerId, 10); + else if (typeof object.singerId === 'number') + message.singerId = object.singerId; + else if (typeof object.singerId === 'object') + message.singerId = new $util.LongBits( + object.singerId.low >>> 0, + object.singerId.high >>> 0 + ).toNumber(); + if (object.birthDate !== null) + message.birthDate = String(object.birthDate); + if (object.nationality !== null) + message.nationality = String(object.nationality); + switch (object.genre) { + default: + if (typeof object.genre === 'number') { + message.genre = object.genre; + break; + } + break; + case 'POP': + case 0: + message.genre = 0; + break; + case 'JAZZ': + case 1: + message.genre = 1; + break; + case 'FOLK': + case 2: + message.genre = 2; + break; + case 'ROCK': + case 3: + message.genre = 3; + break; + } + return message; + }; + + /** + * Creates a plain object from a SingerInfo message. Also converts values to other types if specified. + * @function toObject + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {examples.spanner.music.SingerInfo} message SingerInfo + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + SingerInfo.toObject = function toObject(message, options) { + if (!options) options = {}; + var object = {}; + if (message.singerId !== null && message.hasOwnProperty('singerId')) { + if (typeof message.singerId === 'number') + object.singerId = + options.longs === String + ? String(message.singerId) + : message.singerId; + else + object.singerId = + options.longs === String + ? $util.Long.prototype.toString.call(message.singerId) + : options.longs === Number + ? new $util.LongBits( + message.singerId.low >>> 0, + message.singerId.high >>> 0 + ).toNumber() + : message.singerId; + if (options.oneofs) object._singerId = 'singerId'; + } + if ( + message.birthDate !== null && + message.hasOwnProperty('birthDate') + ) { + object.birthDate = message.birthDate; + if (options.oneofs) object._birthDate = 'birthDate'; + } + if ( + message.nationality !== null && + message.hasOwnProperty('nationality') + ) { + object.nationality = message.nationality; + if (options.oneofs) object._nationality = 'nationality'; + } + if (message.genre !== null && message.hasOwnProperty('genre')) { + object.genre = + options.enums === String + ? $root.examples.spanner.music.Genre[message.genre] === + undefined + ? message.genre + : $root.examples.spanner.music.Genre[message.genre] + : message.genre; + if (options.oneofs) object._genre = 'genre'; + } + return object; + }; + + /** + * Converts this SingerInfo to JSON. + * @function toJSON + * @memberof examples.spanner.music.SingerInfo + * @instance + * @returns {Object.} JSON object + */ + SingerInfo.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for SingerInfo + * @function getTypeUrl + * @memberof examples.spanner.music.SingerInfo + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + SingerInfo.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = 'type.googleapis.com'; + } + return typeUrlPrefix + '/examples.spanner.music.SingerInfo'; + }; + + return SingerInfo; + })(); + + /** + * Genre enum. + * @name examples.spanner.music.Genre + * @enum {number} + * @property {number} POP=0 POP value + * @property {number} JAZZ=1 JAZZ value + * @property {number} FOLK=2 FOLK value + * @property {number} ROCK=3 ROCK value + */ + music.Genre = (function () { + var valuesById = {}, + values = Object.create(valuesById); + values[(valuesById[0] = 'POP')] = 0; + values[(valuesById[1] = 'JAZZ')] = 1; + values[(valuesById[2] = 'FOLK')] = 2; + values[(valuesById[3] = 'ROCK')] = 3; + return values; + })(); + + return music; + })(); + + return spanner; + })(); + + return examples; +})(); + +module.exports = $root; diff --git a/test/data/singer.proto b/test/data/singer.proto new file mode 100644 index 000000000..adc79d18c --- /dev/null +++ b/test/data/singer.proto @@ -0,0 +1,31 @@ +// 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. +// 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. + +syntax = "proto3"; + +package examples.spanner.music; + +message SingerInfo { + optional int64 singer_id = 1; + optional string birth_date = 2; + optional string nationality = 3; + optional Genre genre = 4; +} + +enum Genre { + POP = 0; + JAZZ = 1; + FOLK = 2; + ROCK = 3; +} diff --git a/test/database.ts b/test/database.ts index bd697cd9e..b3cdf1465 100644 --- a/test/database.ts +++ b/test/database.ts @@ -52,6 +52,7 @@ const fakePfy = extend({}, pfy, { 'batchTransaction', 'getRestoreInfo', 'getState', + 'getDatabaseDialect', 'getOperations', 'runTransaction', 'runTransactionAsync', @@ -2543,7 +2544,8 @@ describe('Database', () => { const [query] = runUpdateStub.lastCall.args; - assert.strictEqual(query, QUERY); + assert.strictEqual(query.sql, QUERY.sql); + assert.deepStrictEqual(query.params, QUERY.params); assert.ok(fakeCallback.calledOnce); }); @@ -2580,6 +2582,24 @@ describe('Database', () => { assert.ok(fakeCallback.calledOnce); }); + it('should accept excludeTxnFromChangeStreams', () => { + const fakeCallback = sandbox.spy(); + + database.runPartitionedUpdate( + { + excludeTxnFromChangeStream: true, + }, + fakeCallback + ); + + const [query] = runUpdateStub.lastCall.args; + + assert.deepStrictEqual(query, { + excludeTxnFromChangeStream: true, + }); + assert.ok(fakeCallback.calledOnce); + }); + it('should ignore directedReadOptions set for client', () => { const fakeCallback = sandbox.spy(); @@ -2795,6 +2815,35 @@ describe('Database', () => { }); }); + describe('getDatabaseDialect', () => { + it('should get database dialect from database metadata', async () => { + database.getMetadata = async () => [ + {databaseDialect: 'GOOGLE_STANDARD_SQL'}, + ]; + const result = await database.getDatabaseDialect(); + assert.strictEqual(result, 'GOOGLE_STANDARD_SQL'); + }); + + it('should accept and pass gaxOptions to getMetadata', async () => { + const options = {}; + database.getMetadata = async gaxOptions => { + assert.strictEqual(gaxOptions, options); + return [{}]; + }; + await database.getDatabaseDialect(options); + }); + + it('should accept callback and return database dialect', done => { + const databaseDialect = 'GOOGLE_STANDARD_SQL'; + database.getMetadata = async () => [{databaseDialect}]; + database.getDatabaseDialect((err, result) => { + assert.ifError(err); + assert.strictEqual(result, databaseDialect); + done(); + }); + }); + }); + describe('getRestoreInfo', () => { it('should get restore info from database metadata', async () => { const restoreInfo = {sourceType: 'BACKUP'}; diff --git a/test/index.ts b/test/index.ts index dd2b4b7b9..636bb845e 100644 --- a/test/index.ts +++ b/test/index.ts @@ -38,6 +38,8 @@ import { GetInstancesOptions, } from '../src'; import {CLOUD_RESOURCE_HEADER} from '../src/common'; +const singer = require('./data/singer'); +const music = singer.examples.spanner.music; // Verify that CLOUD_RESOURCE_HEADER is set to a correct value. assert.strictEqual(CLOUD_RESOURCE_HEADER, 'google-cloud-resource-prefix'); @@ -619,6 +621,66 @@ describe('Spanner', () => { }); }); + describe('protoMessage', () => { + it('should create a ProtoMessage instance', () => { + const protoMessageParams = { + value: music.SingerInfo.create({ + singerId: 2, + genre: music.Genre.POP, + birthDate: 'January', + }), + messageFunction: music.SingerInfo, + fullName: 'examples.spanner.music.SingerInfo', + }; + + const customValue = { + value: { + singerId: 2, + genre: music.Genre.POP, + birthDate: 'January', + }, + messageFunction: music.SingerInfo, + fullName: 'examples.spanner.music.SingerInfo', + }; + + fakeCodec.ProtoMessage = class { + constructor(value_) { + assert.strictEqual(value_, protoMessageParams); + return customValue; + } + }; + + const protoMessage = Spanner.protoMessage(protoMessageParams); + assert.strictEqual(protoMessage, customValue); + }); + }); + + describe('protoEnum', () => { + it('should create a ProtoEnum instance', () => { + const enumParams = { + value: music.Genre.JAZZ, + enumObject: music.Genre, + fullName: 'examples.spanner.music.Genre', + }; + + const customValue = { + value: music.Genre.JAZZ, + enumObject: music.Genre, + fullName: 'examples.spanner.music.Genre', + }; + + fakeCodec.ProtoEnum = class { + constructor(value_) { + assert.strictEqual(value_, enumParams); + return customValue; + } + }; + + const protoEnum = Spanner.protoEnum(enumParams); + assert.strictEqual(protoEnum, customValue); + }); + }); + describe('createInstance', () => { const NAME = 'instance-name'; let PATH; diff --git a/test/spanner.ts b/test/spanner.ts index 293b8bbd2..e2e715710 100644 --- a/test/spanner.ts +++ b/test/spanner.ts @@ -3133,6 +3133,23 @@ describe('Spanner with mock server', () => { assert.strictEqual(request.requestOptions!.requestTag, 'request-tag'); await database.close(); }); + + it('should use excludeTxnFromChangeStreams', async () => { + const database = newTestDatabase(); + await database.runPartitionedUpdate({ + sql: updateSql, + excludeTxnFromChangeStreams: true, + }); + const beginTxnRequest = spannerMock.getRequests().find(val => { + return (val as v1.BeginTransactionRequest).options + ?.excludeTxnFromChangeStreams; + }) as v1.BeginTransactionRequest; + assert.strictEqual( + beginTxnRequest.options?.excludeTxnFromChangeStreams, + true + ); + await database.close(); + }); }); }); @@ -3243,6 +3260,28 @@ describe('Spanner with mock server', () => { assert.strictEqual(commitRequest.mutations.length, 1); }); + it('should apply blind writes only once with excludeTxnFromChangeStreams option', async () => { + const database = newTestDatabase(); + await database.runTransactionAsync( + { + excludeTxnFromChangeStreams: true, + }, + async tx => { + await tx!.insert('foo', {id: 1, value: 'One'}); + await tx.commit(); + } + ); + await database.close(); + + const beginTxnRequest = spannerMock.getRequests().find(val => { + return (val as v1.BeginTransactionRequest).options?.readWrite; + }) as v1.BeginTransactionRequest; + assert.strictEqual( + beginTxnRequest.options?.excludeTxnFromChangeStreams, + true + ); + }); + it('should use optimistic lock for runTransactionAsync', async () => { const database = newTestDatabase(); await database.runTransactionAsync( @@ -3266,6 +3305,29 @@ describe('Spanner with mock server', () => { ); }); + it('should use exclude transaction from change streams for runTransactionAsync', async () => { + const database = newTestDatabase(); + await database.runTransactionAsync( + { + excludeTxnFromChangeStreams: 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?.excludeTxnFromChangeStreams, + true + ); + }); + it('should use optimistic lock for runTransaction', done => { const database = newTestDatabase(); database.runTransaction({optimisticLock: true}, async (err, tx) => { @@ -3286,6 +3348,29 @@ describe('Spanner with mock server', () => { }); }); + it('should use exclude transaction from change stream for runTransaction', done => { + const database = newTestDatabase(); + database.runTransaction( + {excludeTxnFromChangeStreams: 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!.excludeTxnFromChangeStreams, + true + ); + done(); + } + ); + }); + it('should use optimistic lock and transaction tag for getTransaction', async () => { const database = newTestDatabase(); const promise = await database.getTransaction({ @@ -3422,6 +3507,33 @@ describe('Spanner with mock server', () => { assert.ok(beginTxnRequest, 'beginTransaction was called'); }); + it('should use beginTransaction on retry with excludeTxnFromChangeStreams', async () => { + const database = newTestDatabase(); + let attempts = 0; + await database.runTransactionAsync( + {excludeTxnFromChangeStreams: 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?.excludeTxnFromChangeStreams, + true + ); + }); + it('should use beginTransaction on retry with optimistic lock', async () => { const database = newTestDatabase(); let attempts = 0; @@ -3469,6 +3581,38 @@ describe('Spanner with mock server', () => { assert.ok(beginTxnRequest, 'beginTransaction was called'); }); + it('should use beginTransaction on retry for unknown reason with excludeTxnFromChangeStreams', async () => { + const database = newTestDatabase(); + await database.runTransactionAsync( + { + excludeTxnFromChangeStreams: true, + }, + async tx => { + try { + await tx.runUpdate(invalidSql); + assert.fail('missing expected error'); + } catch (e) { + assert.strictEqual( + (e as ServiceError).message, + `${grpc.status.NOT_FOUND} NOT_FOUND: ${fooNotFoundErr.message}` + ); + } + await tx.run(selectSql); + 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?.excludeTxnFromChangeStreams, + true + ); + }); + it('should use beginTransaction for streaming on retry for unknown reason', async () => { const database = newTestDatabase(); await database.runTransactionAsync(async tx => { @@ -3492,6 +3636,38 @@ describe('Spanner with mock server', () => { assert.ok(beginTxnRequest, 'beginTransaction was called'); }); + it('should use beginTransaction for streaming on retry for unknown reason with excludeTxnFromChangeStreams', async () => { + const database = newTestDatabase(); + await database.runTransactionAsync( + { + excludeTxnFromChangeStreams: true, + }, + async tx => { + try { + await getRowCountFromStreamingSql(tx!, {sql: invalidSql}); + assert.fail('missing expected error'); + } catch (e) { + assert.strictEqual( + (e as ServiceError).message, + `${grpc.status.NOT_FOUND} NOT_FOUND: ${fooNotFoundErr.message}` + ); + } + await tx.run(selectSql); + 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?.excludeTxnFromChangeStreams, + true + ); + }); + it('should fail if beginTransaction fails', async () => { const database = newTestDatabase(); const err = { @@ -3558,6 +3734,28 @@ describe('Spanner with mock server', () => { assert.ok(beginTxnRequest, 'beginTransaction was called'); }); + it('should run begin transaction on blind commit with excludeTxnFromChangeStreams', async () => { + const database = newTestDatabase(); + await database.runTransactionAsync( + { + excludeTxnFromChangeStreams: true, + }, + async tx => { + tx.insert('foo', {id: 1, name: 'One'}); + await tx.commit(); + } + ); + await database.close(); + + const beginTxnRequest = spannerMock.getRequests().find(val => { + return (val as v1.BeginTransactionRequest).options?.readWrite; + }) as v1.BeginTransactionRequest; + assert.strictEqual( + beginTxnRequest.options?.excludeTxnFromChangeStreams, + true + ); + }); + it('should throw error if begin transaction fails on blind commit', async () => { const database = newTestDatabase(); const err = { @@ -3581,6 +3779,43 @@ describe('Spanner with mock server', () => { await database.close(); } }); + + it('should throw error if begin transaction fails on blind commit with excludeTxnFromChangeStreams', async () => { + const database = newTestDatabase(); + const err = { + message: 'Test error', + } as MockError; + spannerMock.setExecutionTime( + spannerMock.beginTransaction, + SimulatedExecutionTime.ofError(err) + ); + try { + await database.runTransactionAsync( + { + excludeTxnFromChangeStreams: true, + }, + async tx => { + tx.insert('foo', {id: 1, name: 'One'}); + await tx.commit(); + } + ); + } catch (e) { + const beginTxnRequest = spannerMock.getRequests().find(val => { + return (val as v1.BeginTransactionRequest).options?.readWrite; + }) as v1.BeginTransactionRequest; + + assert.strictEqual( + beginTxnRequest.options?.excludeTxnFromChangeStreams, + true + ); + assert.strictEqual( + (e as ServiceError).message, + '2 UNKNOWN: Test error' + ); + } finally { + await database.close(); + } + }); }); describe('table', () => { @@ -3613,6 +3848,24 @@ describe('Spanner with mock server', () => { await database.close(); }); + it('should use excludeTxnFromChangeStreams for mutations', async () => { + const database = newTestDatabase(); + await database.table('foo').upsert( + {id: 1, name: 'bar'}, + { + excludeTxnFromChangeStreams: true, + } + ); + const beginTxnRequest = spannerMock.getRequests().find(val => { + return (val as v1.BeginTransactionRequest).options?.readWrite; + }) as v1.BeginTransactionRequest; + assert.strictEqual( + beginTxnRequest.options?.excludeTxnFromChangeStreams, + true + ); + await database.close(); + }); + it('should encode object to JSON', async () => { const database = newTestDatabase(); await database diff --git a/test/table.ts b/test/table.ts index c61e8b40f..2886c3287 100644 --- a/test/table.ts +++ b/test/table.ts @@ -67,6 +67,7 @@ describe('Table', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let TableCached: any; let table; + let tableWithSchema; let transaction: FakeTransaction; const DATABASE = { @@ -75,6 +76,7 @@ describe('Table', () => { }; const NAME = 'table-name'; + const NAMEWITHSCHEMA = 'schema.' + NAME; before(() => { Table = proxyquire('../src/table.js', { @@ -86,6 +88,7 @@ describe('Table', () => { beforeEach(() => { extend(Table, TableCached); table = new Table(DATABASE, NAME); + tableWithSchema = new Table(DATABASE, NAMEWITHSCHEMA); transaction = new FakeTransaction(); }); @@ -209,26 +212,124 @@ describe('Table', () => { }); describe('delete', () => { - it('should update the schema on the database', () => { - const updateSchemaReturnValue = {}; + it('should update the schema on the database for GoogleSQL using await', async () => { + table.database = { + getDatabaseDialect: () => { + return 'GOOGLE_STANDARD_SQL'; + }, + updateSchema: schema => { + assert.strictEqual(schema, 'DROP TABLE `table-name`'); + }, + }; + + await table.delete(); + }); + + it('should update the schema on the database for GoogleSQL using callbacks', () => { + function callback() {} + table.database = { + getDatabaseDialect: () => { + return 'GOOGLE_STANDARD_SQL'; + }, + updateSchema: (schema, gaxOptions, callback_) => { + assert.strictEqual(schema, 'DROP TABLE `table-name`'); + assert.strictEqual(callback_, callback); + }, + }; + table.delete(callback); + }); + + it('should update the schema on the database for GoogleSQL with schema in the table name using await', async () => { + tableWithSchema.database = { + getDatabaseDialect: () => { + return 'GOOGLE_STANDARD_SQL'; + }, + updateSchema: schema => { + assert.strictEqual(schema, 'DROP TABLE `schema`.`table-name`'); + }, + }; + + await tableWithSchema.delete(); + }); + + it('should update the schema on the database for GoogleSQL with schema in the table name using callbacks', () => { + function callback() {} + tableWithSchema.database = { + getDatabaseDialect: () => { + return 'GOOGLE_STANDARD_SQL'; + }, + updateSchema: (schema, gaxOptions, callback_) => { + assert.strictEqual(schema, 'DROP TABLE `schema`.`table-name`'); + assert.strictEqual(callback_, callback); + }, + }; + tableWithSchema.delete(callback); + }); + + it('should update the schema on the database for PostgresSQL using await', async () => { + table.database = { + getDatabaseDialect: () => { + return 'POSTGRESQL'; + }, + updateSchema: schema => { + assert.strictEqual(schema, `DROP TABLE "${table.name}"`); + }, + }; + await table.delete(); + }); + + it('should update the schema on the database for PostgresSQL using callbacks', () => { function callback() {} table.database = { + getDatabaseDialect: () => { + return 'POSTGRESQL'; + }, updateSchema: (schema, gaxOptions, callback_) => { - assert.strictEqual(schema, 'DROP TABLE `' + table.name + '`'); + assert.strictEqual(schema, 'DROP TABLE "table-name"'); assert.strictEqual(callback_, callback); - return updateSchemaReturnValue; }, }; - const returnValue = table.delete(callback); - assert.strictEqual(returnValue, updateSchemaReturnValue); + table.delete(callback); + }); + + it('should update the schema on the database for PostgresSQL with schema in the table name using await', async () => { + tableWithSchema.database = { + getDatabaseDialect: () => { + return 'POSTGRESQL'; + }, + updateSchema: schema => { + assert.strictEqual(schema, `DROP TABLE "schema"."${table.name}"`); + }, + }; + + await tableWithSchema.delete(); + }); + + it('should update the schema on the database for PostgresSQL with schema in the table name using callbacks', () => { + function callback() {} + tableWithSchema.database = { + getDatabaseDialect: () => { + return 'POSTGRESQL'; + }, + updateSchema: (schema, gaxOptions, callback_) => { + assert.strictEqual(schema, `DROP TABLE "schema"."${table.name}"`); + assert.strictEqual(callback_, callback); + }, + }; + + tableWithSchema.delete(callback); }); it('should accept and pass gaxOptions to updateSchema', done => { const gaxOptions = {}; table.database = { + getDatabaseDialect: gaxOptionsFromTable => { + assert.strictEqual(gaxOptionsFromTable, gaxOptions); + return 'GOOGLE_STANDARD_SQL'; + }, updateSchema: (schema, gaxOptionsFromTable) => { assert.strictEqual(gaxOptionsFromTable, gaxOptions); done(); diff --git a/test/transaction.ts b/test/transaction.ts index 11a8647e0..392370ab7 100644 --- a/test/transaction.ts +++ b/test/transaction.ts @@ -395,6 +395,7 @@ describe('Transaction', () => { json: true, jsonOptions: {a: 'b'}, maxResumeRetries: 10, + columnsMetadata: {column1: {test: 'ss'}, column2: Function}, }; snapshot.createReadStream(TABLE, fakeOptions); @@ -769,6 +770,7 @@ describe('Transaction', () => { json: true, jsonOptions: {a: 'b'}, maxResumeRetries: 10, + columnsMetadata: {column1: {test: 'ss'}, column2: Function}, }; const fakeQuery = Object.assign({}, QUERY, expectedOptions);